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

201 lines
7.3 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 "strings"
// Router 把角色映射到模型名。未知角色解析为已配置的
// 默认模型。Router 还按角色携带一个提供商(默认用
// 全局提供商),好让多厂商部署能把角色的模型与
// 正确协议配对;提供商选择通过 RouteProvider 暴露。
//
// 按裁决 03.4.1,按角色路由里的「角色」是本回合以之
// 作答的 **persona**,而不是 LLM 消息角色:persona 绑定优先于
// 消息角色绑定,而未绑定的 persona 保持原有
// 行为。绑定的载体是生效的 models.json 组
// (runtimeconfig.Group.PersonaModels / PersonaProviders)。
//
// 图像路由刻意不再是本 Router 的职责(裁决
// 14.4.1/14.4.5):分层图像路由的唯一权威是
// runtime/multimodal/vision.Router,由 loop 带着主模型真实的
// 图像输入能力去调用它。这里曾有的「带图 → 切到视觉模型」规则
// 绕过了那个决策。
type Router struct {
defaultModel string
byRole map[Role]string
visionModel string
defaultProvider string
providerByRole map[Role]string
personaModel map[string]string
personaProvider map[string]string
}
// RouterOption 用于配置 Router。
type RouterOption func(*Router)
// WithRoleModel 把模型绑定到角色。
func WithRoleModel(role Role, model string) RouterOption {
return func(r *Router) { r.byRole[role] = model }
}
// WithVisionModel 配置专用视觉模型。它保留下来是为了
// 向后兼容(测试与按模型名路由纯图像请求的调用方),
// 但生产路径不再设置它:图像路由属于
// vision.Router(裁决 14.4.1/14.4.5)。
func WithVisionModel(model string) RouterOption {
return func(r *Router) { r.visionModel = model }
}
// WithPersonaModel 把模型绑定到 persona(裁决 03.4.1)。空的 persona 或
// 模型会被忽略,因此半配置的组无法造出一个把 persona 路由到
// 空模型的绑定。
func WithPersonaModel(persona, model string) RouterOption {
return func(r *Router) {
if p := trimKey(persona); p != "" && model != "" {
r.personaModel[p] = model
}
}
}
// WithPersonaProvider 把厂商协议绑定到 persona,这样一个 persona 可以
// 走与组默认不同的厂商作答(裁决 03.4.1)。请与
// WithPersonaModel 配对使用。
func WithPersonaProvider(persona, provider string) RouterOption {
return func(r *Router) {
if p := trimKey(persona); p != "" && provider != "" {
r.personaProvider[p] = provider
}
}
}
// WithDefaultProvider 设置 RouteProvider 对没有显式绑定的角色
// 返回的提供商。它默认是 openai。
func WithDefaultProvider(provider string) RouterOption {
return func(r *Router) { r.defaultProvider = provider }
}
// WithRoleProvider 把提供商绑定到角色,好让多厂商部署
// 能把角色路由到另一厂商的协议(例如一个角色走 anthropic、一个
// 走 openai)。请与 WithRoleModel 配对以给出该角色的模型名。
func WithRoleProvider(role Role, provider string) RouterOption {
return func(r *Router) { r.providerByRole[role] = provider }
}
// NewRouter 构建 Router,并对任何没有显式绑定的角色
// 套用默认模型。
func NewRouter(defaultModel string, opts ...RouterOption) *Router {
r := &Router{
defaultModel: defaultModel,
byRole: map[Role]string{},
defaultProvider: ProviderOpenAI,
providerByRole: map[Role]string{},
personaModel: map[string]string{},
personaProvider: map[string]string{},
}
for _, o := range opts {
o(r)
}
return r
}
// trimKey 在存入之前归一化 persona id 或提供商名,因此
// " cyrene" 与 "cyrene" 不会变成两个不同的绑定。
func trimKey(k string) string { return strings.TrimSpace(k) }
// Route 返回角色的模型,回退到默认模型。
func (r *Router) Route(role Role) string {
if m, ok := r.byRole[role]; ok && m != "" {
return m
}
return r.defaultModel
}
// RouteProvider 返回角色的提供商,回退到 Router 的
// 默认提供商。它是多厂商部署的路由钩子:每个
// 角色可以携带自己的提供商+模型配对,而未绑定的角色回
// 退到全局提供商。runtime/loop 应据该决策构建按角色的
// 适配器或链(提供商感知工厂就是构建器)。
func (r *Router) RouteProvider(role Role) string {
if p, ok := r.providerByRole[role]; ok && p != "" {
return p
}
return r.defaultProvider
}
// RouteRequest 为请求挑出模型,并报告该请求
// 是否携带图像内容。当请求携带图像内容且已配置
// 视觉模型时,返回视觉模型并把 needsVision 置为
// true。否则由最后一条消息的角色驱动路由(默认
// user)。
//
// 在生产路径中已弃用:loop 改经
// RouteRequestForPersona 按 persona 路由,图像路由属于 vision.Router(裁决
// 14.4.1/14.4.5)。本方法保留是因为 agent 包与
// 既有测试都建立在 ModelRouter 契约之上。
func (r *Router) RouteRequest(req Request) (model string, needsVision bool) {
role := RoleUser
if n := len(req.Messages); n > 0 {
role = req.Messages[n-1].Role
}
needsVision = HasImage(req.Messages)
if needsVision && r.visionModel != "" {
return r.visionModel, true
}
return r.Route(role), needsVision
}
// PersonaRouter 是 ModelRouter 可选的 persona 感知路由扩展
// (裁决 03.4.1)。loop 会在 agent 的 router 上探测它,并优先于
// RouteRequest 使用,从而让按 persona 的模型/提供商绑定真正到达
// 请求。它单列为一个接口,好让只知道消息角色的 router
// 保持可用、不被改动。
type PersonaRouter interface {
RouteRequestForPersona(personaID string, req Request) (model string, needsVision bool)
}
// RouteForPersona 返回绑定到 persona 的模型,回退到
// Router 的默认模型。与 Route(消息角色)不同,这是 persona 级
// 绑定:裁决 03.4.1 把按角色路由的「角色」定义为 persona。
func (r *Router) RouteForPersona(persona string) string {
if m, ok := r.personaModel[trimKey(persona)]; ok && m != "" {
return m
}
return r.defaultModel
}
// RouteProviderForPersona 返回绑定到 persona 的提供商,回退到
// Router 的默认提供商(其本身即生效组的协议)。
func (r *Router) RouteProviderForPersona(persona string) string {
if p, ok := r.personaProvider[trimKey(persona)]; ok && p != "" {
return p
}
return r.defaultProvider
}
// RouteRequestForPersona 以给定 persona 路由请求:persona
// 绑定优先于消息角色决策,而没有绑定的 persona
// 回退到 RouteRequest,因此未配置的情形毫无变化。它
// 报告请求是否携带图片,但绝不会把模型切到
// 视觉模型——那个决策属于 vision.Router(裁决 14.4.1/14.4.5)。
func (r *Router) RouteRequestForPersona(personaID string, req Request) (model string, needsVision bool) {
needsVision = HasImage(req.Messages)
if m, ok := r.personaModel[trimKey(personaID)]; ok && m != "" {
return m, needsVision
}
role := RoleUser
if n := len(req.Messages); n > 0 {
role = req.Messages[n-1].Role
}
return r.Route(role), needsVision
}
// HasImage 报告是否有任何消息携带图片内容片段。
func HasImage(msgs []Message) bool {
for _, m := range msgs {
for _, part := range m.Content {
if part.Type == ContentTypeImage {
return true
}
}
}
return false
}