@broberg/ai-sdk 0.25.1 → 0.27.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/README.md CHANGED
@@ -87,6 +87,56 @@ const ai = createAI({
87
87
  Sinks: `upmetricsSink` (canonical), `discordSink`, `sqliteSink`, `multiSink`,
88
88
  `noopSink`. A failing sink never crashes a call.
89
89
 
90
+ ### Cost-tracking is on by default (v0.24+)
91
+
92
+ You don't have to wire a sink. If `UPMETRICS_API_KEY` is in the env, a bare
93
+ `createAI()` auto-attaches the upmetrics sink — this exists because most
94
+ call-sites passed no sink, leaving ~91% of Mistral spend invisible. No key in
95
+ the env → no sink, no crash (ship-dark).
96
+
97
+ ```ts
98
+ createAI(); // key in env → tracked; no key → nothing happens
99
+ createAI({ costSink: mySink }); // explicit sink wins
100
+ createAI({ costSink: null }); // explicit OPT-OUT — see below
101
+ ```
102
+
103
+ | Env var | Effect |
104
+ |---|---|
105
+ | `UPMETRICS_API_KEY` | **The only switch.** Present → tracking on. |
106
+ | `UPMETRICS_AGENT_NAME` | Row label. Set it — the fallback is `npm_package_name`, which is empty for a process not started via an npm/bun script (a `node dist/…` or Docker service logs as `unknown`). |
107
+ | `UPMETRICS_BASE_URL` | Ingest host. Defaults to `https://upmetrics.org`. |
108
+ | `UPMETRICS_COMPLIANCE` | `1` sets compliance mode. `Usage` carries no prompt/response text either way. |
109
+
110
+ **Adopting it in a repo — two things to do first, or the adoption corrupts the
111
+ numbers it was meant to reveal:**
112
+
113
+ 1. **Opt out wherever you already report your own costs.** Pass
114
+ `costSink: null`. Otherwise the SDK adds a *second* reporting path on top of
115
+ yours and every call is counted **twice**, in production, with no error
116
+ anywhere. (Found by `buddy`, who aggregates to `cli_usage` and pushes hourly.)
117
+
118
+ 2. **Keep the key out of your test run.** Test runners auto-load `.env` (Bun
119
+ does; Vitest with a dotenv setup does), so any suite that reaches a
120
+ `createAI()` will POST **fabricated usage into production telemetry**. Note
121
+ the trigger is *importing* a module that builds the client — a module-level
122
+ `createAI()` arms it without anything calling it. Strip the whole prefix:
123
+
124
+ ```ts
125
+ // test-setup.ts — bunfig.toml: [test] preload = ["./test-setup.ts"]
126
+ for (const k of Object.keys(process.env)) {
127
+ if (k.startsWith("UPMETRICS_")) delete process.env[k];
128
+ }
129
+ ```
130
+
131
+ Strip the **prefix**, not a list of names — a guard you must remember to
132
+ update is one that silently rots. And assert the *effect* (no `UPMETRICS_*`
133
+ visible to a test) with a control proving the probe can go red; a test that
134
+ checks "the key is undefined" passes while protecting nothing on a machine
135
+ that never had the key.
136
+
137
+ Adoption is **per repo, opt-in** — there is no fleet-wide push. Turn it on when
138
+ a repo has spend worth watching, with both guards in place.
139
+
90
140
  ## License
91
141
 
92
142
  FSL-1.1-Apache-2.0
@@ -67,6 +67,12 @@ var PRICING = {
67
67
  "gemini:gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },
68
68
  // flash-lite is the default `video` tier (F019) — cheap native video understanding.
69
69
  "gemini:gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, version: "2026-06-04-or-xref" },
70
+ // Vertex AI (F038) — the EU-resident route to the SAME Gemini models, so Google's
71
+ // published Gemini token prices apply. Listed separately because cost lookups key on
72
+ // `provider:model`: without these rows an EU vision/video call would silently log
73
+ // $0, which is worse than no tracking (a confident wrong number).
74
+ "vertex:gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },
75
+ "vertex:gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, version: "2026-06-04-or-xref" },
70
76
  // Mistral (direct, La Plateforme). Official prices from mistral.ai/pricing
71
77
  // (2026-06-04, per Christian's CD report). EU/Paris-hosted — the designated
72
78
  // GDPR-safe provider for client/personal-data workloads (see F015). NB:
@@ -105,4 +111,4 @@ export {
105
111
  PRICING,
106
112
  getPrice
107
113
  };
108
- //# sourceMappingURL=chunk-IZG5UZH5.js.map
114
+ //# sourceMappingURL=chunk-LUPUAAHZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cost/pricing.ts"],"sourcesContent":["// Versioned per-(provider, model) pricing. F3.6 populates the table + adds tests\n// + MiniMax coverage. F3.1 ships the type + lookup with an empty table, so\n// computeCost returns 0 for every model until F3.6 lands (calls still complete).\nexport interface PricingEntry {\n /** USD per 1M input tokens. */\n inputPer1M: number;\n /** USD per 1M output tokens. */\n outputPer1M: number;\n /** USD per 1M cache-read tokens (falls back to input rate if unset). */\n cacheReadPer1M?: number;\n /** USD per 1M cache-write/creation tokens (falls back to input rate if unset). */\n cacheWritePer1M?: number;\n /** Pricing snapshot version (date or tag) so stale entries are detectable. */\n version: string;\n}\n\n// USD per 1M tokens. Anthropic cache multipliers follow the standard model:\n// cache-read ≈ 0.1× input, cache-write ≈ 1.25× input. Verified against the\n// pricing tables in cms (packages/cms-ai/src/providers) + trail (model-lab).\n// MiniMax M2.7 is an estimate pending confirmation against OpenRouter's live\n// price page — flagged in its version string.\nconst V = \"2026-06-02\";\n// Mistral prices come straight from mistral.ai/pricing (per Christian's CD report).\nconst MS = \"2026-06-04-mistral.ai\";\n\n/** Keyed `${provider}:${model}`. Exported so the catalogue-research job (F014)\n * can enumerate every priced entry and diff it against the live provider lists. */\nexport const PRICING: Record<string, PricingEntry> = {\n // Anthropic (direct API). DEFAULT_TIER_MAP: fast/cheap=haiku, smart/vision=sonnet, powerful=opus.\n \"anthropic:claude-haiku-4-5\": {\n inputPer1M: 0.8,\n outputPer1M: 4.0,\n cacheReadPer1M: 0.08,\n cacheWritePer1M: 1.0,\n version: V,\n },\n \"anthropic:claude-sonnet-4-6\": {\n inputPer1M: 3.0,\n outputPer1M: 15.0,\n cacheReadPer1M: 0.3,\n cacheWritePer1M: 3.75,\n version: V,\n },\n \"anthropic:claude-opus-4-8\": {\n inputPer1M: 15.0,\n outputPer1M: 75.0,\n cacheReadPer1M: 1.5,\n cacheWritePer1M: 18.75,\n version: V,\n },\n\n // OpenAI. embedding default tier = text-embedding-3-small (no output tokens).\n \"openai:text-embedding-3-small\": { inputPer1M: 0.02, outputPer1M: 0, version: V },\n \"openai:text-embedding-3-large\": { inputPer1M: 0.13, outputPer1M: 0, version: V },\n \"openai:gpt-4o\": { inputPer1M: 2.5, outputPer1M: 10.0, version: V },\n \"openai:gpt-4o-mini\": { inputPer1M: 0.15, outputPer1M: 0.6, version: V },\n // Whisper is priced per minute, not per token — not representable here; transcribe\n // (F5.6) computes its own cost. Listed as 0 so token-based compute never charges it.\n \"openai:whisper-1\": { inputPer1M: 0, outputPer1M: 0, version: V },\n\n // OpenRouter (meta-router — model slugs include the upstream vendor). Slugs use\n // dots (claude-sonnet-4.6) to match OpenRouter's live ids; the dashed forms\n // never matched a real call. Caught by the F014 catalogue research.\n \"openrouter:anthropic/claude-sonnet-4.6\": { inputPer1M: 3.0, outputPer1M: 15.0, version: V },\n // OpenRouter ground-truth $1/$5 — a markup over Anthropic-direct's $0.8/$4\n // (the `anthropic:` entry above). Was masked while the slug used dashes.\n \"openrouter:anthropic/claude-haiku-4.5\": { inputPer1M: 1.0, outputPer1M: 5.0, version: \"2026-06-04\" },\n \"openrouter:google/gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n // Ground-truth from OpenRouter /api/v1/models (was a 0.3 estimate; now 0.279).\n \"openrouter:minimax/minimax-m2.7\": {\n inputPer1M: 0.279,\n outputPer1M: 1.2,\n version: \"2026-06-04\",\n },\n // DeepSeek V4 (CN-hosted — NOT GDPR-safe; non-personal-data workloads only).\n // On 2026-05-22 DeepSeek made the \"75% off\" promo the permanent official price.\n // V4-Pro $0.435/$0.87 is ~34x cheaper than GPT-5.5 on output; flash is cheaper\n // still. Numbers match OpenRouter /api/v1/models 1:1 (no router markup). A strong\n // cheap route for fleet background work once `claude -p` is API-billed (15 Jun).\n \"openrouter:deepseek/deepseek-v4-pro\": { inputPer1M: 0.435, outputPer1M: 0.87, version: \"2026-05-22-deepseek-official\" },\n \"openrouter:deepseek/deepseek-v4-flash\": { inputPer1M: 0.0983, outputPer1M: 0.1966, version: \"2026-05-22-deepseek-official\" },\n // DeepSeek DIRECT API (provider \"deepseek\", F030 non-PII secondary). Rates from\n // api-docs.deepseek.com 2026-06-30 ($0.14/$0.28 per 1M; both map to deepseek-v4-flash).\n // `deepseek-chat` (non-thinking) + `deepseek-reasoner` (thinking) DEPRECATE 2026-07-24.\n // (The bare `deepseek-v4-flash` basename is already priced via the openrouter entry\n // above — kept distinct here to avoid a basename collision in the F027 pricing-API.)\n // Verify against a real key when it lands.\n \"deepseek:deepseek-chat\": { inputPer1M: 0.14, outputPer1M: 0.28, version: \"2026-06-30-deepseek-direct\" },\n \"deepseek:deepseek-reasoner\": { inputPer1M: 0.14, outputPer1M: 0.28, version: \"2026-06-30-deepseek-direct\" },\n\n // Google Gemini (direct). Provider key is \"gemini\" — matches the adapter's\n // usage.provider + the override.provider callers pass. (Image-gen models are\n // priced per-image in the adapter, not here.)\n \"gemini:gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n // flash-lite is the default `video` tier (F019) — cheap native video understanding.\n \"gemini:gemini-2.5-flash-lite\": { inputPer1M: 0.1, outputPer1M: 0.4, version: \"2026-06-04-or-xref\" },\n\n // Vertex AI (F038) — the EU-resident route to the SAME Gemini models, so Google's\n // published Gemini token prices apply. Listed separately because cost lookups key on\n // `provider:model`: without these rows an EU vision/video call would silently log\n // $0, which is worse than no tracking (a confident wrong number).\n \"vertex:gemini-2.5-flash\": { inputPer1M: 0.3, outputPer1M: 2.5, version: V },\n \"vertex:gemini-2.5-flash-lite\": { inputPer1M: 0.1, outputPer1M: 0.4, version: \"2026-06-04-or-xref\" },\n\n // Mistral (direct, La Plateforme). Official prices from mistral.ai/pricing\n // (2026-06-04, per Christian's CD report). EU/Paris-hosted — the designated\n // GDPR-safe provider for client/personal-data workloads (see F015). NB:\n // medium-3.5 is the premium \"Vibe\" coding tier ($1.5/$7.5); Large 3 ($0.5/$1.5)\n // is the cheaper frontier general-purpose model despite the higher number.\n \"mistral:mistral-large-latest\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:mistral-large-2512\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:mistral-medium-latest\": { inputPer1M: 1.5, outputPer1M: 7.5, version: MS },\n \"mistral:mistral-medium-3.5\": { inputPer1M: 1.5, outputPer1M: 7.5, version: MS },\n \"mistral:mistral-medium-3\": { inputPer1M: 0.4, outputPer1M: 2.0, version: \"2026-06-04-or-xref\" },\n \"mistral:mistral-small-latest\": { inputPer1M: 0.1, outputPer1M: 0.3, version: MS },\n \"mistral:mistral-small-2603\": { inputPer1M: 0.1, outputPer1M: 0.3, version: MS },\n \"mistral:ministral-3b-latest\": { inputPer1M: 0.1, outputPer1M: 0.1, version: MS },\n \"mistral:ministral-8b-latest\": { inputPer1M: 0.15, outputPer1M: 0.15, version: MS },\n \"mistral:ministral-14b-latest\": { inputPer1M: 0.2, outputPer1M: 0.2, version: MS },\n \"mistral:magistral-medium-latest\": { inputPer1M: 2.0, outputPer1M: 5.0, version: MS },\n \"mistral:magistral-small-latest\": { inputPer1M: 0.5, outputPer1M: 1.5, version: MS },\n \"mistral:devstral-latest\": { inputPer1M: 0.4, outputPer1M: 2.0, version: MS },\n \"mistral:codestral-latest\": { inputPer1M: 0.3, outputPer1M: 0.9, version: MS },\n \"mistral:open-mistral-nemo\": { inputPer1M: 0.15, outputPer1M: 0.15, version: MS },\n // Moderation (F016.4) — per input token; output 0. (OCR is per-page in the adapter.)\n \"mistral:mistral-moderation-latest\": { inputPer1M: 0.1, outputPer1M: 0, version: MS },\n // Embeddings (F016.5) — per input token.\n \"mistral:mistral-embed\": { inputPer1M: 0.1, outputPer1M: 0, version: MS },\n \"mistral:codestral-embed\": { inputPer1M: 0.15, outputPer1M: 0, version: MS },\n};\n\nexport function getPrice(provider: string, model: string): PricingEntry | undefined {\n const exact = PRICING[`${provider}:${model}`];\n if (exact) return exact;\n // Providers ship dated model snapshots, e.g. \"claude-haiku-4-5-20251001\".\n // Strip a trailing -YYYYMMDD and retry the base lookup so a dated variant\n // prices the same as its base model instead of falling through to 0 — a real\n // paid call must never be logged as $0 (F012). Covers openrouter slugs too.\n const base = model.replace(/-\\d{8}$/, \"\");\n if (base !== model) return PRICING[`${provider}:${base}`];\n return undefined;\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1697,7 +1697,10 @@ declare const aiConfigSchema: z.ZodObject<{
1697
1697
  transport: "http" | "subprocess";
1698
1698
  }>>>;
1699
1699
  providers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<ProviderAdapter, z.ZodTypeDef, ProviderAdapter>>>;
1700
- costSink: z.ZodOptional<z.ZodType<CostSink, z.ZodTypeDef, CostSink>>;
1700
+ /** Omit → cost-tracking auto-wires from the upmetrics env (F034). Pass `null`
1701
+ * to opt OUT explicitly — for a consumer that already reports its own costs
1702
+ * and would otherwise be counted twice (F034.3). An object is used as-is. */
1703
+ costSink: z.ZodOptional<z.ZodNullable<z.ZodType<CostSink, z.ZodTypeDef, CostSink>>>;
1701
1704
  budget: z.ZodOptional<z.ZodObject<{
1702
1705
  perCallUsd: z.ZodOptional<z.ZodNumber>;
1703
1706
  rollingUsd: z.ZodOptional<z.ZodNumber>;
@@ -1725,7 +1728,7 @@ declare const aiConfigSchema: z.ZodObject<{
1725
1728
  transport: "http" | "subprocess";
1726
1729
  }>> | undefined;
1727
1730
  providers?: Record<string, ProviderAdapter> | undefined;
1728
- costSink?: CostSink | undefined;
1731
+ costSink?: CostSink | null | undefined;
1729
1732
  budget?: {
1730
1733
  perCallUsd?: number | undefined;
1731
1734
  rollingUsd?: number | undefined;
@@ -1741,7 +1744,7 @@ declare const aiConfigSchema: z.ZodObject<{
1741
1744
  transport: "http" | "subprocess";
1742
1745
  }>> | undefined;
1743
1746
  providers?: Record<string, ProviderAdapter> | undefined;
1744
- costSink?: CostSink | undefined;
1747
+ costSink?: CostSink | null | undefined;
1745
1748
  budget?: {
1746
1749
  perCallUsd?: number | undefined;
1747
1750
  rollingUsd?: number | undefined;
@@ -2045,8 +2048,8 @@ declare const falStubAdapter: ProviderAdapter;
2045
2048
  * wires the live adapters. */
2046
2049
  declare const stubProviders: Record<string, ProviderAdapter>;
2047
2050
 
2048
- declare const VERSION: "0.25.1";
2049
- declare const SDK_TAG: "@broberg/ai-sdk@0.25.1";
2051
+ declare const VERSION: "0.27.0";
2052
+ declare const SDK_TAG: "@broberg/ai-sdk@0.27.0";
2050
2053
 
2051
2054
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2052
2055
  * per-call override.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-IT7HNKLY.js";
9
9
  import {
10
10
  getPrice
11
- } from "./chunk-IZG5UZH5.js";
11
+ } from "./chunk-LUPUAAHZ.js";
12
12
 
13
13
  // src/routing/tier-map.ts
14
14
  var DEFAULT_TIER_MAP = {
@@ -1633,8 +1633,8 @@ function vertexAdapter(config = {}) {
1633
1633
  return config.region ?? process.env.GOOGLE_VERTEX_REGION ?? DEFAULT_REGION2;
1634
1634
  }
1635
1635
  function project() {
1636
- const p = config.project ?? process.env.GOOGLE_VERTEX_PROJECT;
1637
- if (!p) throw new Error("vertex adapter: project not set (config.project or env GOOGLE_VERTEX_PROJECT)");
1636
+ const p = config.project ?? process.env.GOOGLE_VERTEX_PROJECT ?? resolveCredentials(config).project_id;
1637
+ if (!p) throw new Error("vertex adapter: project not set (config.project, env GOOGLE_VERTEX_PROJECT, or project_id in the credentials)");
1638
1638
  return p;
1639
1639
  }
1640
1640
  async function accessToken() {
@@ -1710,7 +1710,47 @@ function vertexAdapter(config = {}) {
1710
1710
  usage.costUsd = perSec * (req.durationSec ?? 8);
1711
1711
  return { url: `vertex://${op.name}`, bytes: Buffer.from(videoB64, "base64"), mimeType: videoMime, usage };
1712
1712
  }
1713
- return { name: "vertex", animate };
1713
+ async function vision(req) {
1714
+ const token = await accessToken();
1715
+ const proj = project();
1716
+ const reg = region();
1717
+ const systemParts = [];
1718
+ const contents = [];
1719
+ for (const m of req.messages) {
1720
+ if (m.role === "system") systemParts.push(...partsFrom(m.content));
1721
+ else contents.push({ role: m.role === "assistant" ? "model" : "user", parts: partsFrom(m.content) });
1722
+ }
1723
+ const body = { contents };
1724
+ if (systemParts.length > 0) body.systemInstruction = { parts: systemParts };
1725
+ const genConfig = {};
1726
+ if (req.maxTokens !== void 0) genConfig.maxOutputTokens = req.maxTokens;
1727
+ if (req.temperature !== void 0) genConfig.temperature = req.temperature;
1728
+ if (Object.keys(genConfig).length > 0) body.generationConfig = genConfig;
1729
+ const url = `https://${reg}-aiplatform.googleapis.com/v1/projects/${proj}/locations/${reg}/publishers/google/models/${req.spec.model}:generateContent`;
1730
+ const res = await fetchImpl(url, {
1731
+ method: "POST",
1732
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1733
+ body: JSON.stringify(body)
1734
+ });
1735
+ if (!res.ok) {
1736
+ throw new Error(`vertex vision ${res.status}: ${(await res.text().catch(() => "")).slice(0, 300)}`);
1737
+ }
1738
+ const data = await res.json();
1739
+ const text = (data.candidates?.[0]?.content?.parts ?? []).map((p) => p.text ?? "").join("").trim();
1740
+ const inputTokens = data.usageMetadata?.promptTokenCount ?? 0;
1741
+ const outputTokens = data.usageMetadata?.candidatesTokenCount ?? 0;
1742
+ const usage = freshUsage({
1743
+ provider: "vertex",
1744
+ model: req.spec.model,
1745
+ transport: "http",
1746
+ capability: "vision",
1747
+ inputTokens,
1748
+ outputTokens
1749
+ });
1750
+ usage.costUsd = computeCost("vertex", req.spec.model, inputTokens, outputTokens);
1751
+ return { text, usage };
1752
+ }
1753
+ return { name: "vertex", animate, vision };
1714
1754
  }
1715
1755
 
1716
1756
  // src/providers/deepl.ts
@@ -2601,14 +2641,17 @@ var aiConfigSchema = z.object({
2601
2641
  // Functions can't be deeply validated — z.custom asserts the TS type and
2602
2642
  // passes the value through untouched.
2603
2643
  providers: z.record(z.string(), z.custom()).optional(),
2604
- costSink: z.custom().optional(),
2644
+ /** Omit → cost-tracking auto-wires from the upmetrics env (F034). Pass `null`
2645
+ * to opt OUT explicitly — for a consumer that already reports its own costs
2646
+ * and would otherwise be counted twice (F034.3). An object is used as-is. */
2647
+ costSink: z.custom().nullable().optional(),
2605
2648
  budget: budgetSchema.optional(),
2606
2649
  availability: availabilitySchema.optional()
2607
2650
  });
2608
2651
 
2609
2652
  // src/version.ts
2610
- var VERSION = "0.25.1";
2611
- var SDK_TAG = "@broberg/ai-sdk@0.25.1";
2653
+ var VERSION = "0.27.0";
2654
+ var SDK_TAG = "@broberg/ai-sdk@0.27.0";
2612
2655
 
2613
2656
  // src/cost/sinks/upmetrics.ts
2614
2657
  function upmetricsSink(config) {
@@ -2728,7 +2771,7 @@ function defaultCostSink() {
2728
2771
  function createAI(config = {}) {
2729
2772
  const cfg = aiConfigSchema.parse(config);
2730
2773
  const providers = cfg.providers ?? defaultProviders;
2731
- const costSink = cfg.costSink ?? defaultCostSink();
2774
+ const costSink = cfg.costSink === void 0 ? defaultCostSink() : cfg.costSink ?? void 0;
2732
2775
  const budget = cfg.budget ? new BudgetGuard(cfg.budget) : void 0;
2733
2776
  const estTokens = (s) => Math.ceil(s.length / 4);
2734
2777
  async function preflight(spec, estInTokens, estOutTokens) {