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。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
+152
@@ -0,0 +1,152 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user