Documentation
¶
Overview ¶
Package llm LLM 客户端栈(eino BaseChatModel 之上),本包唯一 package doc。 文件族分工:
- client/errors/config:客户端薄封装——生成失败指数退避+jitter 重试(429 退避 下限提高)、JSON 输出解析失败带错误回喂重试、token 记账(优先响应 usage, 缺省 CJK 感知估算(EstTokensCJK))、fitInput 输入自守恒、可选 token bucket 限速
- provider/openai:Provider 抽象与多后端 + FallbackChain 降级链
- resilient/failover:弹性客户端(降级矩阵/熔断/预算短路)+ 裸模型路径的 FailoverModel 装饰器
- budget/cost/usage:预算、成本、完整用量采集三套记账
- stage_router:按业务阶段路由模型
FailoverModel 模型链 failover 装饰器(eino BaseChatModel/ToolCallingChatModel 双形态)。
Resilient/FallbackChain 只覆盖 agentkit 自己的 Generator 客户端路径;ReAct 主路径 (ADK ChatModelAgent 直调模型)拿到的就是裸 BaseChatModel,没有降级链。本装饰器 填补这一层:按序尝试模型,前一个失败且调用方 ctx 仍存活时切下一个重放同一次请求。
刻意保持薄:不做熔断/限速/预算/同模型重试(那是 Resilient 的职责,二者可叠加—— Resilient.RawModelWithFailover() 即本装饰器包住整条 Provider 链)。切换决策对任何 错误恒真:429/5xx/网络类错误切换必然正确;401/403/上下文超限类确定性错误在模型 同端点时切了也白切,但异端点/异凭证时能救——误切代价仅一次模型调用。 调用方语义的失败(ctx 取消/超时)不切换,原样上抛。
从 bianque 生产装配提炼(v0.9.4);v0.10.9 由主备二元泛化为 N 模型链。
pricing.go — 模型定价:内建价表 + 归一化查价 + 调用方覆盖。
为什么价格要进 llm 层 ¶
此前 `llm` 有完整的成本追踪能力(CostTracker/CostRecord),但**价格不在这一层**: `CostTracker.Record` 要求调用方每次传入单价。于是每个下游都得自己维护一张 「模型名 → 价格」的表(通常落在应用配置里,键是模型名精确匹配),而:
- 漏配 = 成本**静默记 0**——0 成本在账单上与「免费模型」不可区分;
- 路由后缀对不上(`mimo-v2.5-pro-20250101`、部署名前缀)→ 静默漏配;
- 换模型、改价目表要改每个下游的配置。
本文件把价格收进框架层:内建价表随包维护,价目表变动时升版本即可,下游自动受益。
⚠️ 价目表是**参考价**,不是权威报价 ¶
- 采集时间见 builtinPricing 的注释;**厂商随时调价,本表必然滞后于厂商公告**。
- 同一模型在不同渠道(官方 / 代理 / 云托管)价格不同,缓存命中、批量、 区域定价也另算。本表只反映官方 list price。
- 契约价、代理加价、缓存折扣一律用 NewPriceTable 的 overrides 覆盖。
单位 ¶
**所有单价一律是「美元 / 每 100 万 token」**(Price.InputPerM / OutputPerM)。 本仓历史上并存过「每 1K token」(Provider.CostPer1KTokens、CostTracker.Record 的 costPer1K 参数)——两套单位并存是**一个数量级的错账**来源,故新 API 全部 收敛到每 1M;旧入口保留但标记 Deprecated。
本文件族是 LLM Provider 抽象与多后端支持(Provider 接口 + FallbackChain 模型降级链)。包级总览见 client.go 的唯一 package doc。
流恢复原语(对标 ZCode streaming-recovery.ts + model-errors.ts):turn 级 流式请求的瞬态错误分类、空终态检测、上下文溢出识别与 busy 准入退避。 单请求重试骨架见 errors.go(ClassifyLLMError)与 retryafter.go;本文件补 「流断在哪、什么算瞬态、什么时候放弃」的 turn 级语义。
StageRouter 分阶段模型路由:不同业务阶段(stage)用不同的 Generator—— 大窗口模型吃长输入(R1)、快模型跑判定(R3)、强模型保质量(R4)。
匹配语义:精确命中优先,其次最长前缀(注册 "R1" 可命中 "R1"/"R1a"); 未命中走缺省 Generator。装配示例:
sr := llm.NewStageRouter(defaultGen) // 缺省 = 主链(含预算注入)
sr.Use("R1", r1Gen) // 前缀路由,命中 "R1"/"R1a"
sr.Use("qa", qaGen) // 精确路由
// 之后 sr.Generate(ctx, "R1a", ...) 走 r1Gen
预算:路由不改变预算语义——各 Generator 应由调用方完成预算注入 (BudgetInjector.WithBudget)后再注册;UsedTokens 聚合全部已注册链。
usage.go — LLM 用量采集(eino callbacks)——**全仓唯一的 callbacks 侧记账 出口**:拿得到 Cached/Reasoning tokens、FinishReason、Duration、Iteration。 归因经 Labels(map)透传,领域键由调用方决定。 (v0.10.12 前 obsx.Options.OnUsage 是并行的五数字出口,防重护栏跨包放在 obsx——现护栏归位于本包,obsx 回归纯 trace,ReAct/RawModel 旁路记账统一 走 NewUsageHandler 注入 callbacks。)
Index ¶
- Constants
- Variables
- func BusyAdmissionDelay(retry int) time.Duration
- func IsContextExceededError(err error) bool
- func IsContextExceededFinish(finishReason, rawFinishReason string) bool
- func IsProviderBusyCode(code string) bool
- func IsRateLimitError(err error) bool
- func IsTransientStreamError(err error) bool
- func IsTruncatedError(err error) bool
- func LookupPrice(model string) (Price, PriceLookup)
- func NewUsageHandler(onRecord Sink) callbacks.Handler
- func PriceOf(pricing map[string]Price, model string, prompt, completion int) float64deprecated
- func RetryAfterFrom(ctx context.Context) time.Duration
- func RetryableLLMError(err error) bool
- func SelectRetryDelay(local, hint time.Duration) time.Duration
- func SuspiciousEmptyResult(finishReason string, responseLen, toolCallCount, usageTotal int) bool
- func WithCallCounter(ctx context.Context) context.Context
- func WithRetryAfterSink(ctx context.Context) context.Context
- func WithUsageLabels(ctx context.Context, labels map[string]string) context.Context
- type Attempt
- type AttemptError
- type Budget
- type BudgetConfig
- type BudgetHolder
- type BudgetInjector
- type ChainLink
- type Client
- func (c *Client) BoundBudget() TokenAccountant
- func (c *Client) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
- func (c *Client) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
- func (c *Client) RawModel() model.BaseChatModel
- func (c *Client) UsedTokens() int
- func (c *Client) WithBudget(b TokenAccountant) Generator
- type CostRecord
- type CostTracker
- func (ct *CostTracker) Record(model, stage string, promptTokens, completionTokens int, costPer1K [2]float64)deprecated
- func (ct *CostTracker) RecordAuto(model, stage string, promptTokens, completionTokens int)
- func (ct *CostTracker) RecordPriced(model, stage string, promptTokens, completionTokens int, price Price)
- func (ct *CostTracker) RecordUnpriced(model, stage string, promptTokens, completionTokens int)
- func (ct *CostTracker) Records() []CostRecord
- func (ct *CostTracker) Summary() map[string]ModelSummary
- func (ct *CostTracker) Unpriced() []CostRecord
- func (ct *CostTracker) UnpricedCount() int
- type FailoverModel
- func (m *FailoverModel) Generate(ctx context.Context, input []*schema.Message, opts ...einomodel.Option) (*schema.Message, error)
- func (m *FailoverModel) Stream(ctx context.Context, input []*schema.Message, opts ...einomodel.Option) (*schema.StreamReader[*schema.Message], error)
- func (m *FailoverModel) WithTools(tools []*schema.ToolInfo) (einomodel.ToolCallingChatModel, error)
- type FallbackChain
- type Generator
- type MatchKind
- type ModelSummary
- type OpenAIProvider
- func (p *OpenAIProvider) AttemptTimeout() time.Duration
- func (p *OpenAIProvider) ContextTokens() int
- func (p *OpenAIProvider) CostPer1KTokens() (float64, float64)
- func (p *OpenAIProvider) MaxOutputTokens() int
- func (p *OpenAIProvider) Model() model.BaseChatModel
- func (p *OpenAIProvider) ModelName() string
- func (p *OpenAIProvider) Name() string
- type OpenAIProviderConfig
- type Price
- type PriceLookup
- type PriceStatus
- type PriceTable
- type PricedProvider
- type Provider
- type Resilient
- func (r *Resilient) BoundBudget() TokenAccountant
- func (r *Resilient) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
- func (r *Resilient) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
- func (r *Resilient) LastModel() string
- func (r *Resilient) PrimaryModel() model.BaseChatModel
- func (r *Resilient) RawModel() model.BaseChatModel
- func (r *Resilient) RawModelWithFailover() model.BaseChatModel
- func (r *Resilient) String() string
- func (r *Resilient) UsedTokens() int
- func (r *Resilient) WithBudget(b TokenAccountant) Generator
- type ResilientConfig
- type RetryHint
- type Sink
- type StageRouter
- func (r *StageRouter) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
- func (r *StageRouter) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
- func (r *StageRouter) RawModel() model.BaseChatModel
- func (r *StageRouter) Use(stage string, gen Generator)
- func (r *StageRouter) UsedTokens() int
- type TokenAccountant
- type UsageCallCounter
- type UsageRecord
Constants ¶
const MaxServerRetryAfter = httpjson.MaxServerRetryAfter
MaxServerRetryAfter 服务端退避建议的可信上限:超过 5 分钟的建议不直接 采信(长时间限流应交由 failover/降级链处置,而非原地等)。
const MaxStreamRecoveryRetries = 10
MaxStreamRecoveryRetries 流断点恢复预算(对标 STREAM_RECOVERY_MAX_RETRIES: 跨过单请求重试边界后只恢复 1 次与用户预期差距过大,10 次是生产调过的上限)。
Variables ¶
BusyAdmissionDelays busy 准入重试的两档退避(对标 START_PLAN_BUSY_MAIN_TURN_ADMISSION_RETRY_DELAYS_MS:1s/2s)。
var ErrPriceUnknown = errors.New("llm: 模型价格未知(价表未收录该模型)")
ErrPriceUnknown 价表里查不到该模型。
用 LookupPriceStrict 拿这个哨兵;errors.Is 可判定。
Functions ¶
func BusyAdmissionDelay ¶
BusyAdmissionDelay 按 retry 序号(0 起)取准入退避;越界取最后一档。
func IsContextExceededError ¶
IsContextExceededError 错误文案层识别上下文溢出(provider 报文形态)。
func IsContextExceededFinish ¶
IsContextExceededFinish finishReason 层识别上下文溢出。
func IsProviderBusyCode ¶
IsProviderBusyCode provider 业务码是否「繁忙」类(准入级退避重试的判定; 耗尽后调用方应合成不可重试错误,别无限排队)。
func IsTransientStreamError ¶
IsTransientStreamError 流式 turn 中的错误是否瞬态(值得从安全锚点重开流)。 与 ClassifyLLMError 的分工:后者面向单请求 HTTP 重试;本判定面向 turn 级 恢复——额外覆盖流空闲超时与原因码形态(错误链上任意一层的文案都参与匹配)。 调用方主动取消不算瞬态(与降级矩阵「ctx 取消 → 全链中止」一致):若判为 瞬态,取消后的恢复循环会空转 MaxStreamRecoveryRetries 轮、每轮立即再失败。
func LookupPrice ¶ added in v0.13.0
func LookupPrice(model string) (Price, PriceLookup)
LookupPrice 用内建价表查价。
func NewUsageHandler ¶
NewUsageHandler 构建 eino callbacks.Handler(非流式 OnEnd + 流式 OnEndWithStreamOutput——流式分支在后台 goroutine 排空副本流后回调, ⚠ onRecord 可能被多 goroutine 并发调用,须并发安全(第十轮审计钉约)。 OnEndWithStreamOutput——第八轮审计补齐:eino-ext 流式只触发后者,仅注册 OnEnd 的 handler 在开流(agentrun v0.10.32 起默认)下一条记录都不产生, 静默失明。流式分支消费框架给的副本流取末个非 nil usage,末帧即汇总帧)。 无 TokenUsage、无 RunInfo 的调用静默跳过;Labels 缺省不阻断采集。 防重:Client 自身配置了 OnUsage/Budget 时(Client 记账标记已在 ctx), 本 handler 对同一次物理调用自动跳过——两侧不会双倍记账,可同时启用。 ⚠ agentrun 场景:Config.OnUsage 与本 handler 记同一笔——**择一装配** (两者没有共享防重标记,同时挂会双计)。
func PriceOf
deprecated
PriceOf 按模型名匹配定价表估算单次调用成本(USD;未配价返回 0,不阻塞记账)。
Deprecated: 未配价时返回 0,与「免费模型」不可区分——这正是本仓要根除的静默失真。 改用 PriceTable.Lookup / LookupPrice:查不到时是「价格未知」而非 0。
另注意其「最长前缀回落」会把同主干异价 SKU 静默定价错(`gpt-4o-mini` 命中 `gpt-4o`),新查价 API 不再提供该行为。
func RetryAfterFrom ¶
RetryAfterFrom 读取最近一次 429 的服务端建议(0 = 无)。
func SelectRetryDelay ¶
SelectRetryDelay 退避取舍(钳制:建议 ≤5min 或 < 本地曲线才采信)。
func SuspiciousEmptyResult ¶
SuspiciousEmptyResult 空终态检测:零文本 + 零工具调用 + 非 stop 终止 + 零用量 = 可疑空响应(各家 provider 都会偶发;按可重试错误处理而非当正常 完结吞掉)。usageTotal 为 provider 报告的总 token(≤0 视为零用量/未知)。
func WithCallCounter ¶
WithCallCounter ctx 挂轮次计数器(每个 agent 运行作用域一个)。
func WithRetryAfterSink ¶
WithRetryAfterSink 在 ctx 注入 sink(重试环上游调用一次;传输层据此回写)。
Types ¶
type Attempt ¶
type Attempt struct {
Provider string
Model string
// Err error 类型(v0.11 起不再折损为 string):errors.Is/As 经
// AttemptError.Unwrap 可直达 RequestError/context.Canceled 等——下游无须
// 对聚合文案做文本分类(多模型聚合文本里 429 与 401 共存时文本分类结果
// 取决于 marker 优先级,不可靠)。
Err error
Duration time.Duration
}
Attempt 单模型尝试记录(AttemptError 的组成部分)。
type AttemptError ¶
type AttemptError struct {
Stage string
Attempts []Attempt
// BreakerSkipped 被熔断跳过(未真正尝试)的模型数——仅入 Error 文案,
// 区分"链上真失败 N 个"与"另 M 个熔断未试"。
BreakerSkipped int
}
AttemptError 全部模型失败的聚合错误(含每个模型的失败原因链)。
func (*AttemptError) Error ¶
func (e *AttemptError) Error() string
func (*AttemptError) Unwrap ¶
func (e *AttemptError) Unwrap() []error
Unwrap 暴露各模型的原始错误链(Go 多错误 Unwrap 约定):errors.Is/As 穿透 聚合错误直达底层错误类型。
type Budget ¶
type Budget struct {
// contains filtered or unexported fields
}
Budget 任务级 token 预算累计器(TokenAccountant 的标准实现)。 并发安全。
type BudgetConfig ¶
type BudgetConfig struct {
ContextTokens int // 模型上下文窗口(token 数,输入+输出合计)
}
BudgetConfig 模型窗口派生配置("单一基准"哲学:窗口是唯一基准,全部上限按比例派生)。
func (BudgetConfig) MaxDiffChars ¶
func (c BudgetConfig) MaxDiffChars() int
MaxDiffChars diff 总量上限(字符):窗口 35% 折算字符,min 10k。 chars/token 按 4 估算(中文按 2 保守,见 fitInput)。
func (BudgetConfig) MaxOutputTokens ¶
func (c BudgetConfig) MaxOutputTokens() int
MaxOutputTokens 单次生成上限:max(4096, 窗口 5%)。
func (BudgetConfig) ReqBodyMaxChars ¶
func (c BudgetConfig) ReqBodyMaxChars() int
ReqBodyMaxChars 需求文本上限:窗口 6%。
func (BudgetConfig) ReqIssueMaxChars ¶
func (c BudgetConfig) ReqIssueMaxChars() int
ReqIssueMaxChars issue 文本上限:窗口 1.5%。
func (BudgetConfig) TaskTokenBudget ¶
func (c BudgetConfig) TaskTokenBudget() int
TaskTokenBudget 任务 token 预算:窗口 60%。
type BudgetHolder ¶
type BudgetHolder interface {
BoundBudget() TokenAccountant
}
BudgetHolder 可选接口:暴露 Generator 绑定的预算累计器。 聚合方(StageRouter.UsedTokens)按其指针身份去重——各链共享同一 Budget 时 逐链求和会按链数倍增。
type BudgetInjector ¶
type BudgetInjector interface {
WithBudget(b TokenAccountant) Generator
}
BudgetInjector 可选接口:支持运行时注入任务级预算(Dispatcher fan-out 场景)。 调用方通过类型断言检测;不实现则预算保持构造时的值。
type ChainLink ¶
type ChainLink struct {
Model einomodel.BaseChatModel
Name string
}
ChainLink failover 链单链节:模型 + 观测名(结构化链节消平行切片错位—— names[i] 对不上 models[i] 是装配期静默事故)。
type Client ¶
type Client struct {
Model model.BaseChatModel
ModelName string
// 模型窗口(token,输入+输出合计)与单次输出预留。
ContextTokens int
MaxOutputTokens int
// OnUsage 每次调用后的 token 记账回调(stage, promptTokens, completionTokens)。
OnUsage func(stage string, prompt, completion int)
// Budget 任务 token 预算累计器(可 nil)。
Budget TokenAccountant
// Limiter 全局 LLM 请求限速器(token bucket);nil = 不限速。
Limiter *rate.Limiter
MaxRetries int // 最大尝试次数(含首次),默认 3
BaseDelay time.Duration // 指数退避基准,默认 2s
MaxDelay time.Duration // 退避上限,默认 30s
}
Client LLM 客户端(带重试、限速、预算、fitInput)。
func NewClient ¶
func NewClient(m model.BaseChatModel, modelName string, budget TokenAccountant) *Client
NewClient 创建 LLM 客户端。
func (*Client) BoundBudget ¶
func (c *Client) BoundBudget() TokenAccountant
BoundBudget 返回绑定的预算累计器(可 nil)。BudgetHolder 接口实现。
func (*Client) Generate ¶
func (c *Client) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
Generate 带重试的普通生成(指数退避 + jitter)。 确定性失败(鉴权/权限/上下文超限)不重试;429 退避下限提高后值得等 (单客户端无备选可切)。stage 同时注入 ctx(obsx.WithStage)——eino callbacks handler 可读到业务阶段。
func (*Client) GenerateJSON ¶
func (c *Client) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
GenerateJSON 生成并解析 JSON;解析失败把原始输出与错误回喂重试 1 次。 out 必须是 *T。截断在重试核心内已转为错误返回,走到这里的解析失败只会是 json.Unmarshal 错误——回喂提示只有"输出合法 JSON"一种。 刻意走 ExtractJSON+严格 Unmarshal 而非 jsonrepair.Unmarshal 宽容链:宽容修复 会改变输出内容,而本方法的失败路径要回喂重试,lastErr 须是纯解析错误。 宽容回收半损坏输出归 jsonrepair.Unmarshal(无重试回喂的场景用)。
func (*Client) RawModel ¶
func (c *Client) RawModel() model.BaseChatModel
RawModel 返回底层 eino 模型(Generator 接口实现)。
func (*Client) UsedTokens ¶
UsedTokens 返回累计消耗 token 数(无预算绑定时返回 0)。
func (*Client) WithBudget ¶
func (c *Client) WithBudget(b TokenAccountant) Generator
WithBudget Client 的预算注入:浅拷贝(Client 只含无锁值字段,Budget 指针共享是有意为之)。
type CostRecord ¶
type CostRecord struct {
Model string // 模型名
Stage string // 阶段(R1/R2/R3/R4/R5/plugin:xxx)
PromptTokens int
CompletionTokens int
CostUSD float64 // 本次成本(美元)
// PriceStatus 计价状态:PricePriced / PriceUnpriced。
//
// ⚠️ PriceUnpriced 时 CostUSD 是 **0**,但那表示「**价格未知**」而不是
// 「免费」——真实成本无从得知。判读汇总口径时必须同时看
// ModelSummary.UnpricedCalls:TotalCostUSD==0 且 UnpricedCalls>0
// ⇒ 成本口径不完整,不是「这个模型不要钱」。
PriceStatus PriceStatus
// PriceKey 命中的价表键(PricePriced 时非空;PriceUnpriced 时为空)。
PriceKey string
// PriceMatched 命中方式(exact / normalized;未命中为 none)。
PriceMatched MatchKind
}
CostRecord 单次调用成本记录。
type CostTracker ¶
type CostTracker struct {
// contains filtered or unexported fields
}
CostTracker token 成本追踪器。 按模型+阶段统计 token 消耗和成本。 环形缓冲:满窗后 Record O(1) 覆盖最旧,不再每笔整体搬移。
价格从哪来 ¶
三种记录入口,价格来源不同,**但「价格未知」在任何一条路径上都不会被记成 0**:
- RecordAuto:按价表自动查(内建价 + 调用方覆盖)——**推荐**。
- RecordPriced:调用方显式给单价(企业折扣价、代理加价率)。
- Record:调用方每次传单价(**已废弃**,单位是每 1K token,与 Price 的每 1M 差一个数量级)。
查不到价格时记录仍会写入(用量数据不丢),但会被标记为 PriceUnpriced, 可经 Unpriced() / UnpricedCount() 查出来——**0 成本与「价格未知」必须可区分**。
func NewCostTrackerWithTable ¶ added in v0.13.0
func NewCostTrackerWithTable(table *PriceTable) *CostTracker
NewCostTrackerWithTable 创建成本追踪器(指定价表)。
价表是**只读**的:可构造一份带覆盖的表传进来 (NewPriceTable(overrides)),也可在运行期用 PriceTable.WithOverride 造新表 再换一台 tracker。
func (*CostTracker) Record
deprecated
func (ct *CostTracker) Record(model, stage string, promptTokens, completionTokens int, costPer1K [2]float64)
Record 记录一次调用(**调用方自带单价,每 1000 token**)。
Deprecated: 单位是「每 1K token」,而 Price/RecordPriced 是「每 1M token」—— 同一仓里并存两套单位是一个数量级的错账来源。改用 RecordPriced(显式单价, 单位写在类型上)或 RecordAuto(按价表自动计价)。
func (*CostTracker) RecordAuto ¶ added in v0.13.0
func (ct *CostTracker) RecordAuto(model, stage string, promptTokens, completionTokens int)
RecordAuto 按价表自动计价并记录一次调用。
**查不到价格不报错,但也绝不静默记 0**:记录照写(用量数据不丢), 状态标为 PriceUnpriced,调用方可用 Unpriced() / UnpricedCount() 查出来, 汇总里也会在 ModelSummary.UnpricedCalls 上体现。
需要「查不到就硬失败」的路径用 LookupPriceStrict。
func (*CostTracker) RecordPriced ¶ added in v0.13.0
func (ct *CostTracker) RecordPriced(model, stage string, promptTokens, completionTokens int, price Price)
RecordPriced 记录一次调用,单价由调用方显式给出(**美元 / 每 1M token**)。
适用:企业折扣价、代理转发加价率、人民币计价合同的折算价—— 这些都**不属于**内建价表能覆盖的范围。
func (*CostTracker) RecordUnpriced ¶ added in v0.13.0
func (ct *CostTracker) RecordUnpriced(model, stage string, promptTokens, completionTokens int)
RecordUnpriced 记录一次调用,并显式标记为**价格未知**。
与 RecordAuto 查不到价时走的同一条路:CostUSD 是 0,但那表示「未知」不是 「免费」——记录上的 PriceStatus=PriceUnpriced 与汇总里的 UnpricedCalls 就是它的标记,运维据此告警,而不是对着恒为 0 的看板误判模型免费。
func (*CostTracker) Records ¶
func (ct *CostTracker) Records() []CostRecord
Records 返回所有记录的副本(时间序:最旧在前)。
func (*CostTracker) Summary ¶
func (ct *CostTracker) Summary() map[string]ModelSummary
Summary 按模型汇总。
func (*CostTracker) Unpriced ¶ added in v0.13.0
func (ct *CostTracker) Unpriced() []CostRecord
Unpriced 返回**价格未知**的记录(时间序:最旧在前)。
这是「未知价不静默」的可观测出口:账面上 CostUSD 是 0,但这些调用 根本没计价——运维能据此告警,而不是对着一个恒为 0 的看板以为模型免费。
func (*CostTracker) UnpricedCount ¶ added in v0.13.0
func (ct *CostTracker) UnpricedCount() int
UnpricedCount 返回价格未知的记录数。
type FailoverModel ¶
type FailoverModel struct {
// OnFailover 切换观测回调(from/to 模型名 + reason=失败模型错误;nil=静默)。
OnFailover func(from, to, reason string)
// contains filtered or unexported fields
}
FailoverModel 模型链装饰器(构建后只读;OnFailover 构建期设置)。
func NewFailoverModel ¶
func NewFailoverModel(links ...ChainLink) *FailoverModel
NewFailoverModel 模型链按序 failover 装饰(至少一节;主备二元传两节即可)。 nil 模型会在被尝试到时报"failover 链含 nil 模型"错误并继续下一个。
func (*FailoverModel) Generate ¶
func (m *FailoverModel) Generate(ctx context.Context, input []*schema.Message, opts ...einomodel.Option) (*schema.Message, error)
Generate 按链序尝试;失败且 ctx 存活(非调用方取消/超时)时切下一个。 全部失败上抛末个模型的错误——此前各模型的失败经 OnFailover 回调可见。
func (*FailoverModel) Stream ¶
func (m *FailoverModel) Stream(ctx context.Context, input []*schema.Message, opts ...einomodel.Option) (*schema.StreamReader[*schema.Message], error)
Stream 与 Generate 同一降级语义(首块前失败才可切换;已在流中失败无法换模型重放)。
func (*FailoverModel) WithTools ¶
func (m *FailoverModel) WithTools(tools []*schema.ToolInfo) (einomodel.ToolCallingChatModel, error)
WithTools 工具绑定转发:链上各模型分别派生带工具的实例后重新包装(不可变派生, 并发安全语义与 eino ToolCallingChatModel 契约一致)。
type FallbackChain ¶
type FallbackChain struct {
Providers []Provider
}
FallbackChain 模型降级链:按优先级尝试多个 Provider。
func NewFallbackChain ¶
func NewFallbackChain(providers ...Provider) *FallbackChain
NewFallbackChain 创建降级链。
func (*FallbackChain) Fallback ¶
func (fc *FallbackChain) Fallback() []Provider
Fallback 返回备选 Provider 列表(不含主 Provider)。
func (*FallbackChain) Primary ¶
func (fc *FallbackChain) Primary() Provider
Primary 返回主 Provider(优先级最高)。
type Generator ¶
type Generator interface {
Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
// UsedTokens 返回该客户端累计消耗的 token 数(无预算绑定时返回 0)。
UsedTokens() int
// RawModel 返回底层 eino 模型(供 ReAct agent 等需要裸模型的场景;
// Resilient 返回主模型——降级仅覆盖 Generate/GenerateJSON 路径)。
RawModel() model.BaseChatModel
}
Generator LLM 生成接口:*Client 与 *Resilient 都满足,调用方无感切换。
type ModelSummary ¶
type ModelSummary struct {
Model string
TotalPromptTokens int
TotalCompletionTokens int
TotalCostUSD float64
CallCount int
// PricedCalls 已计价的调用数。
PricedCalls int
// UnpricedCalls **价格未知**的调用数——这些调用的成本没进 TotalCostUSD。
//
// 判读口径:TotalCostUSD==0 且 UnpricedCalls>0 ⇒ 成本口径不完整
//(不是「这个模型免费」);TotalCostUSD==0 且 UnpricedCalls==0 ⇒ 确实免费/零价。
UnpricedCalls int
}
ModelSummary 单模型汇总。
type OpenAIProvider ¶
type OpenAIProvider struct {
// contains filtered or unexported fields
}
OpenAIProvider OpenAI 兼容 LLM 提供者。 支持 OpenAI / 百炼 / DeepSeek / 任何 OpenAI 兼容端点。
func NewOpenAIProvider ¶
func NewOpenAIProvider(ctx context.Context, cfg OpenAIProviderConfig) (*OpenAIProvider, error)
NewOpenAIProvider 创建 OpenAI 兼容提供者。
func (*OpenAIProvider) AttemptTimeout ¶
func (p *OpenAIProvider) AttemptTimeout() time.Duration
AttemptTimeout 单次尝试超时(Resilient 降级链按 Provider 独立限时)。
func (*OpenAIProvider) ContextTokens ¶
func (p *OpenAIProvider) ContextTokens() int
func (*OpenAIProvider) CostPer1KTokens ¶
func (p *OpenAIProvider) CostPer1KTokens() (float64, float64)
func (*OpenAIProvider) MaxOutputTokens ¶
func (p *OpenAIProvider) MaxOutputTokens() int
func (*OpenAIProvider) Model ¶
func (p *OpenAIProvider) Model() model.BaseChatModel
func (*OpenAIProvider) ModelName ¶
func (p *OpenAIProvider) ModelName() string
func (*OpenAIProvider) Name ¶
func (p *OpenAIProvider) Name() string
type OpenAIProviderConfig ¶
type OpenAIProviderConfig struct {
BaseURL string
APIKey string
Model string
ContextTokens int // 模型上下文窗口(默认 128k)
MaxOutputTokens int // 单次输出上限(默认从窗口派生)
Timeout time.Duration // 请求超时(默认 60s)
// CaptureRetryAfter 捕获 429 响应的 Retry-After 头(批次四十五,对标 ZCode
// runner-retry 的服务端退避建议):经 ctx sink 透出给重试环的退避取舍。
// 需要 http.Client 注入(Timeout 语义由注入的 Client 自带保留)。
CaptureRetryAfter bool
CostPer1KPrompt float64 // 每 1000 prompt token 成本(美元)
CostPer1KCompletion float64 // 每 1000 completion token 成本(美元)
}
OpenAIProviderConfig OpenAI 提供者配置。
type Price ¶
Price 单价(**美元 / 每 100 万 token**)。
输入、输出各一档。缓存命中价、分档价不在本结构内——那些是**按厂商规则**算的, 硬塞进一个 flat 单价只会算错;需要时由调用方覆盖。
func LookupPriceStrict ¶ added in v0.13.0
LookupPriceStrict 查价,查不到返回 ErrPriceUnknown。
给「必须硬失败」的路径用(例如按量计费的出账链路:宁可报错也不能按 0 记账)。 非硬失败路径用 LookupPrice / CostTracker.RecordAuto——那里会**标记未定价**而不是丢弃用量。
func PriceOfMatched
deprecated
PriceOfMatched PriceOf 的可观测形态:返回命中的定价键与是否经前缀回落。
前缀回落对「同主干异价 SKU」(gpt-4o 前缀命中 gpt-4o-mini)会静默误定价 ——宿主可在装配期对回落命中做校验/上报(第十轮审计)。
Deprecated: 同 PriceOf——未配价返回 0 不可区分「免费」,前缀回落会误定价。 改用 PriceTable.Lookup(返回 PriceLookup,命中方式可见且不做前缀回落)。
type PriceLookup ¶ added in v0.13.0
PriceLookup 查价结果:命中的键 + 命中方式。
命中方式对外可见,是为了让调用方能审计「这个价格是怎么来的」—— 归一化命中与精确命中在**同族异价 SKU** 上的后果不同(见 PriceTable.Lookup)。
type PriceStatus ¶ added in v0.13.0
type PriceStatus string
PriceStatus 计价状态。
const ( // PricePriced 已按单价算出成本。 PricePriced PriceStatus = "priced" // PriceUnpriced **价格未知**:价表里查不到这个模型。 // // 它**不等于「成本为 0」**——CostRecord.CostUSD 此时是 0,但那只是占位, // 真实成本未知。两者必须可区分:把「未知」记成 0,是本仓要根除的静默失真。 PriceUnpriced PriceStatus = "unpriced" )
type PriceTable ¶ added in v0.13.0
type PriceTable struct {
// contains filtered or unexported fields
}
PriceTable 价表:内建价 + 调用方覆盖。**构造后只读**,可并发使用。
func DefaultPriceTable ¶ added in v0.13.0
func DefaultPriceTable() *PriceTable
DefaultPriceTable 返回内建价表(无覆盖)。
返回的是**共享只读实例**,不要修改它;要改价请用 NewPriceTable 或 WithOverride 造一份新的。
func NewPriceTable ¶ added in v0.13.0
func NewPriceTable(overrides map[string]Price) *PriceTable
NewPriceTable 造一份价表:内建价 + overrides 覆盖。
overrides 的键走与内建表**同一套归一化规则**(见 normalizeCandidates), 所以覆盖 `mimo-v2.5-pro` 也能盖住 `mimo-v2.5-pro-20250101` 这类变体。 覆盖优先于内建价。
func (*PriceTable) Lookup ¶ added in v0.13.0
func (t *PriceTable) Lookup(model string) (Price, PriceLookup)
Lookup 查价。
归一化规则(按优先级) ¶
- 原名精确命中(MatchExact);
- 去掉**部署前缀**(取最后一个 '/' 之后)后精确命中,如 `openai/gpt-4o` → `gpt-4o`;
- 去掉**日期后缀**后精确命中,如 `mimo-v2.5-pro-20250101` → `mimo-v2.5-pro`、 `gpt-4o-2024-08-06` → `gpt-4o`;
- 上面组合都试过仍不中 → **未命中**(MatchNone,价格未知)。
为什么不做「最长前缀回落」 ¶
旧版 PriceOf/PriceOfMatched 会做最长前缀回落,那会把**同主干异价 SKU** 静默 定价错:表里有 `gpt-4o` 时,`gpt-4o-mini`(便宜一半)会按 gpt-4o 计费。 「静默算错」比「显式未知」更危险——所以新查价不做前缀回落;需要的模型请显式 收录进表里(旧 API 仍保留该行为,见 PriceOf 的 Deprecated 说明)。
func (*PriceTable) Models ¶ added in v0.13.0
func (t *PriceTable) Models() []string
Models 返回表里的全部模型名(字典序)。
func (*PriceTable) WithOverride ¶ added in v0.13.0
func (t *PriceTable) WithOverride(model string, price Price) *PriceTable
WithOverride 返回一份新价表:当前表 + 该覆盖(不动原表,原表可能正被并发读)。
type PricedProvider ¶ added in v0.13.0
type PricedProvider interface {
Provider
// ModelPricing 返回该模型的单价与是否已定价。
// 返回 (Price{}, false) 表示「未定价」,llm 会继续回落到价表。
ModelPricing() (Price, bool)
}
PricedProvider 可选扩展接口:Provider **显式**给出单价。
这是「模型档案带 pricing」的口子:实现了它的 Provider,其单价**优先于** llm 内建价表;没实现时 llm 才会按模型名去价表里查。
单价单位是**美元 / 每 100 万 token**(Price),与 CostPer1KTokens 的 每 1K 不同——两套单位并存容易错账一个数量级,所以新实现方用这个接口。
type Provider ¶
type Provider interface {
// Name 提供者名称(如 "openai", "anthropic", "ollama")。
Name() string
// Model 返回 eino ChatModel 实例。
Model() model.BaseChatModel
// ModelName 返回模型标识(如 "gpt-4o", "deepseek-v4")。
ModelName() string
// ContextTokens 返回模型上下文窗口大小(token 数)。
ContextTokens() int
// MaxOutputTokens 返回单次生成输出上限。
MaxOutputTokens() int
// CostPer1KTokens 返回每 1000 token 的成本(美元)。
// 用于成本追踪,返回 0 表示不计费。
//
// ⚠️ 返回 0 有歧义:「这个模型免费」与「没配价」分不开。llm 层的定价
// 会把 0 视为「未指定」,回落到内建价表(PriceTable);价表也查不到时
// 记为**价格未知**(CostRecord.PriceStatus = PriceUnpriced),不是 0 成本。
// 想显式声明单价(折扣价/加价率/人民币折算价)见 PricedProvider。
CostPer1KTokens() (prompt, completion float64)
}
Provider LLM 提供者接口:抽象模型创建与配置。
type Resilient ¶
type Resilient struct {
// Budget 任务 token 预算(nil = 不限)。耗尽时全链短路。
Budget TokenAccountant
// Limiter 全局限速器(所有模型共享;nil = 不限速)。
Limiter *rate.Limiter
// Tracker 成本追踪器(nil = 不追踪)。记账跟随实际执行的模型。
Tracker *CostTracker
// PriceTable 价表(nil = llm 内建价表)。
//
// 单价解析优先级:Provider 显式给价(PricedProvider / CostPer1KTokens)
// > 按模型名查本表。**都查不到时记为「价格未知」**(CostRecord.PriceStatus
// = PriceUnpriced),不是 0 成本——否则成本看板恒为 0 而无人察觉。
PriceTable *PriceTable
// OnUsage token 使用回调(model = 实际执行的模型)。
OnUsage func(model, stage string, prompt, completion int)
// OnFallback 降级事件回调(from = 失败模型,to = 切换到的模型)。
OnFallback func(from, to, stage, reason string)
// contains filtered or unexported fields
}
Resilient 弹性 LLM 客户端。
与 *Client 方法签名一致(满足 Generator),可作 drop-in 替换。 额外能力:多模型降级、按模型熔断、预算短路、每模型窗口自适应 fitInput。
func NewResilient ¶
func NewResilient(chain *FallbackChain, cfg ResilientConfig) (*Resilient, error)
NewResilient 创建弹性客户端。空 Provider 列表报错。
func (*Resilient) BoundBudget ¶
func (r *Resilient) BoundBudget() TokenAccountant
BoundBudget 返回绑定的预算累计器(可 nil)。BudgetHolder 接口实现。
func (*Resilient) Generate ¶
func (r *Resilient) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)
Generate 弹性生成:按链序尝试各 Provider,全部失败返回 *AttemptError。
func (*Resilient) GenerateJSON ¶
func (r *Resilient) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error
GenerateJSON 弹性 JSON 生成:每模型内部含解析失败回喂重试,仍败切换下一模型。
func (*Resilient) PrimaryModel ¶
func (r *Resilient) PrimaryModel() model.BaseChatModel
PrimaryModel 返回主模型的 BaseChatModel(供 eino agent 工具表使用)。
func (*Resilient) RawModel ¶
func (r *Resilient) RawModel() model.BaseChatModel
RawModel Generator 接口实现:等价 RawModelWithFailover()——裸模型路径同样获得 链式降级(v0.10.9 起不再返回未装饰的主模型;要主模型裸实例用 PrimaryModel)。 ⚠ 此路径不受 Budget/Tracker/熔断约束(failover 薄切换刻意不做记账——ReAct 主路径依赖此形态时预算可被烧穿、Tracker 对该路径消耗记零;需要硬上限时勿把 RawModel 交给不受限消费方,或消费方自行在回调侧记账)。
func (*Resilient) RawModelWithFailover ¶
func (r *Resilient) RawModelWithFailover() model.BaseChatModel
RawModelWithFailover 把整条 Provider 链包装成一个带 failover 的 BaseChatModel: ReAct 等裸模型路径按链序降级,不再静默丢弃降级链(此前 RawModel 返回未装饰的 主模型——链/熔断只覆盖 Generate/GenerateJSON,两条路径两套命运)。语义与 FailoverModel 一致:薄切换(无熔断/限速/预算/同模型重试),全败上抛末个模型错误, 切换经 OnFallback 观测。
func (*Resilient) UsedTokens ¶
UsedTokens Generator 接口实现:返回预算累计消耗。
func (*Resilient) WithBudget ¶
func (r *Resilient) WithBudget(b TokenAccountant) Generator
WithBudget Resilient 的预算注入:显式构造新实例(Resilient 含互斥字段不可浅拷贝)。 chain/breakers 共享原实例(熔断状态跨任务全局),Budget/Tracker 指向任务级。
type ResilientConfig ¶
type ResilientConfig struct {
// RetriesPerModel 每模型重试次数(不含首次),默认 2。
RetriesPerModel int
// BreakerTrip 模型熔断阈值(连续失败次数),默认 3。
BreakerTrip int
// BreakerCooldown 模型熔断冷却期,默认 5 分钟。
BreakerCooldown time.Duration // BaseDelay 重试退避基准,默认 2s(测试可调小)。
BaseDelay time.Duration
// MaxDelay 重试退避上限,默认 30s。
MaxDelay time.Duration
// BreakerProbeTimeout 熔断半开探测的单次时限。缺省按「单模型一次完整尝试」
// 推导:(RetriesPerModel+1) 次尝试 × (MaxDelay + 90s 预算)。该值必须覆盖
// tryOneProvider 的最长在途时间——否则 Breakers 会把超时探测判为失联并
// 放行并发新探测,半开「单探测隔离」失效(v0.10.28 审计修复)。
BreakerProbeTimeout time.Duration
}
ResilientConfig 弹性客户端配置。
type RetryHint ¶
type RetryHint struct {
Retryable bool // 是否值得重试
IsRateLimit bool // 是否 429/rate limit
IsTruncated bool // 是否输出截断
}
RetryHint 错误分类结果。
func ClassifyLLMError ¶
ClassifyLLMError 判定是否值得重试及错误类型。 分类优先级:结构化 HTTP 状态码(go-openai RequestError)→ 文本 marker。 确定性失败(鉴权/参数错/模型不存在/上下文超限等 4xx)不重试——重试纯浪费 钱和延迟;429 可重试 + 退避下限提高;5xx/网络错误默认可重试。
type StageRouter ¶
type StageRouter struct {
// contains filtered or unexported fields
}
StageRouter 按 stage 分发的 Generator(Generator 接口实现,可无感嵌入现有装配)。
func NewStageRouter ¶
func NewStageRouter(def Generator) *StageRouter
NewStageRouter 创建阶段路由器(def = 未命中阶段的缺省 Generator)。 def 为 nil panic——构造期配置错误尽早暴露(与 toolprior.Table.Add 同纪律), 运行期才炸的 nil 难排查。
func (*StageRouter) GenerateJSON ¶
func (*StageRouter) RawModel ¶
func (r *StageRouter) RawModel() model.BaseChatModel
RawModel 缺省链的底层模型(StageRouter 本身不做 ReAct——ReAct 需要哪个模型 由调用方显式指定)。
func (*StageRouter) Use ¶
func (r *StageRouter) Use(stage string, gen Generator)
Use 注册阶段路由。stage 为精确名("qa")或前缀("R1" 命中 "R1a"); 重名注册覆盖旧路由。
func (*StageRouter) UsedTokens ¶
func (r *StageRouter) UsedTokens() int
UsedTokens 全部链(缺省 + 已注册)的预算累计和——任务预算口径下各链共享 同一 Budget 时即任务总消耗。暴露 BudgetHolder 的链按预算指针身份去重, 避免共享预算被按链数倍增;未暴露的链退回逐链求和。
type TokenAccountant ¶
TokenAccountant token 记账接口(调用方可接预算累计器或 Prometheus)。
type UsageCallCounter ¶
type UsageCallCounter struct {
// contains filtered or unexported fields
}
UsageCallCounter 单作用域 LLM 调用轮次计数(原子递增)。
type UsageRecord ¶
type UsageRecord struct {
Model string
PromptTokens int
CachedTokens int
CompletionTokens int
ReasoningTokens int
TotalTokens int
DurationMS int64
FinishReason string
Iteration int64 // 本作用域第几次 LLM 调用(1 起;无计数器为 0)
Labels map[string]string // 归因标签(session/case/step/agent 等,调用方自定)
}
UsageRecord 单次 LLM 调用的用量记录。
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package llmtest eino ChatModel/Provider 的脚本化测试桩——llm/reflection/router 等包的测试共用(此前四份手写桩已漂移出两种耗尽语义与多种记录口径)。
|
Package llmtest eino ChatModel/Provider 的脚本化测试桩——llm/reflection/router 等包的测试共用(此前四份手写桩已漂移出两种耗尽语义与多种记录口径)。 |
|
Package obsx 可观测性扩展:对齐 eino callbacks 体系的 LLM 调用追踪。
|
Package obsx 可观测性扩展:对齐 eino callbacks 体系的 LLM 调用追踪。 |
|
输出截断续写(对标 ZCode turn-output-token-continuation):finish reason 为 length/max_tokens 且消息无工具调用时,保留部分输出并以「直接续写」指令再调, 内容顺序拼接,最多 maxContinuations 轮。
|
输出截断续写(对标 ZCode turn-output-token-continuation):finish reason 为 length/max_tokens 且消息无工具调用时,保留部分输出并以「直接续写」指令再调, 内容顺序拼接,最多 maxContinuations 轮。 |