ai-lcr 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +57 -1
- package/README.zh-CN.md +56 -0
- package/dist/index.cjs +463 -75
- package/dist/index.d.cts +247 -7
- package/dist/index.d.ts +247 -7
- package/dist/index.js +460 -75
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,138 @@ All notable changes to `ai-lcr` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.6.0] — 2026-06-10
|
|
8
|
+
|
|
9
|
+
Media billing contract v2: **rank by the reference, bill by actual usage.**
|
|
10
|
+
The 0.5 media router used one number for both jobs — the price normalized to a
|
|
11
|
+
reference output (1080p image / 5-second clip) ranked routes *and* estimated
|
|
12
|
+
costs, multiplied by an untyped `units` count. That mispriced off-reference
|
|
13
|
+
outputs (an 8s clip billed as 5s) and made the baseline duration-blind, and the
|
|
14
|
+
bare `units` invited a seconds-as-count 8× overcharge. 0.6 separates the two.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Typed usage (`MediaUsage`).** Adapter results (`MediaGenerateResult`,
|
|
19
|
+
`MediaStatusResult`) carry `usage: { seconds?, outputs?, megapixels? }` —
|
|
20
|
+
explicitly named dimensions that cannot be confused. The bundled adapters
|
|
21
|
+
report it (Kunavo video now safely reports the real `duration_seconds`).
|
|
22
|
+
The legacy bare `units` field is still honored as an output count.
|
|
23
|
+
- **Settle-time billing.** Cost estimates price the route's actual unit on
|
|
24
|
+
actual usage: per-second SKUs bill `usage.seconds` → `input.duration`
|
|
25
|
+
(numbers or `"8s"`-style strings) → the reference (last resort); per-image /
|
|
26
|
+
per-call SKUs bill output count; per-megapixel SKUs bill measured megapixels.
|
|
27
|
+
New public helpers: `billableUnits`, `priceCents`, `durationFromInput`.
|
|
28
|
+
- **Usage-aware savings baseline.** `baselineUsd` is now priced at settle time
|
|
29
|
+
against the same usage as the cost — an 8-second clip is baselined at 8
|
|
30
|
+
seconds of the official rate, not the 5-second reference. Off-reference calls
|
|
31
|
+
can no longer produce negative or understated savings.
|
|
32
|
+
- **`CallRecord` provenance fields** (all optional, backward compatible):
|
|
33
|
+
`modality` ("image" | "video"), `usage`, `baselineKind`
|
|
34
|
+
("official" | "priciest-route" | "last-leg" — the text router now stamps
|
|
35
|
+
"last-leg"), `officialUsd` (the official price for this call's usage), and
|
|
36
|
+
`estCostUsd` (the price-table prediction; `costUsd − estCostUsd` on
|
|
37
|
+
provider-reported rows is price-table drift).
|
|
38
|
+
- **Cost-outlier guard.** A provider-reported cost ≥25× off the table
|
|
39
|
+
prediction (the classic USD-vs-cents slip is exactly 100×) raises `onError`
|
|
40
|
+
with both numbers; the reported bill still stands.
|
|
41
|
+
- `MediaRunResult` and the terminal `MediaPollResult` expose the `usage` that
|
|
42
|
+
backed the bill.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `MediaJobHandle` now carries the serving route's `pricing` and the resolved
|
|
47
|
+
savings `baseline` so settle-time billing works across processes. Handles
|
|
48
|
+
serialized by 0.5.x still poll fine: they settle with the legacy
|
|
49
|
+
reference-price estimate and the submit-time baseline.
|
|
50
|
+
|
|
51
|
+
## [0.5.6] — 2026-06-07
|
|
52
|
+
|
|
53
|
+
All additions are optional and backward compatible. The sync `createMediaLCR`
|
|
54
|
+
router (the callable `generate(modelId, input)`) and every adapter's `run()` are
|
|
55
|
+
**unchanged** in signature and behavior.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **Async media routing — `submit` / `poll` for long-running (video) jobs.**
|
|
60
|
+
The blocking media path holds a serverless invocation open until the file is
|
|
61
|
+
ready: fine for an image (seconds), impossible for a minutes-long video job.
|
|
62
|
+
`createMediaLCR(...)` now returns a callable with two methods attached:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const lcr = createMediaLCR({ registry, adapters })
|
|
66
|
+
|
|
67
|
+
// process A (request handler): route + enqueue, return immediately
|
|
68
|
+
const handle = await lcr.submit('google/veo-3-lite', { prompt, aspect_ratio: '16:9' })
|
|
69
|
+
await db.save(JSON.stringify(handle)) // the handle is plain JSON
|
|
70
|
+
|
|
71
|
+
// process B (cron / queue worker): poll until terminal
|
|
72
|
+
const r = await lcr.poll(handle)
|
|
73
|
+
if (r.done) use(r.outputs, r.costCents) // else keep polling r.handle
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Routing happens at `submit`** — it picks the cheapest provider whose
|
|
77
|
+
adapter supports async, and the returned `MediaJobHandle` carries the
|
|
78
|
+
not-yet-tried fallback routes (cheapest-first), the original input, and the
|
|
79
|
+
telemetry accumulator. The handle is **serializable on purpose**: submit and
|
|
80
|
+
poll typically run in different processes, so it must survive a round-trip
|
|
81
|
+
through a database or queue.
|
|
82
|
+
- **Failover happens at `poll`, not just submit.** When a provider's job fails
|
|
83
|
+
mid-poll (a `status:"error"`, a completed-but-empty job, or a thrown
|
|
84
|
+
retryable transport error such as the video-timeout `504` remap), `poll`
|
|
85
|
+
**re-submits to the next fallback provider** and hands back a fresh handle to
|
|
86
|
+
keep polling — it does not give up. A thrown error uses the standard
|
|
87
|
+
`isRetryableError` gate (so a caller-bug `400` on the poll endpoint doesn't
|
|
88
|
+
loop); a provider's own job failure always earns a fallback attempt.
|
|
89
|
+
- **Telemetry lands once, at the terminal poll.** The single correlated
|
|
90
|
+
`CallRecord` (via `onCall`) and the `onCost` event fire when the job settles
|
|
91
|
+
(`poll` → done/exhausted), carrying the full failover chain across both
|
|
92
|
+
processes — not at `submit`. The one exception: a `submit` that *no* provider
|
|
93
|
+
accepts settles a failed record there (there is no poll to do it).
|
|
94
|
+
|
|
95
|
+
- **`MediaAdapter.submit` / `MediaAdapter.checkStatus` (both optional).** The
|
|
96
|
+
adapter contract gains the async pair, shaped to match ai-art's
|
|
97
|
+
`ProviderAdapter` so a consumer can delegate its own async runtime to ai-lcr
|
|
98
|
+
with no glue:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
submit({ externalId, input, metadata? }) -> { requestId }
|
|
102
|
+
checkStatus({ externalId, requestId }) ->
|
|
103
|
+
{ status: 'queued' | 'running' | 'done' | 'error', outputs?, costCents?, units?, error? }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A sync-only adapter (image-only) omits both; the async router simply skips a
|
|
107
|
+
route whose adapter can't serve async.
|
|
108
|
+
|
|
109
|
+
- **All three bundled adapters now implement the async path:**
|
|
110
|
+
- **Kunavo** — `submit` → `POST /v1/videos`, `checkStatus` → `GET /v1/videos/{id}`
|
|
111
|
+
(video only; submitting an image id throws, since Kunavo images are sync).
|
|
112
|
+
`run()`'s blocking async path now reuses these internally.
|
|
113
|
+
- **fal** — `submit` → `POST queue.fal.run/{model}`, `checkStatus` reconstructs
|
|
114
|
+
the queue base from the id (the `fal-ai/flux/schnell` → `fal-ai/flux`
|
|
115
|
+
sub-path quirk) for cross-process polling.
|
|
116
|
+
- **Runware** — gains an **async video** path (`videoInference` with
|
|
117
|
+
`deliveryMethod:"async"`, polled via `getResponse`). Image stays on the
|
|
118
|
+
synchronous `run()`.
|
|
119
|
+
|
|
120
|
+
- **New exported types:** `MediaSubmitRequest`, `MediaSubmitResult`,
|
|
121
|
+
`MediaStatusRequest`, `MediaStatusResult`, `MediaJobStatus`,
|
|
122
|
+
`MediaSubmitOptions`, `MediaJobHandle`, `MediaPollResult`, and `MediaLCR` (the
|
|
123
|
+
callable-with-methods return type of `createMediaLCR`).
|
|
124
|
+
|
|
125
|
+
- **Live probe `scripts/check-media-async.mjs`** — exercises the real
|
|
126
|
+
`submit`/`poll` API across **every async provider** (kunavo · fal · runware)
|
|
127
|
+
whose key is present: submit → JSON round-trip the handle → poll to done →
|
|
128
|
+
assert the output URL fetches and cost is reported, per provider.
|
|
129
|
+
`PROBE_FAILOVER=1` adds a live submit-time failover case.
|
|
130
|
+
|
|
131
|
+
### Migration
|
|
132
|
+
|
|
133
|
+
Nothing breaks. To adopt async, give your video adapters `submit`/`checkStatus`
|
|
134
|
+
(the bundled fal/kunavo/runware adapters already have them) and call
|
|
135
|
+
`lcr.submit(...)` / `lcr.poll(...)` instead of the blocking `lcr(...)`. The
|
|
136
|
+
blocking call still works for image and for video where holding the request open
|
|
137
|
+
is acceptable.
|
|
138
|
+
|
|
7
139
|
## [0.5.5] — 2026-06-06
|
|
8
140
|
|
|
9
141
|
Kunavo media (image + video) verified live and properly wired. The Kunavo
|
package/README.md
CHANGED
|
@@ -291,11 +291,67 @@ USD per second, as of 2026-05 — verify current rates. Video billing differs by
|
|
|
291
291
|
| Seedance Pro | $0.124 |
|
|
292
292
|
| Veo 3.1 (audio-on) | $0.400 |
|
|
293
293
|
|
|
294
|
+
## Image & video routing (`createMediaLCR`)
|
|
295
|
+
|
|
296
|
+
Image and video are a separate, self-contained side of `ai-lcr` (file outputs, mixed pricing units, async jobs) — see [`src/media.ts`](src/media.ts). You give it a registry (each model's provider routes + per-unit price) and a set of adapters; it routes cheapest-first, fails over, and reports real cost through the same `onCall` sink as text.
|
|
297
|
+
|
|
298
|
+
Two prices, two jobs: routes are **ranked** by their price normalized to one reference output (a 1080p image / a 5-second clip) so mixed units are comparable, but each settled call is **billed** on its actual usage — an 8-second clip on a per-second SKU costs 8 × the per-second rate, and its savings baseline is the official price for those same 8 seconds. Adapters report typed usage (`usage: { seconds, outputs, megapixels }`); when a provider returns its own bill, that wins, and a bill wildly off the price table (the classic USD-vs-cents slip is exactly 100×) raises `onError` so the table gets fixed.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { createMediaLCR, createKunavoMediaAdapter, createFalMediaAdapter } from 'ai-lcr'
|
|
302
|
+
|
|
303
|
+
const lcr = createMediaLCR({
|
|
304
|
+
registry: {
|
|
305
|
+
'google/veo-3-lite': {
|
|
306
|
+
id: 'google/veo-3-lite', modality: 'video',
|
|
307
|
+
routes: [
|
|
308
|
+
{ provider: 'kunavo', externalId: 'veo-3-lite', pricing: { unit: 'call', cents: 16 } },
|
|
309
|
+
{ provider: 'fal', externalId: 'fal-ai/veo3.1/lite', pricing: { unit: 'second', cents: 8 } },
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
adapters: {
|
|
314
|
+
kunavo: createKunavoMediaAdapter({ apiKey: process.env.KUNAVO_API_KEY! }),
|
|
315
|
+
fal: createFalMediaAdapter({ apiKey: process.env.FAL_KEY! }),
|
|
316
|
+
},
|
|
317
|
+
onCall: rec => console.log(rec.winner, rec.costUsd, rec.failedOver),
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Sync: resolves when the file is ready (fine for images).
|
|
321
|
+
const { outputs, provider, costCents } = await lcr('google/veo-3-lite', { prompt: 'a wave' })
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Async (`submit` / `poll`) — for long-running video
|
|
325
|
+
|
|
326
|
+
A minutes-long video job can't hold a serverless request open. `submit` routes + enqueues and returns a **plain-JSON handle**; `poll` checks it. The two run in different processes — the handle survives a database/queue hop.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
// process A — request handler: route + enqueue, return immediately
|
|
330
|
+
const handle = await lcr.submit('google/veo-3-lite', { prompt: 'a wave', aspect_ratio: '16:9' })
|
|
331
|
+
await db.jobs.put(jobId, JSON.stringify(handle))
|
|
332
|
+
|
|
333
|
+
// process B — cron / queue worker: poll until terminal
|
|
334
|
+
let handle = JSON.parse(await db.jobs.get(jobId))
|
|
335
|
+
const r = await lcr.poll(handle)
|
|
336
|
+
if (r.done) {
|
|
337
|
+
save(r.outputs, r.costCents) // settled — telemetry already emitted
|
|
338
|
+
} else {
|
|
339
|
+
await db.jobs.put(jobId, JSON.stringify(r.handle)) // keep polling r.handle
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Design choices worth knowing:
|
|
344
|
+
|
|
345
|
+
- **Routing is at `submit`** (cheapest async-capable provider); the handle carries the not-yet-tried fallbacks, so…
|
|
346
|
+
- **Failover is at `poll`** — a provider whose job fails mid-poll is re-submitted to the next provider automatically (a fresh `r.handle` to keep polling), rather than the request just dying.
|
|
347
|
+
- **Telemetry lands once, at the terminal poll** — one `onCall` `CallRecord` with the full failover chain, threaded across both processes (not at `submit`).
|
|
348
|
+
- An adapter advertises async by implementing `submit` + `checkStatus`; image-only adapters omit them and are skipped by the async router. The bundled Kunavo, fal, and Runware adapters all implement the async path (Kunavo/Runware async is video-only; fal covers both).
|
|
349
|
+
|
|
294
350
|
## Vetting a provider (capability + cost probe)
|
|
295
351
|
|
|
296
352
|
A discount is worthless if the provider quietly breaks the wire protocol. `ai-lcr` ships a zero-dependency check (`scripts/check-provider.sh`, just `bash` + `curl` + `python3`) that vets the things that actually cost you money or corrupt output, **per model**:
|
|
297
353
|
|
|
298
|
-
> **Media providers** have their own
|
|
354
|
+
> **Media providers** have their own probes: `scripts/check-kunavo-media.sh` (`bash` + `curl` + `jq`) live-tests Kunavo's image generation, `*-edit` reference endpoint, and async + sync video; `scripts/check-media-async.mjs` exercises `ai-lcr`'s own `submit`/`poll` API across **every async provider** (kunavo · fal · runware) whose key is present — submit → JSON round-trip the handle → poll to done → assert the URL fetches and cost is reported, per provider (`PROBE_FAILOVER=1` adds a live submit-time failover case). Run them before trusting a media route in production.
|
|
299
355
|
|
|
300
356
|
- **tool calling** — single call and a multi-step round-trip with `content: null` (the shape every agent loop sends)
|
|
301
357
|
- **`max_tokens` honored** — caps must bound output
|
package/README.zh-CN.md
CHANGED
|
@@ -207,10 +207,66 @@ Kunavo 提供 Anthropic + Google。DeepSeek / OpenAI / Grok / Mistral 路由到
|
|
|
207
207
|
| Seedance Pro | $0.124 |
|
|
208
208
|
| Veo 3.1(audio-on) | $0.400 |
|
|
209
209
|
|
|
210
|
+
## 图像与视频路由(`createMediaLCR`)
|
|
211
|
+
|
|
212
|
+
图像和视频是 `ai-lcr` 独立的一侧(输出是文件、计价单位混杂、视频是异步任务)—— 见 [`src/media.ts`](src/media.ts)。你提供一个 registry(每个模型的 provider 路由 + 单位价)和一组 adapter,它就按最便宜优先路由、自动 failover,并通过与文本侧相同的 `onCall` sink 报告真实/归一化成本。
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createMediaLCR, createKunavoMediaAdapter, createFalMediaAdapter } from 'ai-lcr'
|
|
216
|
+
|
|
217
|
+
const lcr = createMediaLCR({
|
|
218
|
+
registry: {
|
|
219
|
+
'google/veo-3-lite': {
|
|
220
|
+
id: 'google/veo-3-lite', modality: 'video',
|
|
221
|
+
routes: [
|
|
222
|
+
{ provider: 'kunavo', externalId: 'veo-3-lite', pricing: { unit: 'call', cents: 16 } },
|
|
223
|
+
{ provider: 'fal', externalId: 'fal-ai/veo3.1/lite', pricing: { unit: 'second', cents: 8 } },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
adapters: {
|
|
228
|
+
kunavo: createKunavoMediaAdapter({ apiKey: process.env.KUNAVO_API_KEY! }),
|
|
229
|
+
fal: createFalMediaAdapter({ apiKey: process.env.FAL_KEY! }),
|
|
230
|
+
},
|
|
231
|
+
onCall: rec => console.log(rec.winner, rec.costUsd, rec.failedOver),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// 同步:出片才 resolve(图像够用)。
|
|
235
|
+
const { outputs, provider, costCents } = await lcr('google/veo-3-lite', { prompt: 'a wave' })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 异步(`submit` / `poll`)—— 给长耗时的视频
|
|
239
|
+
|
|
240
|
+
几分钟的视频任务没法把一个 serverless 请求一直挂住。`submit` 负责路由 + 入队,返回一个**纯 JSON 句柄**;`poll` 负责查它。两者跑在不同进程——句柄能扛过一次数据库/队列的来回。
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// 进程 A —— 请求处理器:路由 + 入队,立即返回
|
|
244
|
+
const handle = await lcr.submit('google/veo-3-lite', { prompt: 'a wave', aspect_ratio: '16:9' })
|
|
245
|
+
await db.jobs.put(jobId, JSON.stringify(handle))
|
|
246
|
+
|
|
247
|
+
// 进程 B —— cron / 队列 worker:轮询到终态
|
|
248
|
+
let handle = JSON.parse(await db.jobs.get(jobId))
|
|
249
|
+
const r = await lcr.poll(handle)
|
|
250
|
+
if (r.done) {
|
|
251
|
+
save(r.outputs, r.costCents) // 已落地——telemetry 此刻已落一条
|
|
252
|
+
} else {
|
|
253
|
+
await db.jobs.put(jobId, JSON.stringify(r.handle)) // 继续轮询 r.handle
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
几个值得知道的设计取舍:
|
|
258
|
+
|
|
259
|
+
- **路由发生在 `submit`**(选中最便宜的、支持异步的 provider);句柄携带尚未尝试的 fallback 列表,所以——
|
|
260
|
+
- **failover 发生在 `poll`**——某个 provider 的任务在轮询途中失败时,会自动 re-submit 到下一个 provider(返回一个新的 `r.handle` 继续轮询),而不是让请求直接死掉。
|
|
261
|
+
- **telemetry 只在终态轮询落一条**——一条 `onCall` `CallRecord`,带完整 failover 链,跨两个进程串起来(不是在 `submit` 时落)。
|
|
262
|
+
- adapter 通过实现 `submit` + `checkStatus` 来声明支持异步;只做图像的 adapter 省略它们,异步路由会跳过这种路由。内置的 Kunavo、fal、Runware adapter 都实现了异步路径(Kunavo/Runware 异步仅视频;fal 图像视频皆可)。
|
|
263
|
+
|
|
210
264
|
## 给 provider 做体检(能力 + 成本探测)
|
|
211
265
|
|
|
212
266
|
折扣再大,如果 provider 偷偷破坏了协议就一文不值。`ai-lcr` 自带一个零依赖的检查脚本(`scripts/check-provider.sh`,只需 `bash` + `curl` + `python3`),**逐模型**核查那些真正会让你多花钱或污染输出的点:
|
|
213
267
|
|
|
268
|
+
> **媒体 provider 有独立探针:** `scripts/check-kunavo-media.sh`(`bash` + `curl` + `jq`)实测 Kunavo 的图像生成、`*-edit` 参考图端点、以及异步 + 同步视频;`scripts/check-media-async.mjs` 则**逐 provider**(kunavo · fal · runware,有 key 的才跑)跑 `ai-lcr` 自己的 `submit`/`poll` API——submit → 把句柄做 JSON 来回 → 轮询到 done → 断言 URL 真能 GET 到、成本有上报(`PROBE_FAILOVER=1` 再加一条实时 submit 期 failover)。上生产前先跑一遍。
|
|
269
|
+
|
|
214
270
|
- **工具调用** —— 单次调用 + 带 `content: null` 的多步 round-trip(每个 agent 循环都会发的形态)
|
|
215
271
|
- **`max_tokens` 是否生效** —— cap 必须能限制输出长度
|
|
216
272
|
- **隐藏 prompt 注入** —— 发一条中性消息,如果模型开始回应一段它从没收到过的 system prompt,就说明 provider 注入了东西
|