Download internal/upstream/thinking.go from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/thinking.go
- Command line
-
hf download hf://spaces/a3216/gcli2api/internal/upstream/thinking.go
-
curl -L -o thinking.go https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/upstream/thinking.go
10.1 kB
| // thinking.go DeepSeek 思维链开启:出站请求体注入 thinking:{type:"enabled"} + 默认档位。 | |
| // | |
| // 根因(issue #43,Hermes 逆向官方客户端 codebuddy.js 已确认): | |
| // 官方客户端对 deepseek 系模型标记 thinkingFormat:"deepseek" + requiresReasoningContentOnAssistantMessages, | |
| // 发请求时「开思考」必须显式带 thinking:{type:"enabled"},否则上游默认按不思考应答 | |
| // (思维链不返回)。网关 payload 层此前完全不感知该字段,透传请求没有这个开关 | |
| // → 上游不给思维链;glm/kimi 走其他 thinkingFormat(qwen 系 enable_thinking 或默认开)所以正常。 | |
| // | |
| // 打回修复(Hermes #43 验收实测): | |
| // | |
| // thinking.type=enabled 单一字段不足——真实上游 deepseek-v4-flash 对「不带 reasoning_effort」的裸请求 | |
| // 仍然按不思考应答(reasoning_content 长度 0),带 reasoning_effort:high 才有思维链。 | |
| // 逆向 codebuddy.js 证实:isThinkingEnabled = !!(reasoning_summary || reasoning_effort || reasoning?.effort), | |
| // case "deepseek" 的 enabled 分支在实际出站里同时保留 reasoning_effort,官方「开思考」= thinking.type:enabled | |
| // + 某档 effort;默认档来自 reasoning.defaultEffort ?? 兜底 "high"(configure thinking 无来源时 warn fallback to 'high')。 | |
| // | |
| // 行为对齐官方客户端(两路组合): | |
| // - thinking.type 已显式 enabled / disabled → 客户端显式控制,绝不覆盖;disabled 时照抄 case 行为 | |
| // 删 reasoning_effort(snake/camel 双字段)。enabled 但缺 effort → 补默认档(官方 configure 行为)。 | |
| // - 无 thinking / thinking.type 空 / 已有 reasoning_effort → 注入 {type:"enabled"} + 补默认档。 | |
| // - 显式 reasoning_effort 一律不覆盖、不降级(降级交给 payload.go normalizeReasoningEffort)。 | |
| // - 非 deepseek 模型(glm/kimi/qwen 等)→ 零改动。 | |
| package upstream | |
| import ( | |
| "strings" | |
| ) | |
| // defaultDeepSeekEffort 官方客户端默认档兜底(configure thinking 无来源时 warn fallback to 'high', | |
| // REASONING_SUPPLEMENTS.defaultEffort 亦为 "high")。补入后走 normalizeReasoningEffort 降级管线, | |
| // 模型不支持 high 时自动落到 ≤high 的最高支持档。 | |
| const defaultDeepSeekEffort = "high" | |
| // lookupDefaultEffort 从 FetchModels 缓存的 defaultEfforts 表按模型名查默认档。 | |
| // nil map 或模型未缓存 → 空串(thinking.go 回退硬编码 high)。 | |
| // 键为模型 ID 原样(与 efforts 缓存对齐:normalizeReasoningEffort 精确匹配 model)。 | |
| func lookupDefaultEffort(defaultEfforts map[string]string, model string) string { | |
| if len(defaultEfforts) == 0 || model == "" { | |
| return "" | |
| } | |
| return defaultEfforts[model] | |
| } | |
| // isDeepSeekModel 模型名以 deepseek 为前缀(不区分大小写)。 | |
| // 覆盖 deepseek-v4.1-flash / deepseek-v4-pro / deepseek-r1 等变体; | |
| // 前缀匹配对齐官方 thinkingFormat:"deepseek" 的判定口径,避免漏注。 | |
| func isDeepSeekModel(model string) bool { | |
| return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek") | |
| } | |
| // backfillReasoningContent DeepSeek 多轮一致性:保证每条 assistant 消息带 | |
| // reasoning_content 字段且值为 string——即 requiresReasoningContentOnAssistantMessages | |
| // (官方客户端 matches 规则,issue #165 对齐官方 apply 门控)。 | |
| // | |
| // 门控(对齐官方 ReasoningContentBackfillRule:thinkingEnabled || hasTrace), | |
| // thinkingEnabled 取自注入后请求体的 thinking.type == "enabled"(injectThinking 先行, | |
| // payload.go 管线顺序已保证;本网关对 deepseek 无条件注入 enabled,等价于非 disabled 一律补): | |
| // - 非 deepseek 模型 → 零改动(isDeepSeekModel 闸不动)。 | |
| // - deepseek + enabled(含 L1 注入后)→ 每条 assistant 保证 reasoning_content 是 | |
| // string:已有 string 原样保留(不覆盖);reasoning 是非空 string 且 rc 非 string → | |
| // 复制 reasoning 值;两者皆无 → 补空串 ""。第三方客户端丢推理回传(零痕迹)形态 | |
| // 下官方本就补,网关此前只移植了 hasTrace 半边(issue #165 修复点)。 | |
| // - deepseek + disabled + 无痕迹 → 零改动(官方 thinkingEnabled=false 且 ec=false → 不补)。 | |
| // - deepseek + disabled + 有痕迹 → 照补(官方 hasTrace 半边,双方一致)。 | |
| // | |
| // 归一化对齐官方 "string"!=typeof 语义:reasoning_content 为 null/数字等非 string | |
| // 值时不算「已有」,落补 ""/复制分支(旧代码键存在即跳过,null 会被当「已有」漏补)。 | |
| // hasTrace = 会话内任一消息带非空 reasoning(string)或已有 reasoning_content 字段 | |
| // (比官方仅扫 assistant 的口径宽,只影响 disabled 分支,装饰性差异)。 | |
| func backfillReasoningContent(obj map[string]any) { | |
| model, _ := obj["model"].(string) | |
| if !isDeepSeekModel(model) { | |
| return | |
| } | |
| msgs, ok := obj["messages"].([]any) | |
| if !ok || len(msgs) == 0 { | |
| return | |
| } | |
| // thinkingEnabled 半边:读注入后的 thinking.type(与官方 el.thinkingEnabled 对应)。 | |
| thinkingEnabled := false | |
| if th, ok := obj["thinking"].(map[string]any); ok { | |
| if typ, _ := th["type"].(string); strings.EqualFold(strings.TrimSpace(typ), "enabled") { | |
| thinkingEnabled = true | |
| } | |
| } | |
| // hasTrace 半边:检测是否有任何 reasoning 痕迹(非空 reasoning 或已有 reasoning_content)。 | |
| hasTrace := false | |
| for _, mm := range msgs { | |
| msg, ok := mm.(map[string]any) | |
| if !ok { | |
| continue | |
| } | |
| if r, ok := msg["reasoning"].(string); ok && r != "" { | |
| hasTrace = true | |
| break | |
| } | |
| if _, ok := msg["reasoning_content"]; ok { | |
| hasTrace = true | |
| break | |
| } | |
| } | |
| if !thinkingEnabled && !hasTrace { | |
| return | |
| } | |
| // 第二遍:所有 assistant 消息补/复制 reasoning_content 字段,并镜像保证 | |
| // reasoning 字段存在且非空(issue #165 追评——部分账号/租户对 thinking 形态 | |
| // 校验 len(reasoning)>0:缺失/null/空串 400,空白串 200;官方 CLI 本就给 | |
| // assistant 挂上一轮 reasoning 文本,见 itemsToMessages 的 applyPendingReasoning)。 | |
| // 跳过条件只认 string(官方 "string"!=typeof 才动手):null/数字归一化。 | |
| // - reasoning 已是非空 string → 不动; | |
| // - rc 是非空 string → 镜像写入 rc 值(两字段最终都存在且非空); | |
| // - 两者皆无/皆空 → 补单个空格 " "(上游 len>0 不 trim:空白串过闸、空串 | |
| // 不过——空白串占位有官方 Moonshot 规则 "-" 同款先例,且对模型上下文 | |
| // 无语义影响:该字段是透传校验位非内容消费位)。 | |
| for _, mm := range msgs { | |
| msg, ok := mm.(map[string]any) | |
| if !ok { | |
| continue | |
| } | |
| role, _ := msg["role"].(string) | |
| if role != "assistant" { | |
| continue | |
| } | |
| rc, hasRC := msg["reasoning_content"].(string) | |
| if hasRC { | |
| // rc 已有 string → 不覆盖(原有语义保留)。 | |
| } else if r, ok := msg["reasoning"].(string); ok { | |
| rc = r | |
| msg["reasoning_content"] = rc | |
| } else { | |
| rc = "" | |
| msg["reasoning_content"] = rc | |
| } | |
| // 镜像:reasoning 缺失/null/空串 → 归一化(非空 rc 优先,皆无补 " ")。 | |
| if r, ok := msg["reasoning"].(string); ok && r != "" { | |
| continue // 已非空 → 不覆盖 | |
| } | |
| if rc != "" { | |
| msg["reasoning"] = rc | |
| } else { | |
| msg["reasoning"] = " " | |
| } | |
| } | |
| } | |
| // injectThinking 按 DeepSeek 思维链开关规则改写请求体。非 deepseek 零改动。 | |
| // | |
| // 核心逻辑(对齐官方客户端): | |
| // - 「开思考」必须 thinking.type=enabled + 有 effort 档位(Hermes #43 打回证据)。 | |
| // - 显式 thinking.type 非空 → 客户端显式控制:enabled 缺 effort 时补默认档; | |
| // disabled 尊重并删 reasoning_effort(snake/camel 双字段)。 | |
| // - 无 thinking / type 空 / 已有 effort → 注入 enabled 并补默认档(已有 effort 不覆盖)。 | |
| // | |
| // defaultEffort 为该模型声明的默认档(来自 FetchModels 缓存 reasoning.defaultEffort); | |
| // 空串时回退硬编码 defaultDeepSeekEffort(向后兼容)。 | |
| func injectThinking(obj map[string]any, defaultEffort string) { | |
| model, _ := obj["model"].(string) | |
| if !isDeepSeekModel(model) { | |
| return | |
| } | |
| th, ok := obj["thinking"].(map[string]any) | |
| typ := "" | |
| if ok { | |
| typ, _ = th["type"].(string) | |
| typ = strings.TrimSpace(typ) | |
| } | |
| // 显式控制分支:type 非空(enabled/disabled 均为明确意图)→ 不改 type。 | |
| if typ != "" { | |
| if strings.EqualFold(typ, "disabled") { | |
| delete(obj, "reasoning_effort") | |
| delete(obj, "reasoningEffort") | |
| return // disabled:关思考且不带任何 effort(照抄客户端 case 行为) | |
| } | |
| ensureDeepSeekEffort(obj, defaultEffort) // 显式 enabled 缺 effort → 补默认档 | |
| return | |
| } | |
| // 无 thinking(或 thinking 非法非对象值)或 thinking 对象 type 缺失/为空: | |
| // 注入 enabled(客户端 case "deepseek" 行为)。有 reasoning_effort 也走此分支 | |
| // (effort 保留给既有降级逻辑,开关照开)。 | |
| if !ok { | |
| obj["thinking"] = map[string]any{"type": "enabled"} | |
| } else { | |
| th["type"] = "enabled" | |
| } | |
| ensureDeepSeekEffort(obj, defaultEffort) | |
| } | |
| // ensureDeepSeekEffort 缺 effort 档位时补默认档(snake 优先,camel 兜底)。 | |
| // 已有任一 effort → 不覆盖(显式档位不做任何改写,降级交给 normalizeReasoningEffort)。 | |
| // defaultEffort 空串 → 回退 defaultDeepSeekEffort(硬编码 "high")。 | |
| func ensureDeepSeekEffort(obj map[string]any, defaultEffort string) { | |
| _, hasSnake := obj["reasoning_effort"] | |
| if hasSnake { | |
| return | |
| } | |
| _, hasCamel := obj["reasoningEffort"] | |
| if hasCamel { | |
| return | |
| } | |
| if defaultEffort == "" { | |
| defaultEffort = defaultDeepSeekEffort | |
| } | |
| obj["reasoning_effort"] = defaultEffort | |
| } | |