File size: 12,107 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | package upstream
import (
"encoding/json"
"strings"
"testing"
)
// getThinkingType 从输出 body 提取 thinking.type(缺字段返回空串 + 是否存在)。
func getThinkingType(t *testing.T, out []byte) (typ string, present bool) {
t.Helper()
var m map[string]any
if err := json.Unmarshal(out, &m); err != nil {
t.Fatalf("unmarshal: %v (out=%s)", err, out)
}
th, ok := m["thinking"].(map[string]any)
if !ok {
return "", false
}
s, ok := th["type"].(string)
if !ok {
return "", true
}
return s, true
}
// TestInjectThinkingDeepSeekEnabled 开思考开关注入:deepseek 系模型请求体不带
// thinking 时必须注入 {type:"enabled"},否则上游默认按不思考应答(思维链不显示)。
func TestInjectThinkingDeepSeekEnabled(t *testing.T) {
cases := []struct {
name string
body string
wantTyp string
}{
{"deepseek 无 thinking 注入 enabled",
`{"model":"deepseek-v4-flash","messages":[]}`, "enabled"},
{"DeepSeek 大小写不敏感",
`{"model":"DeepSeek-v4.1-flash","messages":[]}`, "enabled"},
{"DEEPSEEK 全大写不敏感",
`{"model":"DEEPSEEK-R1","messages":[]}`, "enabled"},
{"deepseek 带 reasoning_effort 无 thinking 注入 enabled",
`{"model":"deepseek-v4-flash","reasoning_effort":"medium","messages":[]}`, "enabled"},
{"deepseek thinking 对象 type 空 补 enabled",
`{"model":"deepseek-v4-flash","thinking":{},"messages":[]}`, "enabled"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out := PrepareBodyOptWithEfforts([]byte(c.body), false, nil)
typ, present := getThinkingType(t, out)
if !present {
t.Fatalf("thinking 字段缺失 (out=%s)", out)
}
if typ != c.wantTyp {
t.Errorf("thinking.type = %q want %q (out=%s)", typ, c.wantTyp, out)
}
})
}
}
// TestInjectThinkingDefaultEffort 打回修复主证据:无 effort 裸请求必须同时带
// thinking.type=enabled 与默认档 reasoning_effort(否则上游 deepseek-v4-flash 不开思维链)。
// 默认档 = 官方客户端兜底 "high",并带上 supportedEfforts 时经降级管线落到合法档。
func TestInjectThinkingDefaultEffort(t *testing.T) {
// 裸请求无任何思考参数 → 注入 enabled + reasoning_effort="high"。
out := PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}`),
false, nil)
typ, present := getThinkingType(t, out)
if !present || typ != "enabled" {
t.Fatalf("thinking.type=%q present=%v want enabled (out=%s)", typ, present, out)
}
eff, ok := objFieldString(t, out, "reasoning_effort")
if !ok || eff != "high" {
t.Errorf("reasoning_effort=%q ok=%v want high(默认档)(out=%s)", eff, ok, out)
}
// 模型只支持 low/high → 默认 high 经降级管线后仍是 high(合法档)。
out = PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","messages":[]}`),
false, map[string][]string{"deepseek-v4-flash": {"low", "high"}})
eff, _ = objFieldString(t, out, "reasoning_effort")
if eff != "high" {
t.Errorf("supportedEfforts=[low high] 下默认档=%q want high (out=%s)", eff, out)
}
// 模型只支持 minimal/low → 默认 high 降级到 low(≤high 的最高支持档)。
out = PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","messages":[]}`),
false, map[string][]string{"deepseek-v4-flash": {"minimal", "low"}})
eff, _ = objFieldString(t, out, "reasoning_effort")
if eff != "low" {
t.Errorf("supportedEfforts=[minimal low] 下默认档降级=%q want low (out=%s)", eff, out)
}
// 显式 enabled + 缺 effort → 同样补默认档(官方 configure thinking 行为)。
out = PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","thinking":{"type":"enabled"},"messages":[]}`),
false, nil)
if eff, _ = objFieldString(t, out, "reasoning_effort"); eff != "high" {
t.Errorf("显式 enabled 缺 effort 应补默认档, got %q (out=%s)", eff, out)
}
}
// TestInjectThinkingEffortNotOverridden 已有显式 reasoning_effort(snake/camel)
// 一律不覆盖、不降级、不删除;降级由 normalizeReasoningEffort 单独负责。
func TestInjectThinkingEffortNotOverridden(t *testing.T) {
cases := []struct {
name string
body string
}{
{"snake effort 原样保留",
`{"model":"deepseek-v4-flash","thinking":{"type":"enabled"},"reasoning_effort":"medium","messages":[]}`},
{"camel effort 原样保留",
`{"model":"deepseek-v4-flash","reasoningEffort":"low","messages":[]}`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out := PrepareBodyOptWithEfforts([]byte(c.body), false, nil)
typ, _ := getThinkingType(t, out)
if typ != "enabled" {
t.Fatalf("thinking.type=%q want enabled (out=%s)", typ, out)
}
// camel 分支:输入只有 camel,injectThinking 不得另加 snake 默认档。
if strings.Contains(c.body, "reasoningEffort") {
if _, ok := objFieldString(t, out, "reasoning_effort"); ok {
t.Errorf("已有 reasoningEffort 却新增 reasoning_effort 默认档 (out=%s)", out)
}
}
})
}
// 显式 effort + 无 thinking → 注入 enabled 但 effort 不覆盖。
out := PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","reasoning_effort":"medium","messages":[]}`),
false, nil)
if eff, _ := objFieldString(t, out, "reasoning_effort"); eff != "medium" {
t.Errorf("显式 effort 被改写 %q (out=%s)", eff, out)
}
}
// TestInjectThinkingDisabledNoDefaultEffort disabled 保持既有语义:
// thinking.type=disabled 尊重关闭意图;reasoning_effort 删除;不得再补默认档。
func TestInjectThinkingDisabledNoDefaultEffort(t *testing.T) {
out := PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","thinking":{"type":"disabled"},"reasoning_effort":"high","messages":[]}`),
false, nil)
typ, present := getThinkingType(t, out)
if !present || typ != "disabled" {
t.Fatalf("thinking.type=%q present=%v want disabled (out=%s)", typ, present, out)
}
for _, k := range []string{"reasoning_effort", "reasoningEffort"} {
if _, ok := objFieldString(t, out, k); ok {
t.Errorf("%s 应被删除且不得补默认档(disabled 时)(out=%s)", k, out)
}
}
}
// objFieldString 提取顶层字段 string 值。
func objFieldString(t *testing.T, out []byte, key string) (string, bool) {
t.Helper()
var m map[string]any
if err := json.Unmarshal(out, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
s, ok := m[key].(string)
return s, ok
}
// TestInjectThinkingDeepSeekExplicitControl 客户端显式控制时必须尊重:
// thinking.type 非空(enabled/disabled)都不得被覆盖。
func TestInjectThinkingDeepSeekExplicitControl(t *testing.T) {
cases := []struct {
name string
body string
wantTyp string
wantEff string // 期望 reasoning_effort 值;"" 且 wantEffAbsent=true 表示应删除
}{
{"已有 enabled 不动",
`{"model":"deepseek-v4-flash","thinking":{"type":"enabled"},"messages":[]}`,
"enabled", ""},
{"已有 enabled 且带 reasoning_effort 保持(不删 effort)",
`{"model":"deepseek-v4-flash","thinking":{"type":"enabled"},"reasoning_effort":"high","messages":[]}`,
"enabled", "high"},
{"已有 disabled 保留(尊重关闭意图)",
`{"model":"deepseek-v4-flash","thinking":{"type":"disabled"},"messages":[]}`,
"disabled", ""},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out := PrepareBodyOptWithEfforts([]byte(c.body), false, nil)
typ, present := getThinkingType(t, out)
if !present {
t.Fatalf("thinking 字段缺失 (out=%s)", out)
}
if typ != c.wantTyp {
t.Errorf("thinking.type = %q want %q (out=%s)", typ, c.wantTyp, out)
}
if c.wantEff != "" {
if eff, _ := objFieldString(t, out, "reasoning_effort"); eff != c.wantEff {
t.Errorf("reasoning_effort = %q want %q (out=%s)", eff, c.wantEff, out)
}
}
})
}
}
// TestInjectThinkingDisabledDeletesEffort 显式 disabled 时 reasoning_effort 一并删除
// (照抄官方客户端 case "deepseek" 行为:默认分支 delete reasoning_effort)。
// snow/camel 双字段都删。
func TestInjectThinkingDisabledDeletesEffort(t *testing.T) {
cases := []struct {
name string
body string
}{
{"disabled + snake effort 删除",
`{"model":"deepseek-v4-flash","thinking":{"type":"disabled"},"reasoning_effort":"high","messages":[]}`},
{"disabled + camel effort 删除",
`{"model":"deepseek-v4-flash","thinking":{"type":"disabled"},"reasoningEffort":"high","messages":[]}`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
out := PrepareBodyOptWithEfforts([]byte(c.body), false, nil)
typ, present := getThinkingType(t, out)
if !present || typ != "disabled" {
t.Fatalf("thinking.type = %q present=%v want disabled (out=%s)", typ, present, out)
}
for _, k := range []string{"reasoning_effort", "reasoningEffort"} {
if _, ok := objFieldString(t, out, k); ok {
t.Errorf("%s 应被删除(显式 disabled 时)(out=%s)", k, out)
}
}
})
}
}
// TestInjectThinkingSkipNonDeepSeek 非 deepseek 模型零改动:无 thinking 不得凭空添加,
// 已有 thinking 原样保留。
func TestInjectThinkingSkipNonDeepSeek(t *testing.T) {
cases := []struct {
name string
body string
}{
{"glm 无 thinking 不注入", `{"model":"glm-5.2","messages":[]}`},
{"glm 已有 thinking 保留", `{"model":"glm-5.2","thinking":{"type":"enabled"},"messages":[]}`},
{"kimi 无 thinking 不注入", `{"model":"kimi-k2.5","messages":[]}`},
{"qwen 系不注入", `{"model":"qwen2.5-coder-32b","messages":[]}`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// 记录输入是否已带 thinking 及值,输出必须逐字节语义不变。
var in map[string]any
if err := json.Unmarshal([]byte(c.body), &in); err != nil {
t.Fatalf("unmarshal input: %v", err)
}
inTh, inHadThink := in["thinking"]
out := PrepareBodyOptWithEfforts([]byte(c.body), false, nil)
var got map[string]any
if err := json.Unmarshal(out, &got); err != nil {
t.Fatalf("unmarshal out: %v (out=%s)", err, out)
}
outTh, outHadThink := got["thinking"]
if inHadThink != outHadThink {
t.Errorf("thinking 出现性变化: in=%v(had=%v) out=%v(had=%v)",
inTh, inHadThink, outTh, outHadThink)
}
if inHadThink {
// 保留的 thinking 必须原值相等(JSON 语义级)。
inJSON, _ := json.Marshal(inTh)
outJSON, _ := json.Marshal(outTh)
if string(inJSON) != string(outJSON) {
t.Errorf("thinking 被改动: in=%s out=%s", inJSON, outJSON)
}
}
// 非 deepseek 不得新增 thinking 相关字段;允许的既有新增字段:
// stream(强制流式)+ stream_options(D7 include_usage,CLI 流式必发)。
allowedNew := map[string]bool{"stream": true, "stream_options": true}
if len(got) > len(in)+len(allowedNew) {
for k := range got {
if _, had := in[k]; !had && !allowedNew[k] {
t.Errorf("非 deepseek 新增字段 %q (out=%s)", k, out)
}
}
}
})
}
}
// TestInjectThinkingStringPreserved 注入不得破坏 model/messages 等既有字段。
func TestInjectThinkingStringPreserved(t *testing.T) {
out := PrepareBodyOptWithEfforts(
[]byte(`{"model":"deepseek-v4-flash","messages":[{"role":"user","content":"hi"}],"temperature":0.7}`),
false, nil)
var m map[string]any
if err := json.Unmarshal(out, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if m["model"] != "deepseek-v4-flash" || m["temperature"] != 0.7 {
t.Errorf("既有字段被改动: %v", m)
}
if _, ok := m["messages"].([]any); !ok {
t.Errorf("messages 结构破坏: %v", m)
}
if !strings.Contains(string(out), `"stream":true`) {
t.Errorf("stream 未强制: %s", out)
}
}
|