Files
Cyrene_v2/internal/runtime/model/deepseek_reasoning_test.go
T
AskaEth 3dc1232bc2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`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
2026-10-04 16:27:54 +08:00

308 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})
}
}