package model import ( "errors" "fmt" "log/slog" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // 提供商感知工厂接受的提供商名。这是工厂目前理解的仅有的 // 两种协议;新增厂商意味着在这里加一个 case // 再加一个配套适配器,而不必改动包内其它部分。 const ( // ProviderOpenAI 选择 OpenAI 兼容的 /chat/completions 协议。 ProviderOpenAI = "openai" // ProviderAnthropic 选择 Anthropic Messages 的 /v1/messages 协议。 ProviderAnthropic = "anthropic" ) // 配置的提供商名无法识别时,由提供商感知工厂返回 // ErrUnknownProvider。 var ErrUnknownProvider = errors.New("model: unknown provider") // NewChatAdapter 按 cfg.Provider 构建单个提供商适配器。 // 提供商为空时为向后兼容默认用 openai。无法识别的 // 提供商返回 ErrUnknownProvider。 func NewChatAdapter(cfg config.LLMConfig) (Adapter, error) { switch cfg.Provider { case ProviderOpenAI, "": return NewOpenAIChat(cfg), nil case ProviderAnthropic: return NewAnthropicChat(cfg), nil default: return nil, fmt.Errorf("model: %w: provider %q", ErrUnknownProvider, cfg.Provider) } } // NewChatChain 为配置的提供商构建降级 Chain:先是主 // 模型,再是提供商已设置且不同的降级模型。每一步 // 都由所选协议的适配器服务。提供商为空 // 时默认用 openai。无法识别的提供商返回 ErrUnknownProvider。 func NewChatChain(cfg config.LLMConfig) (*Chain, error) { return NewChatChainWithLog(cfg, nil) } // NewChatChainWithLog 是 NewChatChain,并为它构建的每个适配器 // 挂上日志器,好让提供商层面的推理决策(为容纳 // max_tokens 而抬高的思考预算、被降级或忽略的 reasoning_effort 值) // 带着模块来源上报。nil 日志器则保留适配器的丢弃式默认值。 // // 它也是唯一会追加可选本地第三级(local rung)的构建器 // (裁决 03.4.3,primary → fallback → local),因为那一级可能说 // 与生效组不同的协议,因此需要原始日志器来 // 构建自己的适配器。只有当本地级已启用且完全 // 配置好时才会追加;否则链保持两级形状,并 // 上报原因。 func NewChatChainWithLog(cfg config.LLMConfig, log *slog.Logger) (*Chain, error) { return newChainWith(cfg, log, nil) } // newChainWith 是 NewChatChainWithLog 加上用量 sink(切片 U1)。sink 为 nil 时 // 逐字等于后者:适配器只记日志。组合根经 RuntimeLLM 注入真实写入器。 func newChainWith(cfg config.LLMConfig, log *slog.Logger, sink UsageSink) (*Chain, error) { var chain *Chain var err error switch cfg.Provider { case ProviderOpenAI, "": chain, err = newOpenAIChain(cfg, WithOpenAILogger(log), WithUsageSink(sink)) case ProviderAnthropic: chain, err = newAnthropicChain(cfg, WithAnthropicLogger(log), WithAnthropicUsageSink(sink)) default: return nil, fmt.Errorf("model: %w: provider %q", ErrUnknownProvider, cfg.Provider) } if err != nil { return nil, err } if step, ok := localChainStep(cfg, log, sink); ok { chain.steps = append(chain.steps, step) } return chain, nil } // localChainStep 把本地第三级降级构造成一个链步骤。 // 当该级关闭、未配置或其提供商未知时,返回 ok=false // (若该级被要求但配置不全,则附一条点出缺失字段的 WARN) // ——一个半配置的步骤只会失败,那还不如没有 // 它。用量 sink 照旧透传:本地级也是模型调用,同样要计成本。 func localChainStep(cfg config.LLMConfig, log *slog.Logger, sink UsageSink) (Step, bool) { local := cfg.Local // Configured() 本身已要求 Enabled,所以该级默认关闭, // 被禁用的级永远不会成为链步骤。只有当该级确实被 // 要求、但配置不全时才发出该警告。 if !local.Configured() { if local.Enabled { warn(log, "local model rung enabled but not configured; skipping it", "base_url_set", strings.TrimSpace(local.BaseURL) != "", "model_set", strings.TrimSpace(local.Model) != "") } return Step{}, false } localCfg := config.LLMConfig{ Provider: local.Provider, BaseURL: local.BaseURL, APIKey: local.APIKey, Model: local.Model, } switch local.Provider { case ProviderAnthropic: return Step{Model: local.Model, Adapter: NewAnthropicChat(localCfg, WithAnthropicLogger(log), WithAnthropicUsageSink(sink))}, true case ProviderOpenAI, "": return Step{Model: local.Model, Adapter: NewOpenAIChat(localCfg, WithOpenAILogger(log), WithUsageSink(sink))}, true default: warn(log, "local model rung skipped: unknown provider", "provider", local.Provider, "field", "LLM_LOCAL_PROVIDER") return Step{}, false } } // newOpenAIChain 由 OpenAI 兼容配置块构建降级链。 // 每一步都是针对该端点的 OpenAIChatAdapter。 func newOpenAIChain(cfg config.LLMConfig, opts ...OpenAIOption) (*Chain, error) { if cfg.BaseURL == "" || cfg.Model == "" { return nil, ErrNoModel } steps := []Step{{Model: cfg.Model, Adapter: NewOpenAIChat(cfg, opts...)}} if cfg.Fallback != "" && cfg.Fallback != cfg.Model { steps = append(steps, Step{Model: cfg.Fallback, Adapter: NewOpenAIChat(cfg, opts...)}) } return NewChain(steps...), nil } // newAnthropicChain 由 Anthropic 配置块构建降级链。每一步 // 都是针对该 /v1/messages 端点的 AnthropicChatAdapter。 func newAnthropicChain(cfg config.LLMConfig, opts ...AnthropicOption) (*Chain, error) { if cfg.AnthropicBaseURL == "" || cfg.AnthropicModel == "" { return nil, ErrNoModel } steps := []Step{{Model: cfg.AnthropicModel, Adapter: NewAnthropicChat(cfg, opts...)}} if cfg.AnthropicFallback != "" && cfg.AnthropicFallback != cfg.AnthropicModel { steps = append(steps, Step{Model: cfg.AnthropicFallback, Adapter: NewAnthropicChat(cfg, opts...)}) } return NewChain(steps...), nil } // DefaultModelForProvider 返回配置提供商的主模型名: // "openai" 取扁平配置里的 OpenAI Model,"anthropic" 取 Anthropic 模型。 // 调用方(如 cmd/cyrene)用它播种 Router 的默认值。 func DefaultModelForProvider(cfg config.LLMConfig) string { if cfg.Provider == ProviderAnthropic { return cfg.AnthropicModel } return cfg.Model } // warn 通过注入的日志器上报,降级到一个仍带有模块来源的丢弃式 // 日志器,因此绝不会产生无来源日志 // (项目铁律 §10)。它之所以存在,是因为链可能在没有日志器 // 的情况下构建(NewChatChain),而被跳过的级仍须上报。 func warn(log *slog.Logger, msg string, args ...any) { if log == nil { log = discardLogger("model") } log.Warn(msg, args...) }