Help us shape the future of Go! Take the 2026 Go Developer Survey

llm

package module
v0.18.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 25 Imported by: 0

README

llm

模型客户端——多模型降级链/任务级预算/成本记账/分阶段模型路由/主备切换 —— oagentkit 模块矩阵 tier 2(集成层,依赖 tier0/tier1 + 重第三方地板)。

go get github.com/oagentkit/llm
包 职责
obsx eino callbacks 体系的 LLM 调用追踪 + token 用量采集 + 分阶段打标(自 obs/obsx 迁入 v0.12.0)
llm 模型客户端:多模型降级链、任务级预算、成本记账、分阶段路由、主备切换
port 窄接口层(不依赖 eino):Complete(ctx, req) (string, error) 形态的降级链 / 预算 / 成本记账
decision 判定面客户端(非自回归判定模型,System One 协议:Jev/Laya/Kev/Clef…):choice/score/noul 三原语 + 校准置信口径(本地算,两端同义) + mock/预热
wrap 调用包装与限流适配
llmtest 模型层契约测试套件
llm/llm 兼容层(v0.18.0 起):历史 import 路径 github.com/oagentkit/llm/llm 的过渡期镜像 —— 见下节

兼容层 llm/llm:历史 import 路径的过渡期镜像

本仓的根包在 v0.7.0 ~ v0.8.0 期间住在 llm/ 子目录,import 路径是 github.com/oagentkit/llm/llm。v0.9.0(2026-10-19)把根包上移到 module 根, 路径改成 github.com/oagentkit/llm,并标了 BREAKING CHANGE(见 CHANGELOG v0.9.0)。

于是:任何在那四个版本期间把本仓钉进 go.mod 的下游,升级时都会遇到一批 github.com/oagentkit/llm/llm 的 import 全部编译失败 —— 而它们用到的符号一个都没变, 变的只是 import 路径这一层。

llm/llm 子目录把老路径重新提供一次,让这类下游不必整仓改 import:

// 老路径继续可用(过渡期)
import "github.com/oagentkit/llm/llm"

// 新代码一律用这个
import "github.com/oagentkit/llm"

它不含任何自己的实现:

  • 类型是 type X = root.X(别名)—— 与根包是同一个类型,不是结构相同的两份;
  • 函数是 var X = root.X 转发 —— 拿到的是同一个函数值(不写包装函数:包装要复制签名, 根包改签名时它会静默分叉);
  • 常量原样转发;包级变量按值复制(与根包共享底层数据)。

镜像范围由 llm/compat_test.go 的 AST 比对强制:根包增删导出符号而兼容层没跟上 ⇒ 那条测试红。

⚠️ 新代码不要用这一层。它是过渡期产物:只镜像根包已有符号、不新增能力, 与门面仓一样只为存量下游存在,随 0.x 版本线退役。

⚠️ 它不在矩阵的规范包清单里。矩阵的 import 路径规则(matrix_paths.new_import_path) 把「包名末段 == 仓名」收口到 module 根,llm/llm 那条 stutter 路径是它刻意不产出的 —— 规范清单里登记它会让 versions.json 给两个包登记同一个 importPath。 它在矩阵里的记录位置是 modules.json 的 retiringPackages 账本(与 breaker 同形状: 只转发、无本地语义)。

定位

tier 2 — 集成层,依赖 tier0/tier1 + 重第三方地板。

  • 本仓依赖的矩阵内仓:textx。 (obsx 已于 v0.12.0 从 obs/obsx 迁入本仓,obs 不再是外部依赖。)

窄接口层(port):不引 eino 的那一半

本仓的对外能力原先都长在 eino 的类型上(model.BaseChatModel / schema.Message)。 非 eino 生态的宿主想接一个单方法窄接口时,只能自己重写:拼请求体、解析响应、 判错误码、算超时——代价不只是代码量,还有边界易错(HTTP 状态码判错就静默把失败当成功)。

port 包把这些收在一处,不引入 eino(go list -deps ./port 可验证):

// 宿主实现一个方法即可接进本仓的降级链
type Completer interface {
    Complete(ctx context.Context, req port.Request) (string, error)
}

chain := port.NewChain(
    port.WithCompleter("gpt-4o", myCompleter),   // 先注册者优先
    port.WithCompleter("deepseek-chat", backup),  // 失败切下一环
    port.WithBudget(port.NewBudget(100000)),     // 预算耗尽 → 调用前拦下
    port.WithTracker(port.NewCostTracker()),     // 按价表计价并记账
)
out, err := chain.Complete(ctx, port.Request{Prompt: "…", Stage: "R1"})

降级链 / 任务级预算 / 成本记账在窄接口路径上真的生效(不是只把请求转发出去): 预算判定在调用前(事后扣减等于没有预算)、失败按错误分类重试或切链、 成本按内建价表记账(价表未收录的模型记为「未定价」,不记 0)。 本包还提供 ClassifyStatus(按 HTTP 状态码判可重试),免得每个宿主各写一遍、各判错一遍。

怎么选
场景 用哪条路径
已在 eino 生态:要 ToolCall / 多模态 / 流式 / JSON 解析进 struct 根包 llm.NewClient / llm.NewResilient(能力更全)
不在 eino 生态:只要「一句话进、一句话出」,或不想把 eino 带进依赖图 port 包:宿主实现 Completer,本包管降级/预算/成本

两条路径共用同一份预算 / 成本 / 价表实现(llm.CostTracker 与 port.CostTracker 是同一个类型),不会出现两套算账。

外部依赖地板

  • github.com/cloudwego/eino
  • github.com/meguminnnnnnnnn/go-openai
  • github.com/cloudwego/eino-ext/components/model/openai
  • golang.org/x/time

成本与定价

llm 自带内建模型价表,成本核算不必由每个下游各自维护一张表。

tracker := llm.NewCostTracker()            // 用内建价表
tracker.RecordAuto("gpt-4o", "R1", 1200, 300)

单位统一为「美元 / 每 100 万 token」(Price.InputPerM / OutputPerM)。 历史上的 Provider.CostPer1KTokens() 与 CostTracker.Record 的 costPer1K 参数 是「每 1K」,两套单位并存容易错账一个数量级,故新 API 全部收敛到每 1M, 旧入口保留但标记 Deprecated。

查价与归一化

PriceTable.Lookup 按下列顺序取候选,命中即返回(命中方式对外可见, 见 PriceLookup.Kind):

  1. 原名精确命中;
  2. 去掉部署前缀(取最后一个 / 之后):openai/gpt-4o → gpt-4o;
  3. 去掉日期后缀:mimo-v2.5-pro-20250101 → mimo-v2.5-pro、 gpt-4o-2024-08-06 → gpt-4o。

不做「最长前缀回落」:那会把同主干异价 SKU 静默定价错(表里有 gpt-4o 时, gpt-4o-mini 会按 gpt-4o 计费)。「静默算错」比「显式未知」更危险。

覆盖
table := llm.NewPriceTable(map[string]llm.Price{
    "gpt-4o": {InputPerM: 1.25, OutputPerM: 3.5}, // 企业折扣价
})
tracker := llm.NewCostTrackerWithTable(table)

覆盖优先于内建价。契约价、代理加价、人民币计价合同的折算价,都走这条路径。 Provider 也可以实现 llm.PricedProvider(ModelPricing() (Price, bool))显式给价, 它同样优先于价表。

「价格未知」不记 0

查不到价格时,记录不会被丢掉,也不会被记成「成本 0 的正常调用」:

  • CostRecord.PriceStatus 取 PriceUnpriced(CostUSD 的 0 是占位,不是「免费」);
  • CostTracker.Unpriced() / UnpricedCount() 列出是哪些调用没计价;
  • ModelSummary.UnpricedCalls 让汇总层也能区分: TotalCostUSD == 0 && UnpricedCalls == 0 才是真免费, TotalCostUSD == 0 && UnpricedCalls > 0 是成本口径不完整。

需要「查不到就硬失败」的出账链路用 llm.LookupPriceStrict(返回 llm.ErrPriceUnknown)。

⚠️ 价目表是参考价

内建价表是 2026-10-20 采集的官方 list price 快照:

  • 厂商随时调价,本表必然滞后于厂商公告——它不是权威报价;
  • 同一模型在不同渠道(官方 / 代理 / 云托管)价格不同,缓存命中、批量、区域定价另算;
  • 只收录厂商以美元公布的模型;人民币计价与阶梯计价的(通义/百炼等)不收—— 换算汇率会漂、阶梯价也无法用单一「每 1M 常数」表达,硬折算等于给出一个 看起来精确的错误数字。这类模型请用覆盖。

与 oekit 的关系

本矩阵(oagentkit,Agent/LLM 编排工具链)依赖 oekit(Open Easy Kit,Go 微服务工具链): oekit/pkg / oekit/concurrency / oekit/logx 等。oekit 是另一套独立矩阵,不是子品牌。

开发

go test ./... -race
go vet ./...
gofmt -l .

规范见 AGENTS.md。

License

MIT

  • 降级链的每一跳都必须记录原因(成本/限流/超时/内容策略),不得静默换模型。
  • 预算判定必须在调用前,不能事后扣减 —— 事后扣减等于没有预算。
  • panic 不能只转成 error。 RunSafe* 系列属于 github.com/oekit/concurrency/async, 会把 panic 收进全局 async.PanicHandler(默认 log.Printf,进程入口可调 async.SetPanicHandler 换成结构化日志),并以 async.PanicError 哨兵类型返回。 本仓只用 async.GoSafe 系列,不在库内暴露 panic 处理注入点。 要拿回结构化上下文,自己在 recover 分支里转成 errors.Wrap。

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 模型链。

本文件族是 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

View Source
const (
	// PricePriced 已按单价算出成本。
	PricePriced = port.PricePriced
	// PriceUnpriced 价格未知(**不等于成本为 0**)。
	PriceUnpriced = port.PriceUnpriced

	// MatchNone 未命中(价格未知)。
	MatchNone = port.MatchNone
	// MatchExact 原名精确命中。
	MatchExact = port.MatchExact
	// MatchNormalized 归一化后命中(去部署前缀 / 去日期后缀)。
	MatchNormalized = port.MatchNormalized
)
View Source
const MaxServerRetryAfter = httpjson.MaxServerRetryAfter

MaxServerRetryAfter 服务端退避建议的可信上限:超过 5 分钟的建议不直接 采信(长时间限流应交由 failover/降级链处置,而非原地等)。

View Source
const MaxStreamRecoveryRetries = 10

MaxStreamRecoveryRetries 流断点恢复预算(对标 STREAM_RECOVERY_MAX_RETRIES: 跨过单请求重试边界后只恢复 1 次与用户预期差距过大,10 次是生产调过的上限)。

Variables

View Source
var BusyAdmissionDelays = []time.Duration{time.Second, 2 * time.Second}

BusyAdmissionDelays busy 准入重试的两档退避(对标 START_PLAN_BUSY_MAIN_TURN_ADMISSION_RETRY_DELAYS_MS:1s/2s)。

View Source
var ErrPriceUnknown = port.ErrPriceUnknown

ErrPriceUnknown 价表里查不到该模型。

Functions

func BusyAdmissionDelay

func BusyAdmissionDelay(retry int) time.Duration

BusyAdmissionDelay 按 retry 序号(0 起)取准入退避;越界取最后一档。

func IsContextExceededError

func IsContextExceededError(err error) bool

IsContextExceededError 错误文案层识别上下文溢出(provider 报文形态)。

func IsContextExceededFinish

func IsContextExceededFinish(finishReason, rawFinishReason string) bool

IsContextExceededFinish finishReason 层识别上下文溢出。

func IsProviderBusyCode

func IsProviderBusyCode(code string) bool

IsProviderBusyCode provider 业务码是否「繁忙」类(准入级退避重试的判定; 耗尽后调用方应合成不可重试错误,别无限排队)。

func IsRateLimitError

func IsRateLimitError(err error) bool

IsRateLimitError 快速判定是否 429 类错误。

func IsTransientStreamError

func IsTransientStreamError(err error) bool

IsTransientStreamError 流式 turn 中的错误是否瞬态(值得从安全锚点重开流)。 与 ClassifyLLMError 的分工:后者面向单请求 HTTP 重试;本判定面向 turn 级 恢复——额外覆盖流空闲超时与原因码形态(错误链上任意一层的文案都参与匹配)。 调用方主动取消不算瞬态(与降级矩阵「ctx 取消 → 全链中止」一致):若判为 瞬态,取消后的恢复循环会空转 MaxStreamRecoveryRetries 轮、每轮立即再失败。

func IsTruncatedError

func IsTruncatedError(err error) bool

IsTruncatedError 快速判定是否输出截断错误。

func LookupPrice added in v0.13.0

func LookupPrice(model string) (Price, PriceLookup)

LookupPrice 用内建价表查价。

func NewUsageHandler

func NewUsageHandler(onRecord Sink) callbacks.Handler

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

func PriceOf(pricing map[string]Price, model string, prompt, completion int) float64

PriceOf 按模型名匹配定价表估算单次调用成本(USD;未配价返回 0,不阻塞记账)。

Deprecated: 未配价时返回 0,与「免费模型」不可区分——这正是本仓要根除的静默失真。 改用 PriceTable.Lookup / LookupPrice:查不到时是「价格未知」而非 0。

另注意其「最长前缀回落」会把同主干异价 SKU 静默定价错(gpt-4o-mini 命中 gpt-4o),新查价 API 不再提供该行为。

func RetryAfterFrom

func RetryAfterFrom(ctx context.Context) time.Duration

RetryAfterFrom 读取最近一次 429 的服务端建议(0 = 无)。

func RetryableLLMError

func RetryableLLMError(err error) bool

RetryableLLMError 判定错误是否值得重试。

func SelectRetryDelay

func SelectRetryDelay(local, hint time.Duration) time.Duration

SelectRetryDelay 退避取舍(钳制:建议 ≤5min 或 < 本地曲线才采信)。

func SuspiciousEmptyResult

func SuspiciousEmptyResult(finishReason string, responseLen, toolCallCount, usageTotal int) bool

SuspiciousEmptyResult 空终态检测:零文本 + 零工具调用 + 非 stop 终止 + 零用量 = 可疑空响应(各家 provider 都会偶发;按可重试错误处理而非当正常 完结吞掉)。usageTotal 为 provider 报告的总 token(≤0 视为零用量/未知)。

func WithCallCounter

func WithCallCounter(ctx context.Context) context.Context

WithCallCounter ctx 挂轮次计数器(每个 agent 运行作用域一个)。

func WithRetryAfterSink

func WithRetryAfterSink(ctx context.Context) context.Context

WithRetryAfterSink 在 ctx 注入 sink(重试环上游调用一次;传输层据此回写)。

func WithUsageLabels

func WithUsageLabels(ctx context.Context, labels map[string]string) context.Context

WithUsageLabels ctx 注入归因标签;不注入或无标签时 UsageRecord.Labels 为 nil。

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 = port.Budget

Budget 任务级 token 预算累计器(TokenAccountant 的标准实现)。

func NewBudget

func NewBudget(limit int) *Budget

NewBudget 创建预算累计器。

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 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)
	// OnUsageDetailed 每次调用后的完整用量回调(含前缀缓存命中量等)。
	//
	// 与 OnUsage 并行触发、两者可同置:OnUsage 保持旧签名不破坏;需要 cached 的
	// 消费方(成本缓存折扣、命中率观测)用本回调。UsageRecord.CachedTokens 是
	// 命中前缀缓存的 prompt token(DeepSeek/MiMo 等端点的 PromptTokenDetails)。
	OnUsageDetailed func(stage string, rec UsageRecord)
	// 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

func (c *Client) UsedTokens() int

UsedTokens 返回累计消耗 token 数(无预算绑定时返回 0)。

func (*Client) WithBudget

func (c *Client) WithBudget(b TokenAccountant) Generator

WithBudget Client 的预算注入:浅拷贝(Client 只含无锁值字段,Budget 指针共享是有意为之)。

type CostRecord

type CostRecord = port.CostRecord

CostRecord 单次调用成本记录。

type CostTracker

type CostTracker = port.CostTracker

CostTracker token 成本追踪器。

func NewCostTracker

func NewCostTracker() *CostTracker

NewCostTracker 创建成本追踪器(用内建价表)。

func NewCostTrackerWithTable added in v0.13.0

func NewCostTrackerWithTable(table *port.PriceTable) *CostTracker

NewCostTrackerWithTable 创建成本追踪器(指定价表)。

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

Stream 与 Generate 同一降级语义(首块前失败才可切换;已在流中失败无法换模型重放)。

func (*FailoverModel) WithTools

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 MatchKind added in v0.13.0

type MatchKind = port.MatchKind

MatchKind 查价命中方式。

type ModelSummary

type ModelSummary = port.ModelSummary

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
	// HTTPClient 注入自定义 HTTP 客户端(**可选**,nil = 走缺省)。
	//
	// 为什么需要:出站守卫要真正闭合「写面校验 → 拨号」的 DNS rebinding 窗口,
	// 守卫必须挂在 Transport.DialContext 上——写面校验(按 URL 判)拦不住
	// 「解析与拨号之间权威 DNS 改口」。而 LLM 的 base_url 是**用户可配**的
	// (平台代发 LLM 请求的目标),是这条链上暴露最大的一个面。
	//
	// 语义:**显式注入优先于内部构造**。注入后由注入的 Client 完全接管超时
	// (Timeout 语义由注入的 Client 自带保留;eino 侧同一口径——设了 HTTPClient
	// 就不再用 config.Timeout),本仓不再叠加自己的 Timeout。
	// 注入的 Client 的 Transport 为 nil 时由 net/http 回落 http.DefaultTransport。
	//
	// 与 CaptureRetryAfter 可叠加:注入的同时要捕获 Retry-After 时,本仓在
	// **注入的 Transport 外面**套一层 RetryAfterTransport——守卫与退避建议
	// 捕获不再互相排斥(此前二者是排斥关系:自建 client 走 http.DefaultTransport,
	// 宿主的守卫挂不上去)。
	//
	// 宿主侧参考(agent-harness 的 netguard):
	//
	//	hc, err := netguard.NewClient(nil, netguard.Allow{Private: true, Loopback: true})
	//	cfg.HTTPClient = hc   // hc.Transport 挂了 netguard.DialFunc
	HTTPClient          *http.Client
	CostPer1KPrompt     float64 // 每 1000 prompt token 成本(美元)
	CostPer1KCompletion float64 // 每 1000 completion token 成本(美元)
}

OpenAIProviderConfig OpenAI 提供者配置。

type Price

type Price = port.Price

Price 单价(美元 / 每 100 万 token)。

func LookupPriceStrict added in v0.13.0

func LookupPriceStrict(model string) (Price, error)

LookupPriceStrict 查价,查不到返回 ErrPriceUnknown。

func PriceOfMatched deprecated

func PriceOfMatched(pricing map[string]Price, model string) (Price, string, bool)

PriceOfMatched PriceOf 的可观测形态:返回命中的定价键与是否经前缀回落。

前缀回落对「同主干异价 SKU」(gpt-4o 前缀命中 gpt-4o-mini)会静默误定价 ——宿主可在装配期对回落命中做校验/上报(第十轮审计)。

Deprecated: 同 PriceOf——未配价返回 0 不可区分「免费」,前缀回落会误定价。 改用 PriceTable.Lookup(返回 PriceLookup,命中方式可见且不做前缀回落)。

type PriceLookup added in v0.13.0

type PriceLookup = port.PriceLookup

PriceLookup 查价结果:命中的键 + 命中方式。

type PriceStatus added in v0.13.0

type PriceStatus = port.PriceStatus

PriceStatus 计价状态。

type PriceTable added in v0.13.0

type PriceTable = port.PriceTable

PriceTable 价表:内建价 + 调用方覆盖。

func DefaultPriceTable added in v0.13.0

func DefaultPriceTable() *PriceTable

DefaultPriceTable 返回内建价表(无覆盖)。

func NewPriceTable added in v0.13.0

func NewPriceTable(overrides map[string]Price) *PriceTable

NewPriceTable 造一份价表:内建价 + overrides 覆盖。

覆盖键与内建表走**同一套归一化规则**(折算成基座形态再入库),所以覆盖 `mimo-v2.5-pro` 与覆盖 `mimo-v2.5-pro-20250101` 是等价的——同一基座上的 多个覆盖键按固定的优先级规则只留一个生效(详见 port.NewPriceTable 的文档)。

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) LastModel

func (r *Resilient) LastModel() string

LastModel 返回最近一次成功调用使用的模型名(无成功调用返回空)。

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) String

func (r *Resilient) String() string

String 返回降级链描述(如 "openai(gpt-4o) → openai(deepseek-v4)")。

func (*Resilient) UsedTokens

func (r *Resilient) UsedTokens() int

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

func ClassifyLLMError(err error) RetryHint

ClassifyLLMError 判定是否值得重试及错误类型。 分类优先级:结构化 HTTP 状态码(go-openai RequestError)→ 文本 marker。 确定性失败(鉴权/参数错/模型不存在/上下文超限等 4xx)不重试——重试纯浪费 钱和延迟;429 可重试 + 退避下限提高;5xx/网络错误默认可重试。

type Sink

type Sink func(UsageRecord)

Sink 用量出口。

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) Generate

func (r *StageRouter) Generate(ctx context.Context, stage string, msgs []*schema.Message) (*schema.Message, error)

func (*StageRouter) GenerateJSON

func (r *StageRouter) GenerateJSON(ctx context.Context, stage string, msgs []*schema.Message, out any) error

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

type TokenAccountant = port.TokenAccountant

TokenAccountant token 记账接口。

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 调用的用量记录。

Directories

Path Synopsis
Package decision 是 **System One 协议**(非自回归判定模型)的客户端。
Package decision 是 **System One 协议**(非自回归判定模型)的客户端。
Package llm 是历史 import 路径 github.com/oagentkit/llm/llm 的**兼容层**。
Package llm 是历史 import 路径 github.com/oagentkit/llm/llm 的**兼容层**。
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 调用追踪。
Package port 是 oagentkit/llm 的**窄接口层**——本仓不依赖 eino 的那一半。
Package port 是 oagentkit/llm 的**窄接口层**——本仓不依赖 eino 的那一半。
输出截断续写(对标 ZCode turn-output-token-continuation):finish reason 为 length/max_tokens 且消息无工具调用时,保留部分输出并以「直接续写」指令再调, 内容顺序拼接,最多 maxContinuations 轮。
输出截断续写(对标 ZCode turn-output-token-continuation):finish reason 为 length/max_tokens 且消息无工具调用时,保留部分输出并以「直接续写」指令再调, 内容顺序拼接,最多 maxContinuations 轮。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL