mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +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["tools"] = tools
|
||||||
}
|
}
|
||||||
body["system"] = anthropicProviderSystemBlocks(systemParts)
|
body["system"] = anthropicProviderSystemBlocks(systemParts)
|
||||||
if thinkingConfig != nil {
|
|
||||||
body["thinking"] = thinkingConfig
|
|
||||||
if normalizeRuntimeThinkingEffort(req.ThinkingEffort) != "disabled" {
|
|
||||||
body["output_config"] = buildAnthropicOutputConfig(req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frontier := buildAnthropicCacheFrontier(body, stableMessageCount)
|
frontier := buildAnthropicCacheFrontier(body, stableMessageCount)
|
||||||
req.RequestKnobs = annotateAnthropicRequestKnobs(req.RequestKnobs, body, frontier)
|
req.RequestKnobs = annotateAnthropicRequestKnobs(req.RequestKnobs, body, frontier)
|
||||||
body = cloneRequestBodyOverride(body)
|
body = cloneRequestBodyOverride(body)
|
||||||
applyAnthropicCacheBreakpoints(body, frontier.BreakpointPositions)
|
applyAnthropicCacheBreakpoints(body, frontier.BreakpointPositions)
|
||||||
frontier.BreakpointCount = len(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 {
|
if err := ApplyAnthropicExtraParams(body, req.AnthropicExtraParamsEnabled, req.AnthropicExtraParamsJSON); err != nil {
|
||||||
finishedAt = time.Now().UTC()
|
finishedAt = time.Now().UTC()
|
||||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "anthropic", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
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 {
|
func buildAnthropicOutputConfig(req StreamRequest) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"effort": anthropicThinkingEffort(req),
|
"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