mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
Merge pull request #179 from kael-odin/fix/anthropic-thinking-disable
fix(anthropic): apply thinking config on override path, symmetric with openai
This commit is contained in:
@@ -274,18 +274,17 @@ func (adapter *AnthropicAdapter) Stream(ctx context.Context, req StreamRequest,
|
||||
body["tools"] = tools
|
||||
}
|
||||
body["system"] = anthropicProviderSystemBlocks(systemParts)
|
||||
if thinkingConfig != nil {
|
||||
body["thinking"] = thinkingConfig
|
||||
if normalizeRuntimeThinkingEffort(req.ThinkingEffort) != "disabled" {
|
||||
body["output_config"] = buildAnthropicOutputConfig(req)
|
||||
}
|
||||
}
|
||||
frontier := buildAnthropicCacheFrontier(body, stableMessageCount)
|
||||
req.RequestKnobs = annotateAnthropicRequestKnobs(req.RequestKnobs, body, frontier)
|
||||
body = cloneRequestBodyOverride(body)
|
||||
applyAnthropicCacheBreakpoints(body, frontier.BreakpointPositions)
|
||||
frontier.BreakpointCount = len(frontier.BreakpointPositions)
|
||||
}
|
||||
// applyAnthropicThinkingConfig 在 override 块之外无条件调用,确保 RequestBodyOverride
|
||||
// 路径与正常构造路径行为一致:disabled 时强制 thinking:{type:disabled} 并清理冲突字段,
|
||||
// 非 disabled 时按 AnthropicThinkingEffort 写 adaptive 配置。与 openai.go 的
|
||||
// applyOpenAIThinkingDisable 对称——后者也是无条件在两条路径之后调用。
|
||||
applyAnthropicThinkingConfig(body, req)
|
||||
if err := ApplyAnthropicExtraParams(body, req.AnthropicExtraParamsEnabled, req.AnthropicExtraParamsJSON); err != nil {
|
||||
finishedAt = time.Now().UTC()
|
||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "anthropic", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
||||
@@ -1441,6 +1440,34 @@ func buildAnthropicThinkingConfig(req StreamRequest) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// applyAnthropicThinkingConfig 在请求体构造完成后(含 RequestBodyOverride 路径)无条件调用,
|
||||
// 与 openai.go 的 applyOpenAIThinkingDisable 对称。它把 thinking 配置写入 body 并在 disabled
|
||||
// 时清理与之冲突的字段,确保两条构造路径行为一致:
|
||||
// - disabled: 强制 thinking:{type:"disabled"},删除 output_config / 残留 thinking adaptive 配置,
|
||||
// 记录 thinking_disabled_provider_param=thinking.type knob
|
||||
// - adaptive: 按 AnthropicThinkingEffort 写 thinking:{type:adaptive,display:summarized} + output_config
|
||||
//
|
||||
// 在 override 路径下,上层若已在 override body 里塞了 thinking/output_config,disabled 时会被正确覆盖。
|
||||
func applyAnthropicThinkingConfig(body map[string]any, req StreamRequest) {
|
||||
if len(body) == 0 {
|
||||
return
|
||||
}
|
||||
if normalizeRuntimeThinkingEffort(req.ThinkingEffort) != "disabled" {
|
||||
if strings.TrimSpace(req.AnthropicThinkingEffort) == "" {
|
||||
return
|
||||
}
|
||||
body["thinking"] = map[string]any{
|
||||
"type": "adaptive",
|
||||
"display": "summarized",
|
||||
}
|
||||
body["output_config"] = buildAnthropicOutputConfig(req)
|
||||
return
|
||||
}
|
||||
body["thinking"] = map[string]any{"type": "disabled"}
|
||||
delete(body, "output_config")
|
||||
setRequestKnob(req, "thinking_disabled_provider_param", "thinking.type")
|
||||
}
|
||||
|
||||
func buildAnthropicOutputConfig(req StreamRequest) map[string]any {
|
||||
return map[string]any{
|
||||
"effort": anthropicThinkingEffort(req),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package modeladapter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestApplyAnthropicThinkingConfig covers the three effort branches on a body
|
||||
// built by the normal construction path, plus the override path symmetry that
|
||||
// was the original bug (thinking config was only applied inside the
|
||||
// `if len(body)==0` block, so RequestBodyOverride skipped it entirely).
|
||||
func TestApplyAnthropicThinkingConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body map[string]any
|
||||
thinkingEffort string
|
||||
adaptiveEffort string
|
||||
wantThinking any
|
||||
wantOutputCfg bool // expect output_config key present
|
||||
}{
|
||||
{
|
||||
name: "disabled_writes_disabled_and_drops_output_config",
|
||||
body: map[string]any{"model": "m", "output_config": map[string]any{"effort": "high"}},
|
||||
thinkingEffort: "disabled",
|
||||
wantThinking: map[string]any{"type": "disabled"},
|
||||
wantOutputCfg: false,
|
||||
},
|
||||
{
|
||||
name: "adaptive_writes_adaptive_and_output_config",
|
||||
body: map[string]any{"model": "m"},
|
||||
adaptiveEffort: "high",
|
||||
wantThinking: map[string]any{"type": "adaptive", "display": "summarized"},
|
||||
wantOutputCfg: true,
|
||||
},
|
||||
{
|
||||
name: "empty_effort_no_op",
|
||||
body: map[string]any{"model": "m"},
|
||||
wantThinking: nil,
|
||||
wantOutputCfg: false,
|
||||
},
|
||||
{
|
||||
name: "disabled_overrides_existing_adaptive_thinking",
|
||||
body: map[string]any{"thinking": map[string]any{"type": "adaptive", "display": "summarized"}, "output_config": map[string]any{"effort": "high"}},
|
||||
thinkingEffort: "disabled",
|
||||
wantThinking: map[string]any{"type": "disabled"},
|
||||
wantOutputCfg: false,
|
||||
},
|
||||
{
|
||||
name: "disabled_normalizes_aliases",
|
||||
body: map[string]any{"model": "m"},
|
||||
thinkingEffort: "off",
|
||||
wantThinking: map[string]any{"type": "disabled"},
|
||||
wantOutputCfg: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := StreamRequest{
|
||||
ThinkingEffort: tc.thinkingEffort,
|
||||
AnthropicThinkingEffort: tc.adaptiveEffort,
|
||||
RequestKnobs: map[string]any{},
|
||||
}
|
||||
applyAnthropicThinkingConfig(tc.body, req)
|
||||
|
||||
gotThinking, hasThinking := tc.body["thinking"]
|
||||
if tc.wantThinking == nil {
|
||||
if hasThinking {
|
||||
t.Fatalf("expected no thinking key, got %v", gotThinking)
|
||||
}
|
||||
} else {
|
||||
if !hasThinking {
|
||||
t.Fatalf("expected thinking=%v, key absent", tc.wantThinking)
|
||||
}
|
||||
if !mapEqual(gotThinking, tc.wantThinking) {
|
||||
t.Fatalf("thinking mismatch\nwant: %v\ngot: %v", tc.wantThinking, gotThinking)
|
||||
}
|
||||
}
|
||||
|
||||
_, hasOutputCfg := tc.body["output_config"]
|
||||
if hasOutputCfg != tc.wantOutputCfg {
|
||||
t.Fatalf("output_config presence=%v, want %v", hasOutputCfg, tc.wantOutputCfg)
|
||||
}
|
||||
|
||||
if tc.thinkingEffort == "disabled" || tc.thinkingEffort == "off" {
|
||||
gotKnob := req.RequestKnobs["thinking_disabled_provider_param"]
|
||||
if gotKnob != "thinking.type" {
|
||||
t.Fatalf("knob thinking_disabled_provider_param=%v, want thinking.type", gotKnob)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyAnthropicThinkingConfigOverridePath simulates the RequestBodyOverride
|
||||
// branch: the adapter receives a body that did NOT go through the normal
|
||||
// construction (no tools/messages/system written by the adapter). Before the
|
||||
// fix, thinking config was skipped entirely on this path. Now
|
||||
// applyAnthropicThinkingConfig runs unconditionally and disables thinking.
|
||||
func TestApplyAnthropicThinkingConfigOverridePath(t *testing.T) {
|
||||
overrideBody := map[string]any{
|
||||
"model": "claude-x",
|
||||
"messages": []any{map[string]any{"role": "user", "content": "hi"}},
|
||||
"thinking": map[string]any{"type": "adaptive", "display": "summarized"},
|
||||
"output_config": map[string]any{"effort": "high"},
|
||||
"stream": true,
|
||||
}
|
||||
req := StreamRequest{
|
||||
ThinkingEffort: "disabled",
|
||||
RequestKnobs: map[string]any{},
|
||||
}
|
||||
|
||||
applyAnthropicThinkingConfig(overrideBody, req)
|
||||
|
||||
gotThinking := overrideBody["thinking"]
|
||||
if !mapEqual(gotThinking, map[string]any{"type": "disabled"}) {
|
||||
t.Fatalf("override path: thinking not forced to disabled, got %v", gotThinking)
|
||||
}
|
||||
if _, hasOutputCfg := overrideBody["output_config"]; hasOutputCfg {
|
||||
t.Fatalf("override path: output_config should be dropped on disabled, still present")
|
||||
}
|
||||
if gotKnob := req.RequestKnobs["thinking_disabled_provider_param"]; gotKnob != "thinking.type" {
|
||||
t.Fatalf("override path: knob=%v, want thinking.type", gotKnob)
|
||||
}
|
||||
}
|
||||
|
||||
func mapEqual(a, b any) bool {
|
||||
ma, okA := a.(map[string]any)
|
||||
mb, okB := b.(map[string]any)
|
||||
if !okA || !okB {
|
||||
return a == b
|
||||
}
|
||||
if len(ma) != len(mb) {
|
||||
return false
|
||||
}
|
||||
for k, va := range ma {
|
||||
vb, ok := mb[k]
|
||||
if !ok || !mapEqual(va, vb) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user