From 0b834fcb543859a35b7141a4c5d750b0f7c45c4f Mon Sep 17 00:00:00 2001 From: Muzhen Gaming <61100393+XInTheDark@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:15:56 +0800 Subject: [PATCH 01/13] fix(translator): preserve built-in tools across openai<->responses - Pass through non-function tool definitions like web_search - Translate tool_choice for built-in tools and function tools - Add regression tests for built-in tool passthrough --- .../chat-completions/codex_openai_request.go | 41 +++++++++++++- .../openai_openai-responses_request.go | 8 +++ test/builtin_tools_translation_test.go | 54 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 test/builtin_tools_translation_test.go diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index 272037da..309c974e 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -275,7 +275,15 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b arr := tools.Array() for i := 0; i < len(arr); i++ { t := arr[i] - if t.Get("type").String() == "function" { + toolType := t.Get("type").String() + // Pass through built-in tools (e.g. {"type":"web_search"}) directly for the Responses API. + // Only "function" needs structural conversion because Chat Completions nests details under "function". + if toolType != "" && toolType != "function" && t.IsObject() { + out, _ = sjson.SetRaw(out, "tools.-1", t.Raw) + continue + } + + if toolType == "function" { item := `{}` item, _ = sjson.Set(item, "type", "function") fn := t.Get("function") @@ -304,6 +312,37 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b } } + // Map tool_choice when present. + // Chat Completions: "tool_choice" can be a string ("auto"/"none") or an object (e.g. {"type":"function","function":{"name":"..."}}). + // Responses API: keep built-in tool choices as-is; flatten function choice to {"type":"function","name":"..."}. + if tc := gjson.GetBytes(rawJSON, "tool_choice"); tc.Exists() { + switch { + case tc.Type == gjson.String: + out, _ = sjson.Set(out, "tool_choice", tc.String()) + case tc.IsObject(): + tcType := tc.Get("type").String() + if tcType == "function" { + name := tc.Get("function.name").String() + if name != "" { + if short, ok := originalToolNameMap[name]; ok { + name = short + } else { + name = shortenNameIfNeeded(name) + } + } + choice := `{}` + choice, _ = sjson.Set(choice, "type", "function") + if name != "" { + choice, _ = sjson.Set(choice, "name", name) + } + out, _ = sjson.SetRaw(out, "tool_choice", choice) + } else if tcType != "" { + // Built-in tool choices (e.g. {"type":"web_search"}) are already Responses-compatible. + out, _ = sjson.SetRaw(out, "tool_choice", tc.Raw) + } + } + } + out, _ = sjson.Set(out, "store", false) return []byte(out) } diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request.go b/internal/translator/openai/openai/responses/openai_openai-responses_request.go index 687c2a30..86cf19f8 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request.go @@ -163,6 +163,14 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu var chatCompletionsTools []interface{} tools.ForEach(func(_, tool gjson.Result) bool { + // Built-in tools (e.g. {"type":"web_search"}) are already compatible with the Chat Completions schema. + // Only function tools need structural conversion because Chat Completions nests details under "function". + toolType := tool.Get("type").String() + if toolType != "" && toolType != "function" && tool.IsObject() { + chatCompletionsTools = append(chatCompletionsTools, tool.Value()) + return true + } + chatTool := `{"type":"function","function":{}}` // Convert tool structure from responses format to chat completions format diff --git a/test/builtin_tools_translation_test.go b/test/builtin_tools_translation_test.go new file mode 100644 index 00000000..b4ca7b0d --- /dev/null +++ b/test/builtin_tools_translation_test.go @@ -0,0 +1,54 @@ +package test + +import ( + "testing" + + _ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator" + + sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + "github.com/tidwall/gjson" +) + +func TestOpenAIToCodex_PreservesBuiltinTools(t *testing.T) { + in := []byte(`{ + "model":"gpt-5", + "messages":[{"role":"user","content":"hi"}], + "tools":[{"type":"web_search","search_context_size":"high"}], + "tool_choice":{"type":"web_search"} + }`) + + out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAI, sdktranslator.FormatCodex, "gpt-5", in, false) + + if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 { + t.Fatalf("expected 1 tool, got %d: %s", got, string(out)) + } + if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" { + t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out)) + } + if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "high" { + t.Fatalf("expected tools[0].search_context_size=high, got %q: %s", got, string(out)) + } + if got := gjson.GetBytes(out, "tool_choice.type").String(); got != "web_search" { + t.Fatalf("expected tool_choice.type=web_search, got %q: %s", got, string(out)) + } +} + +func TestOpenAIResponsesToOpenAI_PreservesBuiltinTools(t *testing.T) { + in := []byte(`{ + "model":"gpt-5", + "input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}], + "tools":[{"type":"web_search","search_context_size":"low"}] + }`) + + out := sdktranslator.TranslateRequest(sdktranslator.FormatOpenAIResponse, sdktranslator.FormatOpenAI, "gpt-5", in, false) + + if got := gjson.GetBytes(out, "tools.#").Int(); got != 1 { + t.Fatalf("expected 1 tool, got %d: %s", got, string(out)) + } + if got := gjson.GetBytes(out, "tools.0.type").String(); got != "web_search" { + t.Fatalf("expected tools[0].type=web_search, got %q: %s", got, string(out)) + } + if got := gjson.GetBytes(out, "tools.0.search_context_size").String(); got != "low" { + t.Fatalf("expected tools[0].search_context_size=low, got %q: %s", got, string(out)) + } +} From 31bd90c74865a9ef4509e36972bf2420b06d70b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8C=80=ED=9D=AC?= Date: Fri, 19 Dec 2025 08:15:54 +0900 Subject: [PATCH 02/13] feature: Improves Amp client compatibility Ensures compatibility with the Amp client by suppressing "thinking" blocks when "tool_use" blocks are also present in the response. The Amp client has issues rendering both types of blocks simultaneously. This change filters out "thinking" blocks in such cases, preventing rendering problems. --- internal/api/modules/amp/response_rewriter.go | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/api/modules/amp/response_rewriter.go b/internal/api/modules/amp/response_rewriter.go index de6ba137..35888116 100644 --- a/internal/api/modules/amp/response_rewriter.go +++ b/internal/api/modules/amp/response_rewriter.go @@ -69,7 +69,32 @@ func (rw *ResponseRewriter) Flush() { var modelFieldPaths = []string{"model", "modelVersion", "response.modelVersion", "message.model"} // rewriteModelInResponse replaces all occurrences of the mapped model with the original model in JSON +// It also suppresses "thinking" blocks if "tool_use" is present to ensure Amp client compatibility func (rw *ResponseRewriter) rewriteModelInResponse(data []byte) []byte { + // 1. Amp Compatibility: Suppress thinking blocks if tool use is detected + // The Amp client struggles when both thinking and tool_use blocks are present + // 1. Amp Compatibility: Suppress thinking blocks if tool use is detected + // The Amp client struggles when both thinking and tool_use blocks are present + if gjson.GetBytes(data, `content.#(type=="tool_use")`).Exists() { + filtered := gjson.GetBytes(data, `content.#(type!="thinking")#`) + if filtered.Exists() { + originalCount := gjson.GetBytes(data, "content.#").Int() + filteredCount := filtered.Get("#").Int() + + if originalCount > filteredCount { + var err error + data, err = sjson.SetBytes(data, "content", filtered.Value()) + if err != nil { + log.Warnf("Amp ResponseRewriter: failed to suppress thinking blocks: %v", err) + } else { + log.Debugf("Amp ResponseRewriter: Suppressed %d thinking blocks due to tool usage", originalCount-filteredCount) + // Log the result for verification + log.Debugf("Amp ResponseRewriter: Resulting content: %s", gjson.GetBytes(data, "content").String()) + } + } + } + } + if rw.originalModel == "" { return data } From 0e4148b2296f25df2df5966909268c29a0cf5d22 Mon Sep 17 00:00:00 2001 From: Michael Velbaum Date: Sun, 28 Dec 2025 15:22:36 +0200 Subject: [PATCH 03/13] feat(logging): disambiguate OAuth credential selection in debug logs When multiple OAuth providers share an account email, the existing "Use OAuth" debug lines are ambiguous and hard to correlate with management usage stats. Include provider, auth file, and auth index in the selection log, and only compute these fields when debug logging is enabled to avoid impacting normal request performance. Before: [debug] Use OAuth user@example.com for model gemini-3-flash-preview [debug] Use OAuth user@example.com (project-1234) for model gemini-3-flash-preview After: [debug] Use OAuth provider=antigravity auth_file=antigravity-user_example_com.json auth_index=1a2b3c4d5e6f7788 account="user@example.com" for model gemini-3-flash-preview [debug] Use OAuth provider=gemini-cli auth_file=gemini-user@example.com-project-1234.json auth_index=99aabbccddeeff00 account="user@example.com (project-1234)" for model gemini-3-flash-preview --- sdk/cliproxy/auth/conductor.go | 126 +++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index d16fc1ae..df3d8b3e 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "net/http" + "path/filepath" "strconv" "strings" "sync" @@ -385,20 +386,23 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req return cliproxyexecutor.Response{}, errPick } - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() entry := logEntryWithRequestID(ctx) - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", accountInfo, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + accountType, accountInfo := auth.AccountInfo() + proxyInfo := auth.ProxyInfo() + if accountType == "api_key" { + if proxyInfo != "" { + entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) + } else { + entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) + } + } else if accountType == "oauth" { + ident := formatOauthIdentity(auth, provider, accountInfo) + if proxyInfo != "" { + entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) + } else { + entry.Debugf("Use OAuth %s for model %s", ident, req.Model) + } } } @@ -446,20 +450,23 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, return cliproxyexecutor.Response{}, errPick } - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() entry := logEntryWithRequestID(ctx) - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", accountInfo, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + accountType, accountInfo := auth.AccountInfo() + proxyInfo := auth.ProxyInfo() + if accountType == "api_key" { + if proxyInfo != "" { + entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) + } else { + entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) + } + } else if accountType == "oauth" { + ident := formatOauthIdentity(auth, provider, accountInfo) + if proxyInfo != "" { + entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) + } else { + entry.Debugf("Use OAuth %s for model %s", ident, req.Model) + } } } @@ -507,20 +514,23 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string return nil, errPick } - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() entry := logEntryWithRequestID(ctx) - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", accountInfo, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + accountType, accountInfo := auth.AccountInfo() + proxyInfo := auth.ProxyInfo() + if accountType == "api_key" { + if proxyInfo != "" { + entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) + } else { + entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) + } + } else if accountType == "oauth" { + ident := formatOauthIdentity(auth, provider, accountInfo) + if proxyInfo != "" { + entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) + } else { + entry.Debugf("Use OAuth %s for model %s", ident, req.Model) + } } } @@ -1610,6 +1620,44 @@ func logEntryWithRequestID(ctx context.Context) *log.Entry { return log.NewEntry(log.StandardLogger()) } +func formatOauthIdentity(auth *Auth, provider string, accountInfo string) string { + if auth == nil { + return "" + } + authIndex := auth.EnsureIndex() + // Prefer the auth's provider when available. + providerName := strings.TrimSpace(auth.Provider) + if providerName == "" { + providerName = strings.TrimSpace(provider) + } + // Only log the basename to avoid leaking host paths. + // FileName may be unset for some auth backends; fall back to ID. + authFile := strings.TrimSpace(auth.FileName) + if authFile == "" { + authFile = strings.TrimSpace(auth.ID) + } + if authFile != "" { + authFile = filepath.Base(authFile) + } + parts := make([]string, 0, 3) + if providerName != "" { + parts = append(parts, "provider="+providerName) + } + if authFile != "" { + parts = append(parts, "auth_file="+authFile) + } + if authIndex != "" { + parts = append(parts, "auth_index="+authIndex) + } + if len(parts) == 0 { + return accountInfo + } + if accountInfo == "" { + return strings.Join(parts, " ") + } + return strings.Join(parts, " ") + " account=\"" + accountInfo + "\"" +} + // InjectCredentials delegates per-provider HTTP request preparation when supported. // If the registered executor for the auth provider implements RequestPreparer, // it will be invoked to modify the request (e.g., add headers). From 79fbcb3ec40ba919d72fbe2d17e0392e7b5dfa4a Mon Sep 17 00:00:00 2001 From: Michael Velbaum Date: Sun, 28 Dec 2025 15:32:54 +0200 Subject: [PATCH 04/13] fix(logging): quote OAuth account field Use strconv.Quote when embedding the OAuth account in debug logs so unexpected characters (e.g. quotes) can't break key=value parsing. --- sdk/cliproxy/auth/conductor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index df3d8b3e..281216ed 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -1655,7 +1655,7 @@ func formatOauthIdentity(auth *Auth, provider string, accountInfo string) string if accountInfo == "" { return strings.Join(parts, " ") } - return strings.Join(parts, " ") + " account=\"" + accountInfo + "\"" + return strings.Join(parts, " ") + " account=" + strconv.Quote(accountInfo) } // InjectCredentials delegates per-provider HTTP request preparation when supported. From 48f6d7abdf8409fef1f67c5a3c5ff49123a500e1 Mon Sep 17 00:00:00 2001 From: Michael Velbaum Date: Sun, 28 Dec 2025 15:42:35 +0200 Subject: [PATCH 05/13] refactor(logging): dedupe auth selection debug logs Extract repeated debug logging for selected auth credentials into a helper so execute, count, and stream paths stay consistent. --- sdk/cliproxy/auth/conductor.go | 89 ++++++++++++---------------------- 1 file changed, 32 insertions(+), 57 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 281216ed..fe41ae01 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -386,25 +386,7 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req return cliproxyexecutor.Response{}, errPick } - entry := logEntryWithRequestID(ctx) - if log.IsLevelEnabled(log.DebugLevel) { - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - ident := formatOauthIdentity(auth, provider, accountInfo) - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", ident, req.Model) - } - } - } + debugLogAuthSelection(ctx, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -450,25 +432,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, return cliproxyexecutor.Response{}, errPick } - entry := logEntryWithRequestID(ctx) - if log.IsLevelEnabled(log.DebugLevel) { - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - ident := formatOauthIdentity(auth, provider, accountInfo) - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", ident, req.Model) - } - } - } + debugLogAuthSelection(ctx, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -514,25 +478,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string return nil, errPick } - entry := logEntryWithRequestID(ctx) - if log.IsLevelEnabled(log.DebugLevel) { - accountType, accountInfo := auth.AccountInfo() - proxyInfo := auth.ProxyInfo() - if accountType == "api_key" { - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), req.Model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), req.Model) - } - } else if accountType == "oauth" { - ident := formatOauthIdentity(auth, provider, accountInfo) - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", ident, req.Model, proxyInfo) - } else { - entry.Debugf("Use OAuth %s for model %s", ident, req.Model) - } - } - } + debugLogAuthSelection(ctx, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -1620,6 +1566,35 @@ func logEntryWithRequestID(ctx context.Context) *log.Entry { return log.NewEntry(log.StandardLogger()) } +func debugLogAuthSelection(ctx context.Context, auth *Auth, provider string, model string) { + if !log.IsLevelEnabled(log.DebugLevel) { + return + } + if auth == nil { + return + } + entry := logEntryWithRequestID(ctx) + accountType, accountInfo := auth.AccountInfo() + proxyInfo := auth.ProxyInfo() + if accountType == "api_key" { + if proxyInfo != "" { + entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), model, proxyInfo) + } else { + entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), model) + } + return + } + if accountType != "oauth" { + return + } + ident := formatOauthIdentity(auth, provider, accountInfo) + if proxyInfo != "" { + entry.Debugf("Use OAuth %s for model %s %s", ident, model, proxyInfo) + return + } + entry.Debugf("Use OAuth %s for model %s", ident, model) +} + func formatOauthIdentity(auth *Auth, provider string, accountInfo string) string { if auth == nil { return "" From 48f19aab51d6656469e255c53689f61f0337b692 Mon Sep 17 00:00:00 2001 From: Michael Velbaum Date: Sun, 28 Dec 2025 15:51:11 +0200 Subject: [PATCH 06/13] refactor(logging): pass request entry into auth selection log Avoid re-creating the request-scoped log entry in the helper and use a switch for account type dispatch. --- sdk/cliproxy/auth/conductor.go | 47 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index fe41ae01..330d308a 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -386,7 +386,10 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req return cliproxyexecutor.Response{}, errPick } - debugLogAuthSelection(ctx, auth, provider, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -432,7 +435,10 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, return cliproxyexecutor.Response{}, errPick } - debugLogAuthSelection(ctx, auth, provider, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -478,7 +484,10 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string return nil, errPick } - debugLogAuthSelection(ctx, auth, provider, req.Model) + if log.IsLevelEnabled(log.DebugLevel) { + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) + } tried[auth.ID] = struct{}{} execCtx := ctx @@ -1566,33 +1575,27 @@ func logEntryWithRequestID(ctx context.Context) *log.Entry { return log.NewEntry(log.StandardLogger()) } -func debugLogAuthSelection(ctx context.Context, auth *Auth, provider string, model string) { - if !log.IsLevelEnabled(log.DebugLevel) { +func debugLogAuthSelection(entry *log.Entry, auth *Auth, provider string, model string) { + if entry == nil || auth == nil { return } - if auth == nil { - return - } - entry := logEntryWithRequestID(ctx) accountType, accountInfo := auth.AccountInfo() proxyInfo := auth.ProxyInfo() - if accountType == "api_key" { + switch accountType { + case "api_key": if proxyInfo != "" { entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), model, proxyInfo) - } else { - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), model) + return } - return + entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), model) + case "oauth": + ident := formatOauthIdentity(auth, provider, accountInfo) + if proxyInfo != "" { + entry.Debugf("Use OAuth %s for model %s %s", ident, model, proxyInfo) + return + } + entry.Debugf("Use OAuth %s for model %s", ident, model) } - if accountType != "oauth" { - return - } - ident := formatOauthIdentity(auth, provider, accountInfo) - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", ident, model, proxyInfo) - return - } - entry.Debugf("Use OAuth %s for model %s", ident, model) } func formatOauthIdentity(auth *Auth, provider string, accountInfo string) string { From cb3bdffb43a58ff32760a22222980648da18be32 Mon Sep 17 00:00:00 2001 From: Michael Velbaum Date: Sun, 28 Dec 2025 16:10:11 +0200 Subject: [PATCH 07/13] refactor(logging): streamline auth selection debug messages Reduce duplicate Debugf calls by appending proxy info via an optional suffix and keep the debug-level guard inside the helper. --- sdk/cliproxy/auth/conductor.go | 37 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/sdk/cliproxy/auth/conductor.go b/sdk/cliproxy/auth/conductor.go index 330d308a..ada40063 100644 --- a/sdk/cliproxy/auth/conductor.go +++ b/sdk/cliproxy/auth/conductor.go @@ -386,10 +386,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req return cliproxyexecutor.Response{}, errPick } - if log.IsLevelEnabled(log.DebugLevel) { - entry := logEntryWithRequestID(ctx) - debugLogAuthSelection(entry, auth, provider, req.Model) - } + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -435,10 +433,8 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, return cliproxyexecutor.Response{}, errPick } - if log.IsLevelEnabled(log.DebugLevel) { - entry := logEntryWithRequestID(ctx) - debugLogAuthSelection(entry, auth, provider, req.Model) - } + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -484,10 +480,8 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string return nil, errPick } - if log.IsLevelEnabled(log.DebugLevel) { - entry := logEntryWithRequestID(ctx) - debugLogAuthSelection(entry, auth, provider, req.Model) - } + entry := logEntryWithRequestID(ctx) + debugLogAuthSelection(entry, auth, provider, req.Model) tried[auth.ID] = struct{}{} execCtx := ctx @@ -1576,25 +1570,24 @@ func logEntryWithRequestID(ctx context.Context) *log.Entry { } func debugLogAuthSelection(entry *log.Entry, auth *Auth, provider string, model string) { + if !log.IsLevelEnabled(log.DebugLevel) { + return + } if entry == nil || auth == nil { return } accountType, accountInfo := auth.AccountInfo() proxyInfo := auth.ProxyInfo() + suffix := "" + if proxyInfo != "" { + suffix = " " + proxyInfo + } switch accountType { case "api_key": - if proxyInfo != "" { - entry.Debugf("Use API key %s for model %s %s", util.HideAPIKey(accountInfo), model, proxyInfo) - return - } - entry.Debugf("Use API key %s for model %s", util.HideAPIKey(accountInfo), model) + entry.Debugf("Use API key %s for model %s%s", util.HideAPIKey(accountInfo), model, suffix) case "oauth": ident := formatOauthIdentity(auth, provider, accountInfo) - if proxyInfo != "" { - entry.Debugf("Use OAuth %s for model %s %s", ident, model, proxyInfo) - return - } - entry.Debugf("Use OAuth %s for model %s", ident, model) + entry.Debugf("Use OAuth %s for model %s%s", ident, model, suffix) } } From 414db44c006c9448575da091fd1f3a36a7697a37 Mon Sep 17 00:00:00 2001 From: sususu Date: Tue, 30 Dec 2025 16:07:32 +0800 Subject: [PATCH 08/13] fix(antigravity): parse retry-after delay from 429 response body When receiving HTTP 429 (Too Many Requests) responses, parse the retry delay from the response body using parseRetryDelay and populate the statusErr.retryAfter field. This allows upstream callers to respect the server's requested retry timing. Applied to all error paths in Execute, executeClaudeNonStream, ExecuteStream, CountTokens, and refreshToken functions. --- .../runtime/executor/antigravity_executor.go | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 9ade4fbb..38dc0b84 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -155,7 +155,13 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au log.Debugf("antigravity executor: rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } - err = statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + sErr := statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + if httpResp.StatusCode == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(bodyBytes); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr return resp, err } @@ -169,7 +175,13 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au switch { case lastStatus != 0: - err = statusErr{code: lastStatus, msg: string(lastBody)} + sErr := statusErr{code: lastStatus, msg: string(lastBody)} + if lastStatus == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(lastBody); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr case lastErr != nil: err = lastErr default: @@ -259,7 +271,13 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * log.Debugf("antigravity executor: rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } - err = statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + sErr := statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + if httpResp.StatusCode == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(bodyBytes); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr return resp, err } @@ -324,7 +342,13 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * switch { case lastStatus != 0: - err = statusErr{code: lastStatus, msg: string(lastBody)} + sErr := statusErr{code: lastStatus, msg: string(lastBody)} + if lastStatus == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(lastBody); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr case lastErr != nil: err = lastErr default: @@ -599,7 +623,13 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya log.Debugf("antigravity executor: rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } - err = statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + sErr := statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + if httpResp.StatusCode == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(bodyBytes); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr return nil, err } @@ -654,7 +684,13 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya switch { case lastStatus != 0: - err = statusErr{code: lastStatus, msg: string(lastBody)} + sErr := statusErr{code: lastStatus, msg: string(lastBody)} + if lastStatus == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(lastBody); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + err = sErr case lastErr != nil: err = lastErr default: @@ -795,12 +831,24 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut log.Debugf("antigravity executor: rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } - return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + sErr := statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + if httpResp.StatusCode == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(bodyBytes); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + return cliproxyexecutor.Response{}, sErr } switch { case lastStatus != 0: - return cliproxyexecutor.Response{}, statusErr{code: lastStatus, msg: string(lastBody)} + sErr := statusErr{code: lastStatus, msg: string(lastBody)} + if lastStatus == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(lastBody); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + return cliproxyexecutor.Response{}, sErr case lastErr != nil: return cliproxyexecutor.Response{}, lastErr default: @@ -963,7 +1011,13 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau } if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices { - return auth, statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + sErr := statusErr{code: httpResp.StatusCode, msg: string(bodyBytes)} + if httpResp.StatusCode == http.StatusTooManyRequests { + if retryAfter, parseErr := parseRetryDelay(bodyBytes); parseErr == nil && retryAfter != nil { + sErr.retryAfter = retryAfter + } + } + return auth, sErr } var tokenResp struct { From 963a0950fade9998621736b33d4bbdbb7f6e9ea9 Mon Sep 17 00:00:00 2001 From: Jianyang Zhao Date: Wed, 7 Jan 2026 20:02:50 -0500 Subject: [PATCH 09/13] Add Claude Proxy VSCode extension to README Added Claude Proxy VSCode extension to the README. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 93cb125a..a7477404 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,10 @@ Native macOS SwiftUI app for managing CLI AI sessions (Codex, Claude Code, Gemin Windows-native CLIProxyAPI fork with TUI, system tray, and multi-provider OAuth for AI coding tools - no API keys needed. +### [Claude Proxy VSCode](https://github.com/uzhao/claude-proxy-vscode) + +VSCode extension 'Claude Proxy' for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management. + > [!NOTE] > If you developed a project based on CLIProxyAPI, please open a PR to add it to this list. From 3b484aea9e7149a2cd0d7411d248b95b29aba216 Mon Sep 17 00:00:00 2001 From: Jianyang Zhao Date: Wed, 7 Jan 2026 20:03:07 -0500 Subject: [PATCH 10/13] Add Claude Proxy VSCode to README_CN.md Added information about Claude Proxy VSCode extension. --- README_CN.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README_CN.md b/README_CN.md index b15808f7..bf78bf7d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -125,6 +125,10 @@ CLI 封装器,用于通过 CLIProxyAPI OAuth 即时切换多个 Claude 账户 原生 Windows CLIProxyAPI 分支,集成 TUI、系统托盘及多服务商 OAuth 认证,专为 AI 编程工具打造,无需 API 密钥。 +### [Claude Proxy VSCode](https://github.com/uzhao/claude-proxy-vscode) + +VSCode 扩展 Claude Proxy,提供了在 VSCode 中快速切换 Claude Code 模型的功能,内置 CLIProxyAPI 作为其后端,支持后台自动启动和关闭。 + > [!NOTE] > 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。 From 9fc2e1b3c8e5a479039dfa7bbd3a701ab5125f14 Mon Sep 17 00:00:00 2001 From: Jianyang Zhao Date: Wed, 7 Jan 2026 20:06:55 -0500 Subject: [PATCH 11/13] Update README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7477404..7875a989 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Windows-native CLIProxyAPI fork with TUI, system tray, and multi-provider OAuth ### [Claude Proxy VSCode](https://github.com/uzhao/claude-proxy-vscode) -VSCode extension 'Claude Proxy' for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management. +VSCode extension for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management. > [!NOTE] > If you developed a project based on CLIProxyAPI, please open a PR to add it to this list. From cbcb061812d1eedf9b0e3b0682f4de00bdf04394 Mon Sep 17 00:00:00 2001 From: Jianyang Zhao Date: Wed, 7 Jan 2026 20:07:01 -0500 Subject: [PATCH 12/13] Update README_CN.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index bf78bf7d..fdc8d64c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -127,7 +127,7 @@ CLI 封装器,用于通过 CLIProxyAPI OAuth 即时切换多个 Claude 账户 ### [Claude Proxy VSCode](https://github.com/uzhao/claude-proxy-vscode) -VSCode 扩展 Claude Proxy,提供了在 VSCode 中快速切换 Claude Code 模型的功能,内置 CLIProxyAPI 作为其后端,支持后台自动启动和关闭。 +一款 VSCode 扩展,提供了在 VSCode 中快速切换 Claude Code 模型的功能,内置 CLIProxyAPI 作为其后端,支持后台自动启动和关闭。 > [!NOTE] > 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。 From d47b7dc79ae9bd78127d3c0e3837ab55799f4948 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 9 Jan 2026 05:20:19 +0800 Subject: [PATCH 13/13] refactor(response): enhance parameter handling for Codex to Claude conversion --- .../codex/claude/codex_claude_response.go | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index e3909d45..c700ef84 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -20,6 +20,12 @@ var ( dataTag = []byte("data:") ) +// ConvertCodexResponseToClaudeParams holds parameters for response conversion. +type ConvertCodexResponseToClaudeParams struct { + HasToolCall bool + BlockIndex int +} + // ConvertCodexResponseToClaude performs sophisticated streaming response format conversion. // This function implements a complex state machine that translates Codex API responses // into Claude Code-compatible Server-Sent Events (SSE) format. It manages different response types @@ -38,8 +44,10 @@ var ( // - []string: A slice of strings, each containing a Claude Code-compatible JSON response func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { if *param == nil { - hasToolCall := false - *param = &hasToolCall + *param = &ConvertCodexResponseToClaudeParams{ + HasToolCall: false, + BlockIndex: 0, + } } // log.Debugf("rawJSON: %s", string(rawJSON)) @@ -62,46 +70,49 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.reasoning_summary_part.added" { template = `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) output = "event: content_block_start\n" output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.reasoning_summary_text.delta" { template = `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "delta.thinking", rootResult.Get("delta").String()) output = "event: content_block_delta\n" output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.reasoning_summary_part.done" { template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ output = "event: content_block_stop\n" output += fmt.Sprintf("data: %s\n\n", template) + } else if typeStr == "response.content_part.added" { template = `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) output = "event: content_block_start\n" output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.output_text.delta" { template = `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "delta.text", rootResult.Get("delta").String()) output = "event: content_block_delta\n" output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.content_part.done" { template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ output = "event: content_block_stop\n" output += fmt.Sprintf("data: %s\n\n", template) } else if typeStr == "response.completed" { template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` - p := (*param).(*bool) - if *p { + p := (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall + if p { template, _ = sjson.Set(template, "delta.stop_reason", "tool_use") } else { template, _ = sjson.Set(template, "delta.stop_reason", "end_turn") @@ -118,10 +129,9 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa itemResult := rootResult.Get("item") itemType := itemResult.Get("type").String() if itemType == "function_call" { - p := true - *param = &p + (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall = true template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "content_block.id", itemResult.Get("call_id").String()) { // Restore original tool name if shortened @@ -137,7 +147,7 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa output += fmt.Sprintf("data: %s\n\n", template) template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) output += "event: content_block_delta\n" output += fmt.Sprintf("data: %s\n\n", template) @@ -147,14 +157,15 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa itemType := itemResult.Get("type").String() if itemType == "function_call" { template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ output = "event: content_block_stop\n" output += fmt.Sprintf("data: %s\n\n", template) } } else if typeStr == "response.function_call_arguments.delta" { template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int()) + template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String()) output += "event: content_block_delta\n"