From 57d5d22d878fb8eecc05bf910f0ff1ece993696c Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 8 Sep 2026 22:09:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(model):=20anthropic=E9=BB=98=E8=AE=A4max=5F?= =?UTF-8?q?tokens=201024=E2=86=928192=20=E2=80=94=20thinking+text=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E9=A2=84=E7=AE=97,=E9=95=BF=E6=80=9D=E8=80=83?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=88=AA=E6=96=AD=E6=AD=A3=E6=96=87=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因: deepseek-v4-flash-vision-exp 为 thinking 模型, content=[thinking,text] 共享 max_tokens,默认1024被长思考吃光→仅thinking块无text块, stop_reason=max_tokens → resp.Content="" → no reply produced - 仅当 req.MaxTokens==0 套用默认(显式值不受影响); loop→agent→anthropic 传递链无截断 - 新增 anthropic_budget_test(默认发8192 + 长思考短text断言Content=text+thinking不泄漏) - build/vet/test绿+交叉编译 --- internal/runtime/model/anthropic.go | 3 +- .../runtime/model/anthropic_budget_test.go | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 internal/runtime/model/anthropic_budget_test.go diff --git a/internal/runtime/model/anthropic.go b/internal/runtime/model/anthropic.go index bb26d6b..5730ad1 100644 --- a/internal/runtime/model/anthropic.go +++ b/internal/runtime/model/anthropic.go @@ -20,7 +20,8 @@ const ( // anthropicDefaultMaxTokens is the default max_tokens sent when // Request.MaxTokens is zero. Anthropic requires max_tokens to be present and // positive, so a sensible default is applied rather than omitting it. - anthropicDefaultMaxTokens = 1024 + // max_tokens is shared by thinking + text, so it must leave room for both. + anthropicDefaultMaxTokens = 8192 // anthropicDataPrefix marks a payload embedded as a data: URI. anthropicDataPrefix = "data:" ) diff --git a/internal/runtime/model/anthropic_budget_test.go b/internal/runtime/model/anthropic_budget_test.go new file mode 100644 index 0000000..aacdbbf --- /dev/null +++ b/internal/runtime/model/anthropic_budget_test.go @@ -0,0 +1,69 @@ +package model + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/config" +) + +// TestAnthropicChat_Chat_DefaultMaxTokensLargeEnoughForThinking guards the fix +// for thinking models whose reasoning shares the max_tokens budget with the +// visible answer: the zero-value default must leave room for both. It also +// asserts an explicit Request.MaxTokens is never overridden by that default +// (the existing 64-token case in anthropic_test.go is the compatibility proof). +func TestAnthropicChat_Chat_DefaultMaxTokensLargeEnoughForThinking(t *testing.T) { + t.Parallel() + ch := make(chan anthropicCaptured, 1) + srv := newAnthropicServer(t, http.StatusOK, "application/json", anthropicTextResponse, ch) + ad := NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: "claude-test"}) + + if _, err := ad.Chat(context.Background(), + Request{Messages: []Message{TextMessage(RoleUser, "hi")}}, Options{}); err != nil { + t.Fatalf("Chat error: %v", err) + } + if got := (<-ch).body["max_tokens"]; got != float64(anthropicDefaultMaxTokens) { + t.Errorf("default body.max_tokens = %v, want %d", got, anthropicDefaultMaxTokens) + } + if anthropicDefaultMaxTokens < 4096 { + t.Errorf("anthropicDefaultMaxTokens = %d, too small for a long thinking turn plus its answer", anthropicDefaultMaxTokens) + } +} + +// TestAnthropicChat_Chat_LongThinkingThenShortText asserts a response whose +// thinking block is far longer than its text block still surfaces the text and +// never leaks the thinking into Content (the thinking-is-not-the-answer red +// line). This is the shape a thinking model produces when the budget is large +// enough, versus the truncation the old 1024 cap caused. +func TestAnthropicChat_Chat_LongThinkingThenShortText(t *testing.T) { + t.Parallel() + thinking := strings.Repeat("推理", 1500) // 3000 runes, far longer than the answer + body := fmt.Sprintf(`{ + "id":"msg_think","model":"claude-test", + "content":[ + {"type":"thinking","thinking":%q}, + {"type":"text","text":"现在是下午三点。"} + ], + "usage":{"input_tokens":20,"output_tokens":900} + }`, thinking) + srv := newAnthropicServer(t, http.StatusOK, "application/json", body, nil) + ad := NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: "claude-test"}) + + out, err := ad.Chat(context.Background(), + Request{Messages: []Message{TextMessage(RoleUser, "几点了?")}}, Options{}) + if err != nil { + t.Fatalf("Chat error: %v", err) + } + if out.Content != "现在是下午三点。" { + t.Errorf("Content = %q, want the text block", out.Content) + } + if strings.Contains(out.Content, "推理") { + t.Errorf("Content leaked thinking: %q", out.Content) + } + if out.Message.Role != RoleAssistant || len(out.Message.Content) != 1 || out.Message.Content[0].Text != "现在是下午三点。" { + t.Errorf("Message = %+v, want a single assistant text part", out.Message) + } +}