@broberg/ai-sdk 0.35.1 → 0.36.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.
@@ -22,14 +22,17 @@ var DEFAULT_TIER_MAP = {
22
22
  // NOT Anthropic → out of F030 (EU-embedding migration is its own future epic).
23
23
  embedding: { provider: "openai", model: "text-embedding-3-small", transport: "http" }
24
24
  };
25
+ function assertOverrideProvider(base, override, label, knownProviders) {
26
+ if (!override?.provider || override.model !== void 0) return;
27
+ if (override.provider === base.provider) return;
28
+ if (knownProviders !== void 0 && !knownProviders.includes(override.provider)) return;
29
+ throw new Error(
30
+ `createAI: override sets provider "${override.provider}", but "${label}" resolves to model "${base.model}", which belongs to "${base.provider}". Set a model too, e.g. override: { provider: "${override.provider}", model: "<a ${override.provider} model>" }.`
31
+ );
32
+ }
25
33
  function resolveTier(tier, override, configMap, knownProviders) {
26
34
  const base = configMap?.[tier] ?? DEFAULT_TIER_MAP[tier];
27
- const providerIsReal = knownProviders === void 0 || knownProviders.includes(override?.provider ?? "");
28
- if (providerIsReal && override?.provider && override.model === void 0 && override.provider !== base.provider) {
29
- throw new Error(
30
- `createAI: override sets provider "${override.provider}", but tier "${tier}" resolves to model "${base.model}", which belongs to "${base.provider}". Set a model too, e.g. override: { provider: "${override.provider}", model: "<a ${override.provider} model>" }.`
31
- );
32
- }
35
+ assertOverrideProvider(base, override, tier, knownProviders);
33
36
  return { ...base, ...override };
34
37
  }
35
38
 
@@ -186,6 +189,7 @@ function resolveModel(requested, opts = {}) {
186
189
 
187
190
  export {
188
191
  DEFAULT_TIER_MAP,
192
+ assertOverrideProvider,
189
193
  resolveTier,
190
194
  resetRegistry,
191
195
  providerIds,
@@ -194,4 +198,4 @@ export {
194
198
  listModels,
195
199
  resolveModel
196
200
  };
197
- //# sourceMappingURL=chunk-Y3EMGZFH.js.map
201
+ //# sourceMappingURL=chunk-ZFWSLSE7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/routing/tier-map.ts","../src/availability/registry.ts","../src/availability/types.ts","../src/availability/resolve.ts"],"sourcesContent":["// Tier routing: a named Tier resolves to a concrete (provider, model, transport).\n// Precedence is per-call override > client config map > built-in defaults.\nimport type { Tier, TierSpec } from \"../types.js\";\n\n/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a\n * per-call override.\n *\n * F030 — Anthropic API phase-out: `ANTHROPIC_API_KEY` was globally removed, so the\n * default cloud route may NOT hit Anthropic Console. `fast`/`smart`/`powerful`/\n * `vision` now default to **Mistral EU** (Paris-hosted, Schrems II-safe — so every\n * default text/vision call is GDPR-safe by default). Claude stays reachable as a\n * NON-default quality fallback for non-PII via `override:{provider:\"openrouter\",\n * model:\"anthropic/claude-…\"}`. DeepSeek (CN) is the opt-in non-PII secondary\n * (`provider:\"deepseek\"`), never a default. Magistral (reasoning) / mistral-large\n * for vision are per-call overrides, not defaults (don't pay the premium on all). */\nexport const DEFAULT_TIER_MAP: Record<Tier, TierSpec> = {\n fast: { provider: \"mistral\", model: \"mistral-small-latest\", transport: \"http\" },\n smart: { provider: \"mistral\", model: \"mistral-large-latest\", transport: \"http\" },\n powerful: { provider: \"mistral\", model: \"mistral-large-latest\", transport: \"http\" },\n cheap: { provider: \"mistral\", model: \"mistral-small-latest\", transport: \"http\" },\n // Vision: small-latest (vision-capable, cheap EU) is the default; override to\n // mistral-large-latest for demanding image/spatial/composition work.\n // F041 — bumped from mistral-small on Christian's ask, and the choice is MEASURED,\n // not assumed from price. On a fine-discrimination test (an 8x10 grid where one cell\n // differs only in its blue channel, 190->150) across 9 cases:\n // mistral-medium-latest 4/9 <- best\n // mistral-small-latest 1/6 + 0/3 = 1/9\n // mistral-large-latest 0/9 <- WORSE than small, despite costing 5x more\n // Large ties small on easy colour blocks (4/4 each) and collapses on subtle ones,\n // so \"bigger is better at vision\" does not hold in Mistral's lineup. Nobody is good\n // at this task; medium is simply the only one that sees anything.\n vision: { provider: \"mistral\", model: \"mistral-medium-latest\", transport: \"http\" },\n // Native video understanding — Gemini leads; flash-lite is the cheap default (F019).\n // NOT Anthropic → out of the F030 phase-out (its own EU epic if/when needed).\n video: { provider: \"gemini\", model: \"gemini-2.5-flash-lite\", transport: \"http\" },\n // NOT Anthropic → out of F030 (EU-embedding migration is its own future epic).\n embedding: { provider: \"openai\", model: \"text-embedding-3-small\", transport: \"http\" },\n};\n\n/**\n * Resolve a Tier to a concrete TierSpec.\n *\n * Merge order (later wins): DEFAULT_TIER_MAP < configMap < override.\n * - `configMap` is the client-level AiConfig.defaults (per-tier full specs).\n * - `override` is a per-call Partial<TierSpec> — only the fields it sets win.\n */\n/** Refuse an override that names a different provider without naming a model (F043.2).\n *\n * Exported and called at EVERY spec merge, not only inside resolveTier. The first\n * version guarded only the six capabilities that route via a tier; image, animate,\n * trainStyle, ocr, moderate, podcast, tts, transcribe and batch merge the override\n * themselves and skipped it entirely — so `ai.image({override:{provider:\"fal\"}})`\n * still posted BFL's flux-2-pro to fal, the exact misleading upstream error this was\n * written to kill. A guard at six of fifteen call sites is a guard you cannot rely on.\n *\n * We REFUSE rather than re-resolve: choosing a model for the new provider would be\n * the SDK making a price decision on the caller's behalf, visible only on the bill.\n *\n * `knownProviders` (when given) lets an UNREGISTERED provider through — that is a\n * typo, and \"no provider adapter registered\" is the useful thing to say about it. */\nexport function assertOverrideProvider(\n base: { provider: string; model: string },\n override: Partial<TierSpec> | undefined,\n label: string,\n knownProviders?: readonly string[],\n): void {\n if (!override?.provider || override.model !== undefined) return;\n if (override.provider === base.provider) return;\n if (knownProviders !== undefined && !knownProviders.includes(override.provider)) return;\n throw new Error(\n `createAI: override sets provider \"${override.provider}\", but \"${label}\" resolves to model ` +\n `\"${base.model}\", which belongs to \"${base.provider}\". Set a model too, e.g. ` +\n `override: { provider: \"${override.provider}\", model: \"<a ${override.provider} model>\" }.`,\n );\n}\n\nexport function resolveTier(\n tier: Tier,\n override?: Partial<TierSpec>,\n configMap?: Partial<Record<Tier, TierSpec>>,\n knownProviders?: readonly string[],\n): TierSpec {\n const base = configMap?.[tier] ?? DEFAULT_TIER_MAP[tier];\n assertOverrideProvider(base, override, tier, knownProviders);\n return { ...base, ...override };\n}\n","// F022 — the model-availability registry: the ONE source both resolveModel()\n// (spawn / call path) and listModels() (UI picker) read. A curated default seed\n// (works offline — the durable floor) plus a mutable overlay that\n// refreshAvailability() updates from the live provider list.\n//\n// Scope note: this is a LIVENESS view (is this id alive right now?), not the\n// rich capability/price inventory (that is F017 src/catalogue). We only track\n// ids we want to assert status on; anything not here is fail-open (treated\n// available) so we never block a model we simply do not track.\nimport { DEFAULT_TIER_MAP } from \"../routing/tier-map.js\";\nimport type { AvailabilityStatus, AvailabilitySource, ModelStatus } from \"./types.js\";\n\n/** Internal registry row. `aliases[0]` surfaces as ModelStatus.alias. */\nexport interface RegistryEntry {\n id: string;\n aliases: string[];\n provider: string;\n available: boolean;\n status: AvailabilityStatus;\n note?: string;\n source: AvailabilitySource;\n}\n\nconst SUSPENDED_FABLE_MYTHOS = \"suspended — US export-control directive (2026-06-12)\";\n\n/** Curated defaults. Aliases here are MODEL-IDENTITY names only (short names for\n * the model itself). TIER aliases — fast/smart/powerful/cheap/vision/video/\n * embedding — are NOT written here: they are derived from DEFAULT_TIER_MAP below,\n * because hand-maintaining them in two places is exactly how they drifted.\n *\n * They HAD drifted, and it crossed the EU border: this list said `smart` was\n * claude-sonnet-4-6 (Anthropic, US) for the ~3 months after F030 pointed the\n * default tiers at Mistral EU, so resolveModel('smart') named a US model while\n * ai.chat({tier:'smart'}) called an EU one. Anyone using resolveModel to SHOW or\n * DECIDE where data goes — the obvious use — got the wrong answer. Measured by\n * fd-sundhed in 0.21.1, still true in 0.28.0, fixed here by deleting the second\n * list rather than correcting it. */\nconst DEFAULTS: RegistryEntry[] = [\n // ── Anthropic ────────────────────────────────────────────────────────────\n { id: \"claude-haiku-4-5\", aliases: [\"haiku\"], provider: \"anthropic\", available: true, status: \"available\", source: \"default\" },\n { id: \"claude-sonnet-4-6\", aliases: [\"sonnet\"], provider: \"anthropic\", available: true, status: \"available\", source: \"default\" },\n { id: \"claude-opus-4-8\", aliases: [\"opus\"], provider: \"anthropic\", available: true, status: \"available\", source: \"default\" },\n { id: \"claude-fable-5\", aliases: [\"fable\"], provider: \"anthropic\", available: true, status: \"available\", source: \"default\" },\n { id: \"claude-mythos-5\", aliases: [\"mythos\"], provider: \"anthropic\", available: false, status: \"suspended\", note: SUSPENDED_FABLE_MYTHOS, source: \"default\" },\n // ── Gemini ───────────────────────────────────────────────────────────────\n { id: \"gemini-2.5-flash\", aliases: [\"gemini-flash\"], provider: \"gemini\", available: true, status: \"available\", source: \"default\" },\n { id: \"gemini-2.5-flash-lite\", aliases: [\"gemini-flash-lite\"], provider: \"gemini\", available: true, status: \"available\", source: \"default\" },\n // ── OpenAI ───────────────────────────────────────────────────────────────\n { id: \"text-embedding-3-small\", aliases: [], provider: \"openai\", available: true, status: \"available\", source: \"default\" },\n // ── Mistral (EU / GDPR) ──────────────────────────────────────────────────\n { id: \"mistral-large-latest\", aliases: [\"mistral-large\"], provider: \"mistral\", available: true, status: \"available\", source: \"default\" },\n { id: \"mistral-medium-latest\", aliases: [\"mistral-medium\"], provider: \"mistral\", available: true, status: \"available\", source: \"default\" },\n { id: \"mistral-small-latest\", aliases: [\"mistral-small\"], provider: \"mistral\", available: true, status: \"available\", source: \"default\" },\n];\n\n/** Attach every tier name as an alias of the model that tier ACTUALLY calls.\n * One source: DEFAULT_TIER_MAP decides, the registry follows. A tier whose model\n * is not in the registry is left unaliased on purpose — resolveModel then reports\n * status \"unknown\" (fail-open) instead of inventing a row, and the drift test\n * catches it. */\nexport const TIER_ALIAS_CONFLICTS: string[] = [];\n\nfunction applyTierAliases(entries: RegistryEntry[]): RegistryEntry[] {\n for (const [tier, spec] of Object.entries(DEFAULT_TIER_MAP)) {\n const owner = entries.find((e) => e.id === spec.model && e.provider === spec.provider);\n // A tier name hand-written on any OTHER row is the drift bug returning. It\n // would resolve correctly TODAY only because seed() lets the last write win\n // and the rows happen to be ordered favourably — reorder the array and the\n // tier silently points at the wrong provider again. Record it so a test can\n // fail on it, then strip it so runtime is right regardless.\n for (const e of entries) {\n if (e !== owner && e.aliases.includes(tier)) {\n TIER_ALIAS_CONFLICTS.push(`${tier} is hand-declared on ${e.id} (${e.provider}) but tier ${tier} calls ${spec.model} (${spec.provider})`);\n e.aliases = e.aliases.filter((a) => a !== tier);\n }\n }\n if (owner && !owner.aliases.includes(tier)) owner.aliases.push(tier);\n }\n return entries;\n}\napplyTierAliases(DEFAULTS);\n\n/** The live overlay, keyed by canonical id. Seeded from DEFAULTS (deep-copied so\n * resetting is clean). refreshAvailability() mutates this; resolve/listModels\n * read it synchronously. */\nlet OVERLAY = new Map<string, RegistryEntry>();\n/** alias → canonical id, rebuilt whenever the overlay is seeded. */\nlet ALIAS_INDEX = new Map<string, string>();\n\nfunction seed(): void {\n OVERLAY = new Map(DEFAULTS.map((e) => [e.id, { ...e, aliases: [...e.aliases] }]));\n ALIAS_INDEX = new Map();\n for (const e of DEFAULTS) for (const a of e.aliases) ALIAS_INDEX.set(a, e.id);\n}\nseed();\n\n/** Reset the overlay back to the curated defaults. For tests. */\nexport function resetRegistry(): void {\n seed();\n}\n\n/** Canonical id for a model id OR alias; null when we track neither. */\nexport function canonicalId(requested: string): string | null {\n if (OVERLAY.has(requested)) return requested;\n return ALIAS_INDEX.get(requested) ?? null;\n}\n\n/** The current entry for an id/alias, or undefined when untracked (fail-open). */\nexport function getEntry(requested: string): RegistryEntry | undefined {\n const id = canonicalId(requested);\n return id ? OVERLAY.get(id) : undefined;\n}\n\n/** All tracked entries (optionally provider-scoped), as a public ModelStatus[]. */\nexport function allEntries(provider?: string): ModelStatus[] {\n const rows: ModelStatus[] = [];\n for (const e of OVERLAY.values()) {\n if (provider && e.provider !== provider) continue;\n rows.push({\n id: e.id,\n alias: e.aliases[0],\n provider: e.provider,\n available: e.available,\n status: e.status,\n note: e.note,\n source: e.source,\n });\n }\n return rows;\n}\n\n/** Provider-scoped canonical ids (for refresh reconciliation). */\nexport function providerIds(provider: string): string[] {\n return [...OVERLAY.values()].filter((e) => e.provider === provider).map((e) => e.id);\n}\n\n/** Mark a tracked id available/suspended from a live refresh. No-op if untracked. */\nexport function setAvailability(id: string, available: boolean, note?: string): void {\n const e = OVERLAY.get(id);\n if (!e) return;\n e.available = available;\n e.status = available ? \"available\" : \"suspended\";\n e.source = \"refresh\";\n if (note !== undefined) e.note = note;\n else if (available) e.note = undefined;\n}\n","// F022 — Model Availability Harness. Public types for the availability layer:\n// the shared status read (ModelStatus), the resolve result, and the structured\n// error a caller can flag on. The registry is the one source both the spawn /\n// call path (resolveModel) and UI pickers (listModels) read.\n\nexport type AvailabilityStatus = \"available\" | \"suspended\" | \"unknown\";\n\n/** Where a model's current availability came from: the curated default seed,\n * or a live provider refresh (Anthropic GET /v1/models). */\nexport type AvailabilitySource = \"default\" | \"refresh\";\n\n/** One row of the shared status read — what a UI model-picker renders. */\nexport interface ModelStatus {\n /** Canonical provider model id, e.g. \"claude-fable-5\". */\n id: string;\n /** Short/tier alias, e.g. \"fable\" (the first registered alias). */\n alias?: string;\n /** \"anthropic\" | \"openai\" | \"gemini\" | \"mistral\" | … */\n provider: string;\n available: boolean;\n status: AvailabilityStatus;\n /** Friendly reason, e.g. \"suspended — US export-control directive (2026-06-12)\". */\n note?: string;\n source: AvailabilitySource;\n}\n\n/** Result of resolveModel — the spawn / call path consumes this synchronously. */\nexport interface ResolveResult {\n /** True when the requested model itself is available. */\n ok: boolean;\n /** The id to actually use: `requested` when ok, else the first available fallback. */\n model: string;\n /** What the caller asked for (id or alias, normalized to the canonical id). */\n requested: string;\n provider?: string;\n /** True when `model` differs from `requested` because we fell back. */\n fellBack: boolean;\n status: AvailabilityStatus;\n /** Why it degraded / why it is unavailable. */\n reason?: string;\n}\n\n/** Thrown by resolveModel when the requested model is unavailable, no usable\n * fallback exists, and the caller passed `throwIfUnavailable`. Callers flag on\n * `.code === \"model_unavailable\"`. */\nexport class ModelUnavailableError extends Error {\n readonly code = \"model_unavailable\";\n readonly requested: string;\n readonly provider?: string;\n readonly note?: string;\n constructor(requested: string, note?: string, provider?: string) {\n super(`model \"${requested}\" is unavailable${note ? ` (${note})` : \"\"}`);\n this.name = \"ModelUnavailableError\";\n this.requested = requested;\n this.note = note;\n this.provider = provider;\n }\n}\n","// F022 — the synchronous, zero-I/O resolve + status read. This is the spawn /\n// call hot path (buddy's launcher calls resolveModel per spawn, cardmem #4842):\n// it MUST never await and never touch the network. Freshness comes only from a\n// prior async refreshAvailability(); resolve just reads the in-memory registry.\nimport { allEntries, canonicalId, getEntry } from \"./registry.js\";\nimport { ModelUnavailableError } from \"./types.js\";\nimport type { ModelStatus, ResolveResult } from \"./types.js\";\n\nexport interface ResolveOptions {\n /** One id/alias or an ordered chain to try when `requested` is unavailable. */\n fallback?: string | string[];\n /** Scope hint (passed through to the result); does not gate lookup. */\n provider?: string;\n /** Throw ModelUnavailableError instead of returning ok:false when there is no\n * usable fallback. For callers that want to flag rather than degrade. */\n throwIfUnavailable?: boolean;\n /** Treat an id we do not track as UNUSABLE instead of fail-open.\n *\n * Default is fail-open: we never block a model we simply do not track. That is\n * right for liveness, and wrong for a caller who is GATING — cms measured the\n * consequence: resolveModel(\"cheap\") answered {ok:true, model:\"cheap\"}, so a\n * consumer following our own instruction to gate on `ok` passed the gate and\n * then sent the literal string \"cheap\" to a provider as a model id. A\n * success-shaped non-answer is worse than an error, because an error gets\n * handled and a shape does not.\n *\n * Set this when you need \"a model you actually know about\". Off by default so\n * no existing caller changes behaviour. */\n requireKnown?: boolean;\n}\n\n/** The shared status read — UI pickers grey out `available:false` rows. */\nexport function listModels(opts: { provider?: string } = {}): ModelStatus[] {\n return allEntries(opts.provider);\n}\n\n/** Is this id/alias currently usable? Untracked ids are fail-open (true) unless\n * the caller asked for requireKnown. */\nfunction isAvailable(requested: string, requireKnown = false): boolean {\n const e = getEntry(requested);\n if (!e) return !requireKnown; // fail-open on unknown, unless gating\n return e.available;\n}\n\n/**\n * Resolve a requested model (id or alias) to one that is actually usable.\n * Synchronous + offline by contract (cardmem #4842) — reads the registry only.\n *\n * - Available → pass through ({ ok:true, fellBack:false }).\n * - Unavailable + a fallback that IS available → swap ({ ok:false, fellBack:true }).\n * - Unavailable + no usable fallback → throw (throwIfUnavailable) or return ok:false.\n * - Unknown id → treated available (never block a model we do not track).\n */\nexport function resolveModel(requested: string, opts: ResolveOptions = {}): ResolveResult {\n const id = canonicalId(requested) ?? requested;\n const entry = getEntry(requested);\n const provider = opts.provider ?? entry?.provider;\n\n if (isAvailable(requested, opts.requireKnown)) {\n return {\n ok: true,\n model: id,\n requested: id,\n provider,\n fellBack: false,\n status: entry?.status ?? \"unknown\",\n };\n }\n\n // Requested is suspended — walk the fallback chain for the first available one.\n const chain = opts.fallback === undefined ? [] : Array.isArray(opts.fallback) ? opts.fallback : [opts.fallback];\n for (const fb of chain) {\n if (isAvailable(fb, opts.requireKnown)) {\n const fbId = canonicalId(fb) ?? fb;\n return {\n ok: false,\n model: fbId,\n requested: id,\n provider,\n fellBack: true,\n status: entry?.status ?? \"suspended\",\n reason: entry?.note ?? `${id} is unavailable`,\n };\n }\n }\n\n // No usable fallback.\n const unknownAndGating = !entry && opts.requireKnown;\n const reason = unknownAndGating\n ? `${id} is not a model this registry knows — requireKnown was set, so it is not assumed usable`\n : (entry?.note ?? `${id} is unavailable`);\n if (opts.throwIfUnavailable) {\n throw new ModelUnavailableError(id, reason, provider);\n }\n return {\n ok: false,\n model: id,\n requested: id,\n provider,\n fellBack: false,\n status: entry?.status ?? \"unknown\",\n reason,\n };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -579,18 +579,31 @@ declare const messageSchema: z.ZodObject<{
579
579
  type: "image";
580
580
  mimeType?: string | undefined;
581
581
  }>]>, "many">]>;
582
- toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
582
+ toolCalls: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
583
583
  id: z.ZodString;
584
584
  name: z.ZodString;
585
- arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
585
+ arguments: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
586
+ args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
586
587
  }, "strip", z.ZodTypeAny, {
587
588
  id: string;
588
589
  name: string;
589
- arguments: Record<string, unknown>;
590
+ args?: Record<string, unknown> | undefined;
591
+ arguments?: Record<string, unknown> | undefined;
590
592
  }, {
591
593
  id: string;
592
594
  name: string;
593
- arguments: Record<string, unknown>;
595
+ args?: Record<string, unknown> | undefined;
596
+ arguments?: Record<string, unknown> | undefined;
597
+ }>, {
598
+ id: string;
599
+ name: string;
600
+ args?: Record<string, unknown> | undefined;
601
+ arguments?: Record<string, unknown> | undefined;
602
+ }, {
603
+ id: string;
604
+ name: string;
605
+ args?: Record<string, unknown> | undefined;
606
+ arguments?: Record<string, unknown> | undefined;
594
607
  }>, "many">>;
595
608
  toolCallId: z.ZodOptional<z.ZodString>;
596
609
  }, "strip", z.ZodTypeAny, {
@@ -606,7 +619,8 @@ declare const messageSchema: z.ZodObject<{
606
619
  toolCalls?: {
607
620
  id: string;
608
621
  name: string;
609
- arguments: Record<string, unknown>;
622
+ args?: Record<string, unknown> | undefined;
623
+ arguments?: Record<string, unknown> | undefined;
610
624
  }[] | undefined;
611
625
  toolCallId?: string | undefined;
612
626
  }, {
@@ -622,7 +636,8 @@ declare const messageSchema: z.ZodObject<{
622
636
  toolCalls?: {
623
637
  id: string;
624
638
  name: string;
625
- arguments: Record<string, unknown>;
639
+ args?: Record<string, unknown> | undefined;
640
+ arguments?: Record<string, unknown> | undefined;
626
641
  }[] | undefined;
627
642
  toolCallId?: string | undefined;
628
643
  }>;
@@ -689,18 +704,31 @@ declare const chatInputSchema: z.ZodObject<{
689
704
  type: "image";
690
705
  mimeType?: string | undefined;
691
706
  }>]>, "many">]>;
692
- toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
707
+ toolCalls: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
693
708
  id: z.ZodString;
694
709
  name: z.ZodString;
695
- arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
710
+ arguments: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
711
+ args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
696
712
  }, "strip", z.ZodTypeAny, {
697
713
  id: string;
698
714
  name: string;
699
- arguments: Record<string, unknown>;
715
+ args?: Record<string, unknown> | undefined;
716
+ arguments?: Record<string, unknown> | undefined;
717
+ }, {
718
+ id: string;
719
+ name: string;
720
+ args?: Record<string, unknown> | undefined;
721
+ arguments?: Record<string, unknown> | undefined;
722
+ }>, {
723
+ id: string;
724
+ name: string;
725
+ args?: Record<string, unknown> | undefined;
726
+ arguments?: Record<string, unknown> | undefined;
700
727
  }, {
701
728
  id: string;
702
729
  name: string;
703
- arguments: Record<string, unknown>;
730
+ args?: Record<string, unknown> | undefined;
731
+ arguments?: Record<string, unknown> | undefined;
704
732
  }>, "many">>;
705
733
  toolCallId: z.ZodOptional<z.ZodString>;
706
734
  }, "strip", z.ZodTypeAny, {
@@ -716,7 +744,8 @@ declare const chatInputSchema: z.ZodObject<{
716
744
  toolCalls?: {
717
745
  id: string;
718
746
  name: string;
719
- arguments: Record<string, unknown>;
747
+ args?: Record<string, unknown> | undefined;
748
+ arguments?: Record<string, unknown> | undefined;
720
749
  }[] | undefined;
721
750
  toolCallId?: string | undefined;
722
751
  }, {
@@ -732,7 +761,8 @@ declare const chatInputSchema: z.ZodObject<{
732
761
  toolCalls?: {
733
762
  id: string;
734
763
  name: string;
735
- arguments: Record<string, unknown>;
764
+ args?: Record<string, unknown> | undefined;
765
+ arguments?: Record<string, unknown> | undefined;
736
766
  }[] | undefined;
737
767
  toolCallId?: string | undefined;
738
768
  }>, "many">>;
@@ -771,7 +801,8 @@ declare const chatInputSchema: z.ZodObject<{
771
801
  toolCalls?: {
772
802
  id: string;
773
803
  name: string;
774
- arguments: Record<string, unknown>;
804
+ args?: Record<string, unknown> | undefined;
805
+ arguments?: Record<string, unknown> | undefined;
775
806
  }[] | undefined;
776
807
  toolCallId?: string | undefined;
777
808
  }[] | undefined;
@@ -814,7 +845,8 @@ declare const chatInputSchema: z.ZodObject<{
814
845
  toolCalls?: {
815
846
  id: string;
816
847
  name: string;
817
- arguments: Record<string, unknown>;
848
+ args?: Record<string, unknown> | undefined;
849
+ arguments?: Record<string, unknown> | undefined;
818
850
  }[] | undefined;
819
851
  toolCallId?: string | undefined;
820
852
  }[] | undefined;
@@ -2146,8 +2178,8 @@ declare const falStubAdapter: ProviderAdapter;
2146
2178
  * wires the live adapters. */
2147
2179
  declare const stubProviders: Record<string, ProviderAdapter>;
2148
2180
 
2149
- declare const VERSION: "0.35.1";
2150
- declare const SDK_TAG: "@broberg/ai-sdk@0.35.1";
2181
+ declare const VERSION: "0.36.0";
2182
+ declare const SDK_TAG: "@broberg/ai-sdk@0.36.0";
2151
2183
 
2152
2184
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2153
2185
  * per-call override.
@@ -2161,13 +2193,6 @@ declare const SDK_TAG: "@broberg/ai-sdk@0.35.1";
2161
2193
  * (`provider:"deepseek"`), never a default. Magistral (reasoning) / mistral-large
2162
2194
  * for vision are per-call overrides, not defaults (don't pay the premium on all). */
2163
2195
  declare const DEFAULT_TIER_MAP: Record<Tier, TierSpec>;
2164
- /**
2165
- * Resolve a Tier to a concrete TierSpec.
2166
- *
2167
- * Merge order (later wins): DEFAULT_TIER_MAP < configMap < override.
2168
- * - `configMap` is the client-level AiConfig.defaults (per-tier full specs).
2169
- * - `override` is a per-call Partial<TierSpec> — only the fields it sets win.
2170
- */
2171
2196
  declare function resolveTier(tier: Tier, override?: Partial<TierSpec>, configMap?: Partial<Record<Tier, TierSpec>>, knownProviders?: readonly string[]): TierSpec;
2172
2197
 
2173
2198
  interface RefreshOptions {
@@ -2523,6 +2548,10 @@ interface HttpResponse {
2523
2548
  ok: boolean;
2524
2549
  status: number;
2525
2550
  json: unknown;
2551
+ /** The raw body as text. `json` is undefined when it would not parse; `text` still
2552
+ * holds what the server actually said, which is the only useful thing to show on a
2553
+ * gateway error. */
2554
+ text: string;
2526
2555
  }
2527
2556
  /** Subprocess transport result — already normalized from the `claude -p` JSON.
2528
2557
  * costUsd is always 0 (Max plan is not a metered charge); subprocess flags it. */
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  DEFAULT_TIER_MAP,
3
3
  ModelUnavailableError,
4
+ assertOverrideProvider,
4
5
  listModels,
5
6
  providerIds,
6
7
  resetRegistry,
7
8
  resolveModel,
8
9
  resolveTier,
9
10
  setAvailability
10
- } from "./chunk-Y3EMGZFH.js";
11
+ } from "./chunk-ZFWSLSE7.js";
11
12
  import {
12
13
  getPrice
13
14
  } from "./chunk-TENEIW7I.js";
@@ -23,10 +24,17 @@ async function httpTransport(req) {
23
24
  headers,
24
25
  body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body)
25
26
  });
26
- const json = await res.json().catch(() => void 0);
27
- return { ok: res.ok, status: res.status, json };
27
+ const text = await res.text().catch(() => "");
28
+ let json;
29
+ try {
30
+ json = text ? JSON.parse(text) : void 0;
31
+ } catch {
32
+ json = void 0;
33
+ }
34
+ return { ok: res.ok, status: res.status, json, text };
28
35
  }
29
- function errorBody(json, fallback = "(no body)") {
36
+ function errorBody(json, rawText) {
37
+ const fallback = rawText && rawText.trim() ? rawText.slice(0, 300) : "(no body)";
30
38
  if (json === void 0 || json === null) return fallback;
31
39
  if (typeof json === "string") return json.slice(0, 300);
32
40
  try {
@@ -216,11 +224,36 @@ function toolCallArgs(tc) {
216
224
  }
217
225
 
218
226
  // src/cost/region.ts
227
+ var HOST_REGION = {
228
+ "api.mistral.ai": "eu",
229
+ "api.deepl.com": "eu",
230
+ "api-free.deepl.com": "eu",
231
+ "api.eu.bfl.ai": "eu",
232
+ "api.openai.com": "us",
233
+ "api.anthropic.com": "us",
234
+ "api.deepinfra.com": "us",
235
+ "generativelanguage.googleapis.com": "us",
236
+ "api.elevenlabs.io": "us",
237
+ "fal.run": "us",
238
+ "queue.fal.run": "us",
239
+ "api.deepseek.com": "cn",
240
+ // Aggregators: the host is theirs, the upstream is not ours to know.
241
+ "openrouter.ai": "unknown",
242
+ "router.requesty.ai": "unknown"
243
+ };
244
+ function regionOfHost(url) {
245
+ if (!url) return "unknown";
246
+ try {
247
+ const host = new URL(url).host.toLowerCase();
248
+ return Object.hasOwn(HOST_REGION, host) ? HOST_REGION[host] : "unknown";
249
+ } catch {
250
+ return "unknown";
251
+ }
252
+ }
219
253
  var FIXED_PROVIDER_REGION = {
220
- // api.mistral.ai Paris. The designated EU/GDPR route for personal data.
221
- mistral: "eu",
222
- // api.deepl.com / api-free.deepl.com EU-hosted (Falun, Sweden).
223
- deepl: "eu",
254
+ // NB: mistral and deepl are deliberately ABSENT both take a config.baseUrl, so
255
+ // their region is a property of the URL, not of the name. They derive via
256
+ // regionOfHost. Anything left here genuinely cannot be moved by config.
224
257
  openai: "us",
225
258
  anthropic: "us",
226
259
  // generativelanguage.googleapis.com is Google's GLOBAL endpoint, not a US-pinned one.
@@ -418,7 +451,7 @@ function anthropicAdapter(config = {}) {
418
451
  body
419
452
  }
420
453
  });
421
- if (!res.ok) throw new Error(`anthropic ${res.status}: ${errorBody(res.json)}`);
454
+ if (!res.ok) throw new Error(`anthropic ${res.status}: ${errorBody(res.json, res.text)}`);
422
455
  const data = res.json;
423
456
  const blocks = data.content ?? [];
424
457
  const text = blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
@@ -633,7 +666,7 @@ function makeOpenAICompatibleAdapter(config) {
633
666
  }
634
667
  });
635
668
  if (!res.ok) {
636
- throw new Error(`${config.name} ${res.status}: ${errorBody(res.json)}`);
669
+ throw new Error(`${config.name} ${res.status}: ${errorBody(res.json, res.text)}`);
637
670
  }
638
671
  const data = res.json;
639
672
  const msg = data.choices?.[0]?.message;
@@ -645,6 +678,9 @@ function makeOpenAICompatibleAdapter(config) {
645
678
  const usage = freshUsage({
646
679
  provider: config.name,
647
680
  model: req.spec.model,
681
+ // From the URL we actually called, not from config.name — mistral and deepseek
682
+ // both take a baseUrl, and a custom gateway must not inherit their EU/CN claim.
683
+ region: regionOfHost(config.baseUrl),
648
684
  transport: "http",
649
685
  capability: "chat",
650
686
  // prompt_tokens INCLUDES the cached ones; computeCost adds cacheReadTokens on
@@ -714,6 +750,7 @@ function makeOpenAICompatibleAdapter(config) {
714
750
  const usage = freshUsage({
715
751
  provider: config.name,
716
752
  model: req.spec.model,
753
+ region: regionOfHost(config.baseUrl),
717
754
  transport: "http",
718
755
  capability: "chat",
719
756
  inputTokens: chunk.usage.prompt_tokens ?? 0,
@@ -787,7 +824,7 @@ function openaiAdapter(config = {}) {
787
824
  }
788
825
  });
789
826
  if (!res.ok) {
790
- throw new Error(`openai ${res.status}: ${errorBody(res.json)}`);
827
+ throw new Error(`openai ${res.status}: ${errorBody(res.json, res.text)}`);
791
828
  }
792
829
  const data = res.json;
793
830
  const vectors = (data.data ?? []).map((d) => d.embedding);
@@ -952,7 +989,7 @@ function geminiAdapter(config = {}) {
952
989
  }
953
990
  });
954
991
  if (!res.ok) {
955
- throw new Error(`gemini ${res.status}: ${errorBody(res.json)}`);
992
+ throw new Error(`gemini ${res.status}: ${errorBody(res.json, res.text)}`);
956
993
  }
957
994
  const data = res.json;
958
995
  const parts = data.candidates?.[0]?.content?.parts ?? [];
@@ -1102,7 +1139,7 @@ function geminiAdapter(config = {}) {
1102
1139
  if (opData.error) throw new Error(`gemini animate: ${opData.error.message ?? "operation error"}`);
1103
1140
  if (opData.done) {
1104
1141
  videoUri = opData.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
1105
- if (!videoUri) throw new Error(`gemini animate: done but no video uri: ${JSON.stringify(opData.response).slice(0, 300)}`);
1142
+ if (!videoUri) throw new Error(`gemini animate: done but no video uri: ${errorBody(opData.response)}`);
1106
1143
  break;
1107
1144
  }
1108
1145
  if (Date.now() >= deadline) throw new Error("gemini animate: timed out");
@@ -1546,6 +1583,12 @@ function azureAdapter(config = {}) {
1546
1583
  function region() {
1547
1584
  return config.region ?? process.env.AZURE_SPEECH_REGION ?? DEFAULT_REGION;
1548
1585
  }
1586
+ function sttRegion() {
1587
+ if (config.sttBaseUrl || config.resource || process.env.AZURE_SPEECH_RESOURCE) {
1588
+ return config.region ?? process.env.AZURE_SPEECH_REGION ? classifyRegionName(region()) : "unknown";
1589
+ }
1590
+ return classifyRegionName(region());
1591
+ }
1549
1592
  function sttBaseUrl() {
1550
1593
  if (config.sttBaseUrl) return config.sttBaseUrl.replace(/\/$/, "");
1551
1594
  const resource = config.resource ?? process.env.AZURE_SPEECH_RESOURCE;
@@ -1619,7 +1662,7 @@ function azureAdapter(config = {}) {
1619
1662
  const usage = freshUsage({
1620
1663
  provider: "azure",
1621
1664
  model: req.spec.model,
1622
- region: classifyRegionName(region()),
1665
+ region: sttRegion(),
1623
1666
  transport: "http",
1624
1667
  capability: "transcribe",
1625
1668
  inputTokens: 0,
@@ -1773,7 +1816,7 @@ function vertexAdapter(config = {}) {
1773
1816
  if (opData.done) {
1774
1817
  const video = opData.response?.videos?.[0];
1775
1818
  if (!video) {
1776
- throw new Error(`vertex animate: done but no video in response: ${JSON.stringify(opData.response).slice(0, 300)}`);
1819
+ throw new Error(`vertex animate: done but no video in response: ${errorBody(opData.response)}`);
1777
1820
  }
1778
1821
  if (!video.bytesBase64Encoded) {
1779
1822
  if (video.gcsUri) {
@@ -1781,7 +1824,7 @@ function vertexAdapter(config = {}) {
1781
1824
  `vertex animate: response returned a gcsUri ("${video.gcsUri}") \u2014 GCS download not yet supported (F031.x); this build only handles inline bytes`
1782
1825
  );
1783
1826
  }
1784
- throw new Error(`vertex animate: done but no bytesBase64Encoded in response: ${JSON.stringify(opData.response).slice(0, 300)}`);
1827
+ throw new Error(`vertex animate: done but no bytesBase64Encoded in response: ${errorBody(opData.response)}`);
1785
1828
  }
1786
1829
  videoB64 = video.bytesBase64Encoded;
1787
1830
  videoMime = video.mimeType ?? "video/mp4";
@@ -1884,6 +1927,7 @@ function deeplAdapter(config = {}) {
1884
1927
  if (text === void 0) throw new Error("deepl translate: response contained no translation");
1885
1928
  const usage = freshUsage({
1886
1929
  provider: "deepl",
1930
+ region: regionOfHost(baseUrl(apiKey)),
1887
1931
  model: req.spec.model,
1888
1932
  transport: "http",
1889
1933
  capability: "translate",
@@ -2720,7 +2764,11 @@ var toolSchema = z.object({
2720
2764
  var toolCallSchema = z.object({
2721
2765
  id: z.string(),
2722
2766
  name: z.string(),
2723
- arguments: z.record(z.unknown())
2767
+ arguments: z.record(z.unknown()).optional(),
2768
+ args: z.record(z.unknown()).optional()
2769
+ }).refine((tc) => tc.arguments !== void 0 || tc.args !== void 0, {
2770
+ message: "tool call needs `arguments` (canonical) or `args` (@broberg/chat's spelling)",
2771
+ path: ["arguments"]
2724
2772
  });
2725
2773
  var contentPartSchema = z.union([
2726
2774
  z.object({ type: z.literal("text"), text: z.string() }),
@@ -2918,8 +2966,8 @@ var aiConfigSchema = z.object({
2918
2966
  });
2919
2967
 
2920
2968
  // src/version.ts
2921
- var VERSION = "0.35.1";
2922
- var SDK_TAG = "@broberg/ai-sdk@0.35.1";
2969
+ var VERSION = "0.36.0";
2970
+ var SDK_TAG = "@broberg/ai-sdk@0.36.0";
2923
2971
 
2924
2972
  // src/cost/sinks/upmetrics.ts
2925
2973
  function upmetricsSink(config) {
@@ -2955,6 +3003,10 @@ function upmetricsSink(config) {
2955
3003
  ...usage.labels,
2956
3004
  capability: usage.capability,
2957
3005
  transport: usage.transport,
3006
+ // F042: data residency of the route that answered. Rides in tags like
3007
+ // capability/transport — no ingest-schema change, and without it the one
3008
+ // field built for auditability existed only in memory.
3009
+ region: usage.region,
2958
3010
  sdk: SDK_TAG
2959
3011
  }
2960
3012
  };
@@ -3050,6 +3102,10 @@ function createAI(config = {}) {
3050
3102
  if (budget) await budget.record(usage.costUsd);
3051
3103
  }
3052
3104
  const providerNames = Object.keys(providers);
3105
+ function withOverride(base, override, label) {
3106
+ assertOverrideProvider(base, override, label, providerNames);
3107
+ return { ...base, ...override };
3108
+ }
3053
3109
  function pickProvider(name) {
3054
3110
  const adapter = Object.hasOwn(providers, name) ? providers[name] : void 0;
3055
3111
  if (!adapter) {
@@ -3288,7 +3344,7 @@ function createAI(config = {}) {
3288
3344
  ];
3289
3345
  const base = input.referenceImages?.length ? DEFAULT_BFL_REFERENCE_SPEC : input.finetune ? DEFAULT_BFL_FINETUNE_SPEC : loras.length > 0 ? DEFAULT_LORA_IMAGE_SPEC : DEFAULT_IMAGE_SPEC;
3290
3346
  return runCapability({
3291
- primary: { ...base, ...input.override },
3347
+ primary: withOverride(base, input.override, "image"),
3292
3348
  fallback: input.fallback,
3293
3349
  capability: "image",
3294
3350
  purpose: input.purpose,
@@ -3320,7 +3376,7 @@ function createAI(config = {}) {
3320
3376
  input = animateInputSchema.parse(input);
3321
3377
  const prompt = input.prompt?.trim() ? `${input.prompt.trim()} ${ANIMATE_AUDIO_DIRECTIVE}` : ANIMATE_AUDIO_DIRECTIVE;
3322
3378
  return runCapability({
3323
- primary: { ...DEFAULT_ANIMATE_SPEC, ...input.override },
3379
+ primary: withOverride(DEFAULT_ANIMATE_SPEC, input.override, "animate"),
3324
3380
  fallback: input.fallback,
3325
3381
  capability: "animate",
3326
3382
  purpose: input.purpose,
@@ -3344,7 +3400,7 @@ function createAI(config = {}) {
3344
3400
  async trainStyle(input) {
3345
3401
  input = trainStyleInputSchema.parse(input);
3346
3402
  return runCapability({
3347
- primary: { ...DEFAULT_TRAINSTYLE_SPEC, ...input.override },
3403
+ primary: withOverride(DEFAULT_TRAINSTYLE_SPEC, input.override, "trainStyle"),
3348
3404
  fallback: input.fallback,
3349
3405
  capability: "trainStyle",
3350
3406
  purpose: input.purpose,
@@ -3370,7 +3426,7 @@ function createAI(config = {}) {
3370
3426
  async ocr(input) {
3371
3427
  input = ocrInputSchema.parse(input);
3372
3428
  return runCapability({
3373
- primary: { ...DEFAULT_OCR_SPEC, ...input.override },
3429
+ primary: withOverride(DEFAULT_OCR_SPEC, input.override, "ocr"),
3374
3430
  fallback: input.fallback,
3375
3431
  capability: "ocr",
3376
3432
  purpose: input.purpose,
@@ -3389,7 +3445,7 @@ function createAI(config = {}) {
3389
3445
  input = moderationInputSchema.parse(input);
3390
3446
  const items = Array.isArray(input.input) ? input.input : [input.input];
3391
3447
  return runCapability({
3392
- primary: { ...DEFAULT_MODERATION_SPEC, ...input.override },
3448
+ primary: withOverride(DEFAULT_MODERATION_SPEC, input.override, "moderate"),
3393
3449
  fallback: input.fallback,
3394
3450
  capability: "moderation",
3395
3451
  purpose: input.purpose,
@@ -3489,7 +3545,7 @@ function createAI(config = {}) {
3489
3545
  },
3490
3546
  batch: {
3491
3547
  async submit(input) {
3492
- const spec = { ...DEFAULT_BATCH_SPEC, ...input.override };
3548
+ const spec = withOverride(DEFAULT_BATCH_SPEC, input.override, "batch");
3493
3549
  const adapter = pickProvider(spec.provider);
3494
3550
  if (!adapter.batchSubmit) throw new Error(`createAI: provider "${spec.provider}" does not support batch`);
3495
3551
  return adapter.batchSubmit({ items: input.requests, spec });
@@ -3752,6 +3808,7 @@ CREATE TABLE IF NOT EXISTS ai_usage (
3752
3808
  tier TEXT,
3753
3809
  transport TEXT NOT NULL,
3754
3810
  capability TEXT NOT NULL,
3811
+ region TEXT NOT NULL DEFAULT 'unknown',
3755
3812
  purpose TEXT,
3756
3813
  input_tokens INTEGER NOT NULL,
3757
3814
  output_tokens INTEGER NOT NULL,
@@ -3770,12 +3827,16 @@ function sqliteSink(config) {
3770
3827
  const init = async () => {
3771
3828
  const db = await openDb(config.dbPath);
3772
3829
  db.run(CREATE_TABLE);
3830
+ const cols = db.query(`PRAGMA table_info(ai_usage)`).all();
3831
+ if (!cols.some((c) => c.name === "region")) {
3832
+ db.run(`ALTER TABLE ai_usage ADD COLUMN region TEXT NOT NULL DEFAULT 'unknown'`);
3833
+ }
3773
3834
  const insert = db.prepare(
3774
3835
  `INSERT INTO ai_usage
3775
- (ts, provider, model, tier, transport, capability, purpose,
3836
+ (ts, provider, model, tier, transport, capability, region, purpose,
3776
3837
  input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
3777
3838
  cost_usd, latency_ms, subprocess)
3778
- VALUES ($ts, $provider, $model, $tier, $transport, $capability, $purpose,
3839
+ VALUES ($ts, $provider, $model, $tier, $transport, $capability, $region, $purpose,
3779
3840
  $input, $output, $cacheRead, $cacheCreation, $cost, $latency, $subprocess)`
3780
3841
  );
3781
3842
  return insert;
@@ -3790,6 +3851,7 @@ function sqliteSink(config) {
3790
3851
  $tier: usage.tier ?? null,
3791
3852
  $transport: usage.transport,
3792
3853
  $capability: usage.capability,
3854
+ $region: usage.region,
3793
3855
  $purpose: usage.purpose ?? null,
3794
3856
  $input: usage.inputTokens,
3795
3857
  $output: usage.outputTokens,