Add protocol selection to Claude and Codex provider forms referencing CCSwitch

This commit is contained in:
kate522 committed 2026-10-18 23:00:05 +08:00
1 parent bae24cc439
commit 16957decb2
14 files changed
+274 -25

No files matched your search

+28 -2
View File
@@ -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
+45
View File
@@ -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)
}
}
+8 -4
View File
@@ -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
}
+20 -8
View File
@@ -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
+13 -9
View File
@@ -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",