npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ai-lcr

v0.6.5

Published

Least Cost Routing for LLMs — route every model call to the cheapest available provider, fall back automatically, and track real cost. Built for the Vercel AI SDK.

Readme

AI-LCR — AI 最低成本路由(Least Cost Routing)

同一个模型在不同 provider 上的价格不同——而且没有任何单一 provider 在所有模型上都最便宜。ai-lcr 为每个模型维护一份「最便宜优先」的列表,路由到其中最便宜且健康的 provider(下表中的 ⭐),失败时向下穿透——这正是电话运营商几十年来一直在做的 最低成本路由(Least Cost Routing)

安装

npm install ai-lcr

ai(Vercel AI SDK)是 peer dependency。

快速开始

import { createLCR } from "ai-lcr";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from "ai";

const kunavo = createOpenAICompatible({
  name: "kunavo",
  baseURL: "https://api.kunavo.com/v1",
  apiKey: process.env.KUNAVO_API_KEY,
});
const openrouter = createOpenAICompatible({
  name: "openrouter",
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});

const lcr = createLCR({
  autoSort: true, // 按 `cost` 把每个模型的 provider 排成最便宜优先
  models: {
    // 一个逻辑模型,跨多个 provider 最便宜优先地提供服务。
    "gemini-3-flash": [
      { model: kunavo("gemini-3-flash"), label: "kunavo", cost: { input: 0.40, output: 2.40 } },
      { model: openrouter("google/gemini-3-flash-preview"), label: "openrouter", cost: { input: 0.5, output: 3.0 } },
    ],
  },
  // 看清每次调用的实际花费,以及由哪个 provider 提供。
  onCost: ({ provider, costUsd }) => console.log(`${provider}: $${costUsd.toFixed(6)}`),
});

const { text } = await generateText({
  model: lcr("gemini-3-flash"),
  prompt: "Explain Least Cost Routing in one sentence.",
});

costlabel 都是可选的——如果你不需要成本核算或 autoSort,可以直接传裸模型(kunavo("gemini-3-flash"))。lcr("gemini-3-flash") 返回一个标准的 AI SDK 模型,因此可与 generateTextstreamTextgenerateObject、工具调用和 agent 一起使用。

直连模型厂商官方 API(原生 provider)

「provider」不一定是聚合器。模型厂商自己的官方 API 就是列表里的又一个 entry——往往是最便宜的那个(没有聚合器加价),也最不容易悄悄破坏原生特性(prompt 缓存、工具调用)。任何 AI SDK 的 provider 包都返回标准模型,所以厂商的原生 API 和 OpenAI 兼容的聚合器可以并排放在同一个列表里:

import { createLCR } from "ai-lcr";
import { createDeepSeek } from "@ai-sdk/deepseek";          // DeepSeek 官方 API
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";

const deepseek = createDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY });
const openrouter = createOpenAICompatible({
  name: "openrouter",
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});

const lcr = createLCR({
  autoSort: true,
  models: {
    "deepseek-v4": [
      // 官方 API 优先——无加价,原生特性齐全(缓存、错峰折扣)。
      { model: deepseek("deepseek-chat"), label: "deepseek", cost: { input: 0.43, output: 0.87 } },
      // 聚合器作为兜底,保可用性 + 广覆盖。
      { model: openrouter("deepseek/deepseek-v4"), label: "openrouter", cost: { input: 0.43, output: 0.87 } },
    ],
  },
});

同样的模式适用于任何厂商的原生 SDK provider——@ai-sdk/anthropic@ai-sdk/google@ai-sdk/openai@ai-sdk/xai 等等。ProviderEntry 接受 AnyLanguageModel——一个鸭子类型接口(doGenerate + doStream + provider + modelId),任何 AI SDK model 无论 V2 还是 V3 spec 都满足,集成边界无需 as 强转。原生 API 覆盖窄(只有该厂商自己的模型)但特性全;聚合器覆盖广。官方优先 + 聚合器兜底 正是 LCR 最自然的形态。

开源权重模型的最便宜路由(DeepInfra)

对开源权重模型——DeepSeek、Kimi、MiniMax、GLM、Qwen——专门的推理托管商通常是最便宜的路由,明显低于聚合器价格。DeepInfra 兼容 OpenAI,直接当成列表里的又一个 entry 即可。有一个坑:它的 OpenAI endpoint 在 /v1/openai/v1/openai 前面),不是常规的 /v1

import { createLCR } from "ai-lcr";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";

const deepinfra = createOpenAICompatible({
  name: "deepinfra",
  baseURL: "https://api.deepinfra.com/v1/openai", // 注意:/v1/openai,不是 /v1
  apiKey: process.env.DEEPINFRA_API_KEY,
});
const openrouter = createOpenAICompatible({
  name: "openrouter",
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});

const lcr = createLCR({
  autoSort: true,
  models: {
    // DeepInfra 最便宜;OpenRouter 作广覆盖 / 可用性兜底。
    // DeepInfra 用 HuggingFace 风格的 id(org/Name)。
    "deepseek-v4-flash": [
      { model: deepinfra("deepseek-ai/DeepSeek-V4-Flash"), label: "deepinfra", cost: { input: 0.10, output: 0.20 } },
      { model: openrouter("deepseek/deepseek-v4-flash"), label: "openrouter", cost: { input: 0.27, output: 1.10 } },
    ],
    "minimax-m2.5": [
      { model: deepinfra("MiniMaxAI/MiniMax-M2.5"), label: "deepinfra", cost: { input: 0.15, output: 1.15 } },
    ],
    "kimi-k2.5": [
      { model: deepinfra("moonshotai/Kimi-K2.5"), label: "deepinfra", cost: { input: 0.45, output: 2.25 } },
    ],
  },
});

DeepInfra 只承载开源权重——没有第一方 Claude / GPT / Gemini。那些闭源模型请走 OpenRouter 或折扣中转。

省掉样板代码(DEFAULT_PROVIDERS

每个路由 OpenRouter、DeepInfra、TokenMart、DeepSeek 等的项目都要重复声明相同的 baseURL + apiKeyEnvDEFAULT_PROVIDERS 是一份内置字典——import 你需要的那几个就行,不用再复制粘贴 URL:

import { DEFAULT_PROVIDERS } from "ai-lcr";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";

// 按需取——类型安全,无硬编码 URL。
const deepinfra = createOpenAICompatible({
  name: "deepinfra",
  baseURL: DEFAULT_PROVIDERS.deepinfra.baseURL,
  apiKey: process.env[DEFAULT_PROVIDERS.deepinfra.apiKeyEnv],
});

可用 provider:

| Key | Base URL | Env 变量 | |---|---|---| | openrouter | https://openrouter.ai/api/v1 | OPENROUTER_API_KEY | | deepinfra | https://api.deepinfra.com/v1/openai | DEEPINFRA_API_KEY | | tokenmart | https://model.service-inference.ai/v1 | INFERENCE_API_KEY | | deepseek | https://api.deepseek.com | DEEPSEEK_API_KEY | | kunavo | https://api.kunavo.com/v1 | KUNAVO_API_KEY | | runware | https://api.runware.ai/v1 | RUNWARE_API_KEY | | fal | https://queue.fal.run | FAL_KEY |

常见用法是取 DEFAULT_PROVIDERS 的子集,并声明一个项目级类型保证编译安全:

import { DEFAULT_PROVIDERS } from "ai-lcr";

type ProviderId = "deepinfra" | "openrouter";

export const PROVIDERS = {
  deepinfra: DEFAULT_PROVIDERS.deepinfra,
  openrouter: DEFAULT_PROVIDERS.openrouter,
} satisfies Record<ProviderId, { baseURL: string; apiKeyEnv: string }>;

它如何路由

  1. 最便宜优先。 provider 按顺序依次尝试——把它们排成最便宜优先,或设置 autoSort: true 让它按 cost 自动排序。
  2. 失败时向下穿透。 遇到任何 provider 失败——限流、5xx、超时、额度耗尽(402 / 欠费 / 余额不足),以及 400 这类 client 错误——都会前进到下一个 provider,且对流式安全。400 会 failover 是有意为之:在 OpenAI 兼容聚合层里,400 往往是"这家 provider 不吃这个请求"(不支持的参数、它没上架这个 model、更严格的 schema),而非请求本身坏了——换一家很可能就能服务。若所有 provider 都拒绝,请求仍会失败,并抛出第一个(原始)错误,让真正的调用方 bug 保持可调试。唯一永远不 failover 的是调用方主动取消(AbortSignal)。想恢复旧的"client 错误立即失败"行为,给 createLCRshouldRetry: isRetryableError
  3. 恢复。 在一段空闲窗口(resetIntervalMs,默认 60s)之后,自动回到最便宜的 provider。

缓存

LLM 世界里有两种完全不同的"缓存",ai-lcr 两种都做——都默认关闭,都只是一个配置开关,不需要你跑任何服务

整次调用都省掉(cache)—— 响应缓存

当一个请求和之前回答过的一模一样时,直接重放已存的响应、完全不调用任何 provider:零延迟、costUsd: 0。这正是 Vercel AI Gateway 明显没做的那一层。

const lcr = createLCR({
  models: { /* … */ },
  cache: true, // 精确匹配响应缓存(默认进程内内存)
});

存储可插拔,且 ai-lcr 为此零依赖

  • cache: true 用进程内内存。在长驻 server 上是真缓存;在单次 serverless 调用内(比如 agent 循环里重复的子调用)也有用——但它不会跨 serverless 请求存活,因为不同函数实例之间不共享内存。
  • 想在 serverless 上跨请求命中,自带一个由共享层(Upstash Redis、Vercel KV)支撑的 store:cache: myStore。ai-lcr 自己不跑任何服务——共享层是你的。自定义 store 就是 { get, set }(见 CacheStore);想要带上限的内置实现,用导出的 createMemoryCacheStore({ maxEntries })
  • cache: { store?, ttlMs? } 可设置过期时间。

命中会落一条 CallRecordcacheHit: truecostUsd: 0,并把省下的钱单独记在 cacheHitSavingUsd 一行——这是缓存省的钱,和路由省的钱(baselineUsd − costUsd)分开,绝不混在一起。空回复和无 usage 的结果永不缓存。一个要点:缓存会让相同请求返回相同响应——对幂等 / temperature: 0 的调用正好,对采样型调用则是行为改变。

让这次调用更便宜(promptCache)—— provider 提示缓存

另一套机制:模型照样跑,但提示的静态开头(你的 system prompt)在重复时按 provider 的缓存读价(约 0.1× input)计费。Anthropic 需要显式 cache_control 标记;OpenAI / Gemini / DeepSeek 自动缓存前缀。promptCache: true 帮你在最后一条 system 消息上插入这个标记:

const lcr = createLCR({
  models: { /* … */ },
  promptCache: true,          // 5 分钟窗口;想要更长用 { ttl: "1h" }
});

它只写 anthropic 这个 provider-options 命名空间,其他 provider 一律忽略——所以在混合链路上也安全。而且只要你在 prompt 里自己设了 cacheControl,它就完全让位。省下的钱照旧体现在 CallRecordcachedInputTokenscachedSavingUsd 上。

看清每次调用发生了什么(onCall

onError/onCost 各自独立触发、互不关联,事后很难还原一次 failover 的全貌。onCall 给你每个请求一条记录——完整的尝试链、最终服务者、每跳失败的原因、延迟和成本;formatCallRecord 把它变成一行可扫读的日志:

✓ text  tokenmart                      412ms  $0.0003
⚠ text  tokenmart→openrouter           910ms  $0.0004  ⤷ tokenmart 502
✗ text  deepseek→tokenmart→openrouter  1240ms FAILED   ⤷ deepseek 401, tokenmart 502, openrouter 429

record 是一个纯 CallRecord 对象,关键字段:

interface CallRecord {
  id: string;                 // 每个请求一个关联 id
  model: string;              // 逻辑模型名
  attempts: { provider; ok; latencyMs; errorClass? }[];
  winner?: string;            // 最终服务的 provider;全失败则为 undefined
  ok: boolean;
  failedOver: boolean;        // 尝试了不止一家
  latencyMs: number;
  ttftMs?: number;            // 仅流式:首 token 时间
  inputTokens: number;
  outputTokens: number;
  cachedInputTokens?: number; // 命中 prompt 缓存的输入 token
  costUsd: number;            // 实际成本(已按 cacheRead 折扣)
  baselineUsd?: number;       // 同样用量在「节约基线」上的价格 → 节约 = baselineUsd − costUsd
  baselineKind?: "last-leg" | "official" | "priciest-route"; // 基线的来源(见下)
  cachedSavingUsd?: number;   // provider 自己的缓存折扣——是真金白银,但不是路由的功劳,别混进节约
  usageMissing?: boolean;     // 服务成功但 token 报 0/0 → 成本是「未知」而非「免费」

  // 媒体调用(createMediaLCR)额外携带:
  modality?: "image" | "video";
  usage?: { seconds?; outputs?; megapixels? }; // 账单依据的实际用量
  officialUsd?: number;       // 官方第一方价(按本次实际用量)
  estCostUsd?: number;        // 价格表的预估——与 costUsd 的差 = 价格表漂移
}

节约怎么算才诚实: baselineKind 说明 baselineUsd 是哪种基线——文本是链尾兜底 provider 的列表价"last-leg",故意不取最贵的一条:prompt 缓存可能让标价更便宜的那家在缓存重的调用上反而更贵,取最大值会凭空造出"节约");媒体是模型厂商官方第一方价"official",按实际秒数算),查不到官方价时退化为你配置里最贵的路由("priciest-route",自我参照,仅说明跨 provider 价差)。

送进收集器: createHttpSink 把每条记录 POST 到任意 endpoint(serverless 上传 Next.js 的 afterdispatch 防止被掐断)。如果你用标准环境变量(LCR_INGEST_URLLCR_PROJECTLCR_INGEST_KEY),createEnvSink 全部替你读好——三行搞定:

import { createEnvSink } from "ai-lcr";
import { after } from "next/server";
export const lcrCallSink = createEnvSink(after);

LCR_INGEST_URL 不设 → sink 是 undefined,本地开发自动静默。唯一必传参数是 dispatch——框架相关的 fire-and-forget runner(Next.js: after;Cloudflare: ctx.waitUntil;长驻服务: (fn) => fn())。

配套的自托管 dashboard ai-lcr-dashboard(Next.js + Postgres,Vercel 一键部署)专为这些记录而建:花费 vs 节约趋势、各 provider failover 健康度、媒体 $/秒 与 $/张、以及价格漂移面板——某条 model@provider 路由的实报账单与价格表偏差超过 ±20% 时点名示警(约 100× 基本就是美元当美分的笔误)。只存元数据,不存 prompt 和输出。

支持的 provider

任何 OpenAI 兼容的 endpoint 都可用——任何 AI SDK 的 provider 包也都可用,包括模型厂商自己的官方 API。

  • 模型厂商官方 API(原生): 通过各自的 AI SDK provider 包直连 DeepSeekOpenAIAnthropicGooglexAI 等——无加价,原生特性齐全。见上方「直连模型厂商官方 API(原生 provider)」一节。
  • 文本聚合器: OpenRouter(覆盖最广,列表定价)· Kunavo全模型 8 折)· TokenMart(按模型 85 折–35 折不等)
  • 图像 / 视频: Kunavo8 折)· TokenMart · fal.ai · Runware —— 通过 createMediaLCR 路由。图像:Kunavo(生成 + *-edit 参考图端点)+ Runware + fal。视频:fal(异步队列)、Kunavo(异步 POST /v1/videos + 轮询,另有同步兜底)、Runware(异步 videoInference + getResponse 轮询)——三家都在异步 submit/poll 路径上

文本模型价格

单位为每 100 万 token 的美元价格,input / output。官方价格截至 2026-05——请向各 provider 核对当前价格。OpenRouter 直接透传列表价;Kunavo 在官方价基础上统一 8 折。TokenMart 折扣按模型不同(85 折–35 折),请在 thetokenmart.ai 核对当前价格。

| 模型 | 官方价(in / out) | OpenRouter | Kunavo | TokenMart | 最便宜 | |---|---|---|---|---|---| | Gemini 3 Flash | $0.50 / $3.00 | 无折扣 | −20% | — | ⭐ Kunavo | | Gemini 3 Pro / 3.1 Pro | $2.00 / $12.00 | 无折扣 | −20% | −20% → $2.40 / $9.60 | ⭐ Kunavo | | Gemini 2.5 Pro | $1.25 / $10.00 | 无折扣 | −20% | — | ⭐ Kunavo | | Gemini 2.5 Flash | $0.30 / $2.50 | 无折扣 | −20% | — | ⭐ Kunavo | | Claude Opus 4.7 | $15.00 / $75.00 | 无折扣 | −20% | $4.25 / $21.25 | ⭐ TokenMart | | Claude Sonnet 4.6 | $3.00 / $15.00 | 无折扣 | −20% | −15% → $2.55 / $12.75 | ⭐ Kunavo | | Claude Haiku 4.5 | $1.00 / $5.00 | 无折扣 | −20% | — | ⭐ Kunavo | | DeepSeek V4 | $0.43 / $0.87 | 无折扣 | 未提供 | — | ⭐ DeepSeek(官方) |

Kunavo 提供 Anthropic + Google。DeepSeek / OpenAI / Grok / Mistral 路由到各自的官方 API(最便宜,原生特性齐全),以 OpenRouter 作为广覆盖兜底——一份配置即可混用原生厂商与聚合器。

注: list 价 ≠ 有效价——请始终用 probe 验证。截至 2026-05-28,Kunavo 在 Gemini(~1.1–1.4×)和 Claude(~1.0×)两条路上的 token 计数均已干净。现存问题:两个模型均忽略 max_tokens,Claude 隐藏 prompt 注入仍为间歇性——生产路由前请重新 probe。

注: TokenMart token 计数同样经 probe 验证干净(后端与 Inference.ai 相同,2026-05-27 全项通过:工具调用、max_tokens、无注入、token ~1.0×、prompt 缓存)——如需 Claude 的第二 provider,TokenMart 是可靠备选。生产路由前请重新 probe 确认。

图像模型价格

单位为每张图的美元价格,截至 2026-05(provider 列表价 / 零售价;请核对当前价格)。Kunavo 为官方价 8 折。fal 与 Runware 是算力 provider——ai-lcr 为每个模型挑选最便宜的那个(⭐)。

| 模型 | fal.ai | Runware | Kunavo | TokenMart | 最便宜 | |---|---|---|---|---|---| | Nano Banana 2 | $0.080 | $0.069 | $0.054 | $0.050 | ⭐ TokenMart | | Nano Banana Pro | $0.080 | — | $0.107 | — | ⭐ fal | | GPT-Image-2 | $0.210 | $0.094 | $0.102 | — | ⭐ Runware | | Imagen 4 Ultra | $0.060 | $0.060 | — | — | ⭐ fal / Runware | | Ideogram V3 | $0.060 | $0.060 | — | — | ⭐ fal / Runware | | Seedream 4 | $0.030 | — | — | — | ⭐ fal | | Flux 1.1 Pro | $0.040 | $0.040 | — | — | ⭐ fal / Runware | | Flux Dev | $0.025 | $0.025 | — | — | ⭐ fal / Runware | | Flux Schnell | $0.0030 | $0.0013 | — | — | ⭐ Runware | | Qwen-Image | — | $0.0038 | — | — | ⭐ Runware | | FLUX.2 Klein 4B | — | $0.0006 | — | — | ⭐ Runware |

视频模型价格

单位为每秒的美元价格,截至 2026-05——请核对当前价格。视频计费方式因 provider 而异,因此无法做严格对等的跨 provider 表格:fal.ai 和 Runware 按秒计费,而 Kunavo 的 Veo 按段计费(Fast ~$0.28 / Lite ~$0.168 / Quality ~$1.34)。下表为 fal.ai 的每秒价格(测试中的视频主力);fal / Runware / Kunavo 的归一化对比是一个 TODO。

| 模型 | fal.ai($/s) | |---|---| | Seedance Lite | $0.036 | | Hailuo 02 Standard | $0.045 | | LTX-2 | $0.060 | | Kling 2.6 Pro | $0.070 | | WAN 2.2 | $0.080 | | Veo 3.1 Lite | $0.080 | | Kling V3 Pro | $0.112 | | Seedance Pro | $0.124 | | Veo 3.1(audio-on) | $0.400 |

图像与视频路由(createMediaLCR

图像和视频是 ai-lcr 独立的一侧(输出是文件、计价单位混杂、视频是异步任务)—— 见 src/media.ts。你提供一个 registry(每个模型的 provider 路由 + 单位价)和一组 adapter,它就按最便宜优先路由、自动 failover,并通过与文本侧相同的 onCall sink 报告真实成本。

两个价格、两份职责(0.6+):排序用归一化到参考输出(1080p 一张图 / 5 秒一段片)的价格,让混杂的计价单位可以公平比较;但每次调用的计费按实际用量——按秒计价的 SKU,一条 8 秒的片就按 8 秒收,节约基线也按同样的 8 秒官方价算。adapter 上报带类型的实际用量(usage: { seconds, outputs, megapixels });provider 自己报了账单时以账单为准,而账单与价格表预估差距悬殊时(经典的"美元当美分"笔误正好是 100×)会触发 onError,提醒你修价格表。

import { createMediaLCR, createKunavoMediaAdapter, createFalMediaAdapter } from 'ai-lcr'

const lcr = createMediaLCR({
  registry: {
    'google/veo-3-lite': {
      id: 'google/veo-3-lite', modality: 'video',
      routes: [
        { provider: 'kunavo', externalId: 'veo-3-lite',        pricing: { unit: 'call',   cents: 16 } },
        { provider: 'fal',    externalId: 'fal-ai/veo3.1/lite', pricing: { unit: 'second', cents: 8  } },
      ],
    },
  },
  adapters: {
    kunavo: createKunavoMediaAdapter({ apiKey: process.env.KUNAVO_API_KEY! }),
    fal:    createFalMediaAdapter({ apiKey: process.env.FAL_KEY! }),
  },
  onCall: rec => console.log(rec.winner, rec.costUsd, rec.failedOver),
})

// 同步:出片才 resolve(图像够用)。
const { outputs, provider, costCents } = await lcr('google/veo-3-lite', { prompt: 'a wave' })

异步(submit / poll)—— 给长耗时的视频

几分钟的视频任务没法把一个 serverless 请求一直挂住。submit 负责路由 + 入队,返回一个纯 JSON 句柄poll 负责查它。两者跑在不同进程——句柄能扛过一次数据库/队列的来回。

// 进程 A —— 请求处理器:路由 + 入队,立即返回
const handle = await lcr.submit('google/veo-3-lite', { prompt: 'a wave', aspect_ratio: '16:9' })
await db.jobs.put(jobId, JSON.stringify(handle))

// 进程 B —— cron / 队列 worker:轮询到终态
let handle = JSON.parse(await db.jobs.get(jobId))
const r = await lcr.poll(handle)
if (r.done) {
  save(r.outputs, r.costCents)                          // 已落地——telemetry 此刻已落一条
} else {
  await db.jobs.put(jobId, JSON.stringify(r.handle))    // 继续轮询 r.handle
}

几个值得知道的设计取舍:

  • 路由发生在 submit(选中最便宜的、支持异步的 provider);句柄携带尚未尝试的 fallback 列表,所以——
  • failover 发生在 poll——某个 provider 的任务在轮询途中失败时,会自动 re-submit 到下一个 provider(返回一个新的 r.handle 继续轮询),而不是让请求直接死掉。
  • telemetry 只在终态轮询落一条——一条 onCall CallRecord,带完整 failover 链,跨两个进程串起来(不是在 submit 时落)。
  • adapter 通过实现 submit + checkStatus 来声明支持异步;只做图像的 adapter 省略它们,异步路由会跳过这种路由。内置的 Kunavo、fal、Runware adapter 都实现了异步路径(Kunavo/Runware 异步仅视频;fal 图像视频皆可)。

自己写 adapter

MediaAdapter 很小——同步用 run,异步可选 submit/checkStatus——唯一要紧的合同是如何上报产出

// 落定的结果上报:
{
  outputs: [{ url, type: "image" | "video" }],
  costCents?: number,   // provider 自己的账单,单位是美分——API 返回美元的要 ×100 转换!
  usage?: {             // 带类型的实际用量——账单(或估算)以它为准
    seconds?: number,   //   实际产出的视频秒数(按秒计价的 SKU 按它计费)
    outputs?: number,   //   产出个数——图或片(按张 / 按次计价按它计费)
    megapixels?: number //   产出总百万像素(按 MP 计价按它计费)
  }
}

保证计费正确的几条规则:

  • 维度在 usage 里显式命名,绝不报裸数字。 秒数和产出数是两个不同的字段,按次的平价永远不可能被片长乘爆(经典的 8× 过计)。
  • costCents 是美分。 API 返回美元的,必须在 adapter 里转换(参考 Runware adapter)。万一失手,路由器的异常账单守卫会在偏差 ≥25× 时触发 onError——但上报的数字仍然作数。
  • 什么都不报时,路由器会估算:按秒 SKU 依次读 usage.seconds → 输入的 duration(数字或 "8s" 这类字符串)→ 最后才退到 5 秒参考;按张/按次 SKU 按产出数计。
  • 抛错时带上 HTTP status 属性(见 FalMediaError/KunavoMediaError),路由器才能正确分类并 failover。

给 provider 做体检(能力 + 成本探测)

折扣再大,如果 provider 偷偷破坏了协议就一文不值。ai-lcr 自带一个零依赖的检查脚本(scripts/check-provider.sh,只需 bash + curl + python3),逐模型核查那些真正会让你多花钱或污染输出的点:

媒体 provider 有独立探针: scripts/check-kunavo-media.shbash + 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)。上生产前先跑一遍。

  • 工具调用 —— 单次调用 + 带 content: null 的多步 round-trip(每个 agent 循环都会发的形态)
  • max_tokens 是否生效 —— cap 必须能限制输出长度
  • 隐藏 prompt 注入 —— 发一条中性消息,如果模型开始回应一段它从没收到过的 system prompt,就说明 provider 注入了东西
  • token 超计 —— 把上报的 prompt_tokens 和一个可信基线 provider 对照,>1.5× 说明账单被灌水、"折扣"可能是亏本
  • prompt 缓存 —— cache_control 在重复请求时是否真的产生 cache_read
# 指向你要体检的 provider;模型用通用编号槽位(Gemini / Claude / GPT / Llama 都行)。
# 给某个模型配上 REF_n(可信基线上的对应模型 id)即可启用 token 超计检查。
# CACHE_MODEL(可选)跑 Anthropic 原生 /v1/messages 的 prompt 缓存测试。
API_KEY=$KUNAVO_API_KEY BASE=https://api.kunavo.com \
  MODEL_1=gemini-3-flash    REF_1=google/gemini-3-flash-preview \
  MODEL_2=claude-sonnet-4-6 REF_2=anthropic/claude-sonnet-4.6 \
  CACHE_MODEL=claude-sonnet-4-6 \
  REF_API_KEY=$OPENROUTER_API_KEY REF_BASE=https://openrouter.ai/api \
  bash scripts/check-provider.sh

# TokenMart(Inference AI)使用不带 vendor 前缀的裸模型 ID
API_KEY=$INFERENCE_API_KEY BASE=https://model.service-inference.ai \
  MODEL_1=gemini-3-flash-preview      REF_1=google/gemini-3-flash-preview \
  MODEL_2=claude-sonnet-4-6           REF_2=anthropic/claude-sonnet-4.6 \
  CACHE_MODEL=claude-sonnet-4-6 \
  REF_API_KEY=$OPENROUTER_API_KEY REF_BASE=https://openrouter.ai/api \
  bash scripts/check-provider.sh

注入或 token 超计这两项 FAIL,意味着该 provider 对那个模型来说不是安全的最低成本目标——在它修好之前,别把它放进那个模型的「最便宜优先」列表,修好后重新探测。

信任矩阵(探测于 2026-05-27)

两个 OpenAI 兼容 provider,同一脚本,同一天。单元格覆盖两个家族(G = Gemini,C = Claude)。

| 检查项 | Kunavo | TokenMart | |---|---|---| | 工具调用(单次 + 多步 content: null) | G ⚠️ 间歇性¹ · C ✅ | ✅ 两者 | | token 计数 vs OpenRouter 基线 | G ✅ ~1.1–1.4× · C ✅ ~1.0× | ✅ 两者 ~1.0× | | 隐藏 prompt 注入 | G ✅ 无 · C ❌ 间歇性² | ✅ 无 | | max_tokens 是否生效 | ❌ 被忽略(两者) | ✅ 两者 | | prompt 缓存(cache_control) | C ❌ 未生效(探测中途 endpoint 还卡死) | C ✅ cache_read > 0 |

¹ Kunavo Gemini 一次返回干净的工具调用,下一次相同请求却完全丢掉了 tools——不是稳定通过。 ² Kunavo Claude 一次对着幻觉中的"fake system prompt"作出反应,另一次又干净——注入是间歇性的,不是被移除了。

结论: TokenMart 在 Gemini 和 Claude 两条路上每一项都通过,且结果稳定可复现——可以放心路由。Kunavo:Claude token 计数已干净(2026-05-28 重新 probe),按 8 折 list 价,Kunavo 现在是 Claude 模型的最便宜选择。现存问题:两个模型均忽略 max_tokens、Claude 隐藏 prompt 注入仍为间歇性、Gemini 也会间歇性丢工具调用——用新模型前先重新探测。

路线图

  • [x] 自有 failover 引擎——最便宜优先路由 + 流式安全的 fallback,不依赖外部路由库
  • [x] 真实的逐次调用成本核算(onCost
  • [x] 基于各 provider cost 的自动最便宜优先排序(autoSort
  • [x] 离线能力 + 成本检查(scripts/check-provider.sh)→ 逐模型信任矩阵
  • [ ] 内置价格表,实现零配置定价(省去手填 cost 数字)
  • [ ] provider 怪癖中间件(透明地修补已知怪癖,如 Kunavo 被忽略的 max_tokens
  • [ ] 把 probe 结果自动接入路由(探测失败的 provider×model 自动从列表剔除)
  • [x] 图像与视频模型路由(createMediaLCR)—— 图像走 Kunavo(含 *-edit)+ Runware + fal;视频异步(submit/poll)走 fal、Kunavo、Runware 三家
  • [x] 按实际用量的结算计费(0.6)—— typed usage、时长感知的节约基线、estCostUsd 价格漂移信号、异常账单守卫
  • [x] 自托管 dashboard(ai-lcr-dashboard)—— 节约、failover 健康度、媒体单位成本、价格漂移面板
  • [ ] 内置价格表中的归一化跨 provider 视频价格对比

联盟(Affiliate)披露

ai-lcr 是 provider 中立的,可与任何 OpenAI 兼容的 endpoint 配合使用。作者与 Kunavo 之间存在联盟(affiliate)关系——在官方价 8 折的情况下,它往往(但并非总是)是最便宜的选项,正如上面的表格所示。通过该链接注册可能会让作者获得一份分成。你完全不必使用它;自带 provider,路由功能照常工作。

开发

npm install
npm run typecheck
npm test          # mock 的路由 / failover 测试 + 真实 Kunavo 测试

测试套件覆盖了:最便宜优先路由、可重试错误以及 provider 400 时的 failover(但调用方主动取消时做 failover)、穷尽整条链路时抛出原始错误,以及一次真实的「provider 故障 → Kunavo 恢复」。真实测试仅在环境变量 KUNAVO_API_KEY 设置时运行,否则跳过。

致谢

流式安全的 failover 方案改编自 ai-fallback(MIT)——在内部重新实现,使 ai-lcr 拥有自己的引擎,并把成本核算 + 路由直接融入其中。基于 Vercel AI SDK 构建。

许可证

MIT © Victor