llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
package llm
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// APIFormat 是自定义供应商的 API 协议格式。
|
||
type APIFormat = string
|
||
|
||
const (
|
||
// FormatAnthropic 走 Anthropic Messages API(/v1/messages)。
|
||
FormatAnthropic = "anthropic"
|
||
// FormatChatCompletions 走 OpenAI Chat Completions(/chat/completions)。
|
||
FormatChatCompletions = "chat_completions"
|
||
// FormatResponses 走 OpenAI Responses API(/responses)。
|
||
FormatResponses = "responses"
|
||
)
|
||
|
||
// NormalizeAPIFormat 归一化用户输入的格式别名;未知格式返回空串。
|
||
func NormalizeAPIFormat(s string) APIFormat {
|
||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||
case "anthropic", "anthropic_messages", "messages":
|
||
return FormatAnthropic
|
||
case "chat_completions", "chatcompletions", "openai", "chat":
|
||
return FormatChatCompletions
|
||
case "responses", "response":
|
||
return FormatResponses
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// ProviderSpec 描述一个模型供应商的接入参数。
|
||
type ProviderSpec struct {
|
||
Name string // 展示名
|
||
BaseURL string
|
||
APIKey string
|
||
APIFormat APIFormat
|
||
Model string // 默认模型
|
||
AllowedModels []string // /models 白名单;空 = 不筛选
|
||
MaxTokens int // anthropic 单次回复上限
|
||
}
|
||
|
||
// NewProvider 按 spec 构造对应协议的 Provider。
|
||
func NewProvider(spec ProviderSpec) (Provider, error) {
|
||
format := spec.APIFormat
|
||
if format == "" {
|
||
format = guessFormat(spec.BaseURL)
|
||
}
|
||
switch format {
|
||
case FormatAnthropic:
|
||
return &AnthropicProvider{BaseURL: spec.BaseURL, APIKey: spec.APIKey, Model: spec.Model, MaxTokens: spec.MaxTokens}, nil
|
||
case FormatResponses:
|
||
return &ResponsesProvider{BaseURL: spec.BaseURL, APIKey: spec.APIKey, Model: spec.Model}, nil
|
||
case FormatChatCompletions:
|
||
return &OpenAIProvider{BaseURL: spec.BaseURL, APIKey: spec.APIKey, Model: spec.Model}, nil
|
||
}
|
||
return nil, fmt.Errorf("llm: unknown api_format %q (want anthropic | chat_completions | responses)", spec.APIFormat)
|
||
}
|
||
|
||
// guessFormat 依据 BaseURL 猜协议:含 "anthropic" 视为 anthropic,其余默认 chat_completions。
|
||
func guessFormat(baseURL string) APIFormat {
|
||
if strings.Contains(strings.ToLower(baseURL), "anthropic") {
|
||
return FormatAnthropic
|
||
}
|
||
return FormatChatCompletions
|
||
}
|
||
|
||
// DedupeModels 去空白、大小写不敏感去重(保留首次出现的原始写法与顺序)。
|
||
func DedupeModels(in []string) []string {
|
||
seen := make(map[string]bool, len(in))
|
||
out := make([]string, 0, len(in))
|
||
for _, s := range in {
|
||
s = strings.TrimSpace(s)
|
||
if s == "" || seen[strings.ToLower(s)] {
|
||
continue
|
||
}
|
||
seen[strings.ToLower(s)] = true
|
||
out = append(out, s)
|
||
}
|
||
if len(out) == 0 {
|
||
return nil
|
||
}
|
||
return out
|
||
}
|
||
|
||
// FilterAllowed 按白名单筛选模型列表;白名单为空返回原列表。
|
||
// 支持通配前缀("glm-*")与精确名;大小写不敏感。
|
||
func FilterAllowed(models []string, allowed []string) []string {
|
||
if len(allowed) == 0 {
|
||
return models
|
||
}
|
||
out := make([]string, 0, len(models))
|
||
for _, m := range models {
|
||
for _, a := range allowed {
|
||
if matchesAllowed(m, a) {
|
||
out = append(out, m)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// matchesAllowed 报告单个模型是否匹配一条条目:精确名(EqualFold)或
|
||
// 前缀通配(尾部 *)。FilterAllowed(条目=白名单、模型=候选)与
|
||
// MissingRegisteredModels(条目=登记模型、模型=远端目录)共用同一匹配语义,
|
||
// 两处口径漂移会让"下架判定"和"白名单筛选"打架。
|
||
func matchesAllowed(model, pattern string) bool {
|
||
pattern = strings.ToLower(strings.TrimSpace(pattern))
|
||
if pattern == "" {
|
||
return false
|
||
}
|
||
if strings.HasSuffix(pattern, "*") {
|
||
return strings.HasPrefix(strings.ToLower(model), strings.TrimSuffix(pattern, "*"))
|
||
}
|
||
return strings.EqualFold(model, pattern)
|
||
}
|
||
|
||
// MissingRegisteredModels 返回登记模型里在 fetched(远端 /models 目录)中
|
||
// 已不存在的条目——「已下架」的判定源,TUI 多选表格与 WebUI 供应商页共用。
|
||
// 保序、保留原文写法、大小写不敏感去重;只做精确名比对(EqualFold),
|
||
// 通配条目(含 *)**不参与下架判定**:模式没法对具体 ID 目录作存在性结论,
|
||
// 按"目录里仍有前缀匹配"算存活会掩盖真下架,按字面比对又必然误报。
|
||
//
|
||
// fetched 为空(拉取失败 / anthropic 无枚举端点)返回 nil:没有目录就没有
|
||
// 资格谈下架,误标比漏标危害大。
|
||
func MissingRegisteredModels(registered, fetched []string) []string {
|
||
if len(registered) == 0 || len(fetched) == 0 {
|
||
return nil
|
||
}
|
||
seen := map[string]bool{}
|
||
var out []string
|
||
for _, r := range registered {
|
||
r = strings.TrimSpace(r)
|
||
if r == "" || seen[strings.ToLower(r)] || strings.HasSuffix(r, "*") {
|
||
continue // 通配条目跳过判定(见上),不代表存活也不标下架
|
||
}
|
||
seen[strings.ToLower(r)] = true
|
||
alive := false
|
||
for _, m := range fetched {
|
||
if strings.EqualFold(m, r) {
|
||
alive = true
|
||
break
|
||
}
|
||
}
|
||
if !alive {
|
||
out = append(out, r)
|
||
}
|
||
}
|
||
return out
|
||
}
|