fix(model): anthropic默认max_tokens 1024→8192 — thinking+text共享预算,长思考不再截断正文为空
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 根因: 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绿+交叉编译
This commit is contained in:
AskaEth committed 2026-09-08 22:09:29 +08:00
1 parent f43751fd07
commit 57d5d22d87
2 files changed
+71 -1

No files matched your search

+2 -1
View File
@@ -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:"
)
@@ -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)
}
}