feat: implement support for thinking blocks in Gemini API and enable thinking by default for supported models

This commit is contained in:
CJACK
2026-05-03 01:00:06 +08:00
parent a901250de7
commit 7c0bc9ec0f
10 changed files with 177 additions and 33 deletions

View File

@@ -27,11 +27,32 @@ func TestNormalizeClaudeRequestUsesGlobalAliasMapping(t *testing.T) {
if out.Standard.ResolvedModel != "deepseek-v4-pro-search" {
t.Fatalf("resolved model mismatch: got=%q", out.Standard.ResolvedModel)
}
if out.Standard.Thinking || !out.Standard.Search {
if !out.Standard.Thinking || !out.Standard.Search {
t.Fatalf("unexpected flags: thinking=%v search=%v", out.Standard.Thinking, out.Standard.Search)
}
}
func TestNormalizeClaudeRequestDisablesThinkingWhenRequested(t *testing.T) {
req := map[string]any{
"model": "claude-opus-4-6",
"messages": []any{
map[string]any{"role": "user", "content": "hello"},
},
"thinking": map[string]any{"type": "disabled"},
}
out, err := normalizeClaudeRequest(mockClaudeConfig{
aliases: map[string]string{
"claude-opus-4-6": "deepseek-v4-pro",
},
}, req)
if err != nil {
t.Fatalf("normalizeClaudeRequest error: %v", err)
}
if out.Standard.Thinking {
t.Fatalf("expected explicit Claude thinking disable to win")
}
}
func TestNormalizeClaudeRequestEnablesThinkingWhenRequested(t *testing.T) {
req := map[string]any{
"model": "claude-opus-4-6",

View File

@@ -67,17 +67,12 @@ func (h *Handler) handleClaudeDirect(w http.ResponseWriter, r *http.Request) boo
writeClaudeError(w, http.StatusBadRequest, "invalid json")
return true
}
exposeThinking := false
if enabled, ok := util.ResolveThinkingOverride(req); ok && enabled {
exposeThinking = true
} else if _, ok := util.ResolveThinkingOverride(req); !ok && !util.ToBool(req["stream"]) {
req["thinking"] = map[string]any{"type": "enabled"}
}
norm, err := normalizeClaudeRequest(h.Store, req)
if err != nil {
writeClaudeError(w, http.StatusBadRequest, err.Error())
return true
}
exposeThinking := norm.Standard.Thinking
a, err := h.Auth.Determine(r)
if err != nil {
writeClaudeError(w, http.StatusUnauthorized, err.Error())
@@ -140,7 +135,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C
}
}
translatedReq := translatorcliproxy.ToOpenAI(sdktranslator.FormatClaude, translateModel, raw, stream)
translatedReq, exposeThinking := applyClaudeThinkingPolicyToOpenAIRequest(translatedReq, req, stream)
translatedReq, exposeThinking := applyClaudeThinkingPolicyToOpenAIRequest(translatedReq, req)
isVercelPrepare := strings.TrimSpace(r.URL.Query().Get("__stream_prepare")) == "1"
isVercelRelease := strings.TrimSpace(r.URL.Query().Get("__stream_release")) == "1"
@@ -215,7 +210,7 @@ func (h *Handler) proxyViaOpenAI(w http.ResponseWriter, r *http.Request, store C
return true
}
func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any, stream bool) ([]byte, bool) {
func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[string]any) ([]byte, bool) {
req := map[string]any{}
if err := json.Unmarshal(translated, &req); err != nil {
return translated, false
@@ -225,7 +220,7 @@ func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[st
if _, translatedHasOverride := util.ResolveThinkingOverride(req); translatedHasOverride {
return translated, false
}
enabled = !stream
enabled = true
}
typ := "disabled"
if enabled {
@@ -234,9 +229,9 @@ func applyClaudeThinkingPolicyToOpenAIRequest(translated []byte, original map[st
req["thinking"] = map[string]any{"type": typ}
out, err := json.Marshal(req)
if err != nil {
return translated, ok && enabled
return translated, enabled
}
return out, ok && enabled
return out, enabled
}
func stripClaudeThinkingBlocks(raw []byte) []byte {

View File

@@ -166,7 +166,7 @@ func TestClaudeProxyViaOpenAIEnablesThinkingWhenRequested(t *testing.T) {
}
}
func TestClaudeProxyViaOpenAIKeepsStreamDefaultThinkingDisabled(t *testing.T) {
func TestClaudeProxyViaOpenAIEnablesStreamThinkingByDefault(t *testing.T) {
openAI := &openAIProxyCaptureStub{}
h := &Handler{
Store: claudeProxyStoreStub{aliases: map[string]string{"claude-sonnet-4-6": "deepseek-v4-flash"}},
@@ -178,12 +178,12 @@ func TestClaudeProxyViaOpenAIKeepsStreamDefaultThinkingDisabled(t *testing.T) {
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)
if thinking["type"] != "enabled" {
t.Fatalf("expected Claude stream default to enable downstream thinking, got %#v", openAI.seenReq)
}
}
func TestClaudeProxyViaOpenAIStripsThinkingBlocksFromNonStreamResponse(t *testing.T) {
func TestClaudeProxyViaOpenAIExposesThinkingBlocksByDefault(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}`))
@@ -195,14 +195,31 @@ func TestClaudeProxyViaOpenAIStripsThinkingBlocksFromNonStreamResponse(t *testin
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, `"type":"thinking"`) {
t.Fatalf("expected converted Claude response to expose 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 TestClaudeProxyViaOpenAIStripsThinkingBlocksWhenDisabled(t *testing.T) {
body := `{"id":"chatcmpl_1","object":"chat.completion","created":1,"model":"claude-sonnet-4-5","choices":[{"index":0,"message":{"role":"assistant","content":"ok","reasoning_content":"internal reasoning"},"finish_reason":"stop"}],"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"}],"thinking":{"type":"disabled"},"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 disabled thinking to strip thinking block, got %s", got)
}
}
func TestClaudeProxyTranslatesInlineImageToOpenAIDataURL(t *testing.T) {
openAI := &openAIProxyCaptureStub{}
h := &Handler{OpenAI: openAI}

View File

@@ -32,11 +32,11 @@ func normalizeClaudeRequest(store ConfigReader, req map[string]any) (claudeNorma
dsPayload := convertClaudeToDeepSeek(payload, store)
dsModel, _ := dsPayload["model"].(string)
_, searchEnabled, ok := config.GetModelConfig(dsModel)
defaultThinkingEnabled, searchEnabled, ok := config.GetModelConfig(dsModel)
if !ok {
searchEnabled = false
}
thinkingEnabled := util.ResolveThinkingEnabled(req, false)
thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled)
if config.IsNoThinkingModel(dsModel) {
thinkingEnabled = false
}