CrossRelay stage 2: Codex /v1/responses relay, config.toml manager, Codex page
This commit is contained in:
1 parent
404704171d
commit
5112f7ca06
30 files changed
+3539
-72
No files matched your search
@@ -48,6 +48,7 @@ type Provider struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Models Models `json:"models"`
|
||||
EnabledForClaude bool `json:"enabled_for_claude"`
|
||||
EnabledForCodex bool `json:"enabled_for_codex"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
@@ -63,8 +64,13 @@ type Config struct {
|
||||
RelayToken string `json:"relay_token"`
|
||||
Providers []Provider `json:"providers"`
|
||||
ActiveClaudeID string `json:"active_claude_id,omitempty"`
|
||||
Gateway Gateway `json:"gateway"`
|
||||
Language string `json:"language"`
|
||||
ActiveCodexID string `json:"active_codex_id,omitempty"`
|
||||
// CodexPriorProvider remembers the model_provider value that was in
|
||||
// config.toml before CrossRelay first applied, so restore can put it
|
||||
// back instead of dropping the key.
|
||||
CodexPriorProvider string `json:"codex_prior_provider,omitempty"`
|
||||
Gateway Gateway `json:"gateway"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
// ResolveModel maps the model name Claude Code asked for onto the model
|
||||
|
||||
@@ -128,6 +128,102 @@ func TestDeleteProviderClearsActive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateCodexRoundTrip(t *testing.T) {
|
||||
t.Setenv("CROSSRELAY_HOME", t.TempDir())
|
||||
store, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
p, err := store.AddProvider("DeepSeek", "deepseek", "https://api.deepseek.com/v1", "sk-test",
|
||||
Models{Default: "deepseek-chat"})
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
if _, err := store.ActivateCodex(p.ID); err != nil {
|
||||
t.Fatalf("ActivateCodex: %v", err)
|
||||
}
|
||||
if err := store.SetCodexPriorProvider("custom"); err != nil {
|
||||
t.Fatalf("SetCodexPriorProvider: %v", err)
|
||||
}
|
||||
|
||||
reloaded, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
active := reloaded.ActiveCodex()
|
||||
if active == nil || active.Name != "DeepSeek" {
|
||||
t.Fatalf("active codex provider after reload = %+v", active)
|
||||
}
|
||||
if !active.EnabledForCodex {
|
||||
t.Error("enabled_for_codex not persisted")
|
||||
}
|
||||
if reloaded.Config().CodexPriorProvider != "custom" {
|
||||
t.Errorf("prior provider = %q, want custom", reloaded.Config().CodexPriorProvider)
|
||||
}
|
||||
|
||||
if err := reloaded.ClearActiveCodex(); err != nil {
|
||||
t.Fatalf("ClearActiveCodex: %v", err)
|
||||
}
|
||||
if reloaded.ActiveCodex() != nil {
|
||||
t.Error("active codex provider survived clear")
|
||||
}
|
||||
if _, err := reloaded.ActivateCodex("p_missing"); err == nil {
|
||||
t.Error("ActivateCodex accepted an unknown id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProviderKeepsEnabledFlags(t *testing.T) {
|
||||
t.Setenv("CROSSRELAY_HOME", t.TempDir())
|
||||
store, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
p, err := store.AddProvider("DeepSeek", "deepseek", "https://api.deepseek.com/v1", "sk-test",
|
||||
Models{Default: "deepseek-chat"})
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
if _, err := store.ActivateClaude(p.ID); err != nil {
|
||||
t.Fatalf("ActivateClaude: %v", err)
|
||||
}
|
||||
if _, err := store.ActivateCodex(p.ID); err != nil {
|
||||
t.Fatalf("ActivateCodex: %v", err)
|
||||
}
|
||||
updated, err := store.UpdateProvider(p.ID, "DeepSeek 2", "deepseek", "https://api.deepseek.com/v1", "",
|
||||
Models{Default: "deepseek-reasoner"})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateProvider: %v", err)
|
||||
}
|
||||
if !updated.EnabledForClaude || !updated.EnabledForCodex {
|
||||
t.Errorf("enabled flags lost: claude=%v codex=%v", updated.EnabledForClaude, updated.EnabledForCodex)
|
||||
}
|
||||
if updated.APIKey != "sk-test" {
|
||||
t.Errorf("api key = %q, want the stored key kept", updated.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteProviderClearsCodexActive(t *testing.T) {
|
||||
t.Setenv("CROSSRELAY_HOME", t.TempDir())
|
||||
store, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
p, err := store.AddProvider("DeepSeek", "deepseek", "https://api.deepseek.com/v1", "sk-test",
|
||||
Models{Default: "deepseek-chat"})
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
if _, err := store.ActivateCodex(p.ID); err != nil {
|
||||
t.Fatalf("ActivateCodex: %v", err)
|
||||
}
|
||||
if err := store.DeleteProvider(p.ID); err != nil {
|
||||
t.Fatalf("DeleteProvider: %v", err)
|
||||
}
|
||||
if store.ActiveCodex() != nil {
|
||||
t.Error("deleted provider stayed active for codex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGatewayValidation(t *testing.T) {
|
||||
t.Setenv("CROSSRELAY_HOME", t.TempDir())
|
||||
store, _ := Load()
|
||||
|
||||
@@ -88,6 +88,21 @@ func (s *Store) ActiveClaude() *Provider {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveCodex returns the provider currently applied to Codex.
|
||||
func (s *Store) ActiveCodex() *Provider {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cfg.ActiveCodexID == "" {
|
||||
return nil
|
||||
}
|
||||
for i := range s.cfg.Providers {
|
||||
if s.cfg.Providers[i].ID == s.cfg.ActiveCodexID {
|
||||
return s.cfg.Providers[i].clone()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddProvider validates and appends a provider.
|
||||
func (s *Store) AddProvider(name, preset, baseURL, apiKey string, models Models) (*Provider, error) {
|
||||
s.mu.Lock()
|
||||
@@ -128,6 +143,7 @@ func (s *Store) UpdateProvider(id, name, preset, baseURL, apiKey string, models
|
||||
}
|
||||
updated.ID = existing.ID
|
||||
updated.EnabledForClaude = existing.EnabledForClaude
|
||||
updated.EnabledForCodex = existing.EnabledForCodex
|
||||
updated.CreatedAt = existing.CreatedAt
|
||||
s.cfg.Providers[idx] = *updated
|
||||
if err := s.saveLocked(); err != nil {
|
||||
@@ -154,6 +170,9 @@ func (s *Store) DeleteProvider(id string) error {
|
||||
if s.cfg.ActiveClaudeID == id {
|
||||
s.cfg.ActiveClaudeID = ""
|
||||
}
|
||||
if s.cfg.ActiveCodexID == id {
|
||||
s.cfg.ActiveCodexID = ""
|
||||
}
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
@@ -192,6 +211,50 @@ func (s *Store) ClearActiveClaude() error {
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
// ActivateCodex marks a provider as the one written into Codex's
|
||||
// config.toml and records it as active.
|
||||
func (s *Store) ActivateCodex(id string) (*Provider, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
idx := -1
|
||||
for i := range s.cfg.Providers {
|
||||
if s.cfg.Providers[i].ID == id {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return nil, fmt.Errorf("provider %q not found", id)
|
||||
}
|
||||
s.cfg.Providers[idx].EnabledForCodex = true
|
||||
s.cfg.ActiveCodexID = id
|
||||
if err := s.saveLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.cfg.Providers[idx].clone(), nil
|
||||
}
|
||||
|
||||
// ClearActiveCodex forgets the active Codex provider without touching
|
||||
// the provider list (used after restoring the official config).
|
||||
func (s *Store) ClearActiveCodex() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cfg.ActiveCodexID == "" {
|
||||
return nil
|
||||
}
|
||||
s.cfg.ActiveCodexID = ""
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
// SetCodexPriorProvider records (or clears) the model_provider value
|
||||
// that config.toml carried before CrossRelay took it over.
|
||||
func (s *Store) SetCodexPriorProvider(prior string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.cfg.CodexPriorProvider = strings.TrimSpace(prior)
|
||||
return s.saveLocked()
|
||||
}
|
||||
|
||||
// SetGateway updates listener settings.
|
||||
func (s *Store) SetGateway(g Gateway) error {
|
||||
g.PortMode = strings.TrimSpace(g.PortMode)
|
||||
|
||||
Reference in new issue
Block a user