- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
343 lines
13 KiB
Go
343 lines
13 KiB
Go
package model
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
)
|
|
|
|
// TestDeepSeekAnthropicEndpoint_Classification 钉住端点检测:
|
|
// 即便配置的模型名不是 DeepSeek 的,也必须认出
|
|
// DeepSeek /anthropic URL(该服务会把未知名称静默映射到
|
|
// deepseek-flash,所以那里模型名不是可靠信号)。
|
|
func TestDeepSeekAnthropicEndpoint_Classification(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
model string
|
|
baseURL string
|
|
want bool
|
|
}{
|
|
{model: deepSeekModel, baseURL: "http://127.0.0.1:9", want: true},
|
|
{model: "claude-sonnet-4", baseURL: "https://api.deepseek.com/anthropic", want: true},
|
|
{model: "gpt-4o", baseURL: "https://api.deepseek.com/anthropic", want: true},
|
|
{model: "claude-sonnet-4", baseURL: "https://api.anthropic.com", want: false},
|
|
{model: "deepseek-v4-pro", baseURL: "https://gateway.internal/anthropic", want: true},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := deepSeekAnthropicEndpoint(tc.model, tc.baseURL); got != tc.want {
|
|
t.Errorf("deepSeekAnthropicEndpoint(%q, %q) = %v, want %v", tc.model, tc.baseURL, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// newDeepSeekAnthropicAdapter 构建一个 Anthropic 协议适配器,其模型
|
|
// 名标识 DeepSeek,从而走到 DeepSeek /anthropic 分支,
|
|
// 同时请求仍发往本地 stub。
|
|
func newDeepSeekAnthropicAdapter(t *testing.T, ch chan<- anthropicCaptured, opts ...AnthropicOption) *AnthropicChatAdapter {
|
|
t.Helper()
|
|
srv := newAnthropicServer(t, http.StatusOK, "application/json", anthropicTextResponse, ch)
|
|
return NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: deepSeekModel}, opts...)
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_BudgetDoesNotRaiseMaxTokens 断言抬高 max_tokens
|
|
// 只留在 Anthropic 端点:DeepSeek 的 /anthropic 忽略 budget_tokens,所以在
|
|
// 那里抬高限额会错误反映提供商采用的预算。
|
|
func TestAnthropicDeepSeek_BudgetDoesNotRaiseMaxTokens(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
log, buf := captureLog()
|
|
ad := newDeepSeekAnthropicAdapter(t, ch, WithAnthropicLogger(log))
|
|
|
|
const budget = 8192
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
MaxTokens: 1024,
|
|
Thinking: &Thinking{BudgetTokens: budget},
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
if got := body["max_tokens"]; got != float64(1024) {
|
|
t.Fatalf("body.max_tokens = %v, want the requested 1024 (DeepSeek ignores budget_tokens)", got)
|
|
}
|
|
think, ok := body["thinking"].(map[string]any)
|
|
if !ok || think["type"] != thinkingTypeEnabled {
|
|
t.Fatalf("body.thinking = %v, want an enabled directive", body["thinking"])
|
|
}
|
|
rec := buf.String()
|
|
for _, want := range []string{"level=WARN", "budget_tokens", "ignored", "deepseek"} {
|
|
if !strings.Contains(rec, want) {
|
|
t.Errorf("log %q missing %q", rec, want)
|
|
}
|
|
}
|
|
if strings.Contains(rec, "max_tokens raised") {
|
|
t.Errorf("log %q reports a max_tokens raise that must not happen on the DeepSeek endpoint", rec)
|
|
}
|
|
}
|
|
|
|
// TestAnthropicOfficial_BudgetStillRaisesMaxTokens 是对位守护:在
|
|
// Anthropic 端点上,设计红线(「预算不得吃光答案」)
|
|
// 依然成立。
|
|
func TestAnthropicOfficial_BudgetStillRaisesMaxTokens(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
srv := newAnthropicServer(t, http.StatusOK, "application/json", anthropicTextResponse, ch)
|
|
log, buf := captureLog()
|
|
ad := NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: "claude-test"},
|
|
WithAnthropicLogger(log))
|
|
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: "claude-test",
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
MaxTokens: 1024,
|
|
Thinking: &Thinking{BudgetTokens: 4096},
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
maxTokens, _ := body["max_tokens"].(float64)
|
|
think, _ := body["thinking"].(map[string]any)
|
|
budget, _ := think["budget_tokens"].(float64)
|
|
if maxTokens <= budget {
|
|
t.Fatalf("body.max_tokens = %v, want > budget_tokens %v on the Anthropic endpoint", maxTokens, budget)
|
|
}
|
|
if !strings.Contains(buf.String(), "max_tokens raised") {
|
|
t.Errorf("log %q, want the raise WARN", buf.String())
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_OutputConfigEffort 断言 DeepSeek /anthropic 的
|
|
// 强度路径存在:effort 变成 output_config.effort,别名
|
|
// 被归一化为服务采用的档位。
|
|
func TestAnthropicDeepSeek_OutputConfigEffort(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct{ sent, want string }{
|
|
{"low", "low"},
|
|
{"high", "high"},
|
|
{"max", "max"},
|
|
{"minimal", "low"},
|
|
{"medium", "high"},
|
|
{"xhigh", "high"},
|
|
{"ultra", "max"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.sent, func(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
ad := newDeepSeekAnthropicAdapter(t, ch)
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
ReasoningEffort: tc.sent,
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
output, ok := body["output_config"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("body.output_config = %v, want an effort block", body["output_config"])
|
|
}
|
|
if got := output["effort"]; got != tc.want {
|
|
t.Fatalf("body.output_config.effort = %v, want %q (sent %q)", got, tc.want, tc.sent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_OutputConfigEffortInvalidDropped 断言无法识别的
|
|
// effort 会被丢弃并上报,而不是做近似处理。
|
|
func TestAnthropicDeepSeek_OutputConfigEffortInvalidDropped(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
log, buf := captureLog()
|
|
ad := newDeepSeekAnthropicAdapter(t, ch, WithAnthropicLogger(log))
|
|
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
ReasoningEffort: "foo",
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
if _, present := body["output_config"]; present {
|
|
t.Fatalf("body.output_config = %v, want the block dropped", body["output_config"])
|
|
}
|
|
if _, present := body["reasoning_effort"]; present {
|
|
t.Fatalf("body.reasoning_effort = %v, want the field dropped", body["reasoning_effort"])
|
|
}
|
|
rec := buf.String()
|
|
for _, want := range []string{"level=WARN", "reasoning_effort", "foo", "deepseek"} {
|
|
if !strings.Contains(rec, want) {
|
|
t.Errorf("log %q missing %q", rec, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_EffortNoneDisablesThinking 钉住 output_config
|
|
// 无法承载的那一个 effort 取值的翻译:DeepSeek 的 Anthropic 格式
|
|
// 没有 "none" 档,所以关闭意图经 thinking 开关转达——把它映射
|
|
// 到某个档位会把「关闭思考」变成「开启思考」。
|
|
func TestAnthropicDeepSeek_EffortNoneDisablesThinking(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
log, buf := captureLog()
|
|
ad := newDeepSeekAnthropicAdapter(t, ch, WithAnthropicLogger(log))
|
|
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
ReasoningEffort: effortNone,
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
think, ok := body["thinking"].(map[string]any)
|
|
if !ok || think["type"] != thinkingTypeDisabled {
|
|
t.Fatalf("body.thinking = %v, want %q", body["thinking"], thinkingTypeDisabled)
|
|
}
|
|
if _, present := body["output_config"]; present {
|
|
t.Fatalf("body.output_config = %v, want no effort block", body["output_config"])
|
|
}
|
|
if rec := buf.String(); !strings.Contains(rec, "none") || !strings.Contains(rec, "level=WARN") {
|
|
t.Errorf("log %q, want a WARN explaining the translation", rec)
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_ThinkingSwitch 断言显式开关以
|
|
// Anthropic 格式的 thinking 块携带,且 disabled 时不带预算
|
|
// (反正 DeepSeek 会忽略它)。
|
|
func TestAnthropicDeepSeek_ThinkingSwitch(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
enable *bool
|
|
wantType string
|
|
absent bool
|
|
}{
|
|
{name: "explicit_off", enable: boolPtr(false), wantType: thinkingTypeDisabled},
|
|
{name: "explicit_on", enable: boolPtr(true), wantType: thinkingTypeEnabled},
|
|
{name: "nil_leaves_the_default", enable: nil, absent: true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
ad := newDeepSeekAnthropicAdapter(t, ch)
|
|
req := Request{Model: deepSeekModel, Messages: []Message{TextMessage(RoleUser, "hi")}}
|
|
if tc.enable != nil {
|
|
req.Thinking = &Thinking{Enable: tc.enable}
|
|
}
|
|
if _, err := ad.Chat(context.Background(), req, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
think, present := body["thinking"].(map[string]any)
|
|
if tc.absent {
|
|
if present {
|
|
t.Fatalf("body.thinking = %v, want the field omitted", body["thinking"])
|
|
}
|
|
return
|
|
}
|
|
if !present || think["type"] != tc.wantType {
|
|
t.Fatalf("body.thinking = %v, want type %q", body["thinking"], tc.wantType)
|
|
}
|
|
if _, hasBudget := think["budget_tokens"]; tc.wantType == thinkingTypeDisabled && hasBudget {
|
|
t.Errorf("body.thinking = %v, want no budget_tokens for a disabled switch", think)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_EffortDroppedWhenThinkingDisabled 在 Anthropic 路径
|
|
// 上也守住「关闭优先」规则:output_config.effort 不得重新打开思考。
|
|
func TestAnthropicDeepSeek_EffortDroppedWhenThinkingDisabled(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
log, buf := captureLog()
|
|
ad := newDeepSeekAnthropicAdapter(t, ch, WithAnthropicLogger(log))
|
|
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
Thinking: &Thinking{Enable: boolPtr(false)},
|
|
ReasoningEffort: "high",
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
if _, present := body["output_config"]; present {
|
|
t.Fatalf("body.output_config = %v, want it dropped while thinking is off", body["output_config"])
|
|
}
|
|
if think, _ := body["thinking"].(map[string]any); think["type"] != thinkingTypeDisabled {
|
|
t.Fatalf("body.thinking = %v, want %q", body["thinking"], thinkingTypeDisabled)
|
|
}
|
|
if rec := buf.String(); !strings.Contains(rec, "explicitly disabled") {
|
|
t.Errorf("log %q, want a WARN explaining the dropped effort", rec)
|
|
}
|
|
}
|
|
|
|
// TestAnthropicOfficial_ReasoningEffortReportedIgnored 断言落在
|
|
// Anthropic 端点上的 reasoning_effort 会被上报,而不是静默消失——
|
|
// Messages 协议没有这个字段。
|
|
func TestAnthropicOfficial_ReasoningEffortReportedIgnored(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
srv := newAnthropicServer(t, http.StatusOK, "application/json", anthropicTextResponse, ch)
|
|
log, buf := captureLog()
|
|
ad := NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: srv.URL, AnthropicModel: "claude-test"},
|
|
WithAnthropicLogger(log))
|
|
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: "claude-test",
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
ReasoningEffort: "high",
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
body := (<-ch).body
|
|
for _, key := range []string{"reasoning_effort", "output_config"} {
|
|
if _, present := body[key]; present {
|
|
t.Errorf("body.%s = %v, want the Anthropic endpoint untouched", key, body[key])
|
|
}
|
|
}
|
|
rec := buf.String()
|
|
for _, want := range []string{"level=WARN", "reasoning_effort", "high", "anthropic"} {
|
|
if !strings.Contains(rec, want) {
|
|
t.Errorf("log %q missing %q", rec, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAnthropicDeepSeek_ForcedToolChoiceWithThinkingWarns 覆盖 Anthropic 路径
|
|
// 上的 DeepSeek 400 陷阱:适配器会警告但仍发送该指令——
|
|
// 它绝不自行关闭思考。
|
|
func TestAnthropicDeepSeek_ForcedToolChoiceWithThinkingWarns(t *testing.T) {
|
|
t.Parallel()
|
|
ch := make(chan anthropicCaptured, 1)
|
|
log, buf := captureLog()
|
|
ad := newDeepSeekAnthropicAdapter(t, ch, WithAnthropicLogger(log))
|
|
|
|
choice := map[string]any{"type": "tool", "name": "get_time"}
|
|
if _, err := ad.Chat(context.Background(), Request{
|
|
Model: deepSeekModel,
|
|
Messages: []Message{TextMessage(RoleUser, "hi")},
|
|
ToolChoice: choice,
|
|
}, Options{}); err != nil {
|
|
t.Fatalf("Chat error: %v", err)
|
|
}
|
|
if got := (<-ch).body["tool_choice"]; got == nil {
|
|
t.Fatal("body.tool_choice = nil, want the directive passed through verbatim")
|
|
}
|
|
rec := buf.String()
|
|
for _, want := range []string{"level=WARN", "tool_choice", "deepseek", deepSeekModel} {
|
|
if !strings.Contains(rec, want) {
|
|
t.Errorf("log %q missing %q", rec, want)
|
|
}
|
|
}
|
|
}
|