Files
AutoZero/llm/provider.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
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。
2026-10-26 00:54:46 +08:00

153 lines
4.9 KiB
Go
Raw 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 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
}