package runtime import ( "strings" "agent-system-backend/internal/vision" core "agent-system-core" ) // SecretLookup 由组合根注入。layer 是 owner 或 system。没有独立密钥时 ok 为 false,调用方用设置里的旧字段。 var SecretLookup func(owner, service string) (value, layer string, ok bool) // LookupSecret 给组合根以外的调用方使用。owner 为空时用 local。 func LookupSecret(owner, service, fallback string) string { value, _ := overlayKey(owner, service, fallback) return value } func overlayKey(owner, service, fallback string) (string, string) { if SecretLookup == nil { return fallback, "settings" } if owner == "" { owner = "local" } if v, layer, ok := SecretLookup(owner, service); ok && strings.TrimSpace(v) != "" { if layer == "" { layer = "owner" } return v, layer } return fallback, "settings" } // 模型路由解析(4.7 / 4.9 覆盖链):设置层 defaults → system → user → project, // 画像 ModelRoute 为会话内最高层覆盖(kind 为空回落设置层主模型)。 // 端点与密钥按 kind 独立配置,轻量档空则回落主档。 // ContextInjection 是一条条件注入,由 runmgr 从存储读出,组装时再转换成 ctxasm 的形状。 type ContextInjection struct { Keys []string Secondary string Logic string Content string Constant bool ScanDepth int BudgetChars int } // ResolvedModel 是一次路由解析的结果。 type ResolvedModel struct { Kind string // chat | lightweight Model string Base string Key string KeySource string // owner | system | settings } // ResolveChatModel 解析主对话模型(画像 route > 设置层)。 func ResolveChatModel(prof core.AgentProfile, eff core.EffectiveSettings) ResolvedModel { m := eff.GetString("models.chat.model", "") if prof.Model.Chat != "" { m = prof.Model.Chat } key, source := overlayKey(prof.Owner, "models.chat", eff.GetString("models.chat.apiKey", "")) return ResolvedModel{Kind: "chat", Model: m, Base: eff.GetString("models.chat.baseUrl", ""), Key: key, KeySource: source} } // ResolveVisionModel 在当前轮含图片时核对视觉清单。清单内的模型保持原选择; // 不在清单内则这一轮改用 models.vision.model,端点和密钥仍沿用主模型。 // 没有图片时与 ResolveChatModel 相同。 func ResolveVisionModel(prof core.AgentProfile, eff core.EffectiveSettings, hasImages bool) ResolvedModel { chat := ResolveChatModel(prof, eff) if !hasImages || modelCapable(chat.Model, VisionCapable(eff)) { return chat } name := strings.TrimSpace(eff.GetString("models.vision.model", "")) if name == "" { name = vision.DefaultVisionModel } if name == chat.Model { return chat } chat.Model = name return chat } // VisionCapable 解析 models.vision.capable。空字符串用内置默认清单。 func VisionCapable(eff core.EffectiveSettings) []string { raw := strings.TrimSpace(eff.GetString("models.vision.capable", "")) if raw == "" { raw = vision.DefaultVisionCapable } return splitModelList(raw) } func modelCapable(name string, capable []string) bool { name = strings.TrimSpace(name) if name == "" { return false } for _, item := range capable { if item == name { return true } } return false } func splitModelList(raw string) []string { var out []string seen := map[string]bool{} for _, part := range strings.Split(raw, ",") { name := strings.TrimSpace(part) if name == "" || seen[name] { continue } seen[name] = true out = append(out, name) } return out } // ChatFallbacks 解析 models.chat.fallbacks。默认空,不改变现有模型选择。 func ChatFallbacks(eff core.EffectiveSettings) []string { return splitModelList(eff.GetString("models.chat.fallbacks", "")) } // ChatFallbacksFor 在图片轮只保留视觉清单内的回退模型,避免退到不能看图的模型。 func ChatFallbacksFor(eff core.EffectiveSettings, hasImages bool) []string { all := ChatFallbacks(eff) if !hasImages { return all } capable := VisionCapable(eff) var out []string for _, name := range all { if modelCapable(name, capable) { out = append(out, name) } } return out } // ResolveLightweightModel 解析轻量模型:画像 route > 设置层轻量档 > 回落主档。 func ResolveLightweightModel(prof core.AgentProfile, eff core.EffectiveSettings) ResolvedModel { m := eff.GetString("models.lightweight.model", "") base := eff.GetString("models.lightweight.baseUrl", "") key, source := overlayKey(prof.Owner, "models.lightweight", eff.GetString("models.lightweight.apiKey", "")) if prof.Model.Lightweight != "" { m = prof.Model.Lightweight } if m == "" || base == "" { // 回落主档(模型与端点分别回落,允许轻量仅覆盖模型名)。 chat := ResolveChatModel(prof, eff) if m == "" { m = chat.Model } if base == "" { base, key, source = chat.Base, chat.Key, chat.KeySource } } return ResolvedModel{Kind: "lightweight", Model: m, Base: base, Key: key, KeySource: source} }