Add protocol selection to Claude and Codex provider forms referencing CCSwitch
This commit is contained in:
1 parent
bae24cc439
commit
16957decb2
14 files changed
+274
-25
No files matched your search
@@ -46,7 +46,15 @@ type Models struct {
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// Provider is one upstream Chat Completions endpoint.
|
||||
// Supported upstream protocols (matching CCSwitch format identifiers).
|
||||
const (
|
||||
ProtocolAnthropic = "anthropic"
|
||||
ProtocolOpenAIChat = "openai_chat"
|
||||
ProtocolOpenAIResponses = "openai_responses"
|
||||
ProtocolGeminiNative = "gemini_native"
|
||||
)
|
||||
|
||||
// Provider is one upstream endpoint.
|
||||
type Provider struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -54,6 +62,8 @@ type Provider struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Models Models `json:"models"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
APIFormat string `json:"api_format,omitempty"`
|
||||
EnabledForClaude bool `json:"enabled_for_claude"`
|
||||
EnabledForCodex bool `json:"enabled_for_codex"`
|
||||
Target string `json:"target,omitempty"`
|
||||
@@ -170,6 +180,13 @@ func (c *Config) normalize() {
|
||||
}
|
||||
for i := range c.Providers {
|
||||
c.Providers[i].Models.normalize()
|
||||
if c.Providers[i].Protocol == "" && c.Providers[i].APIFormat != "" {
|
||||
c.Providers[i].Protocol = c.Providers[i].APIFormat
|
||||
}
|
||||
if c.Providers[i].Protocol == "" {
|
||||
c.Providers[i].Protocol = ProtocolOpenAIChat
|
||||
}
|
||||
c.Providers[i].APIFormat = c.Providers[i].Protocol
|
||||
switch c.Providers[i].Target {
|
||||
case TargetClaude, TargetCodex:
|
||||
default:
|
||||
@@ -206,7 +223,7 @@ func (c *Config) clone() *Config {
|
||||
return &out
|
||||
}
|
||||
|
||||
func newProvider(name, preset, baseURL, apiKey string, models Models, target string) (*Provider, error) {
|
||||
func newProvider(name, preset, baseURL, apiKey string, models Models, target string, protocol ...string) (*Provider, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, errors.New("name is required")
|
||||
@@ -231,6 +248,13 @@ func newProvider(name, preset, baseURL, apiKey string, models Models, target str
|
||||
default:
|
||||
return nil, fmt.Errorf("target must be %q or %q", TargetClaude, TargetCodex)
|
||||
}
|
||||
proto := ""
|
||||
if len(protocol) > 0 {
|
||||
proto = strings.TrimSpace(protocol[0])
|
||||
}
|
||||
if proto == "" {
|
||||
proto = ProtocolOpenAIChat
|
||||
}
|
||||
return &Provider{
|
||||
ID: newID("p_"),
|
||||
Name: name,
|
||||
@@ -238,6 +262,8 @@ func newProvider(name, preset, baseURL, apiKey string, models Models, target str
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
Models: models,
|
||||
Protocol: proto,
|
||||
APIFormat: proto,
|
||||
Target: target,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}, nil
|
||||
|
||||
@@ -337,3 +337,48 @@ func TestSetGatewayValidation(t *testing.T) {
|
||||
t.Error("unknown language accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderProtocol(t *testing.T) {
|
||||
t.Setenv("CROSSRELAY_HOME", t.TempDir())
|
||||
store, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
// 1. Default protocol when omitted should be openai_chat.
|
||||
p1, err := store.AddProvider("Claude-Default", "", "https://api.example.com/v1", "sk-1", Models{}, TargetClaude)
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
if p1.Protocol != ProtocolOpenAIChat || p1.APIFormat != ProtocolOpenAIChat {
|
||||
t.Errorf("expected default protocol %q, got protocol=%q, api_format=%q", ProtocolOpenAIChat, p1.Protocol, p1.APIFormat)
|
||||
}
|
||||
|
||||
// 2. Explicit protocol should be saved.
|
||||
p2, err := store.AddProvider("Claude-Anthropic", "", "https://api.anthropic.com/v1", "sk-2", Models{}, TargetClaude, ProtocolAnthropic)
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider with protocol: %v", err)
|
||||
}
|
||||
if p2.Protocol != ProtocolAnthropic {
|
||||
t.Errorf("expected protocol %q, got %q", ProtocolAnthropic, p2.Protocol)
|
||||
}
|
||||
|
||||
// 3. Update protocol.
|
||||
updated, err := store.UpdateProvider(p2.ID, "Claude-Anthropic-Updated", "", "https://api.anthropic.com/v1", "sk-2", Models{}, TargetClaude, ProtocolOpenAIResponses)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateProvider with protocol: %v", err)
|
||||
}
|
||||
if updated.Protocol != ProtocolOpenAIResponses {
|
||||
t.Errorf("expected updated protocol %q, got %q", ProtocolOpenAIResponses, updated.Protocol)
|
||||
}
|
||||
|
||||
// 4. Persistence round-trip.
|
||||
reloaded, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load reloaded: %v", err)
|
||||
}
|
||||
found := reloaded.FindProvider(p2.ID)
|
||||
if found == nil || found.Protocol != ProtocolOpenAIResponses {
|
||||
t.Errorf("expected persisted protocol %q, got %+v", ProtocolOpenAIResponses, found)
|
||||
}
|
||||
}
|
||||
@@ -104,10 +104,10 @@ func (s *Store) ActiveCodex() *Provider {
|
||||
}
|
||||
|
||||
// AddProvider validates and appends a provider for the given target.
|
||||
func (s *Store) AddProvider(name, preset, baseURL, apiKey string, models Models, target string) (*Provider, error) {
|
||||
func (s *Store) AddProvider(name, preset, baseURL, apiKey string, models Models, target string, protocol ...string) (*Provider, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p, err := newProvider(name, preset, baseURL, apiKey, models, target)
|
||||
p, err := newProvider(name, preset, baseURL, apiKey, models, target, protocol...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (s *Store) AddProvider(name, preset, baseURL, apiKey string, models Models,
|
||||
// UpdateProvider replaces a provider's editable fields. An empty apiKey
|
||||
// keeps the stored key so edits never wipe it by accident; an empty
|
||||
// target keeps the stored one.
|
||||
func (s *Store) UpdateProvider(id, name, preset, baseURL, apiKey string, models Models, target string) (*Provider, error) {
|
||||
func (s *Store) UpdateProvider(id, name, preset, baseURL, apiKey string, models Models, target string, protocol ...string) (*Provider, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
idx := -1
|
||||
@@ -141,7 +141,11 @@ func (s *Store) UpdateProvider(id, name, preset, baseURL, apiKey string, models
|
||||
if target == "" {
|
||||
target = existing.Target
|
||||
}
|
||||
updated, err := newProvider(name, preset, baseURL, apiKey, models, target)
|
||||
proto := existing.Protocol
|
||||
if len(protocol) > 0 && strings.TrimSpace(protocol[0]) != "" {
|
||||
proto = strings.TrimSpace(protocol[0])
|
||||
}
|
||||
updated, err := newProvider(name, preset, baseURL, apiKey, models, target, proto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ import (
|
||||
|
||||
// providerInput is the provider payload accepted by the management API.
|
||||
type providerInput struct {
|
||||
Name string `json:"name"`
|
||||
Preset string `json:"preset"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Models config.Models `json:"models"`
|
||||
Target string `json:"target"`
|
||||
Name string `json:"name"`
|
||||
Preset string `json:"preset"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Models config.Models `json:"models"`
|
||||
Target string `json:"target"`
|
||||
Protocol string `json:"protocol"`
|
||||
APIFormat string `json:"api_format"`
|
||||
}
|
||||
|
||||
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -34,6 +36,7 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
"name": p.Name,
|
||||
"base_url": p.BaseURL,
|
||||
"models": p.Models,
|
||||
"protocol": p.Protocol,
|
||||
}
|
||||
}
|
||||
var activeCodex any
|
||||
@@ -43,6 +46,7 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
"name": p.Name,
|
||||
"base_url": p.BaseURL,
|
||||
"models": p.Models,
|
||||
"protocol": p.Protocol,
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
@@ -124,7 +128,11 @@ func (s *Server) handleCreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
p, err := s.store.AddProvider(in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target)
|
||||
proto := strings.TrimSpace(in.Protocol)
|
||||
if proto == "" {
|
||||
proto = strings.TrimSpace(in.APIFormat)
|
||||
}
|
||||
p, err := s.store.AddProvider(in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target, proto)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -139,7 +147,11 @@ func (s *Server) handleUpdateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
p, err := s.store.UpdateProvider(id, in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target)
|
||||
proto := strings.TrimSpace(in.Protocol)
|
||||
if proto == "" {
|
||||
proto = strings.TrimSpace(in.APIFormat)
|
||||
}
|
||||
p, err := s.store.UpdateProvider(id, in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target, proto)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
@@ -6,16 +6,18 @@ import "gitea.rutua.cn/kate522/CrossRelay/internal/config"
|
||||
// Preset is a ready-made provider template. Model names reflect each
|
||||
// vendor's catalog as of 2026-10 and can be edited per provider.
|
||||
type Preset struct {
|
||||
Key string `json:"key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Models config.Models `json:"models"`
|
||||
Key string `json:"key"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Models config.Models `json:"models"`
|
||||
}
|
||||
|
||||
// All lists the built-in presets.
|
||||
var All = []Preset{
|
||||
{
|
||||
Key: "deepseek",
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Key: "deepseek",
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Protocol: config.ProtocolOpenAIChat,
|
||||
Models: config.Models{
|
||||
Default: "deepseek-chat",
|
||||
Sonnet: "deepseek-chat",
|
||||
@@ -25,8 +27,9 @@ var All = []Preset{
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "moonshot",
|
||||
BaseURL: "https://api.moonshot.cn/v1",
|
||||
Key: "moonshot",
|
||||
BaseURL: "https://api.moonshot.cn/v1",
|
||||
Protocol: config.ProtocolOpenAIChat,
|
||||
Models: config.Models{
|
||||
Default: "kimi-k2-0905-preview",
|
||||
Sonnet: "kimi-k2-0905-preview",
|
||||
@@ -36,8 +39,9 @@ var All = []Preset{
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "zhipu",
|
||||
BaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
Key: "zhipu",
|
||||
BaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
Protocol: config.ProtocolOpenAIChat,
|
||||
Models: config.Models{
|
||||
Default: "glm-4.6",
|
||||
Sonnet: "glm-4.6",
|
||||
|
||||
Reference in new issue
Block a user