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) } }) } }