Repository navigation
Releases: oagentkit/llm
Release list
llm v0.15.2 — 依赖地板跟到 textx v0.6.2
下游要做什么
go mod tidy。地板抬高对下游是隐式破坏性变更:不跑就报
updates to go.mod needed,且报错不提真正原因(不是本仓的 API 变了)。
本仓自身代码零改动——只看本仓的 diff 看不出这是一次地板抬高。
为什么抬
上一版 tag(v0.15.1)打出去时,它依赖的 textx 的新版已经先发出了
(textx v0.6.2 早于本 tag 数十小时)——即「打 tag 那一刻地板就落后」。
矩阵的「发版时刻地板时效」门禁(verify_tag_floor.py --org oagentkit --latest-only)
当场报红:
- llm@v0.15.1 / github.com/oagentkit/textx:地板 v0.6.1,发版时刻最新已是 v0.6.2
门禁给的修法是「抬 go.mod 地板 → go mod tidy → 重新发 tag」——本 PR 就是那条修法。
为什么无行为差异
textx v0.6.1 → v0.6.2 本身也是纯依赖地板追平(textx 自身代码零改动,
只抬了 oekit/pkg v0.18.0→v0.20.0),本仓用到的 textx 能力与行为不变。
llm v0.15.1 — 修发布说明:补回 v0.15.0 段漏掉的 1 条条目
下游要做什么
什么都不用做——本版无代码变更,只修发布说明。
什么
v0.15.0 段的 CHANGELOG 漏了 1 条提交——发版时本地克隆是浅克隆(depth 限制),
生成器遍历 tag 区间时看不到更早的提交,于是那条被漏掉:
文档(AGENTS):补「发版顺序」小节,指向 org CONTRIBUTING 单一副本
本版重生成 CHANGELOG(本地已补齐完整历史),把 v0.15.0 段补回。
为什么值得为「只改发布说明」发一版
v0.15.0 的 tag 已发布、不可重打(重打会让下游 go.sum 校验和硬失败),
所以修好的完整 CHANGELOG 要能被下游拿到,就得有一个新 tag 带着它——这正是 v0.15.1 的用途。
矩阵的 CHANGELOG 覆盖门禁(独立 oracle)查的就是这个:它会拿 CHANGELOG 的
每个版本段与 git 历史逐条对账,漏条目即红。
llm v0.15.0
下游要做什么
什么都不用做——纯增量:字段缺省为 nil 时行为逐字不变,现有调用方不需要改。
想给 LLM 出站挂守卫的宿主,改一行:
hc, err := netguard.NewClient(nil, netguard.Allow{Private: true, Loopback: true})
cfg.HTTPClient = hc // hc.Transport 挂了 netguard.DialFunc(netguard 在 agent-harness 仓;这里是说明接线形态,本仓不依赖它。)
为什么需要
写面校验(按 URL 判)拦不住 DNS rebinding——两次解析之间权威 DNS 可以改口:
写面校验:evil.example → 1.2.3.4 (公网,放行)
实际请求:evil.example → 169.254.169.254 (元数据段,已不在判定面内)
要闭合它,守卫必须挂在 Transport.DialContext 上,这要求宿主能提供自己的 *http.Transport(或整个 *http.Client)。而 LLM 档案的 base_url 是用户可配的(平台代发 LLM 请求的目标),是这条链上暴露最大的一个面。
语义:显式注入优先于内部构造
| 宿主注入 | 捕获 Retry-After | 行为 |
|---|---|---|
| 否 | 否 | 返回 nil——走 openai 包内部缺省(既有行为逐字不变) |
| 否 | 是 | 自建 client(既有行为逐字不变) |
| 是 | 否 | 原样使用宿主注入的 client |
| 是 | 是 | 在宿主注入的 Transport 外面套一层 RetryAfterTransport |
最后一行是此前做不到的组合:自建 client 走 http.DefaultTransport,宿主的守卫挂不上去——CaptureRetryAfter 与自定义守卫此前是互相排斥的。现在合流。
派生时复制一份 client,不就地改写宿主传进来的那个。
超时语义:注入后由注入的 Client 完全接管(eino 侧同一口径——设了 HTTPClient 就不再用 config.Timeout)。
顺带抬的地板
oekit/pkg v0.18.0 → v0.20.0
oekit/concurrency v0.18.0 → v0.19.0
对本仓零行为差异:本仓只 import concurrency/async 与 concurrency/breaker,这两个包在 v0.18.0→v0.19.0 之间一字未动。
门禁
go vet / test -race ✅ 5 包全绿
gofmt -l . ✅ 空
golangci-lint ✅ 0 issues
v0.14.1 窄接口层(port)——不依赖 eino 的降级链/预算/成本
窄接口层(port):不依赖 eino 的降级链 / 预算 / 成本
本仓此前全部对外能力都长在 eino 的类型上(model.BaseChatModel / schema.Message)。非 eino 生态的宿主想接一个单方法窄接口,只能自己重写:拼请求体、解析响应、判错误码、算超时——代价不只是代码量,还有边界易错(HTTP 状态码判错就静默把失败当成功)。issue #11 里两个下游就在各写一遍。
type Completer interface {
Complete(ctx context.Context, req 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——0 与「免费模型」不可区分);
- 附
ClassifyStatus(按 HTTP 状态码判可重试),免得每个宿主各写一遍、各判错一遍。
依赖面:不引 eino
$ go list -deps ./port | grep -c cloudwego/eino
0
$ go list -deps ./port | grep -E '^[a-z0-9.-]+\.[a-z]+/' | sort -u
github.com/oagentkit/llm/port
github.com/oagentkit/textx/textutil
github.com/oekit/concurrency/breaker
github.com/oekit/concurrency/window
github.com/oekit/pkg/stringx
标准库 + oekit/concurrency(熔断)+ textx(token 估算)——无 eino、无 go-openai、无 sonic。
无 eino 的内核下沉,根包 API 不变
预算 / 成本 / 价表 / 熔断的实现本来就不依赖 eino,却原先住在根包 llm 里,而根包因为 client.go 依赖 eino 而「整包不可用」——宿主连「记一笔 token」都用不上。下沉到 port 后:
- 根包
llm.Budget/llm.CostTracker/llm.Price/llm.PriceTable全部是port.X的类型别名(=),对外 API 一字未变; - 两条路径共用同一份预算/成本/价表实现 ⇒ 口径完全一致,不会出现两套算账。
怎么选
| 场景 | 用哪条路径 |
|---|---|
| 已在 eino 生态(要 ToolCall / 多模态 / 流式 / JSON 解析进 struct) | 根包 llm.NewClient / llm.NewResilient(能力更全) |
| 不在 eino 生态(只要「一句话进、一句话出」,或不想把 eino 带进依赖图) | port 包:宿主实现 Completer,本包管降级 / 预算 / 成本 |
关于 v0.14.0 → v0.14.1
v0.14.0 的 tag 快照缺它自己的版本段(打 tag 时用错了本地提交),而 tag 受 protect-tags ruleset 保护不可重打(实测 push 被拒)。按组织的根治路径补发补丁 tag:预写 → 提交 → 在该提交上打 tag。本 tag 的快照同时含 v0.14.0 与 v0.14.1 两段,消费方读到的发布说明是完整的;v0.14.0 作为存量欠账记进 matrix-tools/scripts/changelog_tag_debt.json。
代码内容两个版本完全一致(v0.14.1 只多一段 CHANGELOG)。
关联 issue #11
v0.14.0 窄接口层(port)——不依赖 eino 的降级链/预算/成本
窄接口层(port)
新增 port 包:窄接口形态(Complete(ctx, req) (string, error))的降级链、任务级预算、成本记账——供非 eino 生态的宿主直接复用本仓能力。详见 v0.14.1 的发布说明(两版代码内容完全一致)。
protect-tags 保护不可重打——已按组织路径补发 v0.14.1,并把本 tag 记进欠账基线。
v0.13.0 内建模型价表 + 「价格未知」不再静默记 0
成本计价不再依赖调用方自带价格
此前 llm 有完整的成本追踪能力,但价格不在这一层:CostTracker.Record 要求调用方每次传入单价,llm 自己既不持有「模型→价格」映射,也没有按模型名查价的入口。于是每个下游都得自己维护一张表(通常在应用配置里,模型名精确匹配),漏配就静默记 0——0 成本在账单上与「免费模型」不可区分。
1) 内建价表(新增 pricing.go)
| API | 作用 |
|---|---|
DefaultPriceTable() |
内建价表(OpenAI / Anthropic / DeepSeek / 小米 MiMo,共 20 个模型键) |
NewPricing→NewPriceTable(overrides) |
内建价 + 调用方覆盖(覆盖优先) |
LookupPrice(model) |
查价(返回命中的键与命中方式) |
LookupPriceStrict(model) |
查不到返回 ErrPriceUnknown(硬失败路径) |
单位统一为「美元 / 每 100 万 token」。仓里此前并存两套单位(Price 每 1M,Provider.CostPer1KTokens 与 Record 的 costPer1K 每 1K),差一个数量级;新 API 全部收敛到每 1M,旧入口保留但标 Deprecated。
归一化规则:原名 → 去部署前缀(openai/gpt-4o → gpt-4o)→ 去日期后缀(mimo-v2.5-pro-20250101 → mimo-v2.5-pro)。
不做最长前缀回落:那会把同主干异价 SKU 静默定价错(表里有 gpt-4o 时 gpt-4o-mini 会按 gpt-4o 计)。「静默算错」比「显式未知」更危险。
2) 「价格未知」不再静默记 0
CostRecord 增 PriceStatus / PriceKey / PriceMatched;CostTracker 增 RecordAuto / RecordPriced / RecordUnpriced / Unpriced / UnpricedCount;ModelSummary 增 PricedCalls / UnpricedCalls。
判读口径:
TotalCostUSD == 0 && UnpricedCalls == 0→ 真免费TotalCostUSD == 0 && UnpricedCalls > 0→ 成本口径不完整(不是「这个模型不要钱」)
需要硬失败的出账链路用 LookupPriceStrict(ErrPriceUnknown)。
3) 接入生产路径
Provider 新增可选接口 PricedProvider(ModelPricing() (Price, bool),显式单价优先于价表);Resilient 新增 PriceTable 字段,记账改走 providerPrice(p, table):Provider 显式价 > per-1K 非零 > 价表按模型名 > 未知。
此前这条路径在 Provider 不给价时恒记 0——这正是「成本看板恒为 0 而无人察觉」的成因。
⚠️ 价目表是参考价
内建价表是 2026-10-20 采集的官方 list price 快照:厂商随时调价,本表必然滞后于厂商公告;同一模型在不同渠道(官方/代理/云托管)价格不同,缓存命中、批量、区域定价另算;只收录厂商以美元公布的模型,人民币计价与阶梯计价的(通义/百炼等)不收——换算汇率会漂、阶梯价无法用单一「每 1M 常数」表达,硬折算等于给出一个看起来精确的错误数字。契约价、代理加价、折算价一律用 NewPriceTable(overrides) 或 PricedProvider 覆盖。
验证
GOWORK=off go test -race ./... 全绿;新增 14 个用例(含子用例),断言一律用关系(查表单价 × token 数 == 记录成本),不写死价目表数值——价目表随厂商调价变动,写死数字会随表更新假失败。
关联 issue #9
v0.12.1
修正 README/AGENTS.md 的 panic 机制归属:RunSafe* 属于 oekit/concurrency/async 而非本仓;原文「库不接 SetPanicHandler」与事实相反,且 panic 以 async.PanicError 哨兵类型返回。
本版本只含文档修正,不改任何产品逻辑与公共契约。
v0.8.0
本次发布内容:
-
oekit
concurrency依赖地板 v0.16.0 → v0.17.0。
该版本仅含 KeyedLimiter.Create 护栏内原子登记修复与文档更新,
无 breaking change,未触及任何已有导出签名。 -
修复 2026-10-18 遗留的 tag 事故。
当日的依赖地板重写把多个姊妹仓的 tag force 移到了重写后的 main,
tag 指向可变而 sum.golang.org 仍记着首次发布的字节 ——
走 proxy.golang.org 或 GOPROXY=direct 的构建会 SECURITY ERROR。
本地往往不报错(模块缓存预热 + 代理缓存仍留着移动前的字节),
所以「本机绿」并不代表 CI 绿。已为受影响模块重打不可变 tag 并迁移依赖。 -
全部 14 个仓已启用 tag 保护规则集,禁止 tag 更新与删除,
从机制上杜绝同类事故复发。
门禁:CI 按 GOPRIVATE 直连 VCS 校验 go mod tidy -diff,本轮全绿。