diff --git a/internal/httpapi/claude/handler_messages.go b/internal/httpapi/claude/handler_messages.go index e424503..ad8f54e 100644 --- a/internal/httpapi/claude/handler_messages.go +++ b/internal/httpapi/claude/handler_messages.go @@ -52,7 +52,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C } } translatedReq := translatorcliproxy.ToOpenAI(sdktranslator.FormatClaude, translateModel, raw, stream) - translatedReq = applyClaudeThinkingPolicyToOpenAIRequest(translatedReq, req) + translatedReq, exposeThinking := applyClaudeThinkingPolicyToOpenAIRequest(translatedReq, req, stream) isVercelPrepare := strings.TrimSpace(r.URL.Query().Get("__stream_prepare")) == "1" isVercelRelease := strings.TrimSpace(r.URL.Query().Get("__stream_release")) == "1" @@ -118,23 +118,26 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C return true } converted := translatorcliproxy.FromOpenAINonStream(sdktranslator.FormatClaude, model, raw, translatedReq, body) + if !exposeThinking { + converted = stripClaudeThinkingBlocks(converted) + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(converted) return true } -func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any) []byte { +func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any, stream bool) ([]byte, bool) { req := map[string]any{} if err := json.Unmarshal(translated, &req); err != nil { - return translated + return translated, false } enabled, ok := util.ResolveThinkingOverride(original) if !ok { if _, translatedHasOverride := util.ResolveThinkingOverride(req); translatedHasOverride { - return translated + return translated, false } - enabled = false + enabled = !stream } typ := "disabled" if enabled { @@ -143,7 +146,33 @@ func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[st req["thinking"] = map[string]any{"type": typ} out, err := json.Marshal(req) if err != nil { - return translated + return translated, ok && enabled + } + return out, ok && enabled +} + +func stripClaudeThinkingBlocks(raw []byte) []byte { + var payload map[string]any + if err := json.Unmarshal(raw, &payload); err != nil { + return raw + } + content, _ := payload["content"].([]any) + if len(content) == 0 { + return raw + } + filtered := make([]any, 0, len(content)) + for _, item := range content { + block, _ := item.(map[string]any) + blockType, _ := block["type"].(string) + if strings.TrimSpace(blockType) == "thinking" { + continue + } + filtered = append(filtered, item) + } + payload["content"] = filtered + out, err := json.Marshal(payload) + if err != nil { + return raw } return out } diff --git a/internal/httpapi/claude/proxy_vercel_test.go b/internal/httpapi/claude/proxy_vercel_test.go index 2eff38b..a8a9cd4 100644 --- a/internal/httpapi/claude/proxy_vercel_test.go +++ b/internal/httpapi/claude/proxy_vercel_test.go @@ -126,7 +126,7 @@ func TestClaudeProxyViaOpenAIPreservesThinkingOverride(t *testing.T) { } } -func TestClaudeProxyViaOpenAIDisablesThinkingByDefault(t *testing.T) { +func TestClaudeProxyViaOpenAIEnablesThinkingInternallyByDefaultForNonStream(t *testing.T) { openAI := &openAIProxyCaptureStub{} h := &Handler{ Store: claudeProxyStoreStub{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}, @@ -141,8 +141,8 @@ func TestClaudeProxyViaOpenAIDisablesThinkingByDefault(t *testing.T) { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } thinking, _ := openAI.seenReq["thinking"].(map[string]any) - if thinking["type"] != "disabled" { - t.Fatalf("expected Claude default to disable downstream thinking, got %#v", openAI.seenReq) + if thinking["type"] != "enabled" { + t.Fatalf("expected Claude non-stream default to enable downstream thinking internally, got %#v", openAI.seenReq) } } @@ -166,6 +166,43 @@ func TestClaudeProxyViaOpenAIEnablesThinkingWhenRequested(t *testing.T) { } } +func TestClaudeProxyViaOpenAIKeepsStreamDefaultThinkingDisabled(t *testing.T) { + openAI := &openAIProxyCaptureStub{} + h := &Handler{ + Store: claudeProxyStoreStub{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}}, + OpenAI: openAI, + } + req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}],"stream":true}`)) + rec := httptest.NewRecorder() + + h.Messages(rec, req) + + thinking, _ := openAI.seenReq["thinking"].(map[string]any) + if thinking["type"] != "disabled" { + t.Fatalf("expected Claude stream default to keep downstream thinking disabled, got %#v", openAI.seenReq) + } +} + +func TestClaudeProxyViaOpenAIStripsThinkingBlocksFromNonStreamResponse(t *testing.T) { + body := `{"id":"chatcmpl_1","object":"chat.completion","created":1,"model":"claude-sonnet-4-5","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":"internal reasoning","tool_calls":[{"id":"call_1","type":"function","function":{"name":"search","arguments":"{\"q\":\"x\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}` + h := &Handler{OpenAI: openAIProxyStub{status: 200, body: body}} + req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", strings.NewReader(`{"model":"claude-sonnet-4-5","messages":[{"role":"user","content":"hi"}],"stream":false}`)) + rec := httptest.NewRecorder() + + h.Messages(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) + } + got := rec.Body.String() + if strings.Contains(got, `"type":"thinking"`) { + t.Fatalf("expected converted Claude response to strip thinking block, got %s", got) + } + if !strings.Contains(got, `"tool_use"`) { + t.Fatalf("expected converted Claude response to preserve tool_use, got %s", got) + } +} + func TestClaudeProxyTranslatesInlineImageToOpenAIDataURL(t *testing.T) { openAI := &openAIProxyCaptureStub{} h := &Handler{OpenAI: openAI}