mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
fix(anthropic): apply thinking config on override path, symmetric with openai
AnthropicAdapter.Stream built thinking config (buildAnthropicThinkingConfig)
and wrote it into body only inside the `if len(body)==0` normal-construction
block. The RequestBodyOverride branch skipped it entirely — a disabled effort
on the override path left whatever thinking config the override body carried,
violating user intent and diverging from openai.go, where
applyOpenAIThinkingDisable runs unconditionally after both branches.
Also, on the normal path, disabled only wrote thinking:{type:disabled} but
left a stale output_config (set by a prior adaptive turn or by
AnthropicExtraParams) in place — an explicitly disabled request could still
carry output_config.effort=high, a contradictory payload.
Fix:
- Extract applyAnthropicThinkingConfig(body, req), called unconditionally
after the override/normal block, mirroring openai.go:1860.
- disabled: force thinking:{type:"disabled"}, delete output_config, set
thinking_disabled_provider_param=thinking.type knob.
- adaptive (AnthropicThinkingEffort non-empty): write
thinking:{type:adaptive,display:summarized} + output_config.
- empty effort: no-op.
- buildAnthropicThinkingConfig retained for stable-message-count / message
normalization signals inside the normal block.
Tests: anthropic_thinking_disable_test.go covers all three branches, alias
normalization (off→disabled), disabled overriding existing adaptive config,
and the override-path symmetry scenario.
This commit is contained in:
@@ -261,18 +261,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))
|
||||
@@ -1367,6 +1366,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