- model 层新增可选 UsageSink(nil ⇒ 逐字不变,既有用例零改动) - 组合根注入 UsageWriter:有界队列 1024 + 单写者 + 批量事务(128/批、2s 兜底), 队列满丢弃 + 限流 WARN,绝不阻塞模型调用;sink 挂 RuntimeLLM(热重载不断) - 迁移 0017:表 llm_usage(13 列,session_id 可空)+ idx_created_at + idx_session_id - 新端点 GET /api/v1/admin/llm/usage(owner|admin 读;缺省近 24h、上限 90 天、 超限 400 不静默裁剪;hitRate = cacheHit/(cacheHit+cacheMiss),分母 0 ⇒ 0; 绝不含凭据与正文;未接线/查库失败 ⇒ 500 usage_unavailable) - LLM_USAGE_RETENTION(默认 720h,≤0 = 关闭清理):启动清一次 + 每小时 - 面板 LLM 页「用量(近 24h)」小卡 + Docs 两份同步 v0.64 - 已知缺口(如实登记):session_id 恒 NULL,需 loop 侧在请求上打标,留列后接
167 lines
6.6 KiB
Go
167 lines
6.6 KiB
Go
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...)
|
||
}
|