- 03.4.1:per-persona 路由载体选 models.json(组内新增 personaModels/personaProviders + 校验 + PersonaRoute()), Router/RuntimeLLM 快照绑定、loop 流式路径按该 turn 的 persona 路由;主步用路由结果、降级步钉自己的模型 - 03.4.3:本地第三级降级链(LLM_LOCAL_*,Configured() 含 Enabled ⇒ **默认关**),warn 标注来源 - 14.4.4:capacity 接入 L3(分类器接口 + TTL 缓存 + 严格 JSON 解析),CAPACITY_L3_ENABLED **默认 false** - 14.4.1:语义修正 —— group.multimodal 才是「主模型能否收图」(env 路径不再臆断),LLM_VISION_MODEL 只配视觉工具; vision/OCR 可各用不同模型;runtime 不再因有图抢路由(vision.Router 是唯一分层权威) - 14.4.5:新增 image-crop 工具(工具数 5→6,策略默认 owner-only),schema 支持按区域裁切 - 组合根:wire_runtime 读 group.Multimodal;capacity 装配拆出 wire_capacity.go(守 500 行) - 复核:`go build`/`go vet`/各包 `go test -count=1` 全绿;**6 条变异反证**先红后还原(含作者自查出的 2 条假阳性变异) - 未落:按 provider 的 thinking 开关(需 openai_encode.go/openai.go 文件面)、persona awakening(需 loader 文件面)
161 lines
6.4 KiB
Go
161 lines
6.4 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
)
|
|
|
|
// Provider names accepted by the provider-aware factory. These are the only two
|
|
// protocols the factory understands today; adding a vendor means adding a case
|
|
// here plus a matching adapter, not touching the rest of the package.
|
|
const (
|
|
// ProviderOpenAI selects the OpenAI-compatible /chat/completions protocol.
|
|
ProviderOpenAI = "openai"
|
|
// ProviderAnthropic selects the Anthropic Messages /v1/messages protocol.
|
|
ProviderAnthropic = "anthropic"
|
|
)
|
|
|
|
// ErrUnknownProvider is returned by the provider-aware factory when the
|
|
// configured provider name is not recognized.
|
|
var ErrUnknownProvider = errors.New("model: unknown provider")
|
|
|
|
// NewChatAdapter builds a single provider adapter selected by cfg.Provider. The
|
|
// empty provider defaults to openai for backward compatibility. An unrecognized
|
|
// provider returns 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 builds a fallback Chain for the configured provider: the primary
|
|
// model first, then the provider's fallback model when set and distinct. Each
|
|
// step is served by an adapter of the selected protocol. The empty provider
|
|
// defaults to openai. An unrecognized provider returns ErrUnknownProvider.
|
|
func NewChatChain(cfg config.LLMConfig) (*Chain, error) {
|
|
return NewChatChainWithLog(cfg, nil)
|
|
}
|
|
|
|
// NewChatChainWithLog is NewChatChain with a logger attached to every adapter it
|
|
// builds, so a provider-level reasoning decision (a thinking budget raised to fit
|
|
// max_tokens, a reasoning_effort value downgraded or ignored) is reported with
|
|
// its module source. A nil logger leaves the adapters' discard default in place.
|
|
//
|
|
// It is also the only builder that appends the optional local third rung
|
|
// (裁决 03.4.3, primary → fallback → local), because that rung may speak a
|
|
// different protocol than the active group and therefore needs the raw logger to
|
|
// build its own adapter. It is appended only when the local rung is enabled AND
|
|
// fully configured; otherwise the chain keeps its two-level shape and the reason
|
|
// is reported.
|
|
func NewChatChainWithLog(cfg config.LLMConfig, log *slog.Logger) (*Chain, error) {
|
|
var chain *Chain
|
|
var err error
|
|
switch cfg.Provider {
|
|
case ProviderOpenAI, "":
|
|
chain, err = newOpenAIChain(cfg, WithOpenAILogger(log))
|
|
case ProviderAnthropic:
|
|
chain, err = newAnthropicChain(cfg, WithAnthropicLogger(log))
|
|
default:
|
|
return nil, fmt.Errorf("model: %w: provider %q", ErrUnknownProvider, cfg.Provider)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if step, ok := localChainStep(cfg, log); ok {
|
|
chain.steps = append(chain.steps, step)
|
|
}
|
|
return chain, nil
|
|
}
|
|
|
|
// localChainStep builds the local third degradation rung as a chain step. It
|
|
// returns ok=false (and, when the rung was asked for but is incomplete, a WARN
|
|
// naming the missing field) when the rung is off, unconfigured or its provider is
|
|
// unknown — a half-configured step would only ever fail, which is worse than not
|
|
// having it.
|
|
func localChainStep(cfg config.LLMConfig, log *slog.Logger) (Step, bool) {
|
|
local := cfg.Local
|
|
// Configured() already requires Enabled, so the rung is off by default and a
|
|
// disabled rung can never become a chain step. The warning is emitted only
|
|
// when the rung was actually asked for but is incomplete.
|
|
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))}, true
|
|
case ProviderOpenAI, "":
|
|
return Step{Model: local.Model, Adapter: NewOpenAIChat(localCfg, WithOpenAILogger(log))}, true
|
|
default:
|
|
warn(log, "local model rung skipped: unknown provider",
|
|
"provider", local.Provider, "field", "LLM_LOCAL_PROVIDER")
|
|
return Step{}, false
|
|
}
|
|
}
|
|
|
|
// newOpenAIChain builds a fallback chain from the OpenAI-compatible config block.
|
|
// Each step is an OpenAIChatAdapter against the configured endpoint.
|
|
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 builds a fallback chain from the Anthropic config block. Each
|
|
// step is an AnthropicChatAdapter against the configured /v1/messages endpoint.
|
|
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 returns the primary model name for the configured
|
|
// provider: the flat OpenAI Model for "openai", the Anthropic model for
|
|
// "anthropic". Used by callers (e.g. cmd/cyrene) to seed a Router's default.
|
|
func DefaultModelForProvider(cfg config.LLMConfig) string {
|
|
if cfg.Provider == ProviderAnthropic {
|
|
return cfg.AnthropicModel
|
|
}
|
|
return cfg.Model
|
|
}
|
|
|
|
// warn reports through the injected logger, falling back to a discard logger that
|
|
// still carries the module source so an unsourced record can never be emitted
|
|
// (project rule §10). It exists because a chain may be built without a logger
|
|
// (NewChatChain) while a skipped rung must still be reported.
|
|
func warn(log *slog.Logger, msg string, args ...any) {
|
|
if log == nil {
|
|
log = discardLogger("model")
|
|
}
|
|
log.Warn(msg, args...)
|
|
}
|