- 只翻注释/文档字符串:`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
308 lines
11 KiB
Go
308 lines
11 KiB
Go
package model
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
)
|
||
|
||
// newDeepSeekAdapter 构建一个 OpenAI 兼容适配器,指向 stub 服务器
|
||
// 但按其模型名归类为 DeepSeek,从而让 DeepSeek 映射
|
||
// 针对真实请求体被验证。
|
||
func newDeepSeekAdapter(t *testing.T, ch chan<- capturedRequest, opts ...OpenAIOption) *OpenAIChatAdapter {
|
||
t.Helper()
|
||
srv := newCompletionServer(t, http.StatusOK, "application/json", okCompletion, ch)
|
||
return NewOpenAIChat(config.LLMConfig{BaseURL: srv.URL, Model: deepSeekModel}, opts...)
|
||
}
|
||
|
||
// deepSeekModel 是当前的 DeepSeek 模型名(旧的 deepseek-chat /
|
||
// deepseek-reasoner 名称已于 2026-07-24 停用)。
|
||
const deepSeekModel = "deepseek-flash"
|
||
|
||
// deepSeekThinkingType 读取 body.thinking.type,报告该开关是否
|
||
// 被发出。
|
||
func deepSeekThinkingType(body map[string]any) (string, bool) {
|
||
block, ok := body["thinking"].(map[string]any)
|
||
if !ok {
|
||
return "", false
|
||
}
|
||
value, _ := block["type"].(string)
|
||
return value, true
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekReasoningEffortMatrix 钉住官方取值集合,端到
|
||
// 端落到请求体上:none/low/high/max 原样通过,四个
|
||
// 兼容别名被归一化为 DeepSeek 采用的档位
|
||
// (minimal→low、medium/xhigh→high、ultra→max)。旧行为——把 "none"
|
||
// 降级到 "low"——把关闭思考变成了开启思考,所以 "none" 必须
|
||
// 以 "none" 到达。
|
||
func TestOpenAIChat_DeepSeekReasoningEffortMatrix(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct{ sent, want string }{
|
||
{"none", "none"},
|
||
{"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 capturedRequest, 1)
|
||
ad := newDeepSeekAdapter(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
|
||
if got := body["reasoning_effort"]; got != tc.want {
|
||
t.Fatalf("body.reasoning_effort = %v, want %q (sent %q)", got, tc.want, tc.sent)
|
||
}
|
||
if _, present := body["thinking"]; present {
|
||
t.Errorf("body.thinking = %v, want no switch invented from an effort", body["thinking"])
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekReasoningEffortInvalidDropped 断言无法识别的
|
||
// 取值会被丢弃并上报——绝不会被替换成邻近档位,正是那套
|
||
// 「向下取整」逻辑颠倒了调用方的意图。
|
||
func TestOpenAIChat_DeepSeekReasoningEffortInvalidDropped(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
log, buf := captureLog()
|
||
ad := newDeepSeekAdapter(t, ch, WithOpenAILogger(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["reasoning_effort"]; present {
|
||
t.Fatalf("body.reasoning_effort = %v, want the field dropped", body["reasoning_effort"])
|
||
}
|
||
if _, present := body["thinking"]; present {
|
||
t.Fatalf("body.thinking = %v, want no switch invented", body["thinking"])
|
||
}
|
||
rec := buf.String()
|
||
for _, want := range []string{"level=WARN", "reasoning_effort", "foo", deepSeekModel, "deepseek"} {
|
||
if !strings.Contains(rec, want) {
|
||
t.Errorf("log %q missing %q", rec, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekThinkingSwitch 断言 DeepSeek 思考开关
|
||
// 真的被发出:显式关闭必须到达线上(DeepSeek 默认开启,
|
||
// 所以保持沉默会让思考保持开启),而 nil 不得被变成
|
||
// 任何一个取值。
|
||
func TestOpenAIChat_DeepSeekThinkingSwitch(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
enable *bool
|
||
wantType string
|
||
wantAbsent bool
|
||
}{
|
||
{name: "explicit_off", enable: boolPtr(false), wantType: thinkingTypeDisabled},
|
||
{name: "explicit_on", enable: boolPtr(true), wantType: thinkingTypeEnabled},
|
||
{name: "nil_leaves_the_default", enable: nil, wantAbsent: true},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
ad := newDeepSeekAdapter(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)
|
||
}
|
||
got, present := deepSeekThinkingType((<-ch).body)
|
||
if tc.wantAbsent {
|
||
if present {
|
||
t.Fatalf("body.thinking.type = %q, want the field omitted (not equal to off)", got)
|
||
}
|
||
return
|
||
}
|
||
if !present || got != tc.wantType {
|
||
t.Fatalf("body.thinking.type = %q (present=%v), want %q", got, present, tc.wantType)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekEffortNoneAloneKeepsTheOfficialOffSwitch 断言在未
|
||
// 请求 thinking 开关时,官方 reasoning_effort "none" 路径被原样
|
||
// 使用——那是关闭思考的记录在案的做法。
|
||
func TestOpenAIChat_DeepSeekEffortNoneAloneKeepsTheOfficialOffSwitch(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
ad := newDeepSeekAdapter(t, ch)
|
||
|
||
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
|
||
if got := body["reasoning_effort"]; got != effortNone {
|
||
t.Fatalf("body.reasoning_effort = %v, want %q", got, effortNone)
|
||
}
|
||
if _, present := body["thinking"]; present {
|
||
t.Fatalf("body.thinking = %v, want no switch block", body["thinking"])
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekThinkingOffBeatsEffort 是 resolveOpenAIReasoning 里
|
||
// 记录的冲突规则:提供商未说明优先级,所以关闭一侧获胜,
|
||
// 被丢弃的档位会被上报——绝不静默套用。
|
||
func TestOpenAIChat_DeepSeekThinkingOffBeatsEffort(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
log, buf := captureLog()
|
||
ad := newDeepSeekAdapter(t, ch, WithOpenAILogger(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 got, _ := deepSeekThinkingType(body); got != thinkingTypeDisabled {
|
||
t.Fatalf("body.thinking.type = %q, want %q", got, thinkingTypeDisabled)
|
||
}
|
||
if _, present := body["reasoning_effort"]; present {
|
||
t.Fatalf("body.reasoning_effort = %v, want it dropped while thinking is explicitly off", body["reasoning_effort"])
|
||
}
|
||
if rec := buf.String(); !strings.Contains(rec, "explicitly disabled") {
|
||
t.Errorf("log %q, want a WARN explaining the dropped reasoning_effort", rec)
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekEffortNoneBeatsThinkingEnabled 覆盖相反的
|
||
// 冲突:reasoning_effort "none" 与 thinking 开启无法同时被
|
||
// 尊重,所以关闭一侧获胜,请求只携带一个开关。
|
||
func TestOpenAIChat_DeepSeekEffortNoneBeatsThinkingEnabled(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
log, buf := captureLog()
|
||
ad := newDeepSeekAdapter(t, ch, WithOpenAILogger(log))
|
||
|
||
if _, err := ad.Chat(context.Background(), Request{
|
||
Model: deepSeekModel,
|
||
Messages: []Message{TextMessage(RoleUser, "hi")},
|
||
Thinking: &Thinking{Enable: boolPtr(true)},
|
||
ReasoningEffort: effortNone,
|
||
}, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
body := (<-ch).body
|
||
if got, _ := deepSeekThinkingType(body); got != thinkingTypeDisabled {
|
||
t.Fatalf("body.thinking.type = %q, want %q", got, thinkingTypeDisabled)
|
||
}
|
||
if _, present := body["reasoning_effort"]; present {
|
||
t.Fatalf("body.reasoning_effort = %v, want it dropped in favour of the switch", body["reasoning_effort"])
|
||
}
|
||
if rec := buf.String(); !strings.Contains(rec, "contradicts") {
|
||
t.Errorf("log %q, want a WARN about the contradiction", rec)
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekThinkingBudgetReportedIgnored 断言 token 预算
|
||
// (Anthropic 风格的指令)在 DeepSeek chat-completions 端点上
|
||
// 会被上报为无法映射,而不是靠猜去开或关开关。
|
||
func TestOpenAIChat_DeepSeekThinkingBudgetReportedIgnored(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
log, buf := captureLog()
|
||
ad := newDeepSeekAdapter(t, ch, WithOpenAILogger(log))
|
||
|
||
if _, err := ad.Chat(context.Background(), Request{
|
||
Model: deepSeekModel,
|
||
Messages: []Message{TextMessage(RoleUser, "hi")},
|
||
Thinking: &Thinking{BudgetTokens: 8192},
|
||
}, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
body := (<-ch).body
|
||
if _, present := body["thinking"]; present {
|
||
t.Fatalf("body.thinking = %v, want no switch block", body["thinking"])
|
||
}
|
||
rec := buf.String()
|
||
for _, want := range []string{"level=WARN", "thinking.budget_tokens", "deepseek", deepSeekModel} {
|
||
if !strings.Contains(rec, want) {
|
||
t.Errorf("log %q missing %q", rec, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestOpenAIChat_DeepSeekForcedToolChoiceWithThinkingWarns 覆盖 DeepSeek
|
||
// 400 陷阱:思考开启时强制型 tool_choice 会被拒绝,而思考
|
||
// 默认就是开启。适配器只警告——它绝不能自行
|
||
// 关闭思考。
|
||
func TestOpenAIChat_DeepSeekForcedToolChoiceWithThinkingWarns(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
thinking *Thinking
|
||
toolChoice any
|
||
wantWarn bool
|
||
}{
|
||
{name: "required_with_default_thinking", toolChoice: map[string]any{"type": "required"}, wantWarn: true},
|
||
{name: "named_tool_with_thinking_on", thinking: &Thinking{Enable: boolPtr(true)},
|
||
toolChoice: map[string]any{"type": "function", "function": map[string]any{"name": "x"}}, wantWarn: true},
|
||
{name: "auto_is_not_forcing", toolChoice: map[string]any{"type": "auto"}},
|
||
{name: "forced_with_thinking_off", thinking: &Thinking{Enable: boolPtr(false)},
|
||
toolChoice: map[string]any{"type": "required"}},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
ch := make(chan capturedRequest, 1)
|
||
log, buf := captureLog()
|
||
ad := newDeepSeekAdapter(t, ch, WithOpenAILogger(log))
|
||
if _, err := ad.Chat(context.Background(), Request{
|
||
Model: deepSeekModel,
|
||
Messages: []Message{TextMessage(RoleUser, "hi")},
|
||
Thinking: tc.thinking,
|
||
ToolChoice: tc.toolChoice,
|
||
}, Options{}); err != nil {
|
||
t.Fatalf("Chat error: %v", err)
|
||
}
|
||
rec := buf.String()
|
||
if tc.wantWarn {
|
||
for _, want := range []string{"level=WARN", "tool_choice", "deepseek", deepSeekModel} {
|
||
if !strings.Contains(rec, want) {
|
||
t.Errorf("log %q missing %q", rec, want)
|
||
}
|
||
}
|
||
return
|
||
}
|
||
if strings.Contains(rec, "tool_choice") {
|
||
t.Errorf("unexpected tool_choice WARN: %q", rec)
|
||
}
|
||
})
|
||
}
|
||
}
|