@broberg / ai-sdk
一个AI/LLM SDK -一个门面,所有的提供者,所有的功能 一流的 对每个电话进行成本控制.
与提供者无关的外观:您的代码调用 ai.chat(), ai.vision(),
ai.image() -永远不要直接提供SDK。通过改变一个层来交换提供商;
不是你的电话亭。每次调用都会返回 Usage (令牌,成本,延迟,
运输, data-residency地区),并将其扩展到任何成本池。
bun add @broberg/ai-sdk # or: npm i @broberg/ai-sdk
数据到哪里去了?- - - - - - usage.region
每个回应都带有 实际应答的端点的数据驻留:
const { text, usage } = await ai.chat({ prompt, tier: "smart" });
if (usage.region !== "eu") throw new Error(`personal data would have left the EU (${usage.region})`);
只有 "eu" 是一个积极的主张。 "unknown" 意思是我们不能这么说
region !== "us" 是 不 欧盟检查——它通过了每一个OpenRouter调用。
region 是由 主人 使用的请求,而不是来自提供程序的名称-
vertex, azure 和 bfl 默认是EU,但每个都需要重写,所以a
基于名称的表将报告 eu 因为有人指着电话 us-central1.
三件事决定你的防御是否有效:
| 电话结束后 | usage.region - - - - - - "eu" | "us" | "cn" | "unknown" |
| 打电话之前 | regionOfHost(urlOrHostname) ——这是拒绝的唯一方式 之前 字节离开 |
| 从来没有 | regionOfProvider(name) -见下文 |
import { regionOfHost } from "@broberg/ai-sdk";
if (regionOfHost("https://api.mistral.ai/v1") !== "eu") throw new Error("not EU");
不建立常驻警卫吗 regionOfProvider. 一个名字不能回答
托管时,提供商采取 baseUrl,所以 regionOfProvider("mistral") 是
"unknown" ——还有一个守卫,写着 regionOfProvider(p) === "eu" 因此拒绝了
米斯特拉尔,欧盟唯一的航线。 故障关闭和无用:它过滤掉
这是它存在的目的。
容许清单 (provider, model) 对的另一边有相同的孔:a
米斯特拉尔门前的大门还没有动 "mistral"/"mistral-large-latest",两者都匹配
田野和传球。 regionOfHost 答案 "unknown" 这是最诚实的回答。
快速启动
import { createAI } from "@broberg/ai-sdk";
const ai = createAI(); // real adapters, keys from env (ANTHROPIC_API_KEY, …)
const { text, usage } = await ai.chat({ prompt: "Say hi in Danish" });
console.log(text, usage.costUsd);
const v = await ai.vision({ image: "https://…/photo.png", prompt: "Describe" });
const img = await ai.image({ prompt: "a sunlit beach in Blokhus" });
const da = await ai.translate({ text: "hello", to: "Danish" });
const emb = await ai.embedding({ text: ["a", "b"] });
功能
chat · vision · translate · image (歧视。ai default / OpenRouter embedding · transcribe
(耳语)优先 提示合同 具有结构化输出:
import { z } from "zod";
const { data } = await ai.contracts.extract({
text: "Sanne is 40 and lives in Blokhus",
schema: z.object({ name: z.string(), age: z.number(), city: z.string() }),
});
// also: ai.contracts.{ mockup, design, classify, rerank }
ai.judge - Jev (TypeSafe), F066 不是聊天模式:输入是/否,选择和
对一段内容的问题进行评分,每个答案都有一个校准的概率
和一个 confidence 你可以登机了。每百万输入令牌$0.042,输出免费。
美国托管-不用于个人数据。 看到文档/ API。md§4。
大声朗读:实际上所说的不是你的 text
ai.tts({ pronunciations }) 重写您的文本之前,提供者看到它-
broberg.ai 变成四个口语单词。所以 text 是音频的复述吗,和
文字荧光笔建立在 text 孤独会漂向每一个重写的字。
有两个领域弥补了这一差距:
const { audio, wordTimings, ssml } = await ai.tts({
text, voice, wordTimings: true, pronunciations,
override: { provider: "azure" },
});
wordTimings.words // [{ text, startMs, endMs, sourceStart, sourceEnd }]
wordTimings.unaligned // spoken words we could NOT place — never a guessed offset
ssml // the EXACT markup we sent, verbatim — your ground truth
sourceStart/sourceEnd 指数 你的原文,因为Azure会报告音频时间
只有和不携带任何文本偏移量;手稿的链接在这里。
ssml 存在,因此您可以检查派生而不是信任它-它是字符串
我们发送了,而不是重建(由无法验证所说内容的消费者提交,F055.4)。
它是 undefined 在没有建立标记的路由上;空字符串则是另一种说法。
两个让人惊讶的匹配规则,两者均以 pronunciations:
- 不区分大小写的。
ai,Ai和AI所有匹配规则为AI. - 碰到连字符的单词保持其拼写。
broberg.ai-drevet不会被重写 除非这个条目matchInCompounds: true-保持的规则mail的e-mail. 一个消费者得出了他们自己的期望单词列表,得到了这两个 字段存在之前出现错误。
Word计时需要Azure的BATCH路由,不同的呼叫形式(提交→投票→ZIP),
该路由需要资源的自定义子域-区域主机回答401
使用有效的密钥,责备密钥。集 AZURE_SPEECH_RESOURCE;没有它 wordTimings
在调用之前失败,命名修复。
供应商和层级
适配器: 人为 (HTTP + claude -p 子流程), OpenAI, 谷歌
双子座, DeepInfra, OpenRouter (包括极大极小), fal.ai (图片)。
通过命名调用路由 层 - - - - - - fast · smart · powerful · cheap · vision · embedding -每个解析为a (provider, model, transport) 三,
每次调用可重写:
await ai.chat({ prompt: "…", tier: "powerful" });
await ai.chat({ prompt: "…", override: { provider: "openrouter", model: "minimax/minimax-m2.7" } });
图像-光栅vs.矢量。
ai.image()默认为false。ai(栅格PNG)。为 向量/ SVG 输出(标识),覆盖到OpenRouter Recraft - slug是一个 OpenRouter 模型,而不是一个失败的应用程序id:await ai.image({ prompt: "…", override: { provider: "openrouter", model: "recraft/recraft-v4.1-vector" } }); // → data:image/svg+xml;base64,… with ground-truth cost
recraft/recraft-v4.1-pro-vector为复杂向量提供了更高的细节(~$0.30) 每张图片vs0.08美元);0.21美元)。recraft/recraft-v4.1-pro光栅等效(
cheap 默认为最便宜的云模型 据悉,小
(欧盟/巴黎托管,gdp安全,~ 0.10美元/ 0.30美元)-所以成本层呼叫是安全的
默认的个人资料;覆盖每个呼叫,以获得更便宜的非个人路线。
( claude -p 子进程传输仍然可以通过显式传输
override: { transport: "subprocess" },但不再是默认路由。)
这将走向何方?- - - - - - wouldRouteTo (预测,不是事实)
usage.region 答案 电话去了哪里. 它无法回答 我可以寄这个吗?,因为
它只在字节剩余时存在。 wouldRouteTo 回答那一半:
import { wouldRouteTo } from "@broberg/ai-sdk";
const f = wouldRouteTo("smart");
// { kind: "forecast", provider: "mistral", host: "https://api.mistral.ai/v1",
// wouldRouteTo: "eu", onlyIf: [ …the assumptions… ] }
它的形状故意不像 usage.region. 没有所谓的领域 region 在
这是由一个考验来执行的:一个可能是错误的预测不能穿衣服
事实不能。 onlyIf 回来 与 而不是生活在这些
文档-只有当你推翻“否”时,答案才有效 baseUrl 没有退路,因为
退路是一条路线,而这条路线决定了居住权.
"depends-on-config" 是一个答案,而不是一个失败。 azure, vertex, deepl, requesty
和 fal 根据您提供的值构建它们的主机。 requesty 是这个国家存在的原因吗
有 两个 欧盟和非欧盟的东道主,所以我们在那里命名的任何地区都会有居住权
由表而不是由路由决定的。
它什么都不拒绝。 它永远不会抛出和阻止调用-即使是无意义的 提供者。居住权政策属于每个消费者,而不属于这个包裹;这是 使决策成为可能的信息,这是门的反义词。
为什么它会说 "eu" 哪里 regionOfProvider 说 "unknown": 它要求 主人 这个
SDK将实际使用,而不是提供商的 名字. regionOfProvider("mistral") 是
"unknown" 根据设计,适配器需要一个 baseUrl. 由一位消费者提出的
手动复制我们的层表到他们自己的仓库来回答这个问题;现在可以删除该副本
而不是维护。
成本,预算和水槽
import { createAI, upmetricsSink, sqliteSink, multiSink } from "@broberg/ai-sdk";
const ai = createAI({
budget: { perCallUsd: 0.05, rollingUsd: 5 }, // pre-flight guard (throws BudgetExceededError)
costSink: multiSink([
upmetricsSink({ baseUrl: "https://upmetrics.org", apiKey: process.env.UPMETRICS_API_KEY!, agentName: "my-app" }),
sqliteSink({ dbPath: "./ai-cost.db" }), // Bun only — throws on Node, see below
]),
});
汇: upmetricsSink (规范), discordSink, sqliteSink, multiSink,
noopSink. 失败的水槽 在 一个调用永远不会使该调用崩溃。
sqliteSink和getCostSummary是Bun-only (在v0.48.0更改,已发布 2026年9月22日)。他们的背后是bun:sqlite, Node根本无法导入。在Node上 扔到哪里 构建他们 -故意的,这是一个行为改变:直到v0.47你 有个水槽砸在每个record(),客户端吞下每个调用 由于设计上的失误,成本跟踪悄无声息地消失了。空成本数据集 和一个正常工作的在报告里看起来一模一样。 关于Node的使用upmetricsSink.sqliteBudgetStore也是出于同样的原因;它总是彻底失败 在那里,因为预算错误没有被吞下。
成本跟踪是默认开启的(v0.24+)
你不用给水槽装电线。如果 UPMETRICS_API_KEY 是在羡慕,一个赤裸
createAI() 自动附加upmetrics接收器-这存在是因为大多数
呼叫地点没有经过水槽,留下约91%的西北风花费不可见。没有钥匙
环境→没有沉没,没有坠毁(船暗)。
每个客户端都需要一个名称(0.63+,F034.4)。 createAI() 启动时抛出,除非
UPMETRICS_AGENT_NAME 准备好了还是你过去了 agentName -即使没有钥匙,所以
您的测试在生产之前捕获它。业主订单2026-10-15:«Ingen morv ære
匿名»-在30天内,8个回购者发送了他们的支出 unknown. 不需要
有一个明确的 costSink 或者 costSink: null.
createAI({ agentName: "my-app" }); // key in env → tracked; no key → nothing happens
createAI(); // same, if UPMETRICS_AGENT_NAME is set — else throws
createAI({ costSink: mySink }); // explicit sink wins
createAI({ costSink: null }); // explicit OPT-OUT — see below
| Env var | 效果 |
|---|---|
UPMETRICS_API_KEY | 唯一的开关。 Present→tracking on。 |
UPMETRICS_AGENT_NAME | 行标签, 要求 (或 agentName) -在生产环境和测试环境中设置它。到0.62时,回退为 npm_package_name,空在npm/bun脚本外面,所以 node dist/… 和Docker服务日志为 unknown. |
UPMETRICS_BASE_URL | 摄取宿主。默认为 https://upmetrics.org. |
UPMETRICS_COMPLIANCE | 1 设置合规模式。 Usage 两种方式都不携带提示/响应文本。 |
在repo中采用它-首先要做两件事,否则采用会破坏 它本来要透露的数字是:
-
在你已经申报了自己的费用的地方选择退出。 通过
costSink: null. 否则SDK会添加一个 第二个 上面的报告路径 你和每一通电话都算数 两次,在生产中,没有错误 任何地方。(发现的buddy,谁聚集到一起cli_usage每小时推一次。) -
不要让钥匙进入你的测试。 测试运行器自动加载
.env(包 做;Vitest与dotenv设置),所以任何套件达到acreateAI()将 制造用于生产遥测. 请注意 触发因素是 进口 构建客户端的模块——模块级createAI()在没有任何命令的情况下武装它。去掉整个前缀:// test-setup.ts — bunfig.toml: [test] preload = ["./test-setup.ts"] for (const k of Object.keys(process.env)) { if (k.startsWith("UPMETRICS_")) delete process.env[k]; }去掉 前缀不是名单,而是你必须记住的守卫 更新是一种无声腐烂的东西。并断言 效果 (没有
UPMETRICS_*对测试可见)有一个控制,证明探针可以变红;一个测试 检查“密钥未定义”通过,同时不保护机器上的任何内容 他从来没有钥匙。
采用 每个回购,选择加入 -没有舰队范围的推进。打开它 回购值得关注,因为双方都有防范措施。
许可证
目前2.0 - 1.1 - apache -