Files
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

211 lines
7.8 KiB
Go
Raw Permalink 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 (
"io"
"log/slog"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// discardLogger 返回一个丢弃所有记录的日志器,但仍带有
// 适配器的模块来源,因此未注入日志器而构建的适配器
// 绝不会产生无来源记录。
func discardLogger(source string) *slog.Logger {
return logger.WithSource(slog.New(slog.NewTextHandler(io.Discard, nil)), source)
}
// OpenAI 兼容与 Anthropic 适配器共用的推理线取值
// (design/04 §3.2,已于 2026-10-03 对照各提供商文档核对)。
const (
// thinkingTypeEnabled 与 thinkingTypeDisabled 是
// {"thinking":{"type":...}} 开关的取值。DeepSeek 默认开启,所以省略
// 该字段会让思考保持开启——「不干预」不是关闭它的办法;
// 只有显式 disabled(或 reasoning_effort "none")才是。
thinkingTypeEnabled = "enabled"
thinkingTypeDisabled = "disabled"
// effortNone 是关闭思考的 reasoning_effort 取值。它是
// 开关而非强度档位,所以绝不能被替换成某个档位:把它
// 映射到 "low" 会颠倒调用方的意图(关闭思考 → 开启思考)。
effortNone = "none"
)
// reasoningVendor 标识 OpenAI 兼容端点背后的厂商。
// 它们的线协议相同,但可接受的
// reasoning_effort 取值不同,所以必须按正确的
// 白名单校验,而不是盲目转发。
type reasoningVendor string
const (
vendorOpenAI reasoningVendor = "openai"
vendorDeepSeek reasoningVendor = "deepseek"
vendorQwen reasoningVendor = "qwen"
vendorUnknown reasoningVendor = "unknown"
)
// reasoningEffortAliases 是 DeepSeek 记录的兼容映射:
// 服务接受这些别名并采用映射后的档位。我们在自己这一侧
// 也做同一映射,因此请求体总是携带提供商
// 记录为有效值的那一档(design/04 §3.2 事实 1)。
var reasoningEffortAliases = map[string]string{
"minimal": "low",
"medium": "high",
"xhigh": "high",
"ultra": "max",
}
// reasoningEffortAllowed 是按厂商划分的可接受 reasoning_effort
// 取值集合。不在本表里的厂商完全没有 reasoning_effort 支持,
// 这一点会被上报,而不是靠猜。
//
// 这里刻意没有「向下取到最近档位」的降级:不支持的取值
// 会被丢弃并上报,因为静默改写它可能会颠倒调用方的
// 意图——早先的 bug 正是这样把 DeepSeek 的 "none"(关闭思考)
// 映射到了 "low"(开启思考)。
var reasoningEffortAllowed = map[reasoningVendor]map[string]bool{
vendorOpenAI: tierSet("none", "minimal", "low", "medium", "high", "xhigh", "max"),
vendorDeepSeek: tierSet("none", "low", "high", "max"),
}
// tierSet 由给定档位构建一个成员集合。
func tierSet(tiers ...string) map[string]bool {
set := make(map[string]bool, len(tiers))
for _, t := range tiers {
set[t] = true
}
return set
}
// reasoningVendorFor 按模型名分类 OpenAI 兼容请求的厂商,
// 模型名无法识别出厂商时退回到 base URL。
// 无法识别的组合得到 vendorUnknown,好让调用方上报「不
// 支持」而不是假定 OpenAI 语义。Anthropic 适配器用
// 同一套分类来区分 DeepSeek 的 /anthropic 端点(它忽略
// budget_tokens)与 Anthropic 端点。
func reasoningVendorFor(model, baseURL string) reasoningVendor {
m := strings.ToLower(strings.TrimSpace(model))
switch {
case strings.Contains(m, "deepseek"):
return vendorDeepSeek
case strings.Contains(m, "qwen"):
return vendorQwen
case strings.Contains(m, "gpt-"), strings.Contains(m, "chatgpt"),
strings.HasPrefix(m, "o1"), strings.HasPrefix(m, "o3"), strings.HasPrefix(m, "o4"):
return vendorOpenAI
}
u := strings.ToLower(strings.TrimSpace(baseURL))
switch {
case strings.Contains(u, "deepseek"):
return vendorDeepSeek
case strings.Contains(u, "dashscope"), strings.Contains(u, "aliyuncs"):
return vendorQwen
case strings.Contains(u, "openai"):
return vendorOpenAI
}
return vendorUnknown
}
// thinkingSwitchValue 把显式开/关开关映射到它的线取值。
func thinkingSwitchValue(enable bool) string {
if enable {
return thinkingTypeEnabled
}
return thinkingTypeDisabled
}
// normalizeDeepSeekEffort 把 effort 映射到 DeepSeek 实际采用的档位,
// 并报告该值是否为服务记录的取值:记录在案的集合
// (none/low/high/max)原样通过,兼容别名映射到其
// 官方档位。
func normalizeDeepSeekEffort(effort string) (string, bool) {
if tier, ok := reasoningEffortAliases[effort]; ok {
return tier, true
}
if reasoningEffortAllowed[vendorDeepSeek][effort] {
return effort, true
}
return "", false
}
// resolveReasoningEffort 针对厂商白名单校验 effort 并
// 返回要发送的值;"" 表示丢弃该字段。无法识别的值绝不会
// 被换成别的档位——它会被丢弃,并把 provider、field、
// value 与 model 一并上报,因为改动它会颠倒调用方的意图。DeepSeek
// 的兼容别名会被归一化为服务采用的档位并上报。
//
// vendor 必须是带 reasoning_effort 白名单的厂商(DeepSeek / OpenAI):
// 不带该字段的形状(vLLM / DashScope)以及形状未知的
// 端点会自行丢弃 effort 并上报,因此它们的调用方
// 永远不会走到本函数(见 reasoning_shape.go,裁决 03.4.2)。
func resolveReasoningEffort(log *slog.Logger, vendor reasoningVendor, effort, model string) string {
effort = strings.TrimSpace(effort)
if effort == "" {
return ""
}
allowed := reasoningEffortAllowed[vendor]
if allowed[effort] {
return effort
}
if vendor == vendorDeepSeek {
if tier, ok := normalizeDeepSeekEffort(effort); ok {
log.Warn("reasoning_effort alias normalized to the tier DeepSeek applies",
"provider", "deepseek", "field", "reasoning_effort",
"value", effort, "sent", tier, "model", model)
return tier
}
}
log.Warn("reasoning_effort dropped: unrecognized value",
"provider", string(vendor), "field", "reasoning_effort", "value", effort, "model", model)
return ""
}
// toolChoiceForces 报告 tool_choice 指令是否强制模型
// 调用工具("required"、Anthropic 的 "any",或具名工具)。auto/none 不会。
func toolChoiceForces(toolChoice any) bool {
switch v := toolChoice.(type) {
case nil:
return false
case string:
return strings.EqualFold(strings.TrimSpace(v), "required")
case map[string]any:
if forcedToolChoiceType(v["type"]) {
return true
}
_, named := v["function"]
return named
case map[string]string:
if forcedToolChoiceType(v["type"]) {
return true
}
_, named := v["function"]
return named
}
return false
}
// forcedToolChoiceType 报告 tool_choice 的 "type" 取值是否强制调用。
func forcedToolChoiceType(value any) bool {
t, _ := value.(string)
switch strings.ToLower(strings.TrimSpace(t)) {
case "required", "any", "tool":
return true
}
return false
}
// reportForcedToolChoiceWithThinking 就 DeepSeek 红线报警:
// 强制型工具调用——tool_choice "required"/"any"/具名工具——与思考
// 不兼容:思考开启(其默认)时 API 会用 400 拒绝该强制
// 指令,所以调用方必须先把思考显式关掉
// (design/04 §3.2 事实 5)。它只上报:解算过程绝不能自行
// 关掉思考。在 OpenAI 兼容适配器上该指令尚未上线
// (M2),所以那里只是前瞻性警告——它仍然记录了
// 所请求的组合永远无法被满足。
func reportForcedToolChoiceWithThinking(log *slog.Logger, provider, model string, toolChoice any, thinkingOn bool) {
if !thinkingOn || !toolChoiceForces(toolChoice) {
return
}
log.Warn("tool_choice forcing cannot be combined with thinking (forcing is rejected with a 400): disable thinking explicitly before forcing a tool call",
"provider", provider, "field", "tool_choice", "model", model)
}