From 8f9e6622b029164991c6f97b67562f940da327bd Mon Sep 17 00:00:00 2001 From: muzhi1991 <2101044+muzhi1991@users.noreply.github.com> Date: Thu, 16 Apr 2026 20:45:37 +0800 Subject: [PATCH 01/22] fix(util): forward custom Host header to upstream Custom headers configured under openai-compatibility (and any other provider passing through applyCustomHeaders) were silently dropped for the Host key, because Go's net/http reads the wire Host from req.Host, not req.Header["Host"]. As a result, virtual-host routed upstreams (e.g. LiteLLM behind an ingress) saw the base-url's host instead of the user-configured override and returned 404. Detect the Host key with http.CanonicalHeaderKey and assign it to req.Host so it is actually written on the wire. Other headers continue to use Header.Set as before. Fixes #2833 --- internal/util/header_helpers.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/util/header_helpers.go b/internal/util/header_helpers.go index c53c291f10..967903fce5 100644 --- a/internal/util/header_helpers.go +++ b/internal/util/header_helpers.go @@ -47,6 +47,14 @@ func applyCustomHeaders(r *http.Request, headers map[string]string) { if k == "" || v == "" { continue } + // Host is read from req.Host (not req.Header) by net/http when + // writing the request; setting it via Header.Set is silently + // dropped on the wire. Handle it explicitly so user-configured + // virtual-host overrides actually take effect upstream. + if http.CanonicalHeaderKey(k) == "Host" { + r.Host = v + continue + } r.Header.Set(k, v) } } From d9a3b3e5f33ca103f7d349b63a5b5bfea86234a0 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:32:07 +0800 Subject: [PATCH 02/22] fix(tests): update model lookup references and enhance Claude executor tests --- .../registry/model_registry_safety_test.go | 4 +- .../runtime/executor/claude_executor_test.go | 76 ++++++++++++++----- test/thinking_conversion_test.go | 1 - 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/internal/registry/model_registry_safety_test.go b/internal/registry/model_registry_safety_test.go index 5f4f65d298..be5bf7908c 100644 --- a/internal/registry/model_registry_safety_test.go +++ b/internal/registry/model_registry_safety_test.go @@ -136,13 +136,13 @@ func TestGetAvailableModelsReturnsClonedSupportedParameters(t *testing.T) { } func TestLookupModelInfoReturnsCloneForStaticDefinitions(t *testing.T) { - first := LookupModelInfo("glm-4.6") + first := LookupModelInfo("claude-sonnet-4-6") if first == nil || first.Thinking == nil || len(first.Thinking.Levels) == 0 { t.Fatalf("expected static model with thinking levels, got %+v", first) } first.Thinking.Levels[0] = "mutated" - second := LookupModelInfo("glm-4.6") + second := LookupModelInfo("claude-sonnet-4-6") if second == nil || second.Thinking == nil || len(second.Thinking.Levels) == 0 || second.Thinking.Levels[0] == "mutated" { t.Fatalf("expected static lookup clone, got %+v", second) } diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index f456064dc6..c1ce8fc088 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -1714,7 +1714,27 @@ func TestClaudeExecutor_ExecuteStream_AcceptEncodingOverrideCannotBypassIdentity } } -// Test case 1: String system prompt is preserved and converted to a content block +func expectedClaudeCodeStaticPrompt() string { + return strings.Join([]string{ + helps.ClaudeCodeIntro, + helps.ClaudeCodeSystem, + helps.ClaudeCodeDoingTasks, + helps.ClaudeCodeToneAndStyle, + helps.ClaudeCodeOutputEfficiency, + }, "\n\n") +} + +func expectedForwardedSystemReminder(text string) string { + return fmt.Sprintf(` +As you answer the user's questions, you can use the following context from the system: +%s + +IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task. + +`, text) +} + +// Test case 1: String system prompt is preserved by forwarding it to the first user message func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) { payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`) @@ -1733,42 +1753,52 @@ func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) { if !strings.HasPrefix(blocks[0].Get("text").String(), "x-anthropic-billing-header:") { t.Fatalf("blocks[0] should be billing header, got %q", blocks[0].Get("text").String()) } - if blocks[1].Get("text").String() != "You are a Claude agent, built on Anthropic's Claude Agent SDK." { + if blocks[1].Get("text").String() != "You are Claude Code, Anthropic's official CLI for Claude." { t.Fatalf("blocks[1] should be agent block, got %q", blocks[1].Get("text").String()) } - if blocks[2].Get("text").String() != "You are a helpful assistant." { - t.Fatalf("blocks[2] should be user system prompt, got %q", blocks[2].Get("text").String()) + if blocks[2].Get("text").String() != expectedClaudeCodeStaticPrompt() { + t.Fatalf("blocks[2] should be static Claude Code prompt, got %q", blocks[2].Get("text").String()) + } + if blocks[2].Get("cache_control").Exists() { + t.Fatalf("blocks[2] should not have cache_control, got %s", blocks[2].Get("cache_control").Raw) } - if blocks[2].Get("cache_control.type").String() != "ephemeral" { - t.Fatalf("blocks[2] should have cache_control.type=ephemeral") + + if got := gjson.GetBytes(out, "messages.0.content").String(); got != expectedForwardedSystemReminder("You are a helpful assistant.")+"hi" { + t.Fatalf("messages[0].content should include forwarded system prompt, got %q", got) } } -// Test case 2: Strict mode drops the string system prompt +// Test case 2: Strict mode keeps only the injected Claude Code system blocks func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) { payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`) out := checkSystemInstructionsWithMode(payload, true) blocks := gjson.GetBytes(out, "system").Array() - if len(blocks) != 2 { - t.Fatalf("strict mode should produce 2 blocks, got %d", len(blocks)) + if len(blocks) != 3 { + t.Fatalf("strict mode should produce 3 injected blocks, got %d", len(blocks)) + } + if got := gjson.GetBytes(out, "messages.0.content").String(); got != "hi" { + t.Fatalf("strict mode should not forward system prompt into messages, got %q", got) } } -// Test case 3: Empty string system prompt does not produce a spurious block +// Test case 3: Empty string system prompt does not alter the first user message func TestCheckSystemInstructionsWithMode_EmptyStringSystemIgnored(t *testing.T) { payload := []byte(`{"system":"","messages":[{"role":"user","content":"hi"}]}`) out := checkSystemInstructionsWithMode(payload, false) blocks := gjson.GetBytes(out, "system").Array() - if len(blocks) != 2 { - t.Fatalf("empty string system should produce 2 blocks, got %d", len(blocks)) + if len(blocks) != 3 { + t.Fatalf("empty string system should still produce 3 injected blocks, got %d", len(blocks)) + } + if got := gjson.GetBytes(out, "messages.0.content").String(); got != "hi" { + t.Fatalf("empty string system should not alter messages, got %q", got) } } -// Test case 4: Array system prompt is unaffected by the string handling +// Test case 4: Array system prompt is forwarded to the first user message func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) { payload := []byte(`{"system":[{"type":"text","text":"Be concise."}],"messages":[{"role":"user","content":"hi"}]}`) @@ -1778,12 +1808,15 @@ func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) { if len(blocks) != 3 { t.Fatalf("expected 3 system blocks, got %d", len(blocks)) } - if blocks[2].Get("text").String() != "Be concise." { - t.Fatalf("blocks[2] should be user system prompt, got %q", blocks[2].Get("text").String()) + if blocks[2].Get("text").String() != expectedClaudeCodeStaticPrompt() { + t.Fatalf("blocks[2] should be static Claude Code prompt, got %q", blocks[2].Get("text").String()) + } + if got := gjson.GetBytes(out, "messages.0.content").String(); got != expectedForwardedSystemReminder("Be concise.")+"hi" { + t.Fatalf("messages[0].content should include forwarded array system prompt, got %q", got) } } -// Test case 5: Special characters in string system prompt survive conversion +// Test case 5: Special characters in string system prompt survive forwarding func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) { payload := []byte(`{"system":"Use tags & \"quotes\" in output.","messages":[{"role":"user","content":"hi"}]}`) @@ -1793,8 +1826,8 @@ func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) { if len(blocks) != 3 { t.Fatalf("expected 3 system blocks, got %d", len(blocks)) } - if blocks[2].Get("text").String() != `Use tags & "quotes" in output.` { - t.Fatalf("blocks[2] text mangled, got %q", blocks[2].Get("text").String()) + if got := gjson.GetBytes(out, "messages.0.content").String(); got != expectedForwardedSystemReminder(`Use tags & "quotes" in output.`)+"hi" { + t.Fatalf("forwarded system prompt text mangled, got %q", got) } } @@ -1902,8 +1935,11 @@ func TestApplyCloaking_PreservesConfiguredStrictModeAndSensitiveWordsWhenModeOmi out := applyCloaking(context.Background(), cfg, auth, payload, "claude-3-5-sonnet-20241022", "key-123") blocks := gjson.GetBytes(out, "system").Array() - if len(blocks) != 2 { - t.Fatalf("expected strict mode to keep only injected system blocks, got %d", len(blocks)) + if len(blocks) != 3 { + t.Fatalf("expected strict mode to keep the 3 injected Claude Code system blocks, got %d", len(blocks)) + } + if got := gjson.GetBytes(out, "messages.0.content.#").Int(); got != 1 { + t.Fatalf("strict mode should not prepend a forwarded system reminder block, got %d content blocks", got) } if got := gjson.GetBytes(out, "messages.0.content.0.text").String(); !strings.Contains(got, "\u200B") { t.Fatalf("expected configured sensitive word obfuscation to apply, got %q", got) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index c6ade7b2a6..c76b1da101 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -2,7 +2,6 @@ package test import ( "fmt" - "strings" "testing" "time" From da43f63735f747266f8245e8aa2cc328c99c0fad Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:43:19 +0800 Subject: [PATCH 03/22] fix(tests): update Gemini family test case numbers for consistency --- test/thinking_conversion_test.go | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/thinking_conversion_test.go b/test/thinking_conversion_test.go index c76b1da101..51671a9c5f 100644 --- a/test/thinking_conversion_test.go +++ b/test/thinking_conversion_test.go @@ -1065,12 +1065,12 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { expectErr: false, }, - // Gemini Family Cross-Channel Consistency (Cases 106-114) + // Gemini Family Cross-Channel Consistency (Cases 90-95) // Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior - // Case 106: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max + // Case 90: Gemini to Antigravity, budget 64000 (suffix) → clamped to Max { - name: "106", + name: "90", from: "gemini", to: "antigravity", model: "gemini-budget-model(64000)", @@ -1080,9 +1080,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 107: Gemini to Gemini-CLI, budget 64000 (suffix) → clamped to Max + // Case 91: Gemini to Gemini-CLI, budget 64000 (suffix) → clamped to Max { - name: "107", + name: "91", from: "gemini", to: "gemini-cli", model: "gemini-budget-model(64000)", @@ -1092,9 +1092,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 108: Gemini-CLI to Antigravity, budget 64000 (suffix) → clamped to Max + // Case 92: Gemini-CLI to Antigravity, budget 64000 (suffix) → clamped to Max { - name: "108", + name: "92", from: "gemini-cli", to: "antigravity", model: "gemini-budget-model(64000)", @@ -1104,9 +1104,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 109: Gemini-CLI to Gemini, budget 64000 (suffix) → clamped to Max + // Case 93: Gemini-CLI to Gemini, budget 64000 (suffix) → clamped to Max { - name: "109", + name: "93", from: "gemini-cli", to: "gemini", model: "gemini-budget-model(64000)", @@ -1116,9 +1116,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 110: Gemini to Antigravity, budget 8192 → passthrough (normal value) + // Case 94: Gemini to Antigravity, budget 8192 → passthrough (normal value) { - name: "110", + name: "94", from: "gemini", to: "antigravity", model: "gemini-budget-model(8192)", @@ -1128,9 +1128,9 @@ func TestThinkingE2EMatrix_Suffix(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 111: Gemini-CLI to Antigravity, budget 8192 → passthrough (normal value) + // Case 95: Gemini-CLI to Antigravity, budget 8192 → passthrough (normal value) { - name: "111", + name: "95", from: "gemini-cli", to: "antigravity", model: "gemini-budget-model(8192)", @@ -2166,12 +2166,12 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectErr: true, }, - // Gemini Family Cross-Channel Consistency (Cases 106-114) + // Gemini Family Cross-Channel Consistency (Cases 90-95) // Tests that gemini/gemini-cli/antigravity as same API family should have consistent validation behavior - // Case 106: Gemini to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation) + // Case 90: Gemini to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation) { - name: "106", + name: "90", from: "gemini", to: "antigravity", model: "gemini-budget-model", @@ -2179,9 +2179,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: true, }, - // Case 107: Gemini to Gemini-CLI, thinkingBudget=64000 → exceeds Max error (same family strict validation) + // Case 91: Gemini to Gemini-CLI, thinkingBudget=64000 → exceeds Max error (same family strict validation) { - name: "107", + name: "91", from: "gemini", to: "gemini-cli", model: "gemini-budget-model", @@ -2189,9 +2189,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: true, }, - // Case 108: Gemini-CLI to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation) + // Case 92: Gemini-CLI to Antigravity, thinkingBudget=64000 → exceeds Max error (same family strict validation) { - name: "108", + name: "92", from: "gemini-cli", to: "antigravity", model: "gemini-budget-model", @@ -2199,9 +2199,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: true, }, - // Case 109: Gemini-CLI to Gemini, thinkingBudget=64000 → exceeds Max error (same family strict validation) + // Case 93: Gemini-CLI to Gemini, thinkingBudget=64000 → exceeds Max error (same family strict validation) { - name: "109", + name: "93", from: "gemini-cli", to: "gemini", model: "gemini-budget-model", @@ -2209,9 +2209,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { expectField: "", expectErr: true, }, - // Case 110: Gemini to Antigravity, thinkingBudget=8192 → passthrough (normal value) + // Case 94: Gemini to Antigravity, thinkingBudget=8192 → passthrough (normal value) { - name: "110", + name: "94", from: "gemini", to: "antigravity", model: "gemini-budget-model", @@ -2221,9 +2221,9 @@ func TestThinkingE2EMatrix_Body(t *testing.T) { includeThoughts: "true", expectErr: false, }, - // Case 111: Gemini-CLI to Antigravity, thinkingBudget=8192 → passthrough (normal value) + // Case 95: Gemini-CLI to Antigravity, thinkingBudget=8192 → passthrough (normal value) { - name: "111", + name: "95", from: "gemini-cli", to: "antigravity", model: "gemini-budget-model", From eba561bf6f08916a966cef698a5c53c7e273b375 Mon Sep 17 00:00:00 2001 From: muzhi1991 <2101044+muzhi1991@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:28:59 +0800 Subject: [PATCH 04/22] fix(util): also keep Host in header map for synthetic requests Addressing the P1 note from the Codex reviewer: applyCustomHeaders is also called with a synthetic &http.Request{Header: ...} from the websockets executors (aistudio_executor.go, codex_websockets_executor.go), which forward only the header map. The previous continue meant a custom Host was dropped from that map, regressing virtual-host overrides on those flows. Mirror the value to both r.Host (for real net/http) and r.Header (for header-map-only consumers). --- internal/util/header_helpers.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/util/header_helpers.go b/internal/util/header_helpers.go index 967903fce5..0b8d72bcb4 100644 --- a/internal/util/header_helpers.go +++ b/internal/util/header_helpers.go @@ -47,13 +47,13 @@ func applyCustomHeaders(r *http.Request, headers map[string]string) { if k == "" || v == "" { continue } - // Host is read from req.Host (not req.Header) by net/http when - // writing the request; setting it via Header.Set is silently - // dropped on the wire. Handle it explicitly so user-configured - // virtual-host overrides actually take effect upstream. + // net/http reads Host from req.Host (not req.Header) when writing + // a real request, so we must mirror it there. Some callers pass + // synthetic requests (e.g. &http.Request{Header: ...}) and only + // consume r.Header afterwards, so keep the value in the header + // map too. if http.CanonicalHeaderKey(k) == "Host" { r.Host = v - continue } r.Header.Set(k, v) } From 894baad829f5f5a53411edc0d1af4f1af9f9d60d Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 18 Apr 2026 16:44:33 +0800 Subject: [PATCH 05/22] feat(api): integrate auth index into key retrieval endpoints for Gemini, Claude, Codex, OpenAI, and Vertex --- .../handlers/management/config_auth_index.go | 245 ++++++++++++++++++ .../api/handlers/management/config_lists.go | 10 +- 2 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 internal/api/handlers/management/config_auth_index.go diff --git a/internal/api/handlers/management/config_auth_index.go b/internal/api/handlers/management/config_auth_index.go new file mode 100644 index 0000000000..51f71aacf9 --- /dev/null +++ b/internal/api/handlers/management/config_auth_index.go @@ -0,0 +1,245 @@ +package management + +import ( + "strings" + "time" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" +) + +type configAuthIndexViews struct { + gemini []string + claude []string + codex []string + vertex []string + openAIEntries [][]string + openAIFallback []string +} + +type geminiKeyWithAuthIndex struct { + config.GeminiKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type claudeKeyWithAuthIndex struct { + config.ClaudeKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type codexKeyWithAuthIndex struct { + config.CodexKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type vertexCompatKeyWithAuthIndex struct { + config.VertexCompatKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type openAICompatibilityAPIKeyWithAuthIndex struct { + config.OpenAICompatibilityAPIKey + AuthIndex string `json:"auth-index,omitempty"` +} + +type openAICompatibilityWithAuthIndex struct { + Name string `json:"name"` + Priority int `json:"priority,omitempty"` + Prefix string `json:"prefix,omitempty"` + BaseURL string `json:"base-url"` + APIKeyEntries []openAICompatibilityAPIKeyWithAuthIndex `json:"api-key-entries,omitempty"` + Models []config.OpenAICompatibilityModel `json:"models,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + AuthIndex string `json:"auth-index,omitempty"` +} + +func (h *Handler) buildConfigAuthIndexViews() configAuthIndexViews { + cfg := h.cfg + if cfg == nil { + return configAuthIndexViews{} + } + + liveIndexByID := map[string]string{} + if h != nil && h.authManager != nil { + for _, auth := range h.authManager.List() { + if auth == nil || strings.TrimSpace(auth.ID) == "" { + continue + } + auth.EnsureIndex() + if auth.Index == "" { + continue + } + liveIndexByID[auth.ID] = auth.Index + } + } + + views := configAuthIndexViews{ + gemini: make([]string, len(cfg.GeminiKey)), + claude: make([]string, len(cfg.ClaudeKey)), + codex: make([]string, len(cfg.CodexKey)), + vertex: make([]string, len(cfg.VertexCompatAPIKey)), + openAIEntries: make([][]string, len(cfg.OpenAICompatibility)), + openAIFallback: make([]string, len(cfg.OpenAICompatibility)), + } + + auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{ + Config: cfg, + Now: time.Now(), + IDGenerator: synthesizer.NewStableIDGenerator(), + }) + if errSynthesize != nil { + return views + } + + cursor := 0 + nextAuthIndex := func() string { + if cursor >= len(auths) { + return "" + } + auth := auths[cursor] + cursor++ + if auth == nil || strings.TrimSpace(auth.ID) == "" { + return "" + } + // Do not expose an auth-index until it is present in the live auth manager. + // API tools resolve auth_index against h.authManager.List(), so returning + // config-only indexes can temporarily break tool calls around config edits. + return liveIndexByID[auth.ID] + } + + for i := range cfg.GeminiKey { + if strings.TrimSpace(cfg.GeminiKey[i].APIKey) == "" { + continue + } + views.gemini[i] = nextAuthIndex() + } + for i := range cfg.ClaudeKey { + if strings.TrimSpace(cfg.ClaudeKey[i].APIKey) == "" { + continue + } + views.claude[i] = nextAuthIndex() + } + for i := range cfg.CodexKey { + if strings.TrimSpace(cfg.CodexKey[i].APIKey) == "" { + continue + } + views.codex[i] = nextAuthIndex() + } + for i := range cfg.OpenAICompatibility { + entries := cfg.OpenAICompatibility[i].APIKeyEntries + if len(entries) == 0 { + views.openAIFallback[i] = nextAuthIndex() + continue + } + + views.openAIEntries[i] = make([]string, len(entries)) + for j := range entries { + views.openAIEntries[i][j] = nextAuthIndex() + } + } + for i := range cfg.VertexCompatAPIKey { + if strings.TrimSpace(cfg.VertexCompatAPIKey[i].APIKey) == "" { + continue + } + views.vertex[i] = nextAuthIndex() + } + + return views +} + +func (h *Handler) geminiKeysWithAuthIndex() []geminiKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]geminiKeyWithAuthIndex, len(h.cfg.GeminiKey)) + for i := range h.cfg.GeminiKey { + out[i] = geminiKeyWithAuthIndex{ + GeminiKey: h.cfg.GeminiKey[i], + AuthIndex: views.gemini[i], + } + } + return out +} + +func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]claudeKeyWithAuthIndex, len(h.cfg.ClaudeKey)) + for i := range h.cfg.ClaudeKey { + out[i] = claudeKeyWithAuthIndex{ + ClaudeKey: h.cfg.ClaudeKey[i], + AuthIndex: views.claude[i], + } + } + return out +} + +func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]codexKeyWithAuthIndex, len(h.cfg.CodexKey)) + for i := range h.cfg.CodexKey { + out[i] = codexKeyWithAuthIndex{ + CodexKey: h.cfg.CodexKey[i], + AuthIndex: views.codex[i], + } + } + return out +} + +func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + views := h.buildConfigAuthIndexViews() + out := make([]vertexCompatKeyWithAuthIndex, len(h.cfg.VertexCompatAPIKey)) + for i := range h.cfg.VertexCompatAPIKey { + out[i] = vertexCompatKeyWithAuthIndex{ + VertexCompatKey: h.cfg.VertexCompatAPIKey[i], + AuthIndex: views.vertex[i], + } + } + return out +} + +func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAuthIndex { + if h == nil || h.cfg == nil { + return nil + } + + views := h.buildConfigAuthIndexViews() + normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility) + out := make([]openAICompatibilityWithAuthIndex, len(normalized)) + for i := range normalized { + entry := normalized[i] + response := openAICompatibilityWithAuthIndex{ + Name: entry.Name, + Priority: entry.Priority, + Prefix: entry.Prefix, + BaseURL: entry.BaseURL, + Models: entry.Models, + Headers: entry.Headers, + AuthIndex: views.openAIFallback[i], + } + if len(entry.APIKeyEntries) > 0 { + response.APIKeyEntries = make([]openAICompatibilityAPIKeyWithAuthIndex, len(entry.APIKeyEntries)) + for j := range entry.APIKeyEntries { + authIndex := "" + if i < len(views.openAIEntries) && j < len(views.openAIEntries[i]) { + authIndex = views.openAIEntries[i][j] + } + response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{ + OpenAICompatibilityAPIKey: entry.APIKeyEntries[j], + AuthIndex: authIndex, + } + } + } + out[i] = response + } + return out +} diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index fbaad956e0..8d3841335a 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -120,7 +120,7 @@ func (h *Handler) DeleteAPIKeys(c *gin.Context) { // gemini-api-key: []GeminiKey func (h *Handler) GetGeminiKeys(c *gin.Context) { - c.JSON(200, gin.H{"gemini-api-key": h.cfg.GeminiKey}) + c.JSON(200, gin.H{"gemini-api-key": h.geminiKeysWithAuthIndex()}) } func (h *Handler) PutGeminiKeys(c *gin.Context) { data, err := c.GetRawData() @@ -270,7 +270,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) { // claude-api-key: []ClaudeKey func (h *Handler) GetClaudeKeys(c *gin.Context) { - c.JSON(200, gin.H{"claude-api-key": h.cfg.ClaudeKey}) + c.JSON(200, gin.H{"claude-api-key": h.claudeKeysWithAuthIndex()}) } func (h *Handler) PutClaudeKeys(c *gin.Context) { data, err := c.GetRawData() @@ -414,7 +414,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { // openai-compatibility: []OpenAICompatibility func (h *Handler) GetOpenAICompat(c *gin.Context) { - c.JSON(200, gin.H{"openai-compatibility": normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility)}) + c.JSON(200, gin.H{"openai-compatibility": h.openAICompatibilityWithAuthIndex()}) } func (h *Handler) PutOpenAICompat(c *gin.Context) { data, err := c.GetRawData() @@ -540,7 +540,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) { // vertex-api-key: []VertexCompatKey func (h *Handler) GetVertexCompatKeys(c *gin.Context) { - c.JSON(200, gin.H{"vertex-api-key": h.cfg.VertexCompatAPIKey}) + c.JSON(200, gin.H{"vertex-api-key": h.vertexCompatKeysWithAuthIndex()}) } func (h *Handler) PutVertexCompatKeys(c *gin.Context) { data, err := c.GetRawData() @@ -886,7 +886,7 @@ func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) { // codex-api-key: []CodexKey func (h *Handler) GetCodexKeys(c *gin.Context) { - c.JSON(200, gin.H{"codex-api-key": h.cfg.CodexKey}) + c.JSON(200, gin.H{"codex-api-key": h.codexKeysWithAuthIndex()}) } func (h *Handler) PutCodexKeys(c *gin.Context) { data, err := c.GetRawData() From c26936e2e61778ed6be40282c8577428c96d8aa4 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 18 Apr 2026 17:12:14 +0800 Subject: [PATCH 06/22] fix(management): stabilize auth-index mapping --- .../handlers/management/config_auth_index.go | 232 ++++++++-------- .../management/config_auth_index_test.go | 250 ++++++++++++++++++ .../api/handlers/management/config_lists.go | 93 +++++-- internal/api/handlers/management/handler.go | 24 +- 4 files changed, 450 insertions(+), 149 deletions(-) create mode 100644 internal/api/handlers/management/config_auth_index_test.go diff --git a/internal/api/handlers/management/config_auth_index.go b/internal/api/handlers/management/config_auth_index.go index 51f71aacf9..ed0b3ec42d 100644 --- a/internal/api/handlers/management/config_auth_index.go +++ b/internal/api/handlers/management/config_auth_index.go @@ -1,22 +1,13 @@ package management import ( + "fmt" "strings" - "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" ) -type configAuthIndexViews struct { - gemini []string - claude []string - codex []string - vertex []string - openAIEntries [][]string - openAIFallback []string -} - type geminiKeyWithAuthIndex struct { config.GeminiKey AuthIndex string `json:"auth-index,omitempty"` @@ -53,170 +44,174 @@ type openAICompatibilityWithAuthIndex struct { AuthIndex string `json:"auth-index,omitempty"` } -func (h *Handler) buildConfigAuthIndexViews() configAuthIndexViews { - cfg := h.cfg - if cfg == nil { - return configAuthIndexViews{} - } - - liveIndexByID := map[string]string{} - if h != nil && h.authManager != nil { - for _, auth := range h.authManager.List() { - if auth == nil || strings.TrimSpace(auth.ID) == "" { - continue - } - auth.EnsureIndex() - if auth.Index == "" { - continue - } - liveIndexByID[auth.ID] = auth.Index - } - } - - views := configAuthIndexViews{ - gemini: make([]string, len(cfg.GeminiKey)), - claude: make([]string, len(cfg.ClaudeKey)), - codex: make([]string, len(cfg.CodexKey)), - vertex: make([]string, len(cfg.VertexCompatAPIKey)), - openAIEntries: make([][]string, len(cfg.OpenAICompatibility)), - openAIFallback: make([]string, len(cfg.OpenAICompatibility)), - } - - auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{ - Config: cfg, - Now: time.Now(), - IDGenerator: synthesizer.NewStableIDGenerator(), - }) - if errSynthesize != nil { - return views - } - - cursor := 0 - nextAuthIndex := func() string { - if cursor >= len(auths) { - return "" - } - auth := auths[cursor] - cursor++ - if auth == nil || strings.TrimSpace(auth.ID) == "" { - return "" - } - // Do not expose an auth-index until it is present in the live auth manager. - // API tools resolve auth_index against h.authManager.List(), so returning - // config-only indexes can temporarily break tool calls around config edits. - return liveIndexByID[auth.ID] +func (h *Handler) liveAuthIndexByID() map[string]string { + out := map[string]string{} + if h == nil { + return out } - - for i := range cfg.GeminiKey { - if strings.TrimSpace(cfg.GeminiKey[i].APIKey) == "" { - continue - } - views.gemini[i] = nextAuthIndex() + h.mu.Lock() + manager := h.authManager + h.mu.Unlock() + if manager == nil { + return out } - for i := range cfg.ClaudeKey { - if strings.TrimSpace(cfg.ClaudeKey[i].APIKey) == "" { + // authManager.List() returns clones, so EnsureIndex only affects these copies. + for _, auth := range manager.List() { + if auth == nil { continue } - views.claude[i] = nextAuthIndex() - } - for i := range cfg.CodexKey { - if strings.TrimSpace(cfg.CodexKey[i].APIKey) == "" { + id := strings.TrimSpace(auth.ID) + if id == "" { continue } - views.codex[i] = nextAuthIndex() - } - for i := range cfg.OpenAICompatibility { - entries := cfg.OpenAICompatibility[i].APIKeyEntries - if len(entries) == 0 { - views.openAIFallback[i] = nextAuthIndex() - continue - } - - views.openAIEntries[i] = make([]string, len(entries)) - for j := range entries { - views.openAIEntries[i][j] = nextAuthIndex() + idx := strings.TrimSpace(auth.Index) + if idx == "" { + idx = auth.EnsureIndex() } - } - for i := range cfg.VertexCompatAPIKey { - if strings.TrimSpace(cfg.VertexCompatAPIKey[i].APIKey) == "" { + if idx == "" { continue } - views.vertex[i] = nextAuthIndex() + out[id] = idx } - - return views + return out } func (h *Handler) geminiKeysWithAuthIndex() []geminiKeyWithAuthIndex { - if h == nil || h.cfg == nil { + if h == nil { return nil } - views := h.buildConfigAuthIndexViews() + liveIndexByID := h.liveAuthIndexByID() + + h.mu.Lock() + defer h.mu.Unlock() + if h.cfg == nil { + return nil + } + + idGen := synthesizer.NewStableIDGenerator() out := make([]geminiKeyWithAuthIndex, len(h.cfg.GeminiKey)) for i := range h.cfg.GeminiKey { + entry := h.cfg.GeminiKey[i] + authIndex := "" + if key := strings.TrimSpace(entry.APIKey); key != "" { + id, _ := idGen.Next("gemini:apikey", key, entry.BaseURL) + authIndex = liveIndexByID[id] + } out[i] = geminiKeyWithAuthIndex{ - GeminiKey: h.cfg.GeminiKey[i], - AuthIndex: views.gemini[i], + GeminiKey: entry, + AuthIndex: authIndex, } } return out } func (h *Handler) claudeKeysWithAuthIndex() []claudeKeyWithAuthIndex { - if h == nil || h.cfg == nil { + if h == nil { return nil } - views := h.buildConfigAuthIndexViews() + liveIndexByID := h.liveAuthIndexByID() + + h.mu.Lock() + defer h.mu.Unlock() + if h.cfg == nil { + return nil + } + + idGen := synthesizer.NewStableIDGenerator() out := make([]claudeKeyWithAuthIndex, len(h.cfg.ClaudeKey)) for i := range h.cfg.ClaudeKey { + entry := h.cfg.ClaudeKey[i] + authIndex := "" + if key := strings.TrimSpace(entry.APIKey); key != "" { + id, _ := idGen.Next("claude:apikey", key, entry.BaseURL) + authIndex = liveIndexByID[id] + } out[i] = claudeKeyWithAuthIndex{ - ClaudeKey: h.cfg.ClaudeKey[i], - AuthIndex: views.claude[i], + ClaudeKey: entry, + AuthIndex: authIndex, } } return out } func (h *Handler) codexKeysWithAuthIndex() []codexKeyWithAuthIndex { - if h == nil || h.cfg == nil { + if h == nil { + return nil + } + liveIndexByID := h.liveAuthIndexByID() + + h.mu.Lock() + defer h.mu.Unlock() + if h.cfg == nil { return nil } - views := h.buildConfigAuthIndexViews() + + idGen := synthesizer.NewStableIDGenerator() out := make([]codexKeyWithAuthIndex, len(h.cfg.CodexKey)) for i := range h.cfg.CodexKey { + entry := h.cfg.CodexKey[i] + authIndex := "" + if key := strings.TrimSpace(entry.APIKey); key != "" { + id, _ := idGen.Next("codex:apikey", key, entry.BaseURL) + authIndex = liveIndexByID[id] + } out[i] = codexKeyWithAuthIndex{ - CodexKey: h.cfg.CodexKey[i], - AuthIndex: views.codex[i], + CodexKey: entry, + AuthIndex: authIndex, } } return out } func (h *Handler) vertexCompatKeysWithAuthIndex() []vertexCompatKeyWithAuthIndex { - if h == nil || h.cfg == nil { + if h == nil { + return nil + } + liveIndexByID := h.liveAuthIndexByID() + + h.mu.Lock() + defer h.mu.Unlock() + if h.cfg == nil { return nil } - views := h.buildConfigAuthIndexViews() + + idGen := synthesizer.NewStableIDGenerator() out := make([]vertexCompatKeyWithAuthIndex, len(h.cfg.VertexCompatAPIKey)) for i := range h.cfg.VertexCompatAPIKey { + entry := h.cfg.VertexCompatAPIKey[i] + id, _ := idGen.Next("vertex:apikey", entry.APIKey, entry.BaseURL, entry.ProxyURL) + authIndex := liveIndexByID[id] out[i] = vertexCompatKeyWithAuthIndex{ - VertexCompatKey: h.cfg.VertexCompatAPIKey[i], - AuthIndex: views.vertex[i], + VertexCompatKey: entry, + AuthIndex: authIndex, } } return out } func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAuthIndex { - if h == nil || h.cfg == nil { + if h == nil { + return nil + } + liveIndexByID := h.liveAuthIndexByID() + + h.mu.Lock() + defer h.mu.Unlock() + if h.cfg == nil { return nil } - views := h.buildConfigAuthIndexViews() normalized := normalizedOpenAICompatibilityEntries(h.cfg.OpenAICompatibility) out := make([]openAICompatibilityWithAuthIndex, len(normalized)) + idGen := synthesizer.NewStableIDGenerator() for i := range normalized { entry := normalized[i] + providerName := strings.ToLower(strings.TrimSpace(entry.Name)) + if providerName == "" { + providerName = "openai-compatibility" + } + idKind := fmt.Sprintf("openai-compatibility:%s", providerName) + response := openAICompatibilityWithAuthIndex{ Name: entry.Name, Priority: entry.Priority, @@ -224,18 +219,19 @@ func (h *Handler) openAICompatibilityWithAuthIndex() []openAICompatibilityWithAu BaseURL: entry.BaseURL, Models: entry.Models, Headers: entry.Headers, - AuthIndex: views.openAIFallback[i], + AuthIndex: "", } - if len(entry.APIKeyEntries) > 0 { + if len(entry.APIKeyEntries) == 0 { + id, _ := idGen.Next(idKind, entry.BaseURL) + response.AuthIndex = liveIndexByID[id] + } else { response.APIKeyEntries = make([]openAICompatibilityAPIKeyWithAuthIndex, len(entry.APIKeyEntries)) for j := range entry.APIKeyEntries { - authIndex := "" - if i < len(views.openAIEntries) && j < len(views.openAIEntries[i]) { - authIndex = views.openAIEntries[i][j] - } + apiKeyEntry := entry.APIKeyEntries[j] + id, _ := idGen.Next(idKind, apiKeyEntry.APIKey, entry.BaseURL, apiKeyEntry.ProxyURL) response.APIKeyEntries[j] = openAICompatibilityAPIKeyWithAuthIndex{ - OpenAICompatibilityAPIKey: entry.APIKeyEntries[j], - AuthIndex: authIndex, + OpenAICompatibilityAPIKey: apiKeyEntry, + AuthIndex: liveIndexByID[id], } } } diff --git a/internal/api/handlers/management/config_auth_index_test.go b/internal/api/handlers/management/config_auth_index_test.go new file mode 100644 index 0000000000..b7c9809011 --- /dev/null +++ b/internal/api/handlers/management/config_auth_index_test.go @@ -0,0 +1,250 @@ +package management + +import ( + "context" + "testing" + "time" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" +) + +func synthesizeConfigAuths(t *testing.T, cfg *config.Config) []*coreauth.Auth { + t.Helper() + + auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{ + Config: cfg, + Now: time.Unix(0, 0), + IDGenerator: synthesizer.NewStableIDGenerator(), + }) + if errSynthesize != nil { + t.Fatalf("synthesize config auths: %v", errSynthesize) + } + return auths +} + +func findAuth(t *testing.T, auths []*coreauth.Auth, predicate func(*coreauth.Auth) bool) *coreauth.Auth { + t.Helper() + for _, auth := range auths { + if predicate(auth) { + return auth + } + } + return nil +} + +func TestConfigAuthIndexResolvesLiveIndexes(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + GeminiKey: []config.GeminiKey{ + {APIKey: "shared-key", BaseURL: "https://a.example.com"}, + {APIKey: "shared-key", BaseURL: "https://b.example.com"}, + }, + ClaudeKey: []config.ClaudeKey{ + {APIKey: "claude-key", BaseURL: "https://claude.example.com"}, + }, + CodexKey: []config.CodexKey{ + {APIKey: "codex-key", BaseURL: "https://codex.example.com/v1"}, + }, + VertexCompatAPIKey: []config.VertexCompatKey{ + {APIKey: "vertex-key", BaseURL: "https://vertex.example.com", ProxyURL: "http://proxy.example.com:8080"}, + }, + OpenAICompatibility: []config.OpenAICompatibility{ + { + Name: "bohe", + BaseURL: "https://bohe.example.com/v1", + APIKeyEntries: []config.OpenAICompatibilityAPIKey{ + {APIKey: "compat-key"}, + }, + }, + }, + } + + auths := synthesizeConfigAuths(t, cfg) + manager := coreauth.NewManager(nil, nil, nil) + for _, auth := range auths { + if auth == nil { + continue + } + if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { + t.Fatalf("register auth %q: %v", auth.ID, errRegister) + } + } + + h := &Handler{cfg: cfg, authManager: manager} + + geminiAuthA := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://a.example.com" + }) + if geminiAuthA == nil { + t.Fatal("expected synthesized gemini auth (base a)") + } + geminiAuthB := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://b.example.com" + }) + if geminiAuthB == nil { + t.Fatal("expected synthesized gemini auth (base b)") + } + + gemini := h.geminiKeysWithAuthIndex() + if len(gemini) != 2 { + t.Fatalf("gemini keys = %d, want 2", len(gemini)) + } + if got, want := gemini[0].AuthIndex, geminiAuthA.EnsureIndex(); got != want { + t.Fatalf("gemini[0] auth-index = %q, want %q", got, want) + } + if got, want := gemini[1].AuthIndex, geminiAuthB.EnsureIndex(); got != want { + t.Fatalf("gemini[1] auth-index = %q, want %q", got, want) + } + if gemini[0].AuthIndex == gemini[1].AuthIndex { + t.Fatalf("duplicate gemini entries returned the same auth-index %q", gemini[0].AuthIndex) + } + + claudeAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "claude" && auth.Attributes["api_key"] == "claude-key" + }) + if claudeAuth == nil { + t.Fatal("expected synthesized claude auth") + } + + claude := h.claudeKeysWithAuthIndex() + if len(claude) != 1 { + t.Fatalf("claude keys = %d, want 1", len(claude)) + } + if got, want := claude[0].AuthIndex, claudeAuth.EnsureIndex(); got != want { + t.Fatalf("claude auth-index = %q, want %q", got, want) + } + + codexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "codex" && auth.Attributes["api_key"] == "codex-key" + }) + if codexAuth == nil { + t.Fatal("expected synthesized codex auth") + } + + codex := h.codexKeysWithAuthIndex() + if len(codex) != 1 { + t.Fatalf("codex keys = %d, want 1", len(codex)) + } + if got, want := codex[0].AuthIndex, codexAuth.EnsureIndex(); got != want { + t.Fatalf("codex auth-index = %q, want %q", got, want) + } + + vertexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "vertex" && auth.Attributes["api_key"] == "vertex-key" + }) + if vertexAuth == nil { + t.Fatal("expected synthesized vertex auth") + } + + vertex := h.vertexCompatKeysWithAuthIndex() + if len(vertex) != 1 { + t.Fatalf("vertex keys = %d, want 1", len(vertex)) + } + if got, want := vertex[0].AuthIndex, vertexAuth.EnsureIndex(); got != want { + t.Fatalf("vertex auth-index = %q, want %q", got, want) + } + + compatAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + if auth.Provider != "bohe" { + return false + } + if auth.Attributes["provider_key"] != "bohe" || auth.Attributes["compat_name"] != "bohe" { + return false + } + return auth.Attributes["api_key"] == "compat-key" + }) + if compatAuth == nil { + t.Fatal("expected synthesized openai-compat auth") + } + + compat := h.openAICompatibilityWithAuthIndex() + if len(compat) != 1 { + t.Fatalf("openai-compat providers = %d, want 1", len(compat)) + } + if len(compat[0].APIKeyEntries) != 1 { + t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries)) + } + if compat[0].AuthIndex != "" { + t.Fatalf("provider-level auth-index should be empty when api-key-entries exist, got %q", compat[0].AuthIndex) + } + if got, want := compat[0].APIKeyEntries[0].AuthIndex, compatAuth.EnsureIndex(); got != want { + t.Fatalf("openai-compat auth-index = %q, want %q", got, want) + } +} + +func TestConfigAuthIndexOmitsIndexesNotInManager(t *testing.T) { + t.Parallel() + + cfg := &config.Config{ + GeminiKey: []config.GeminiKey{ + {APIKey: "gemini-key", BaseURL: "https://a.example.com"}, + }, + OpenAICompatibility: []config.OpenAICompatibility{ + { + Name: "bohe", + BaseURL: "https://bohe.example.com/v1", + APIKeyEntries: []config.OpenAICompatibilityAPIKey{ + {APIKey: "compat-key"}, + }, + }, + }, + } + + auths := synthesizeConfigAuths(t, cfg) + geminiAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { + if auth == nil { + return false + } + return auth.Provider == "gemini" && auth.Attributes["api_key"] == "gemini-key" + }) + if geminiAuth == nil { + t.Fatal("expected synthesized gemini auth") + } + + manager := coreauth.NewManager(nil, nil, nil) + if _, errRegister := manager.Register(context.Background(), geminiAuth); errRegister != nil { + t.Fatalf("register gemini auth: %v", errRegister) + } + + h := &Handler{cfg: cfg, authManager: manager} + + gemini := h.geminiKeysWithAuthIndex() + if len(gemini) != 1 { + t.Fatalf("gemini keys = %d, want 1", len(gemini)) + } + if gemini[0].AuthIndex == "" { + t.Fatal("expected gemini auth-index to be set") + } + + compat := h.openAICompatibilityWithAuthIndex() + if len(compat) != 1 { + t.Fatalf("openai-compat providers = %d, want 1", len(compat)) + } + if len(compat[0].APIKeyEntries) != 1 { + t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries)) + } + if compat[0].APIKeyEntries[0].AuthIndex != "" { + t.Fatalf("openai-compat auth-index = %q, want empty", compat[0].APIKeyEntries[0].AuthIndex) + } +} diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 8d3841335a..ee3a4714b8 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -139,9 +139,11 @@ func (h *Handler) PutGeminiKeys(c *gin.Context) { } arr = obj.Items } + h.mu.Lock() + defer h.mu.Unlock() h.cfg.GeminiKey = append([]config.GeminiKey(nil), arr...) h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) PatchGeminiKey(c *gin.Context) { type geminiKeyPatch struct { @@ -161,6 +163,9 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + + h.mu.Lock() + defer h.mu.Unlock() targetIndex := -1 if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) { targetIndex = *body.Index @@ -187,7 +192,7 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) { if trimmed == "" { h.cfg.GeminiKey = append(h.cfg.GeminiKey[:targetIndex], h.cfg.GeminiKey[targetIndex+1:]...) h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) return } entry.APIKey = trimmed @@ -209,10 +214,12 @@ func (h *Handler) PatchGeminiKey(c *gin.Context) { } h.cfg.GeminiKey[targetIndex] = entry h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) DeleteGeminiKey(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() if val := strings.TrimSpace(c.Query("api-key")); val != "" { if baseRaw, okBase := c.GetQuery("base-url"); okBase { base := strings.TrimSpace(baseRaw) @@ -226,7 +233,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) { if len(out) != len(h.cfg.GeminiKey) { h.cfg.GeminiKey = out h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) } else { c.JSON(404, gin.H{"error": "item not found"}) } @@ -253,7 +260,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) { } h.cfg.GeminiKey = append(h.cfg.GeminiKey[:matchIndex], h.cfg.GeminiKey[matchIndex+1:]...) h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) return } if idxStr := c.Query("index"); idxStr != "" { @@ -261,7 +268,7 @@ func (h *Handler) DeleteGeminiKey(c *gin.Context) { if _, err := fmt.Sscanf(idxStr, "%d", &idx); err == nil && idx >= 0 && idx < len(h.cfg.GeminiKey) { h.cfg.GeminiKey = append(h.cfg.GeminiKey[:idx], h.cfg.GeminiKey[idx+1:]...) h.cfg.SanitizeGeminiKeys() - h.persist(c) + h.persistLocked(c) return } } @@ -292,9 +299,11 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) { for i := range arr { normalizeClaudeKey(&arr[i]) } + h.mu.Lock() + defer h.mu.Unlock() h.cfg.ClaudeKey = arr h.cfg.SanitizeClaudeKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) PatchClaudeKey(c *gin.Context) { type claudeKeyPatch struct { @@ -315,6 +324,9 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + + h.mu.Lock() + defer h.mu.Unlock() targetIndex := -1 if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) { targetIndex = *body.Index @@ -358,10 +370,12 @@ func (h *Handler) PatchClaudeKey(c *gin.Context) { normalizeClaudeKey(&entry) h.cfg.ClaudeKey[targetIndex] = entry h.cfg.SanitizeClaudeKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) DeleteClaudeKey(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() if val := strings.TrimSpace(c.Query("api-key")); val != "" { if baseRaw, okBase := c.GetQuery("base-url"); okBase { base := strings.TrimSpace(baseRaw) @@ -374,7 +388,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { } h.cfg.ClaudeKey = out h.cfg.SanitizeClaudeKeys() - h.persist(c) + h.persistLocked(c) return } @@ -396,7 +410,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { h.cfg.ClaudeKey = append(h.cfg.ClaudeKey[:matchIndex], h.cfg.ClaudeKey[matchIndex+1:]...) } h.cfg.SanitizeClaudeKeys() - h.persist(c) + h.persistLocked(c) return } if idxStr := c.Query("index"); idxStr != "" { @@ -405,7 +419,7 @@ func (h *Handler) DeleteClaudeKey(c *gin.Context) { if err == nil && idx >= 0 && idx < len(h.cfg.ClaudeKey) { h.cfg.ClaudeKey = append(h.cfg.ClaudeKey[:idx], h.cfg.ClaudeKey[idx+1:]...) h.cfg.SanitizeClaudeKeys() - h.persist(c) + h.persistLocked(c) return } } @@ -440,9 +454,11 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) { filtered = append(filtered, arr[i]) } } + h.mu.Lock() + defer h.mu.Unlock() h.cfg.OpenAICompatibility = filtered h.cfg.SanitizeOpenAICompatibility() - h.persist(c) + h.persistLocked(c) } func (h *Handler) PatchOpenAICompat(c *gin.Context) { type openAICompatPatch struct { @@ -462,6 +478,9 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + + h.mu.Lock() + defer h.mu.Unlock() targetIndex := -1 if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) { targetIndex = *body.Index @@ -492,7 +511,7 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) { if trimmed == "" { h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:targetIndex], h.cfg.OpenAICompatibility[targetIndex+1:]...) h.cfg.SanitizeOpenAICompatibility() - h.persist(c) + h.persistLocked(c) return } entry.BaseURL = trimmed @@ -509,10 +528,12 @@ func (h *Handler) PatchOpenAICompat(c *gin.Context) { normalizeOpenAICompatibilityEntry(&entry) h.cfg.OpenAICompatibility[targetIndex] = entry h.cfg.SanitizeOpenAICompatibility() - h.persist(c) + h.persistLocked(c) } func (h *Handler) DeleteOpenAICompat(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() if name := c.Query("name"); name != "" { out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility)) for _, v := range h.cfg.OpenAICompatibility { @@ -522,7 +543,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) { } h.cfg.OpenAICompatibility = out h.cfg.SanitizeOpenAICompatibility() - h.persist(c) + h.persistLocked(c) return } if idxStr := c.Query("index"); idxStr != "" { @@ -531,7 +552,7 @@ func (h *Handler) DeleteOpenAICompat(c *gin.Context) { if err == nil && idx >= 0 && idx < len(h.cfg.OpenAICompatibility) { h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:idx], h.cfg.OpenAICompatibility[idx+1:]...) h.cfg.SanitizeOpenAICompatibility() - h.persist(c) + h.persistLocked(c) return } } @@ -566,9 +587,11 @@ func (h *Handler) PutVertexCompatKeys(c *gin.Context) { return } } + h.mu.Lock() + defer h.mu.Unlock() h.cfg.VertexCompatAPIKey = append([]config.VertexCompatKey(nil), arr...) h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) PatchVertexCompatKey(c *gin.Context) { type vertexCompatPatch struct { @@ -589,6 +612,9 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + + h.mu.Lock() + defer h.mu.Unlock() targetIndex := -1 if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.VertexCompatAPIKey) { targetIndex = *body.Index @@ -615,7 +641,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) { if trimmed == "" { h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...) h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) return } entry.APIKey = trimmed @@ -628,7 +654,7 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) { if trimmed == "" { h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:targetIndex], h.cfg.VertexCompatAPIKey[targetIndex+1:]...) h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) return } entry.BaseURL = trimmed @@ -648,10 +674,12 @@ func (h *Handler) PatchVertexCompatKey(c *gin.Context) { normalizeVertexCompatKey(&entry) h.cfg.VertexCompatAPIKey[targetIndex] = entry h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) DeleteVertexCompatKey(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() if val := strings.TrimSpace(c.Query("api-key")); val != "" { if baseRaw, okBase := c.GetQuery("base-url"); okBase { base := strings.TrimSpace(baseRaw) @@ -664,7 +692,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) { } h.cfg.VertexCompatAPIKey = out h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) return } @@ -686,7 +714,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) { h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:matchIndex], h.cfg.VertexCompatAPIKey[matchIndex+1:]...) } h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) return } if idxStr := c.Query("index"); idxStr != "" { @@ -695,7 +723,7 @@ func (h *Handler) DeleteVertexCompatKey(c *gin.Context) { if errScan == nil && idx >= 0 && idx < len(h.cfg.VertexCompatAPIKey) { h.cfg.VertexCompatAPIKey = append(h.cfg.VertexCompatAPIKey[:idx], h.cfg.VertexCompatAPIKey[idx+1:]...) h.cfg.SanitizeVertexCompatKeys() - h.persist(c) + h.persistLocked(c) return } } @@ -915,9 +943,11 @@ func (h *Handler) PutCodexKeys(c *gin.Context) { } filtered = append(filtered, entry) } + h.mu.Lock() + defer h.mu.Unlock() h.cfg.CodexKey = filtered h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) PatchCodexKey(c *gin.Context) { type codexKeyPatch struct { @@ -938,6 +968,9 @@ func (h *Handler) PatchCodexKey(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid body"}) return } + + h.mu.Lock() + defer h.mu.Unlock() targetIndex := -1 if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) { targetIndex = *body.Index @@ -968,7 +1001,7 @@ func (h *Handler) PatchCodexKey(c *gin.Context) { if trimmed == "" { h.cfg.CodexKey = append(h.cfg.CodexKey[:targetIndex], h.cfg.CodexKey[targetIndex+1:]...) h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) return } entry.BaseURL = trimmed @@ -988,10 +1021,12 @@ func (h *Handler) PatchCodexKey(c *gin.Context) { normalizeCodexKey(&entry) h.cfg.CodexKey[targetIndex] = entry h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) } func (h *Handler) DeleteCodexKey(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() if val := strings.TrimSpace(c.Query("api-key")); val != "" { if baseRaw, okBase := c.GetQuery("base-url"); okBase { base := strings.TrimSpace(baseRaw) @@ -1004,7 +1039,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) { } h.cfg.CodexKey = out h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) return } @@ -1026,7 +1061,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) { h.cfg.CodexKey = append(h.cfg.CodexKey[:matchIndex], h.cfg.CodexKey[matchIndex+1:]...) } h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) return } if idxStr := c.Query("index"); idxStr != "" { @@ -1035,7 +1070,7 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) { if err == nil && idx >= 0 && idx < len(h.cfg.CodexKey) { h.cfg.CodexKey = append(h.cfg.CodexKey[:idx], h.cfg.CodexKey[idx+1:]...) h.cfg.SanitizeCodexKeys() - h.persist(c) + h.persistLocked(c) return } } diff --git a/internal/api/handlers/management/handler.go b/internal/api/handlers/management/handler.go index 45786b9d3e..30cc973817 100644 --- a/internal/api/handlers/management/handler.go +++ b/internal/api/handlers/management/handler.go @@ -105,10 +105,24 @@ func NewHandlerWithoutConfigFilePath(cfg *config.Config, manager *coreauth.Manag } // SetConfig updates the in-memory config reference when the server hot-reloads. -func (h *Handler) SetConfig(cfg *config.Config) { h.cfg = cfg } +func (h *Handler) SetConfig(cfg *config.Config) { + if h == nil { + return + } + h.mu.Lock() + h.cfg = cfg + h.mu.Unlock() +} // SetAuthManager updates the auth manager reference used by management endpoints. -func (h *Handler) SetAuthManager(manager *coreauth.Manager) { h.authManager = manager } +func (h *Handler) SetAuthManager(manager *coreauth.Manager) { + if h == nil { + return + } + h.mu.Lock() + h.authManager = manager + h.mu.Unlock() +} // SetUsageStatistics allows replacing the usage statistics reference. func (h *Handler) SetUsageStatistics(stats *usage.RequestStatistics) { h.usageStats = stats } @@ -276,6 +290,12 @@ func (h *Handler) Middleware() gin.HandlerFunc { func (h *Handler) persist(c *gin.Context) bool { h.mu.Lock() defer h.mu.Unlock() + return h.persistLocked(c) +} + +// persistLocked saves the current in-memory config to disk. +// It expects the caller to hold h.mu. +func (h *Handler) persistLocked(c *gin.Context) bool { // Preserve comments when writing if err := config.SaveConfigPreserveComments(h.configFilePath, h.cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)}) From a64141a9a6a7628db3b994eed9762aaf6f770727 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Sat, 18 Apr 2026 17:22:16 +0800 Subject: [PATCH 07/22] fix(tests): remove obsolete config_auth_index_test file --- .../management/config_auth_index_test.go | 250 ------------------ 1 file changed, 250 deletions(-) delete mode 100644 internal/api/handlers/management/config_auth_index_test.go diff --git a/internal/api/handlers/management/config_auth_index_test.go b/internal/api/handlers/management/config_auth_index_test.go deleted file mode 100644 index b7c9809011..0000000000 --- a/internal/api/handlers/management/config_auth_index_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package management - -import ( - "context" - "testing" - "time" - - "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/watcher/synthesizer" - coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" -) - -func synthesizeConfigAuths(t *testing.T, cfg *config.Config) []*coreauth.Auth { - t.Helper() - - auths, errSynthesize := synthesizer.NewConfigSynthesizer().Synthesize(&synthesizer.SynthesisContext{ - Config: cfg, - Now: time.Unix(0, 0), - IDGenerator: synthesizer.NewStableIDGenerator(), - }) - if errSynthesize != nil { - t.Fatalf("synthesize config auths: %v", errSynthesize) - } - return auths -} - -func findAuth(t *testing.T, auths []*coreauth.Auth, predicate func(*coreauth.Auth) bool) *coreauth.Auth { - t.Helper() - for _, auth := range auths { - if predicate(auth) { - return auth - } - } - return nil -} - -func TestConfigAuthIndexResolvesLiveIndexes(t *testing.T) { - t.Parallel() - - cfg := &config.Config{ - GeminiKey: []config.GeminiKey{ - {APIKey: "shared-key", BaseURL: "https://a.example.com"}, - {APIKey: "shared-key", BaseURL: "https://b.example.com"}, - }, - ClaudeKey: []config.ClaudeKey{ - {APIKey: "claude-key", BaseURL: "https://claude.example.com"}, - }, - CodexKey: []config.CodexKey{ - {APIKey: "codex-key", BaseURL: "https://codex.example.com/v1"}, - }, - VertexCompatAPIKey: []config.VertexCompatKey{ - {APIKey: "vertex-key", BaseURL: "https://vertex.example.com", ProxyURL: "http://proxy.example.com:8080"}, - }, - OpenAICompatibility: []config.OpenAICompatibility{ - { - Name: "bohe", - BaseURL: "https://bohe.example.com/v1", - APIKeyEntries: []config.OpenAICompatibilityAPIKey{ - {APIKey: "compat-key"}, - }, - }, - }, - } - - auths := synthesizeConfigAuths(t, cfg) - manager := coreauth.NewManager(nil, nil, nil) - for _, auth := range auths { - if auth == nil { - continue - } - if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil { - t.Fatalf("register auth %q: %v", auth.ID, errRegister) - } - } - - h := &Handler{cfg: cfg, authManager: manager} - - geminiAuthA := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://a.example.com" - }) - if geminiAuthA == nil { - t.Fatal("expected synthesized gemini auth (base a)") - } - geminiAuthB := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "gemini" && auth.Attributes["api_key"] == "shared-key" && auth.Attributes["base_url"] == "https://b.example.com" - }) - if geminiAuthB == nil { - t.Fatal("expected synthesized gemini auth (base b)") - } - - gemini := h.geminiKeysWithAuthIndex() - if len(gemini) != 2 { - t.Fatalf("gemini keys = %d, want 2", len(gemini)) - } - if got, want := gemini[0].AuthIndex, geminiAuthA.EnsureIndex(); got != want { - t.Fatalf("gemini[0] auth-index = %q, want %q", got, want) - } - if got, want := gemini[1].AuthIndex, geminiAuthB.EnsureIndex(); got != want { - t.Fatalf("gemini[1] auth-index = %q, want %q", got, want) - } - if gemini[0].AuthIndex == gemini[1].AuthIndex { - t.Fatalf("duplicate gemini entries returned the same auth-index %q", gemini[0].AuthIndex) - } - - claudeAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "claude" && auth.Attributes["api_key"] == "claude-key" - }) - if claudeAuth == nil { - t.Fatal("expected synthesized claude auth") - } - - claude := h.claudeKeysWithAuthIndex() - if len(claude) != 1 { - t.Fatalf("claude keys = %d, want 1", len(claude)) - } - if got, want := claude[0].AuthIndex, claudeAuth.EnsureIndex(); got != want { - t.Fatalf("claude auth-index = %q, want %q", got, want) - } - - codexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "codex" && auth.Attributes["api_key"] == "codex-key" - }) - if codexAuth == nil { - t.Fatal("expected synthesized codex auth") - } - - codex := h.codexKeysWithAuthIndex() - if len(codex) != 1 { - t.Fatalf("codex keys = %d, want 1", len(codex)) - } - if got, want := codex[0].AuthIndex, codexAuth.EnsureIndex(); got != want { - t.Fatalf("codex auth-index = %q, want %q", got, want) - } - - vertexAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "vertex" && auth.Attributes["api_key"] == "vertex-key" - }) - if vertexAuth == nil { - t.Fatal("expected synthesized vertex auth") - } - - vertex := h.vertexCompatKeysWithAuthIndex() - if len(vertex) != 1 { - t.Fatalf("vertex keys = %d, want 1", len(vertex)) - } - if got, want := vertex[0].AuthIndex, vertexAuth.EnsureIndex(); got != want { - t.Fatalf("vertex auth-index = %q, want %q", got, want) - } - - compatAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - if auth.Provider != "bohe" { - return false - } - if auth.Attributes["provider_key"] != "bohe" || auth.Attributes["compat_name"] != "bohe" { - return false - } - return auth.Attributes["api_key"] == "compat-key" - }) - if compatAuth == nil { - t.Fatal("expected synthesized openai-compat auth") - } - - compat := h.openAICompatibilityWithAuthIndex() - if len(compat) != 1 { - t.Fatalf("openai-compat providers = %d, want 1", len(compat)) - } - if len(compat[0].APIKeyEntries) != 1 { - t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries)) - } - if compat[0].AuthIndex != "" { - t.Fatalf("provider-level auth-index should be empty when api-key-entries exist, got %q", compat[0].AuthIndex) - } - if got, want := compat[0].APIKeyEntries[0].AuthIndex, compatAuth.EnsureIndex(); got != want { - t.Fatalf("openai-compat auth-index = %q, want %q", got, want) - } -} - -func TestConfigAuthIndexOmitsIndexesNotInManager(t *testing.T) { - t.Parallel() - - cfg := &config.Config{ - GeminiKey: []config.GeminiKey{ - {APIKey: "gemini-key", BaseURL: "https://a.example.com"}, - }, - OpenAICompatibility: []config.OpenAICompatibility{ - { - Name: "bohe", - BaseURL: "https://bohe.example.com/v1", - APIKeyEntries: []config.OpenAICompatibilityAPIKey{ - {APIKey: "compat-key"}, - }, - }, - }, - } - - auths := synthesizeConfigAuths(t, cfg) - geminiAuth := findAuth(t, auths, func(auth *coreauth.Auth) bool { - if auth == nil { - return false - } - return auth.Provider == "gemini" && auth.Attributes["api_key"] == "gemini-key" - }) - if geminiAuth == nil { - t.Fatal("expected synthesized gemini auth") - } - - manager := coreauth.NewManager(nil, nil, nil) - if _, errRegister := manager.Register(context.Background(), geminiAuth); errRegister != nil { - t.Fatalf("register gemini auth: %v", errRegister) - } - - h := &Handler{cfg: cfg, authManager: manager} - - gemini := h.geminiKeysWithAuthIndex() - if len(gemini) != 1 { - t.Fatalf("gemini keys = %d, want 1", len(gemini)) - } - if gemini[0].AuthIndex == "" { - t.Fatal("expected gemini auth-index to be set") - } - - compat := h.openAICompatibilityWithAuthIndex() - if len(compat) != 1 { - t.Fatalf("openai-compat providers = %d, want 1", len(compat)) - } - if len(compat[0].APIKeyEntries) != 1 { - t.Fatalf("openai-compat api-key-entries = %d, want 1", len(compat[0].APIKeyEntries)) - } - if compat[0].APIKeyEntries[0].AuthIndex != "" { - t.Fatalf("openai-compat auth-index = %q, want empty", compat[0].APIKeyEntries[0].AuthIndex) - } -} From 86c856f56f43bba2d3016a21c3d619f38fba196a Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 19 Apr 2026 03:21:59 +0800 Subject: [PATCH 08/22] feat(translator): add partial and full image generation support in Codex-GPT and Codex-Gemini flows - Introduced `LastImageHashByItemID` in Codex-GPT and `LastImageHashByID` in Codex-Gemini for deduplication of generated images. - Added support for handling `partial_image` and `image_generation_call` types, with inline data embedding for Gemini and URL payload conversion for GPT. - Extended unit tests to verify image handling in both streaming and non-streaming modes. --- .../codex/gemini/codex_gemini_response.go | 92 +++++++++++++ .../gemini/codex_gemini_response_test.go | 76 +++++++++++ .../chat-completions/codex_openai_response.go | 125 +++++++++++++++++- .../codex_openai_response_test.go | 59 +++++++++ 4 files changed, 351 insertions(+), 1 deletion(-) diff --git a/internal/translator/codex/gemini/codex_gemini_response.go b/internal/translator/codex/gemini/codex_gemini_response.go index f6ef87710a..a2e4e20ea2 100644 --- a/internal/translator/codex/gemini/codex_gemini_response.go +++ b/internal/translator/codex/gemini/codex_gemini_response.go @@ -7,6 +7,8 @@ package gemini import ( "bytes" "context" + "crypto/sha256" + "strings" "time" translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" @@ -25,6 +27,7 @@ type ConvertCodexResponseToGeminiParams struct { ResponseID string LastStorageOutput []byte HasOutputTextDelta bool + LastImageHashByID map[string][32]byte } // ConvertCodexResponseToGemini converts Codex streaming response format to Gemini format. @@ -48,6 +51,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR ResponseID: "", LastStorageOutput: nil, HasOutputTextDelta: false, + LastImageHashByID: make(map[string][32]byte), } } @@ -74,10 +78,63 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR template, _ = sjson.SetBytes(template, "responseId", params.ResponseID) } + if typeStr == "response.image_generation_call.partial_image" { + itemID := rootResult.Get("item_id").String() + b64 := rootResult.Get("partial_image_b64").String() + if b64 == "" { + return [][]byte{} + } + if itemID != "" { + if params.LastImageHashByID == nil { + params.LastImageHashByID = make(map[string][32]byte) + } + hash := sha256.Sum256([]byte(b64)) + if last, ok := params.LastImageHashByID[itemID]; ok && last == hash { + return [][]byte{} + } + params.LastImageHashByID[itemID] = hash + } + + outputFormat := rootResult.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + + part := []byte(`{"inlineData":{"data":"","mimeType":""}}`) + part, _ = sjson.SetBytes(part, "inlineData.data", b64) + part, _ = sjson.SetBytes(part, "inlineData.mimeType", mimeType) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) + return [][]byte{template} + } + // Handle function call completion if typeStr == "response.output_item.done" { itemResult := rootResult.Get("item") itemType := itemResult.Get("type").String() + if itemType == "image_generation_call" { + itemID := itemResult.Get("id").String() + b64 := itemResult.Get("result").String() + if b64 == "" { + return [][]byte{} + } + if itemID != "" { + if params.LastImageHashByID == nil { + params.LastImageHashByID = make(map[string][32]byte) + } + hash := sha256.Sum256([]byte(b64)) + if last, ok := params.LastImageHashByID[itemID]; ok && last == hash { + return [][]byte{} + } + params.LastImageHashByID[itemID] = hash + } + + outputFormat := itemResult.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + + part := []byte(`{"inlineData":{"data":"","mimeType":""}}`) + part, _ = sjson.SetBytes(part, "inlineData.data", b64) + part, _ = sjson.SetBytes(part, "inlineData.mimeType", mimeType) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) + return [][]byte{template} + } if itemType == "function_call" { // Create function call part functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`) @@ -270,6 +327,20 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, }) } + case "image_generation_call": + flushPendingFunctionCalls() + b64 := value.Get("result").String() + if b64 == "" { + break + } + outputFormat := value.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + + part := []byte(`{"inlineData":{"data":"","mimeType":""}}`) + part, _ = sjson.SetBytes(part, "inlineData.data", b64) + part, _ = sjson.SetBytes(part, "inlineData.mimeType", mimeType) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) + case "function_call": // Collect function call for potential merging with consecutive ones hasToolCall = true @@ -342,3 +413,24 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { func GeminiTokenCount(ctx context.Context, count int64) []byte { return translatorcommon.GeminiTokenCountJSON(count) } + +func mimeTypeFromCodexOutputFormat(outputFormat string) string { + if outputFormat == "" { + return "image/png" + } + if strings.Contains(outputFormat, "/") { + return outputFormat + } + switch strings.ToLower(outputFormat) { + case "png": + return "image/png" + case "jpg", "jpeg": + return "image/jpeg" + case "webp": + return "image/webp" + case "gif": + return "image/gif" + default: + return "image/png" + } +} diff --git a/internal/translator/codex/gemini/codex_gemini_response_test.go b/internal/translator/codex/gemini/codex_gemini_response_test.go index b8f227beb5..547ee84715 100644 --- a/internal/translator/codex/gemini/codex_gemini_response_test.go +++ b/internal/translator/codex/gemini/codex_gemini_response_test.go @@ -33,3 +33,79 @@ func TestConvertCodexResponseToGemini_StreamEmptyOutputUsesOutputItemDoneMessage t.Fatalf("expected fallback content from response.output_item.done message; outputs=%q", outputs) } } + +func TestConvertCodexResponseToGemini_StreamPartialImageEmitsInlineData(t *testing.T) { + ctx := context.Background() + originalRequest := []byte(`{"tools":[]}`) + var param any + + chunk := []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`) + out := ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, chunk, ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + got := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.data").String() + if got != "aGVsbG8=" { + t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "aGVsbG8=", got, string(out[0])) + } + + gotMime := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.mimeType").String() + if gotMime != "image/png" { + t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/png", gotMime, string(out[0])) + } + + out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, chunk, ¶m) + if len(out) != 0 { + t.Fatalf("expected duplicate image chunk to be suppressed, got %d", len(out)) + } +} + +func TestConvertCodexResponseToGemini_StreamImageGenerationCallDoneEmitsInlineData(t *testing.T) { + ctx := context.Background() + originalRequest := []byte(`{"tools":[]}`) + var param any + + out := ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`), ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"png","result":"aGVsbG8="}}`), ¶m) + if len(out) != 0 { + t.Fatalf("expected output_item.done to be suppressed when identical to last partial image, got %d", len(out)) + } + + out = ConvertCodexResponseToGemini(ctx, "gemini-2.5-pro", originalRequest, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"jpeg","result":"Ymll"}}`), ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + got := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.data").String() + if got != "Ymll" { + t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "Ymll", got, string(out[0])) + } + + gotMime := gjson.GetBytes(out[0], "candidates.0.content.parts.0.inlineData.mimeType").String() + if gotMime != "image/jpeg" { + t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/jpeg", gotMime, string(out[0])) + } +} + +func TestConvertCodexResponseToGemini_NonStreamImageGenerationCallAddsInlineDataPart(t *testing.T) { + ctx := context.Background() + originalRequest := []byte(`{"tools":[]}`) + + raw := []byte(`{"type":"response.completed","response":{"id":"resp_123","created_at":1700000000,"usage":{"input_tokens":1,"output_tokens":1},"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]},{"type":"image_generation_call","output_format":"png","result":"aGVsbG8="}]}}`) + out := ConvertCodexResponseToGeminiNonStream(ctx, "gemini-2.5-pro", originalRequest, nil, raw, nil) + + got := gjson.GetBytes(out, "candidates.0.content.parts.1.inlineData.data").String() + if got != "aGVsbG8=" { + t.Fatalf("expected inlineData.data %q, got %q; chunk=%s", "aGVsbG8=", got, string(out)) + } + + gotMime := gjson.GetBytes(out, "candidates.0.content.parts.1.inlineData.mimeType").String() + if gotMime != "image/png" { + t.Fatalf("expected inlineData.mimeType %q, got %q; chunk=%s", "image/png", gotMime, string(out)) + } +} diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_response.go b/internal/translator/codex/openai/chat-completions/codex_openai_response.go index afae35d48d..75b5b848b3 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_response.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_response.go @@ -8,6 +8,8 @@ package chat_completions import ( "bytes" "context" + "crypto/sha256" + "strings" "time" "github.com/tidwall/gjson" @@ -26,6 +28,7 @@ type ConvertCliToOpenAIParams struct { FunctionCallIndex int HasReceivedArgumentsDelta bool HasToolCallAnnounced bool + LastImageHashByItemID map[string][32]byte } // ConvertCodexResponseToOpenAI translates a single chunk of a streaming response from the @@ -51,6 +54,7 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR FunctionCallIndex: -1, HasReceivedArgumentsDelta: false, HasToolCallAnnounced: false, + LastImageHashByItemID: make(map[string][32]byte), } } @@ -70,6 +74,9 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR (*param).(*ConvertCliToOpenAIParams).ResponseID = rootResult.Get("response.id").String() (*param).(*ConvertCliToOpenAIParams).CreatedAt = rootResult.Get("response.created_at").Int() (*param).(*ConvertCliToOpenAIParams).Model = rootResult.Get("response.model").String() + if (*param).(*ConvertCliToOpenAIParams).LastImageHashByItemID == nil { + (*param).(*ConvertCliToOpenAIParams).LastImageHashByItemID = make(map[string][32]byte) + } return [][]byte{} } @@ -120,6 +127,39 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") template, _ = sjson.SetBytes(template, "choices.0.delta.content", deltaResult.String()) } + } else if dataType == "response.image_generation_call.partial_image" { + itemID := rootResult.Get("item_id").String() + b64 := rootResult.Get("partial_image_b64").String() + if b64 == "" { + return [][]byte{} + } + if itemID != "" { + p := (*param).(*ConvertCliToOpenAIParams) + if p.LastImageHashByItemID == nil { + p.LastImageHashByItemID = make(map[string][32]byte) + } + hash := sha256.Sum256([]byte(b64)) + if last, ok := p.LastImageHashByItemID[itemID]; ok && last == hash { + return [][]byte{} + } + p.LastImageHashByItemID[itemID] = hash + } + + outputFormat := rootResult.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + imageURL := "data:" + mimeType + ";base64," + b64 + + imagesResult := gjson.GetBytes(template, "choices.0.delta.images") + if !imagesResult.Exists() || !imagesResult.IsArray() { + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images", []byte(`[]`)) + } + imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload) } else if dataType == "response.completed" { finishReason := "stop" if (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex != -1 { @@ -183,7 +223,46 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR } else if dataType == "response.output_item.done" { itemResult := rootResult.Get("item") - if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" { + if !itemResult.Exists() { + return [][]byte{} + } + itemType := itemResult.Get("type").String() + if itemType == "image_generation_call" { + itemID := itemResult.Get("id").String() + b64 := itemResult.Get("result").String() + if b64 == "" { + return [][]byte{} + } + if itemID != "" { + p := (*param).(*ConvertCliToOpenAIParams) + if p.LastImageHashByItemID == nil { + p.LastImageHashByItemID = make(map[string][32]byte) + } + hash := sha256.Sum256([]byte(b64)) + if last, ok := p.LastImageHashByItemID[itemID]; ok && last == hash { + return [][]byte{} + } + p.LastImageHashByItemID[itemID] = hash + } + + outputFormat := itemResult.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + imageURL := "data:" + mimeType + ";base64," + b64 + + imagesResult := gjson.GetBytes(template, "choices.0.delta.images") + if !imagesResult.Exists() || !imagesResult.IsArray() { + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images", []byte(`[]`)) + } + imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload) + return [][]byte{template} + } + if itemType != "function_call" { return [][]byte{} } @@ -285,6 +364,7 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original // Process the output array for content and function calls var toolCalls [][]byte + var images [][]byte outputResult := responseResult.Get("output") if outputResult.IsArray() { outputArray := outputResult.Array() @@ -339,6 +419,19 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original } toolCalls = append(toolCalls, functionCallTemplate) + case "image_generation_call": + b64 := outputItem.Get("result").String() + if b64 == "" { + break + } + outputFormat := outputItem.Get("output_format").String() + mimeType := mimeTypeFromCodexOutputFormat(outputFormat) + imageURL := "data:" + mimeType + ";base64," + b64 + + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", len(images)) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + images = append(images, imagePayload) } } @@ -361,6 +454,15 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original } template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant") } + + // Add images if any + if len(images) > 0 { + template, _ = sjson.SetRawBytes(template, "choices.0.message.images", []byte(`[]`)) + for _, image := range images { + template, _ = sjson.SetRawBytes(template, "choices.0.message.images.-1", image) + } + template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant") + } } // Extract and set the finish reason based on status @@ -409,3 +511,24 @@ func buildReverseMapFromOriginalOpenAI(original []byte) map[string]string { } return rev } + +func mimeTypeFromCodexOutputFormat(outputFormat string) string { + if outputFormat == "" { + return "image/png" + } + if strings.Contains(outputFormat, "/") { + return outputFormat + } + switch strings.ToLower(outputFormat) { + case "png": + return "image/png" + case "jpg", "jpeg": + return "image/jpeg" + case "webp": + return "image/webp" + case "gif": + return "image/gif" + default: + return "image/png" + } +} diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go b/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go index 534884c229..a6bb486fdf 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go @@ -90,3 +90,62 @@ func TestConvertCodexResponseToOpenAI_ToolCallArgumentsDeltaOmitsNullContentFiel t.Fatalf("expected tool call arguments delta to exist, got %s", string(out[0])) } } + +func TestConvertCodexResponseToOpenAI_StreamPartialImageEmitsDeltaImages(t *testing.T) { + ctx := context.Background() + var param any + + chunk := []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`) + + out := ConvertCodexResponseToOpenAI(ctx, "gpt-5.4", nil, nil, chunk, ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + gotURL := gjson.GetBytes(out[0], "choices.0.delta.images.0.image_url.url").String() + if gotURL != "data:image/png;base64,aGVsbG8=" { + t.Fatalf("expected image url %q, got %q; chunk=%s", "data:image/png;base64,aGVsbG8=", gotURL, string(out[0])) + } + + out = ConvertCodexResponseToOpenAI(ctx, "gpt-5.4", nil, nil, chunk, ¶m) + if len(out) != 0 { + t.Fatalf("expected duplicate image chunk to be suppressed, got %d", len(out)) + } +} + +func TestConvertCodexResponseToOpenAI_StreamImageGenerationCallDoneEmitsDeltaImages(t *testing.T) { + ctx := context.Background() + var param any + + out := ConvertCodexResponseToOpenAI(ctx, "gpt-5.4", nil, nil, []byte(`data: {"type":"response.image_generation_call.partial_image","item_id":"ig_123","output_format":"png","partial_image_b64":"aGVsbG8=","partial_image_index":0}`), ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + out = ConvertCodexResponseToOpenAI(ctx, "gpt-5.4", nil, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"png","result":"aGVsbG8="}}`), ¶m) + if len(out) != 0 { + t.Fatalf("expected output_item.done to be suppressed when identical to last partial image, got %d", len(out)) + } + + out = ConvertCodexResponseToOpenAI(ctx, "gpt-5.4", nil, nil, []byte(`data: {"type":"response.output_item.done","item":{"id":"ig_123","type":"image_generation_call","output_format":"jpeg","result":"Ymll"}}`), ¶m) + if len(out) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(out)) + } + + gotURL := gjson.GetBytes(out[0], "choices.0.delta.images.0.image_url.url").String() + if gotURL != "data:image/jpeg;base64,Ymll" { + t.Fatalf("expected image url %q, got %q; chunk=%s", "data:image/jpeg;base64,Ymll", gotURL, string(out[0])) + } +} + +func TestConvertCodexResponseToOpenAI_NonStreamImageGenerationCallAddsMessageImages(t *testing.T) { + ctx := context.Background() + + raw := []byte(`{"type":"response.completed","response":{"id":"resp_123","created_at":1700000000,"model":"gpt-5.4","status":"completed","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2},"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]},{"type":"image_generation_call","output_format":"png","result":"aGVsbG8="}]}}`) + out := ConvertCodexResponseToOpenAINonStream(ctx, "gpt-5.4", nil, nil, raw, nil) + + gotURL := gjson.GetBytes(out, "choices.0.message.images.0.image_url.url").String() + if gotURL != "data:image/png;base64,aGVsbG8=" { + t.Fatalf("expected image url %q, got %q; chunk=%s", "data:image/png;base64,aGVsbG8=", gotURL, string(out)) + } +} From f4eb16102b7f5e5a6b83fb7b74d220f4714c82d5 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sun, 19 Apr 2026 10:38:16 +0800 Subject: [PATCH 09/22] fix(executor): drop obsolete context-1m-2025-08-07 beta header (fixes #2866) Anthropic has moved the 1M-context-window feature to General Availability, so the context-1m-2025-08-07 beta flag is no longer accepted and now causes 400 Bad Request errors when forwarded upstream. Remove the X-CPA-CLAUDE-1M detection and the corresponding injection of the now-invalid beta header. Also drop the unused net/textproto import that was only needed for the header-key lookup. --- internal/runtime/executor/claude_executor.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 0311827bae..235db1f3b2 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "net/http" - "net/textproto" "strings" "time" @@ -911,15 +910,8 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, baseBetas += ",interleaved-thinking-2025-05-14" } - hasClaude1MHeader := false - if ginHeaders != nil { - if _, ok := ginHeaders[textproto.CanonicalMIMEHeaderKey("X-CPA-CLAUDE-1M")]; ok { - hasClaude1MHeader = true - } - } - // Merge extra betas from request body and request flags. - if len(extraBetas) > 0 || hasClaude1MHeader { + if len(extraBetas) > 0 { existingSet := make(map[string]bool) for _, b := range strings.Split(baseBetas, ",") { betaName := strings.TrimSpace(b) @@ -934,9 +926,6 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, existingSet[beta] = true } } - if hasClaude1MHeader && !existingSet["context-1m-2025-08-07"] { - baseBetas += ",context-1m-2025-08-07" - } } r.Header.Set("Anthropic-Beta", baseBetas) From 8f4a4eabfc8688e73eb21effe1e077e83494a8b5 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 19 Apr 2026 23:00:09 +0800 Subject: [PATCH 10/22] feat(docs): add VisionCoder sponsorship details and optimize external links - Added VisionCoder sponsorship information to `README.md`, `README_CN.md`, and `README_JA.md`. - Updated external links to include `target="_blank"` for improved user experience. - Added new logo asset `visioncoder.png` for README use. --- README.md | 6 ++++++ README_CN.md | 16 +++++++++++----- README_JA.md | 4 ++++ assets/visioncoder.png | Bin 0 -> 155590 bytes 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 assets/visioncoder.png diff --git a/README.md b/README.md index 53acdd5178..77b8667b2f 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB PoixeAI Thanks to Poixe AI for sponsoring this project! Poixe AI provides reliable LLM API services. You can leverage the platform's API endpoints to seamlessly build AI-powered products. Additionally, you can become a vendor by providing AI API resources to the platform and earn revenue. Register through the exclusive CLIProxyAPI referral link and receive a bonus of $5 USD on your first top-up. + +VisionCoder +Thanks to VisionCoder for supporting this project. VisionCoder Developer Platform is a reliable and efficient API relay service provider, offering access to mainstream AI models such as Claude Code, Codex, and Gemini. It helps developers and teams integrate AI capabilities more easily and improve productivity. +

+VisionCoder is also offering our users a limited-time Token Plan promotion: buy 1 month and get 1 month free. + diff --git a/README_CN.md b/README_CN.md index 86ea954209..75d50e7ac1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -24,23 +24,29 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元 PackyCode -感谢 PackyCode 对本项目的赞助!PackyCode 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。PackyCode 为本软件用户提供了特别优惠:使用此链接注册,并在充值时输入 "cliproxyapi" 优惠码即可享受九折优惠。 +感谢 PackyCode 对本项目的赞助!PackyCode 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。PackyCode 为本软件用户提供了特别优惠:使用此链接注册,并在充值时输入 "cliproxyapi" 优惠码即可享受九折优惠。 AICodeMirror -感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。 Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CLIProxyAPI 的用户提供了特别福利,通过此链接注册的用户,可享受首充8折,企业客户最高可享 7.5 折! +感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。 Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CLIProxyAPI 的用户提供了特别福利,通过此链接注册的用户,可享受首充8折,企业客户最高可享 7.5 折! BmoPlus -感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格! +感谢 BmoPlus 赞助了本项目!BmoPlus 是一家专为AI订阅重度用户打造的可靠 AI 账号代充服务商,提供稳定的 ChatGPT Plus / ChatGPT Pro(全程质保) / Claude Pro / Super Grok / Gemini Pro 的官方代充&成品账号。 通过BmoPlus AI成品号专卖/代充注册下单的用户,可享GPT 官网订阅一折 的震撼价格! LingtrueAPI -感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude Code、Codex、Gemini 等多种顶级模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力。LingtrueAPI为本软件用户提供了特别优惠:使用此链接注册,并在首次充值时输入 "LingtrueAPI" 优惠码即可享受9折优惠。 +感谢 LingtrueAPI 对本项目的赞助!LingtrueAPI 是一家全球大模型API中转服务平台,提供Claude Code、Codex、Gemini 等多种顶级模型API调用服务,致力于让用户以低成本、高稳定性链接全球AI能力。LingtrueAPI为本软件用户提供了特别优惠:使用此链接注册,并在首次充值时输入 "LingtrueAPI" 优惠码即可享受9折优惠。 PoixeAI -感谢 Poixe AI 对本项目的赞助!Poixe AI 提供可靠的 AI 模型接口服务,您可以使用平台提供的 LLM API 接口轻松构建 AI 产品,同时也可以成为供应商,为平台提供大模型资源以赚取收益。通过 CLIProxyAPI 专属链接注册,充值额外赠送 $5 美金 +感谢 Poixe AI 对本项目的赞助!Poixe AI 提供可靠的 AI 模型接口服务,您可以使用平台提供的 LLM API 接口轻松构建 AI 产品,同时也可以成为供应商,为平台提供大模型资源以赚取收益。通过 CLIProxyAPI 专属链接注册,充值额外赠送 $5 美金 + + +VisionCoder +感谢 VisionCoder 对本项目的支持。VisionCoder 开发平台 是一个可靠高效的 API 中继服务提供商,提供 Claude Code、Codex、Gemini 等主流 AI 模型,帮助开发者和团队更轻松地集成 AI 功能,提升工作效率。 +

+VisionCoder 还为我们的用户提供 Token Plan 限时活动:购买 1 个月,赠送 1 个月。 diff --git a/README_JA.md b/README_JA.md index 8c34325b49..cf8a0f77d8 100644 --- a/README_JA.md +++ b/README_JA.md @@ -42,6 +42,10 @@ GLM CODING PLANを10%割引で取得:https://z.ai/subscribe?ic=8JVLJQFSKB PoixeAI Poixe AIのスポンサーシップに感謝します!Poixe AIは信頼できるAIモデルAPIサービスを提供しており、プラットフォームが提供するLLM APIを使って簡単にAI製品を構築できます。また、サプライヤーとしてプラットフォームに大規模モデルのリソースを提供し、収益を得ることも可能です。CLIProxyAPIの専用リンクから登録すると、チャージ時に追加で$5が付与されます。 + +VisionCoder +VisionCoderのご支援に感謝します!VisionCoder 開発プラットフォーム は、信頼性が高く効率的なAPIリレーサービスプロバイダーで、Claude Code、Codex、Geminiなどの主要AIモデルを提供し、開発者やチームがより簡単にAI機能を統合して生産性を向上できるよう支援します。さらに、VisionCoderはユーザー向けに Token Plan の期間限定キャンペーン(1か月購入で1か月分プレゼント)も提供しています。 + diff --git a/assets/visioncoder.png b/assets/visioncoder.png new file mode 100644 index 0000000000000000000000000000000000000000..24b1760ce5afe51d7cedbac1985211c3c4ca78bf GIT binary patch literal 155590 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

hoVN*buH}60`&dlT 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

NS-own{b-@DQN*}Xzo^`N_RT-{imAg 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�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 B6q5h| literal 0 HcmV?d00001 From e6866ff19c26599b0a5df48c0a0c3a3cfd3145dd Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 20 Apr 2026 15:40:43 +0800 Subject: [PATCH 11/22] feat(auth): add refresh backoff for ineffective token updates - Introduced `refreshIneffectiveBackoff` to prevent tight-looping in auto-refresh when token refresh fails to update expiry. - Adjusted refresh logic to apply backoff when `shouldRefresh` evaluates true. Closes: #2830 --- sdk/cliproxy/auth/conductor.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index f58722039c..0a9c157b0a 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -64,8 +64,13 @@ const ( refreshMaxConcurrency = 16 refreshPendingBackoff = time.Minute refreshFailureBackoff = 5 * time.Minute - quotaBackoffBase = time.Second - quotaBackoffMax = 30 * time.Minute + // refreshIneffectiveBackoff throttles refresh attempts when an executor returns + // success but the auth still evaluates as needing refresh (e.g. token expiry + // wasn't updated). Without this guard, the auto-refresh loop can tight-loop and + // burn CPU at idle. + refreshIneffectiveBackoff = 30 * time.Second + quotaBackoffBase = time.Second + quotaBackoffMax = 30 * time.Minute ) var quotaCooldownDisabled atomic.Bool @@ -3240,6 +3245,9 @@ func (m *Manager) refreshAuth(ctx context.Context, id string) { updated.NextRefreshAfter = time.Time{} updated.LastError = nil updated.UpdatedAt = now + if m.shouldRefresh(updated, now) { + updated.NextRefreshAfter = now.Add(refreshIneffectiveBackoff) + } _, _ = m.Update(ctx, updated) } From bb8408cef591cd292ecb41ff4ff805d11a489315 Mon Sep 17 00:00:00 2001 From: stringer07 <1742292793@qq.com> Date: Tue, 21 Apr 2026 16:03:56 +0800 Subject: [PATCH 12/22] fix(codex): backfill streaming response output --- internal/runtime/executor/codex_executor.go | 54 ++++++++++++++++++- .../codex_executor_stream_output_test.go | 51 ++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 41b1c32527..bceeeb6c9d 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -36,6 +36,48 @@ const ( var dataTag = []byte("data:") +// Streamed Codex responses may emit response.output_item.done events while leaving +// response.completed.response.output empty. Keep the stream path aligned with the +// already-patched non-stream path by reconstructing response.output from those items. +func collectCodexOutputItemDone(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback *[][]byte) { + itemResult := gjson.GetBytes(eventData, "item") + if !itemResult.Exists() || itemResult.Type != gjson.JSON { + return + } + outputIndexResult := gjson.GetBytes(eventData, "output_index") + if outputIndexResult.Exists() { + outputItemsByIndex[outputIndexResult.Int()] = []byte(itemResult.Raw) + return + } + *outputItemsFallback = append(*outputItemsFallback, []byte(itemResult.Raw)) +} + +func patchCodexCompletedOutput(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback [][]byte) []byte { + outputResult := gjson.GetBytes(eventData, "response.output") + shouldPatchOutput := (!outputResult.Exists() || !outputResult.IsArray() || len(outputResult.Array()) == 0) && (len(outputItemsByIndex) > 0 || len(outputItemsFallback) > 0) + if !shouldPatchOutput { + return eventData + } + + completedDataPatched := eventData + completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output", []byte(`[]`)) + + indexes := make([]int64, 0, len(outputItemsByIndex)) + for idx := range outputItemsByIndex { + indexes = append(indexes, idx) + } + sort.Slice(indexes, func(i, j int) bool { + return indexes[i] < indexes[j] + }) + for _, idx := range indexes { + completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", outputItemsByIndex[idx]) + } + for _, item := range outputItemsFallback { + completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", item) + } + return completedDataPatched +} + // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. type CodexExecutor struct { @@ -414,20 +456,28 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au scanner := bufio.NewScanner(httpResp.Body) scanner.Buffer(nil, 52_428_800) // 50MB var param any + outputItemsByIndex := make(map[int64][]byte) + var outputItemsFallback [][]byte for scanner.Scan() { line := scanner.Bytes() helps.AppendAPIResponseChunk(ctx, e.cfg, line) + translatedLine := bytes.Clone(line) if bytes.HasPrefix(line, dataTag) { data := bytes.TrimSpace(line[5:]) - if gjson.GetBytes(data, "type").String() == "response.completed" { + switch gjson.GetBytes(data, "type").String() { + case "response.output_item.done": + collectCodexOutputItemDone(data, outputItemsByIndex, &outputItemsFallback) + case "response.completed": if detail, ok := helps.ParseCodexUsage(data); ok { reporter.Publish(ctx, detail) } + data = patchCodexCompletedOutput(data, outputItemsByIndex, outputItemsFallback) + translatedLine = append([]byte("data: "), data...) } } - chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, body, bytes.Clone(line), ¶m) + chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, body, translatedLine, ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } diff --git a/internal/runtime/executor/codex_executor_stream_output_test.go b/internal/runtime/executor/codex_executor_stream_output_test.go index 91d9b0761c..a2da45e199 100644 --- a/internal/runtime/executor/codex_executor_stream_output_test.go +++ b/internal/runtime/executor/codex_executor_stream_output_test.go @@ -1,6 +1,7 @@ package executor import ( + "bytes" "context" "net/http" "net/http/httptest" @@ -44,3 +45,53 @@ func TestCodexExecutorExecute_EmptyStreamCompletionOutputUsesOutputItemDone(t *t t.Fatalf("choices.0.message.content = %q, want %q; payload=%s", gotContent, "ok", string(resp.Payload)) } } + +func TestCodexExecutorExecuteStream_EmptyStreamCompletionOutputUsesOutputItemDone(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]},\"output_index\":0}\n")) + _, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"object\":\"response\",\"created_at\":1775555723,\"status\":\"completed\",\"model\":\"gpt-5.4-mini-2026-03-17\",\"output\":[],\"usage\":{\"input_tokens\":8,\"output_tokens\":28,\"total_tokens\":36}}}\n\n")) + })) + defer server.Close() + + executor := NewCodexExecutor(&config.Config{}) + auth := &cliproxyauth.Auth{Attributes: map[string]string{ + "base_url": server.URL, + "api_key": "test", + }} + + result, err := executor.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{ + Model: "gpt-5.4-mini", + Payload: []byte(`{"model":"gpt-5.4-mini","input":"Say ok"}`), + }, cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + Stream: true, + }) + if err != nil { + t.Fatalf("ExecuteStream error: %v", err) + } + + var completed []byte + for chunk := range result.Chunks { + if chunk.Err != nil { + t.Fatalf("stream chunk error: %v", chunk.Err) + } + payload := bytes.TrimSpace(chunk.Payload) + if !bytes.HasPrefix(payload, []byte("data:")) { + continue + } + data := bytes.TrimSpace(payload[5:]) + if gjson.GetBytes(data, "type").String() == "response.completed" { + completed = append([]byte(nil), data...) + } + } + + if len(completed) == 0 { + t.Fatal("missing response.completed chunk") + } + + gotContent := gjson.GetBytes(completed, "response.output.0.content.0.text").String() + if gotContent != "ok" { + t.Fatalf("response.output[0].content[0].text = %q, want %q; completed=%s", gotContent, "ok", string(completed)) + } +} From b6781d69bedae8693fe156d369b9bf69b98eb7b3 Mon Sep 17 00:00:00 2001 From: stringer07 <1742292793@qq.com> Date: Tue, 21 Apr 2026 16:29:54 +0800 Subject: [PATCH 13/22] perf(codex): avoid repeated output patch writes --- internal/runtime/executor/codex_executor.go | 33 +++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index bceeeb6c9d..7d4d3edf89 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -59,9 +59,6 @@ func patchCodexCompletedOutput(eventData []byte, outputItemsByIndex map[int64][] return eventData } - completedDataPatched := eventData - completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output", []byte(`[]`)) - indexes := make([]int64, 0, len(outputItemsByIndex)) for idx := range outputItemsByIndex { indexes = append(indexes, idx) @@ -69,12 +66,36 @@ func patchCodexCompletedOutput(eventData []byte, outputItemsByIndex map[int64][] sort.Slice(indexes, func(i, j int) bool { return indexes[i] < indexes[j] }) + + items := make([][]byte, 0, len(outputItemsByIndex)+len(outputItemsFallback)) for _, idx := range indexes { - completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", outputItemsByIndex[idx]) + items = append(items, outputItemsByIndex[idx]) } - for _, item := range outputItemsFallback { - completedDataPatched, _ = sjson.SetRawBytes(completedDataPatched, "response.output.-1", item) + items = append(items, outputItemsFallback...) + + outputArray := []byte("[]") + if len(items) > 0 { + var buf bytes.Buffer + totalLen := 2 + for _, item := range items { + totalLen += len(item) + } + if len(items) > 1 { + totalLen += len(items) - 1 + } + buf.Grow(totalLen) + buf.WriteByte('[') + for i, item := range items { + if i > 0 { + buf.WriteByte(',') + } + buf.Write(item) + } + buf.WriteByte(']') + outputArray = buf.Bytes() } + + completedDataPatched, _ := sjson.SetRawBytes(eventData, "response.output", outputArray) return completedDataPatched } From 1716a845eb78a9d2ee39dddc0941d3981bf543ab Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Tue, 21 Apr 2026 20:16:18 +0800 Subject: [PATCH 14/22] feat(api): add support for `HEAD` requests to `/healthz` endpoint - Refactored `/healthz` handler to support `HEAD` requests alongside `GET`. - Updated tests to include validation for `HEAD` requests with expected status and empty body. Closes: #2929 --- internal/api/server.go | 11 +++++++-- internal/api/server_test.go | 45 ++++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/internal/api/server.go b/internal/api/server.go index 075455ba83..9b7452555b 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -319,9 +319,16 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk // setupRoutes configures the API routes for the server. // It defines the endpoints and associates them with their respective handlers. func (s *Server) setupRoutes() { - s.engine.GET("/healthz", func(c *gin.Context) { + healthzHandler := func(c *gin.Context) { + if c.Request.Method == http.MethodHead { + c.Status(http.StatusOK) + return + } + c.JSON(http.StatusOK, gin.H{"status": "ok"}) - }) + } + s.engine.GET("/healthz", healthzHandler) + s.engine.HEAD("/healthz", healthzHandler) s.engine.GET("/management.html", s.serveManagementControlPanel) openaiHandlers := openai.NewOpenAIAPIHandler(s.handlers) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index dbc2cd5a83..db1ef27d17 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -50,23 +50,38 @@ func newTestServer(t *testing.T) *Server { func TestHealthz(t *testing.T) { server := newTestServer(t) - req := httptest.NewRequest(http.MethodGet, "/healthz", nil) - rr := httptest.NewRecorder() - server.engine.ServeHTTP(rr, req) + t.Run("GET", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rr := httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Fatalf("unexpected status code: got %d want %d; body=%s", rr.Code, http.StatusOK, rr.Body.String()) - } + if rr.Code != http.StatusOK { + t.Fatalf("unexpected status code: got %d want %d; body=%s", rr.Code, http.StatusOK, rr.Body.String()) + } - var resp struct { - Status string `json:"status"` - } - if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { - t.Fatalf("failed to parse response JSON: %v; body=%s", err, rr.Body.String()) - } - if resp.Status != "ok" { - t.Fatalf("unexpected response status: got %q want %q", resp.Status, "ok") - } + var resp struct { + Status string `json:"status"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse response JSON: %v; body=%s", err, rr.Body.String()) + } + if resp.Status != "ok" { + t.Fatalf("unexpected response status: got %q want %q", resp.Status, "ok") + } + }) + + t.Run("HEAD", func(t *testing.T) { + req := httptest.NewRequest(http.MethodHead, "/healthz", nil) + rr := httptest.NewRecorder() + server.engine.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("unexpected status code: got %d want %d; body=%s", rr.Code, http.StatusOK, rr.Body.String()) + } + if rr.Body.Len() != 0 { + t.Fatalf("expected empty body for HEAD request, got %q", rr.Body.String()) + } + }) } func TestAmpProviderModelRoutes(t *testing.T) { From 4fc2c619fb350a2b7bea371a5bd15f6e41dcc8e7 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Tue, 21 Apr 2026 20:53:03 +0800 Subject: [PATCH 15/22] feat(models): add Kimi K2.6 model entry to registry JSON --- internal/registry/models/models.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/registry/models/models.json b/internal/registry/models/models.json index 65d8325169..24b96ca95f 100644 --- a/internal/registry/models/models.json +++ b/internal/registry/models/models.json @@ -1670,6 +1670,23 @@ "zero_allowed": true, "dynamic_allowed": true } + }, + { + "id": "kimi-k2.6", + "object": "model", + "created": 1776729600, + "owned_by": "moonshot", + "type": "kimi", + "display_name": "Kimi K2.6", + "description": "Kimi K2.6 - Latest Moonshot AI coding model with improved capabilities", + "context_length": 262144, + "max_completion_tokens": 65536, + "thinking": { + "min": 1024, + "max": 32000, + "zero_allowed": true, + "dynamic_allowed": true + } } ], "antigravity": [ From e935196df43cb9af478fea377571873d07c9a39b Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 22 Apr 2026 20:51:13 +0800 Subject: [PATCH 16/22] feat(models): add hardcoded GPT-Image-2 model support in Codex - Added `GPT-Image-2` as a built-in model to avoid dependency on remote updates for Codex. - Updated model tier functions (`CodexFree`, `CodexTeam`, etc.) to include built-in models via `WithCodexBuiltins`. - Introduced new handlers for image generation and edit operations under `OpenAIAPIHandler`. - Extended tests to validate 503 response for unsupported image model requests. --- internal/api/server.go | 2 + internal/registry/model_definitions.go | 75 +- sdk/api/handlers/handlers.go | 7 + .../handlers/handlers_request_details_test.go | 21 + .../handlers/openai/openai_images_handlers.go | 904 ++++++++++++++++++ sdk/cliproxy/service.go | 2 +- 6 files changed, 1006 insertions(+), 5 deletions(-) create mode 100644 sdk/api/handlers/openai/openai_images_handlers.go diff --git a/internal/api/server.go b/internal/api/server.go index 9b7452555b..7c571e23cf 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -344,6 +344,8 @@ func (s *Server) setupRoutes() { v1.GET("/models", s.unifiedModelsHandler(openaiHandlers, claudeCodeHandlers)) v1.POST("/chat/completions", openaiHandlers.ChatCompletions) v1.POST("/completions", openaiHandlers.Completions) + v1.POST("/images/generations", openaiHandlers.ImagesGenerations) + v1.POST("/images/edits", openaiHandlers.ImagesEdits) v1.POST("/messages", claudeCodeHandlers.ClaudeMessages) v1.POST("/messages/count_tokens", claudeCodeHandlers.ClaudeCountTokens) v1.GET("/responses", openaiResponsesHandlers.ResponsesWebsocket) diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index ab7258f845..7ac6b469ac 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -6,6 +6,8 @@ import ( "strings" ) +const codexBuiltinImageModelID = "gpt-image-2" + // staticModelsJSON mirrors the top-level structure of models.json. type staticModelsJSON struct { Claude []*ModelInfo `json:"claude"` @@ -48,22 +50,22 @@ func GetAIStudioModels() []*ModelInfo { // GetCodexFreeModels returns model definitions for the Codex free plan tier. func GetCodexFreeModels() []*ModelInfo { - return cloneModelInfos(getModels().CodexFree) + return WithCodexBuiltins(cloneModelInfos(getModels().CodexFree)) } // GetCodexTeamModels returns model definitions for the Codex team plan tier. func GetCodexTeamModels() []*ModelInfo { - return cloneModelInfos(getModels().CodexTeam) + return WithCodexBuiltins(cloneModelInfos(getModels().CodexTeam)) } // GetCodexPlusModels returns model definitions for the Codex plus plan tier. func GetCodexPlusModels() []*ModelInfo { - return cloneModelInfos(getModels().CodexPlus) + return WithCodexBuiltins(cloneModelInfos(getModels().CodexPlus)) } // GetCodexProModels returns model definitions for the Codex pro plan tier. func GetCodexProModels() []*ModelInfo { - return cloneModelInfos(getModels().CodexPro) + return WithCodexBuiltins(cloneModelInfos(getModels().CodexPro)) } // GetKimiModels returns the standard Kimi (Moonshot AI) model definitions. @@ -76,6 +78,71 @@ func GetAntigravityModels() []*ModelInfo { return cloneModelInfos(getModels().Antigravity) } +// WithCodexBuiltins injects hard-coded Codex-only model definitions that should +// not depend on remote models.json updates. Built-ins replace any matching IDs +// already present in the provided slice. +func WithCodexBuiltins(models []*ModelInfo) []*ModelInfo { + return upsertModelInfos(models, codexBuiltinImageModelInfo()) +} + +func codexBuiltinImageModelInfo() *ModelInfo { + return &ModelInfo{ + ID: codexBuiltinImageModelID, + Object: "model", + Created: 1704067200, // 2024-01-01 + OwnedBy: "openai", + Type: "openai", + DisplayName: "GPT Image 2", + Version: codexBuiltinImageModelID, + } +} + +func upsertModelInfos(models []*ModelInfo, extras ...*ModelInfo) []*ModelInfo { + if len(extras) == 0 { + return models + } + + extraIDs := make(map[string]struct{}, len(extras)) + extraList := make([]*ModelInfo, 0, len(extras)) + for _, extra := range extras { + if extra == nil { + continue + } + id := strings.TrimSpace(extra.ID) + if id == "" { + continue + } + key := strings.ToLower(id) + if _, exists := extraIDs[key]; exists { + continue + } + extraIDs[key] = struct{}{} + extraList = append(extraList, cloneModelInfo(extra)) + } + + if len(extraList) == 0 { + return models + } + + filtered := make([]*ModelInfo, 0, len(models)+len(extraList)) + for _, model := range models { + if model == nil { + continue + } + id := strings.TrimSpace(model.ID) + if id == "" { + continue + } + if _, exists := extraIDs[strings.ToLower(id)]; exists { + continue + } + filtered = append(filtered, model) + } + + filtered = append(filtered, extraList...) + return filtered +} + // cloneModelInfos returns a shallow copy of the slice with each element deep-cloned. func cloneModelInfos(models []*ModelInfo) []*ModelInfo { if len(models) == 0 { diff --git a/sdk/api/handlers/handlers.go b/sdk/api/handlers/handlers.go index 49e73d4637..1fda8f49f0 100644 --- a/sdk/api/handlers/handlers.go +++ b/sdk/api/handlers/handlers.go @@ -795,6 +795,13 @@ func (h *BaseAPIHandler) getRequestDetails(modelName string) (providers []string parsed := thinking.ParseSuffix(resolvedModelName) baseModel := strings.TrimSpace(parsed.ModelName) + if strings.EqualFold(baseModel, "gpt-image-2") { + return nil, "", &interfaces.ErrorMessage{ + StatusCode: http.StatusServiceUnavailable, + Error: fmt.Errorf("model %s is only supported on /v1/images/generations and /v1/images/edits", baseModel), + } + } + providers = util.GetProviderName(baseModel) // Fallback: if baseModel has no provider but differs from resolvedModelName, // try using the full model name. This handles edge cases where custom models diff --git a/sdk/api/handlers/handlers_request_details_test.go b/sdk/api/handlers/handlers_request_details_test.go index b0f6b13262..c98580f224 100644 --- a/sdk/api/handlers/handlers_request_details_test.go +++ b/sdk/api/handlers/handlers_request_details_test.go @@ -1,7 +1,9 @@ package handlers import ( + "net/http" "reflect" + "strings" "testing" "time" @@ -116,3 +118,22 @@ func TestGetRequestDetails_PreservesSuffix(t *testing.T) { }) } } + +func TestGetRequestDetails_ImageModelReturns503(t *testing.T) { + handler := NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, coreauth.NewManager(nil, nil, nil)) + + _, _, errMsg := handler.getRequestDetails("gpt-image-2") + if errMsg == nil { + t.Fatalf("expected error for gpt-image-2, got nil") + } + if errMsg.StatusCode != http.StatusServiceUnavailable { + t.Fatalf("unexpected status code: got %d want %d", errMsg.StatusCode, http.StatusServiceUnavailable) + } + if errMsg.Error == nil { + t.Fatalf("expected error message, got nil") + } + msg := errMsg.Error.Error() + if !strings.Contains(msg, "/v1/images/generations") || !strings.Contains(msg, "/v1/images/edits") { + t.Fatalf("unexpected error message: %q", msg) + } +} diff --git a/sdk/api/handlers/openai/openai_images_handlers.go b/sdk/api/handlers/openai/openai_images_handlers.go new file mode 100644 index 0000000000..586354dedb --- /dev/null +++ b/sdk/api/handlers/openai/openai_images_handlers.go @@ -0,0 +1,904 @@ +package openai + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +const ( + defaultImagesMainModel = "gpt-5.4-mini" + defaultImagesToolModel = "gpt-image-2" +) + +type imageCallResult struct { + Result string + RevisedPrompt string + OutputFormat string + Size string + Background string + Quality string +} + +type sseFrameAccumulator struct { + pending []byte +} + +func (a *sseFrameAccumulator) AddChunk(chunk []byte) [][]byte { + if len(chunk) == 0 { + return nil + } + + if responsesSSENeedsLineBreak(a.pending, chunk) { + a.pending = append(a.pending, '\n') + } + a.pending = append(a.pending, chunk...) + + var frames [][]byte + for { + frameLen := responsesSSEFrameLen(a.pending) + if frameLen == 0 { + break + } + frames = append(frames, a.pending[:frameLen]) + copy(a.pending, a.pending[frameLen:]) + a.pending = a.pending[:len(a.pending)-frameLen] + } + + if len(bytes.TrimSpace(a.pending)) == 0 { + a.pending = a.pending[:0] + return frames + } + if len(a.pending) == 0 || !responsesSSECanEmitWithoutDelimiter(a.pending) { + return frames + } + frames = append(frames, a.pending) + a.pending = a.pending[:0] + return frames +} + +func (a *sseFrameAccumulator) Flush() [][]byte { + if len(a.pending) == 0 { + return nil + } + + var frames [][]byte + for { + frameLen := responsesSSEFrameLen(a.pending) + if frameLen == 0 { + break + } + frames = append(frames, a.pending[:frameLen]) + copy(a.pending, a.pending[frameLen:]) + a.pending = a.pending[:len(a.pending)-frameLen] + } + + if len(bytes.TrimSpace(a.pending)) == 0 { + a.pending = nil + return frames + } + if responsesSSECanEmitWithoutDelimiter(a.pending) { + frames = append(frames, a.pending) + } + a.pending = nil + return frames +} + +func mimeTypeFromOutputFormat(outputFormat string) string { + if outputFormat == "" { + return "image/png" + } + if strings.Contains(outputFormat, "/") { + return outputFormat + } + switch strings.ToLower(strings.TrimSpace(outputFormat)) { + case "png": + return "image/png" + case "jpg", "jpeg": + return "image/jpeg" + case "webp": + return "image/webp" + default: + return "image/png" + } +} + +func multipartFileToDataURL(fileHeader *multipart.FileHeader) (string, error) { + if fileHeader == nil { + return "", fmt.Errorf("upload file is nil") + } + f, err := fileHeader.Open() + if err != nil { + return "", fmt.Errorf("open upload file failed: %w", err) + } + defer func() { + if errClose := f.Close(); errClose != nil { + log.Errorf("openai images: close upload file error: %v", errClose) + } + }() + + data, err := io.ReadAll(f) + if err != nil { + return "", fmt.Errorf("read upload file failed: %w", err) + } + + mediaType := strings.TrimSpace(fileHeader.Header.Get("Content-Type")) + if mediaType == "" { + mediaType = http.DetectContentType(data) + } + + b64 := base64.StdEncoding.EncodeToString(data) + return "data:" + mediaType + ";base64," + b64, nil +} + +func parseIntField(raw string, fallback int64) int64 { + raw = strings.TrimSpace(raw) + if raw == "" { + return fallback + } + v, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return fallback + } + return v +} + +func parseBoolField(raw string, fallback bool) bool { + raw = strings.TrimSpace(strings.ToLower(raw)) + if raw == "" { + return fallback + } + switch raw { + case "1", "true", "yes", "on": + return true + case "0", "false", "no", "off": + return false + default: + return fallback + } +} + +func (h *OpenAIAPIHandler) ImagesGenerations(c *gin.Context) { + rawJSON, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + if !json.Valid(rawJSON) { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: body must be valid JSON", + Type: "invalid_request_error", + }, + }) + return + } + + prompt := strings.TrimSpace(gjson.GetBytes(rawJSON, "prompt").String()) + if prompt == "" { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: prompt is required", + Type: "invalid_request_error", + }, + }) + return + } + + imageModel := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String()) + if imageModel == "" { + imageModel = defaultImagesToolModel + } + responseFormat := strings.TrimSpace(gjson.GetBytes(rawJSON, "response_format").String()) + if responseFormat == "" { + responseFormat = "b64_json" + } + stream := gjson.GetBytes(rawJSON, "stream").Bool() + + tool := []byte(`{"type":"image_generation","action":"generate"}`) + tool, _ = sjson.SetBytes(tool, "model", imageModel) + + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "size").String()); v != "" { + tool, _ = sjson.SetBytes(tool, "size", v) + } + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "quality").String()); v != "" { + tool, _ = sjson.SetBytes(tool, "quality", v) + } + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "background").String()); v != "" { + tool, _ = sjson.SetBytes(tool, "background", v) + } + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "output_format").String()); v != "" { + tool, _ = sjson.SetBytes(tool, "output_format", v) + } + if v := gjson.GetBytes(rawJSON, "output_compression"); v.Exists() { + if v.Type == gjson.Number { + tool, _ = sjson.SetBytes(tool, "output_compression", v.Int()) + } + } + if v := gjson.GetBytes(rawJSON, "partial_images"); v.Exists() { + if v.Type == gjson.Number { + tool, _ = sjson.SetBytes(tool, "partial_images", v.Int()) + } + } + if v := gjson.GetBytes(rawJSON, "n"); v.Exists() { + if v.Type == gjson.Number { + tool, _ = sjson.SetBytes(tool, "n", v.Int()) + } + } + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "moderation").String()); v != "" { + tool, _ = sjson.SetBytes(tool, "moderation", v) + } + + responsesReq := buildImagesResponsesRequest(prompt, nil, tool) + if stream { + h.streamImagesFromResponses(c, responsesReq, responseFormat, "image_generation") + return + } + h.collectImagesFromResponses(c, responsesReq, responseFormat) +} + +func (h *OpenAIAPIHandler) ImagesEdits(c *gin.Context) { + contentType := strings.ToLower(strings.TrimSpace(c.GetHeader("Content-Type"))) + if strings.HasPrefix(contentType, "application/json") { + h.imagesEditsFromJSON(c) + return + } + if strings.HasPrefix(contentType, "multipart/form-data") || contentType == "" { + h.imagesEditsFromMultipart(c) + return + } + + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: unsupported Content-Type %q", contentType), + Type: "invalid_request_error", + }, + }) +} + +func (h *OpenAIAPIHandler) imagesEditsFromMultipart(c *gin.Context) { + form, err := c.MultipartForm() + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + + prompt := strings.TrimSpace(c.PostForm("prompt")) + if prompt == "" { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: prompt is required", + Type: "invalid_request_error", + }, + }) + return + } + + var imageFiles []*multipart.FileHeader + if files := form.File["image[]"]; len(files) > 0 { + imageFiles = files + } else if files := form.File["image"]; len(files) > 0 { + imageFiles = files + } + if len(imageFiles) == 0 { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: image is required", + Type: "invalid_request_error", + }, + }) + return + } + + images := make([]string, 0, len(imageFiles)) + for _, fh := range imageFiles { + dataURL, err := multipartFileToDataURL(fh) + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + images = append(images, dataURL) + } + + var maskDataURL *string + if maskFiles := form.File["mask"]; len(maskFiles) > 0 && maskFiles[0] != nil { + dataURL, err := multipartFileToDataURL(maskFiles[0]) + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + maskDataURL = &dataURL + } + + imageModel := strings.TrimSpace(c.PostForm("model")) + if imageModel == "" { + imageModel = defaultImagesToolModel + } + responseFormat := strings.TrimSpace(c.PostForm("response_format")) + if responseFormat == "" { + responseFormat = "b64_json" + } + stream := parseBoolField(c.PostForm("stream"), false) + + tool := []byte(`{"type":"image_generation","action":"edit"}`) + tool, _ = sjson.SetBytes(tool, "model", imageModel) + + if v := strings.TrimSpace(c.PostForm("size")); v != "" { + tool, _ = sjson.SetBytes(tool, "size", v) + } + if v := strings.TrimSpace(c.PostForm("quality")); v != "" { + tool, _ = sjson.SetBytes(tool, "quality", v) + } + if v := strings.TrimSpace(c.PostForm("background")); v != "" { + tool, _ = sjson.SetBytes(tool, "background", v) + } + if v := strings.TrimSpace(c.PostForm("output_format")); v != "" { + tool, _ = sjson.SetBytes(tool, "output_format", v) + } + if v := strings.TrimSpace(c.PostForm("input_fidelity")); v != "" { + tool, _ = sjson.SetBytes(tool, "input_fidelity", v) + } + if v := strings.TrimSpace(c.PostForm("moderation")); v != "" { + tool, _ = sjson.SetBytes(tool, "moderation", v) + } + + if v := strings.TrimSpace(c.PostForm("output_compression")); v != "" { + tool, _ = sjson.SetBytes(tool, "output_compression", parseIntField(v, 0)) + } + if v := strings.TrimSpace(c.PostForm("partial_images")); v != "" { + tool, _ = sjson.SetBytes(tool, "partial_images", parseIntField(v, 0)) + } + if v := strings.TrimSpace(c.PostForm("n")); v != "" { + tool, _ = sjson.SetBytes(tool, "n", parseIntField(v, 0)) + } + + if maskDataURL != nil && strings.TrimSpace(*maskDataURL) != "" { + tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", strings.TrimSpace(*maskDataURL)) + } + + responsesReq := buildImagesResponsesRequest(prompt, images, tool) + if stream { + h.streamImagesFromResponses(c, responsesReq, responseFormat, "image_edit") + return + } + h.collectImagesFromResponses(c, responsesReq, responseFormat) +} + +func (h *OpenAIAPIHandler) imagesEditsFromJSON(c *gin.Context) { + rawJSON, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: fmt.Sprintf("Invalid request: %v", err), + Type: "invalid_request_error", + }, + }) + return + } + if !json.Valid(rawJSON) { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: body must be valid JSON", + Type: "invalid_request_error", + }, + }) + return + } + + prompt := strings.TrimSpace(gjson.GetBytes(rawJSON, "prompt").String()) + if prompt == "" { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: prompt is required", + Type: "invalid_request_error", + }, + }) + return + } + + var images []string + imagesResult := gjson.GetBytes(rawJSON, "images") + if imagesResult.IsArray() { + for _, img := range imagesResult.Array() { + url := strings.TrimSpace(img.Get("image_url").String()) + if url == "" { + continue + } + images = append(images, url) + } + } + if len(images) == 0 { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: images[].image_url is required (file_id is not supported)", + Type: "invalid_request_error", + }, + }) + return + } + + var maskDataURL *string + if mask := gjson.GetBytes(rawJSON, "mask.image_url"); mask.Exists() { + url := strings.TrimSpace(mask.String()) + if url != "" { + maskDataURL = &url + } + } else if mask := gjson.GetBytes(rawJSON, "mask.file_id"); mask.Exists() { + c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Invalid request: mask.file_id is not supported (use mask.image_url instead)", + Type: "invalid_request_error", + }, + }) + return + } + + imageModel := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String()) + if imageModel == "" { + imageModel = defaultImagesToolModel + } + responseFormat := strings.TrimSpace(gjson.GetBytes(rawJSON, "response_format").String()) + if responseFormat == "" { + responseFormat = "b64_json" + } + stream := gjson.GetBytes(rawJSON, "stream").Bool() + + tool := []byte(`{"type":"image_generation","action":"edit"}`) + tool, _ = sjson.SetBytes(tool, "model", imageModel) + + for _, field := range []string{"size", "quality", "background", "output_format", "input_fidelity", "moderation"} { + if v := strings.TrimSpace(gjson.GetBytes(rawJSON, field).String()); v != "" { + tool, _ = sjson.SetBytes(tool, field, v) + } + } + + for _, field := range []string{"output_compression", "partial_images", "n"} { + if v := gjson.GetBytes(rawJSON, field); v.Exists() && v.Type == gjson.Number { + tool, _ = sjson.SetBytes(tool, field, v.Int()) + } + } + + if maskDataURL != nil && strings.TrimSpace(*maskDataURL) != "" { + tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", strings.TrimSpace(*maskDataURL)) + } + + responsesReq := buildImagesResponsesRequest(prompt, images, tool) + if stream { + h.streamImagesFromResponses(c, responsesReq, responseFormat, "image_edit") + return + } + h.collectImagesFromResponses(c, responsesReq, responseFormat) +} + +func buildImagesResponsesRequest(prompt string, images []string, toolJSON []byte) []byte { + req := []byte(`{"instructions":"","stream":true,"reasoning":{"effort":"medium","summary":"auto"},"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"model":"","store":false,"tool_choice":{"type":"image_generation"}}`) + req, _ = sjson.SetBytes(req, "model", defaultImagesMainModel) + + input := []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`) + input, _ = sjson.SetBytes(input, "0.content.0.text", prompt) + contentIndex := 1 + for _, img := range images { + if strings.TrimSpace(img) == "" { + continue + } + part := []byte(`{"type":"input_image","image_url":""}`) + part, _ = sjson.SetBytes(part, "image_url", img) + path := fmt.Sprintf("0.content.%d", contentIndex) + input, _ = sjson.SetRawBytes(input, path, part) + contentIndex++ + } + req, _ = sjson.SetRawBytes(req, "input", input) + + req, _ = sjson.SetRawBytes(req, "tools", []byte(`[]`)) + if len(toolJSON) > 0 && json.Valid(toolJSON) { + req, _ = sjson.SetRawBytes(req, "tools.-1", toolJSON) + } + return req +} + +func (h *OpenAIAPIHandler) collectImagesFromResponses(c *gin.Context, responsesReq []byte, responseFormat string) { + c.Header("Content-Type", "application/json") + + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx) + + dataChan, upstreamHeaders, errChan := h.ExecuteStreamWithAuthManager(cliCtx, "openai-response", defaultImagesMainModel, responsesReq, "") + + out, errMsg := collectImagesFromResponsesStream(cliCtx, dataChan, errChan, responseFormat) + stopKeepAlive() + if errMsg != nil { + h.WriteErrorResponse(c, errMsg) + if errMsg.Error != nil { + cliCancel(errMsg.Error) + } else { + cliCancel(nil) + } + return + } + handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders) + _, _ = c.Writer.Write(out) + cliCancel() +} + +func collectImagesFromResponsesStream(ctx context.Context, data <-chan []byte, errs <-chan *interfaces.ErrorMessage, responseFormat string) ([]byte, *interfaces.ErrorMessage) { + acc := &sseFrameAccumulator{} + + processFrame := func(frame []byte) ([]byte, bool, *interfaces.ErrorMessage) { + for _, line := range bytes.Split(frame, []byte("\n")) { + trimmed := bytes.TrimSpace(bytes.TrimRight(line, "\r")) + if len(trimmed) == 0 { + continue + } + if !bytes.HasPrefix(trimmed, []byte("data:")) { + continue + } + payload := bytes.TrimSpace(trimmed[len("data:"):]) + if len(payload) == 0 || bytes.Equal(payload, []byte("[DONE]")) { + continue + } + if !json.Valid(payload) { + return nil, false, &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("invalid SSE data JSON")} + } + + if gjson.GetBytes(payload, "type").String() != "response.completed" { + continue + } + + results, createdAt, usageRaw, firstMeta, err := extractImagesFromResponsesCompleted(payload) + if err != nil { + return nil, false, &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: err} + } + if len(results) == 0 { + return nil, false, &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("upstream did not return image output")} + } + out, err := buildImagesAPIResponse(results, createdAt, usageRaw, firstMeta, responseFormat) + if err != nil { + return nil, false, &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: err} + } + return out, true, nil + } + return nil, false, nil + } + + for { + select { + case <-ctx.Done(): + return nil, &interfaces.ErrorMessage{StatusCode: http.StatusRequestTimeout, Error: ctx.Err()} + case errMsg, ok := <-errs: + if ok && errMsg != nil { + return nil, errMsg + } + errs = nil + case chunk, ok := <-data: + if !ok { + for _, frame := range acc.Flush() { + if out, done, errMsg := processFrame(frame); errMsg != nil { + return nil, errMsg + } else if done { + return out, nil + } + } + return nil, &interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("stream disconnected before completion")} + } + for _, frame := range acc.AddChunk(chunk) { + if out, done, errMsg := processFrame(frame); errMsg != nil { + return nil, errMsg + } else if done { + return out, nil + } + } + } + } +} + +func extractImagesFromResponsesCompleted(payload []byte) (results []imageCallResult, createdAt int64, usageRaw []byte, firstMeta imageCallResult, err error) { + if gjson.GetBytes(payload, "type").String() != "response.completed" { + return nil, 0, nil, imageCallResult{}, fmt.Errorf("unexpected event type") + } + + createdAt = gjson.GetBytes(payload, "response.created_at").Int() + if createdAt <= 0 { + createdAt = time.Now().Unix() + } + + output := gjson.GetBytes(payload, "response.output") + if output.IsArray() { + for _, item := range output.Array() { + if item.Get("type").String() != "image_generation_call" { + continue + } + res := strings.TrimSpace(item.Get("result").String()) + if res == "" { + continue + } + entry := imageCallResult{ + Result: res, + RevisedPrompt: strings.TrimSpace(item.Get("revised_prompt").String()), + OutputFormat: strings.TrimSpace(item.Get("output_format").String()), + Size: strings.TrimSpace(item.Get("size").String()), + Background: strings.TrimSpace(item.Get("background").String()), + Quality: strings.TrimSpace(item.Get("quality").String()), + } + if len(results) == 0 { + firstMeta = entry + } + results = append(results, entry) + } + } + + if usage := gjson.GetBytes(payload, "response.tool_usage.image_gen"); usage.Exists() && usage.IsObject() { + usageRaw = []byte(usage.Raw) + } + + return results, createdAt, usageRaw, firstMeta, nil +} + +func buildImagesAPIResponse(results []imageCallResult, createdAt int64, usageRaw []byte, firstMeta imageCallResult, responseFormat string) ([]byte, error) { + out := []byte(`{"created":0,"data":[]}`) + out, _ = sjson.SetBytes(out, "created", createdAt) + + responseFormat = strings.ToLower(strings.TrimSpace(responseFormat)) + if responseFormat == "" { + responseFormat = "b64_json" + } + + for _, img := range results { + item := []byte(`{}`) + if responseFormat == "url" { + mt := mimeTypeFromOutputFormat(img.OutputFormat) + item, _ = sjson.SetBytes(item, "url", "data:"+mt+";base64,"+img.Result) + } else { + item, _ = sjson.SetBytes(item, "b64_json", img.Result) + } + if img.RevisedPrompt != "" { + item, _ = sjson.SetBytes(item, "revised_prompt", img.RevisedPrompt) + } + out, _ = sjson.SetRawBytes(out, "data.-1", item) + } + + if firstMeta.Background != "" { + out, _ = sjson.SetBytes(out, "background", firstMeta.Background) + } + if firstMeta.OutputFormat != "" { + out, _ = sjson.SetBytes(out, "output_format", firstMeta.OutputFormat) + } + if firstMeta.Quality != "" { + out, _ = sjson.SetBytes(out, "quality", firstMeta.Quality) + } + if firstMeta.Size != "" { + out, _ = sjson.SetBytes(out, "size", firstMeta.Size) + } + + if len(usageRaw) > 0 && json.Valid(usageRaw) { + out, _ = sjson.SetRawBytes(out, "usage", usageRaw) + } + + return out, nil +} + +func (h *OpenAIAPIHandler) streamImagesFromResponses(c *gin.Context, responsesReq []byte, responseFormat string, streamPrefix string) { + flusher, ok := c.Writer.(http.Flusher) + if !ok { + c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{ + Error: handlers.ErrorDetail{ + Message: "Streaming not supported", + Type: "server_error", + }, + }) + return + } + + cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) + dataChan, upstreamHeaders, errChan := h.ExecuteStreamWithAuthManager(cliCtx, "openai-response", defaultImagesMainModel, responsesReq, "") + + setSSEHeaders := func() { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("Access-Control-Allow-Origin", "*") + } + + writeEvent := func(eventName string, dataJSON []byte) { + if strings.TrimSpace(eventName) != "" { + _, _ = fmt.Fprintf(c.Writer, "event: %s\n", eventName) + } + _, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", string(dataJSON)) + flusher.Flush() + } + + // Peek for first chunk/error so we can still return a JSON error body. + for { + select { + case <-c.Request.Context().Done(): + cliCancel(c.Request.Context().Err()) + return + case errMsg, ok := <-errChan: + if !ok { + errChan = nil + continue + } + h.WriteErrorResponse(c, errMsg) + if errMsg != nil { + cliCancel(errMsg.Error) + } else { + cliCancel(nil) + } + return + case chunk, ok := <-dataChan: + if !ok { + setSSEHeaders() + handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders) + _, _ = c.Writer.Write([]byte("\n")) + flusher.Flush() + cliCancel(nil) + return + } + + setSSEHeaders() + handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders) + + h.forwardImagesStream(cliCtx, c, flusher, func(err error) { cliCancel(err) }, dataChan, errChan, chunk, responseFormat, streamPrefix, writeEvent) + return + } + } +} + +func (h *OpenAIAPIHandler) forwardImagesStream(ctx context.Context, c *gin.Context, flusher http.Flusher, cancel func(error), data <-chan []byte, errs <-chan *interfaces.ErrorMessage, firstChunk []byte, responseFormat string, streamPrefix string, writeEvent func(string, []byte)) { + acc := &sseFrameAccumulator{} + + responseFormat = strings.ToLower(strings.TrimSpace(responseFormat)) + if responseFormat == "" { + responseFormat = "b64_json" + } + + emitError := func(errMsg *interfaces.ErrorMessage) { + if errMsg == nil { + return + } + status := http.StatusInternalServerError + if errMsg.StatusCode > 0 { + status = errMsg.StatusCode + } + errText := http.StatusText(status) + if errMsg.Error != nil && strings.TrimSpace(errMsg.Error.Error()) != "" { + errText = errMsg.Error.Error() + } + body := handlers.BuildErrorResponseBody(status, errText) + writeEvent("error", body) + } + + processFrame := func(frame []byte) (done bool) { + for _, line := range bytes.Split(frame, []byte("\n")) { + trimmed := bytes.TrimSpace(bytes.TrimRight(line, "\r")) + if len(trimmed) == 0 || !bytes.HasPrefix(trimmed, []byte("data:")) { + continue + } + payload := bytes.TrimSpace(trimmed[len("data:"):]) + if len(payload) == 0 || bytes.Equal(payload, []byte("[DONE]")) || !json.Valid(payload) { + continue + } + + switch gjson.GetBytes(payload, "type").String() { + case "response.image_generation_call.partial_image": + b64 := strings.TrimSpace(gjson.GetBytes(payload, "partial_image_b64").String()) + if b64 == "" { + continue + } + outputFormat := strings.TrimSpace(gjson.GetBytes(payload, "output_format").String()) + index := gjson.GetBytes(payload, "partial_image_index").Int() + eventName := streamPrefix + ".partial_image" + data := []byte(`{"type":"","partial_image_index":0}`) + data, _ = sjson.SetBytes(data, "type", eventName) + data, _ = sjson.SetBytes(data, "partial_image_index", index) + if responseFormat == "url" { + mt := mimeTypeFromOutputFormat(outputFormat) + data, _ = sjson.SetBytes(data, "url", "data:"+mt+";base64,"+b64) + } else { + data, _ = sjson.SetBytes(data, "b64_json", b64) + } + writeEvent(eventName, data) + case "response.completed": + results, _, usageRaw, _, err := extractImagesFromResponsesCompleted(payload) + if err != nil { + emitError(&interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: err}) + return true + } + if len(results) == 0 { + emitError(&interfaces.ErrorMessage{StatusCode: http.StatusBadGateway, Error: fmt.Errorf("upstream did not return image output")}) + return true + } + eventName := streamPrefix + ".completed" + for _, img := range results { + data := []byte(`{"type":""}`) + data, _ = sjson.SetBytes(data, "type", eventName) + if responseFormat == "url" { + mt := mimeTypeFromOutputFormat(img.OutputFormat) + data, _ = sjson.SetBytes(data, "url", "data:"+mt+";base64,"+img.Result) + } else { + data, _ = sjson.SetBytes(data, "b64_json", img.Result) + } + if len(usageRaw) > 0 && json.Valid(usageRaw) { + data, _ = sjson.SetRawBytes(data, "usage", usageRaw) + } + writeEvent(eventName, data) + } + return true + } + } + return false + } + + for _, frame := range acc.AddChunk(firstChunk) { + if processFrame(frame) { + cancel(nil) + return + } + } + + for { + select { + case <-c.Request.Context().Done(): + cancel(c.Request.Context().Err()) + return + case errMsg, ok := <-errs: + if ok && errMsg != nil { + emitError(errMsg) + cancel(errMsg.Error) + return + } + errs = nil + case chunk, ok := <-data: + if !ok { + for _, frame := range acc.Flush() { + if processFrame(frame) { + cancel(nil) + return + } + } + cancel(nil) + return + } + for _, frame := range acc.AddChunk(chunk) { + if processFrame(frame) { + cancel(nil) + return + } + } + } + } +} diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 5e873d370b..fa0d8a0aa7 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -1410,7 +1410,7 @@ func buildCodexConfigModels(entry *config.CodexKey) []*ModelInfo { if entry == nil { return nil } - return buildConfigModels(entry.Models, "openai", "openai") + return registry.WithCodexBuiltins(buildConfigModels(entry.Models, "openai", "openai")) } func rewriteModelInfoName(name, oldID, newID string) string { From fd71960c3eecf8a56a075dfd83eb275bcd93d9b1 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 22 Apr 2026 21:12:50 +0800 Subject: [PATCH 17/22] fix(handlers): remove handling of unsupported `n` parameter in OpenAI image handlers --- sdk/api/handlers/openai/openai_images_handlers.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/api/handlers/openai/openai_images_handlers.go b/sdk/api/handlers/openai/openai_images_handlers.go index 586354dedb..7c96ae88cb 100644 --- a/sdk/api/handlers/openai/openai_images_handlers.go +++ b/sdk/api/handlers/openai/openai_images_handlers.go @@ -383,9 +383,11 @@ func (h *OpenAIAPIHandler) imagesEditsFromMultipart(c *gin.Context) { if v := strings.TrimSpace(c.PostForm("partial_images")); v != "" { tool, _ = sjson.SetBytes(tool, "partial_images", parseIntField(v, 0)) } - if v := strings.TrimSpace(c.PostForm("n")); v != "" { - tool, _ = sjson.SetBytes(tool, "n", parseIntField(v, 0)) - } + + // Unsupported parameter + // if v := strings.TrimSpace(c.PostForm("n")); v != "" { + // tool, _ = sjson.SetBytes(tool, "n", parseIntField(v, 0)) + // } if maskDataURL != nil && strings.TrimSpace(*maskDataURL) != "" { tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", strings.TrimSpace(*maskDataURL)) From a188159632429b3400d5dadd2b0322afba60de3c Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 22 Apr 2026 21:28:17 +0800 Subject: [PATCH 18/22] fix(handlers): remove references to unsupported `n` parameter in OpenAI image handlers --- sdk/api/handlers/openai/openai_images_handlers.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/sdk/api/handlers/openai/openai_images_handlers.go b/sdk/api/handlers/openai/openai_images_handlers.go index 7c96ae88cb..93d45460d0 100644 --- a/sdk/api/handlers/openai/openai_images_handlers.go +++ b/sdk/api/handlers/openai/openai_images_handlers.go @@ -240,11 +240,6 @@ func (h *OpenAIAPIHandler) ImagesGenerations(c *gin.Context) { tool, _ = sjson.SetBytes(tool, "partial_images", v.Int()) } } - if v := gjson.GetBytes(rawJSON, "n"); v.Exists() { - if v.Type == gjson.Number { - tool, _ = sjson.SetBytes(tool, "n", v.Int()) - } - } if v := strings.TrimSpace(gjson.GetBytes(rawJSON, "moderation").String()); v != "" { tool, _ = sjson.SetBytes(tool, "moderation", v) } @@ -384,11 +379,6 @@ func (h *OpenAIAPIHandler) imagesEditsFromMultipart(c *gin.Context) { tool, _ = sjson.SetBytes(tool, "partial_images", parseIntField(v, 0)) } - // Unsupported parameter - // if v := strings.TrimSpace(c.PostForm("n")); v != "" { - // tool, _ = sjson.SetBytes(tool, "n", parseIntField(v, 0)) - // } - if maskDataURL != nil && strings.TrimSpace(*maskDataURL) != "" { tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", strings.TrimSpace(*maskDataURL)) } @@ -489,7 +479,7 @@ func (h *OpenAIAPIHandler) imagesEditsFromJSON(c *gin.Context) { } } - for _, field := range []string{"output_compression", "partial_images", "n"} { + for _, field := range []string{"output_compression", "partial_images"} { if v := gjson.GetBytes(rawJSON, field); v.Exists() && v.Type == gjson.Number { tool, _ = sjson.SetBytes(tool, field, v.Int()) } From a1540a1e78a2cc7079ac88cc623c9fab0c683699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E9=9B=A8=E6=99=A8?= Date: Thu, 23 Apr 2026 19:20:52 +0800 Subject: [PATCH 19/22] Add release-stable workflow file --- .github/workflows/{release.yaml => release-stable.yaml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{release.yaml => release-stable.yaml} (98%) diff --git a/.github/workflows/release.yaml b/.github/workflows/release-stable.yaml similarity index 98% rename from .github/workflows/release.yaml rename to .github/workflows/release-stable.yaml index 4043e4a5dd..201c1cabe3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release-stable.yaml @@ -4,7 +4,7 @@ on: push: # run only against tags tags: - - '*' + - 'v*' permissions: contents: write From c7450cebb0f8a3c5ec9bb35dbd8fbc84fc648c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E9=9B=A8=E6=99=A8?= Date: Thu, 23 Apr 2026 19:21:51 +0800 Subject: [PATCH 20/22] Change tag filter to 'dev*' for release workflow --- .github/workflows/release-dev.yaml | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/release-dev.yaml diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml new file mode 100644 index 0000000000..4ea31ff83c --- /dev/null +++ b/.github/workflows/release-dev.yaml @@ -0,0 +1,42 @@ +name: goreleaser + +on: + push: + # run only against tags + tags: + - 'dev*' + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Refresh models catalog + run: | + git fetch --depth 1 https://github.com/router-for-me/models.git main + git show FETCH_HEAD:models.json > internal/registry/models/models.json + - run: git fetch --force --tags + - uses: actions/setup-go@v4 + with: + go-version: '>=1.26.0' + cache: true + - name: Generate Build Metadata + run: | + echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV + echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV + echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV + - uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --clean --skip=validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ env.VERSION }} + COMMIT: ${{ env.COMMIT }} + BUILD_DATE: ${{ env.BUILD_DATE }} From 1ea10dfbd1c046644b624a22c1437dbd6f04ebad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E9=9B=A8=E6=99=A8?= Date: Thu, 23 Apr 2026 19:26:06 +0800 Subject: [PATCH 21/22] Update tag pattern for dev release workflow --- .github/workflows/release-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 4ea31ff83c..5db9e4bc19 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -4,7 +4,7 @@ on: push: # run only against tags tags: - - 'dev*' + - 'v*-dev*' permissions: contents: write From 438afd9c32c563af2a165e7f4e7c678b41add333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=BE=E9=9B=A8=E6=99=A8?= Date: Thu, 23 Apr 2026 19:29:14 +0800 Subject: [PATCH 22/22] Delete .github/workflows/release-dev.yaml --- .github/workflows/release-dev.yaml | 42 ------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/release-dev.yaml diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml deleted file mode 100644 index 5db9e4bc19..0000000000 --- a/.github/workflows/release-dev.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: goreleaser - -on: - push: - # run only against tags - tags: - - 'v*-dev*' - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Refresh models catalog - run: | - git fetch --depth 1 https://github.com/router-for-me/models.git main - git show FETCH_HEAD:models.json > internal/registry/models/models.json - - run: git fetch --force --tags - - uses: actions/setup-go@v4 - with: - go-version: '>=1.26.0' - cache: true - - name: Generate Build Metadata - run: | - echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_ENV - echo COMMIT=`git rev-parse --short HEAD` >> $GITHUB_ENV - echo BUILD_DATE=`date -u +%Y-%m-%dT%H:%M:%SZ` >> $GITHUB_ENV - - uses: goreleaser/goreleaser-action@v4 - with: - distribution: goreleaser - version: latest - args: release --clean --skip=validate - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ env.VERSION }} - COMMIT: ${{ env.COMMIT }} - BUILD_DATE: ${{ env.BUILD_DATE }}