@broberg/ai-sdk 0.33.0 → 0.35.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/dist/{chunk-V2PD522L.js → chunk-Y3EMGZFH.js} +19 -3
- package/dist/chunk-Y3EMGZFH.js.map +1 -0
- package/dist/index.d.ts +54 -6
- package/dist/index.js +142 -36
- package/dist/index.js.map +1 -1
- package/dist/pricing.js +2 -2
- package/dist/pricing.js.map +1 -1
- package/dist/registry.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-V2PD522L.js.map +0 -1
|
@@ -6,15 +6,30 @@ var DEFAULT_TIER_MAP = {
|
|
|
6
6
|
cheap: { provider: "mistral", model: "mistral-small-latest", transport: "http" },
|
|
7
7
|
// Vision: small-latest (vision-capable, cheap EU) is the default; override to
|
|
8
8
|
// mistral-large-latest for demanding image/spatial/composition work.
|
|
9
|
-
|
|
9
|
+
// F041 — bumped from mistral-small on Christian's ask, and the choice is MEASURED,
|
|
10
|
+
// not assumed from price. On a fine-discrimination test (an 8x10 grid where one cell
|
|
11
|
+
// differs only in its blue channel, 190->150) across 9 cases:
|
|
12
|
+
// mistral-medium-latest 4/9 <- best
|
|
13
|
+
// mistral-small-latest 1/6 + 0/3 = 1/9
|
|
14
|
+
// mistral-large-latest 0/9 <- WORSE than small, despite costing 5x more
|
|
15
|
+
// Large ties small on easy colour blocks (4/4 each) and collapses on subtle ones,
|
|
16
|
+
// so "bigger is better at vision" does not hold in Mistral's lineup. Nobody is good
|
|
17
|
+
// at this task; medium is simply the only one that sees anything.
|
|
18
|
+
vision: { provider: "mistral", model: "mistral-medium-latest", transport: "http" },
|
|
10
19
|
// Native video understanding — Gemini leads; flash-lite is the cheap default (F019).
|
|
11
20
|
// NOT Anthropic → out of the F030 phase-out (its own EU epic if/when needed).
|
|
12
21
|
video: { provider: "gemini", model: "gemini-2.5-flash-lite", transport: "http" },
|
|
13
22
|
// NOT Anthropic → out of F030 (EU-embedding migration is its own future epic).
|
|
14
23
|
embedding: { provider: "openai", model: "text-embedding-3-small", transport: "http" }
|
|
15
24
|
};
|
|
16
|
-
function resolveTier(tier, override, configMap) {
|
|
25
|
+
function resolveTier(tier, override, configMap, knownProviders) {
|
|
17
26
|
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
|
+
}
|
|
18
33
|
return { ...base, ...override };
|
|
19
34
|
}
|
|
20
35
|
|
|
@@ -34,6 +49,7 @@ var DEFAULTS = [
|
|
|
34
49
|
{ id: "text-embedding-3-small", aliases: [], provider: "openai", available: true, status: "available", source: "default" },
|
|
35
50
|
// ── Mistral (EU / GDPR) ──────────────────────────────────────────────────
|
|
36
51
|
{ id: "mistral-large-latest", aliases: ["mistral-large"], provider: "mistral", available: true, status: "available", source: "default" },
|
|
52
|
+
{ id: "mistral-medium-latest", aliases: ["mistral-medium"], provider: "mistral", available: true, status: "available", source: "default" },
|
|
37
53
|
{ id: "mistral-small-latest", aliases: ["mistral-small"], provider: "mistral", available: true, status: "available", source: "default" }
|
|
38
54
|
];
|
|
39
55
|
var TIER_ALIAS_CONFLICTS = [];
|
|
@@ -178,4 +194,4 @@ export {
|
|
|
178
194
|
listModels,
|
|
179
195
|
resolveModel
|
|
180
196
|
};
|
|
181
|
-
//# sourceMappingURL=chunk-
|
|
197
|
+
//# sourceMappingURL=chunk-Y3EMGZFH.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 */\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 // F043: a provider-only override used to keep the TIER's model, so\n // `override:{provider:\"anthropic\"}` on tier \"cheap\" sent mistral-small-latest to\n // Anthropic's endpoint. Measured by coverletter 4/4 with distinct request_ids, and\n // it is the natural thing to write — it was literally the advice given to a repo\n // working around a missing Mistral key, so the escape hatch produced an error that\n // looked like \"Anthropic is down\".\n //\n // We REFUSE rather than re-resolve. Picking a model for the new provider would mean\n // the SDK making a price choice on the caller's behalf, and the bill would be the\n // only place that choice was visible. An explicit error costs one line to fix and\n // cannot be misread.\n // A provider the client has never heard of is a TYPO, and \"no adapter registered\n // for \\\"nope\\\"\" is the useful thing to say about it. Telling that caller to also set\n // a model would send them down a road that cannot work, so the mismatch guard steps\n // aside and lets the registry answer.\n const providerIsReal = knownProviders === undefined || knownProviders.includes(override?.provider ?? \"\");\n if (providerIsReal && override?.provider && override.model === undefined && override.provider !== base.provider) {\n throw new Error(\n `createAI: override sets provider \"${override.provider}\", but tier \"${tier}\" 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 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
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export { AvailabilitySource, AvailabilityStatus, ModelStatus, ModelUnavailableError, ResolveOptions, ResolveResult, listModels, resolveModel } from './registry.js';
|
|
3
3
|
|
|
4
|
+
/** Coarse data-residency of the endpoint a call went to.
|
|
5
|
+
*
|
|
6
|
+
* - `"eu"` — EU/EEA. **The only positive residency claim.** Nothing else is folded
|
|
7
|
+
* in: an adequacy decision is not EU residency, so the UK, Switzerland
|
|
8
|
+
* and Canada are deliberately NOT `"eu"` here.
|
|
9
|
+
* - `"us"` — United States.
|
|
10
|
+
* - `"cn"` — China.
|
|
11
|
+
* - `"unknown"` — we genuinely cannot say: an aggregator that picks its own upstream,
|
|
12
|
+
* or a region string we do not recognise.
|
|
13
|
+
*
|
|
14
|
+
* **`"unknown"` is NOT a synonym for safe.** `region !== "us"` is not an EU check —
|
|
15
|
+
* it passes every OpenRouter call. Only `region === "eu"` may be treated as EU-resident.
|
|
16
|
+
*/
|
|
17
|
+
type Region = "eu" | "us" | "cn" | "unknown";
|
|
18
|
+
|
|
4
19
|
/** How a call reaches the model. `http` = provider REST API; `subprocess` = local
|
|
5
20
|
* `claude -p` CLI (Max plan, costUsd 0). */
|
|
6
21
|
type Transport = "http" | "subprocess";
|
|
@@ -32,8 +47,9 @@ type ContentPart = {
|
|
|
32
47
|
interface Message {
|
|
33
48
|
role: Role;
|
|
34
49
|
content: string | ContentPart[];
|
|
35
|
-
/** Set on assistant messages that called tools.
|
|
36
|
-
|
|
50
|
+
/** Set on assistant messages that called tools. Accepts either spelling of the
|
|
51
|
+
* arguments field — see ToolCallLike. */
|
|
52
|
+
toolCalls?: ToolCallLike[];
|
|
37
53
|
/** Set on `tool` role messages — which call this result answers. */
|
|
38
54
|
toolCallId?: string;
|
|
39
55
|
}
|
|
@@ -44,12 +60,31 @@ interface Tool {
|
|
|
44
60
|
description: string;
|
|
45
61
|
parameters: Record<string, unknown>;
|
|
46
62
|
}
|
|
47
|
-
/** A model's request to invoke a tool, normalized across providers (F4.5).
|
|
63
|
+
/** A model's request to invoke a tool, normalized across providers (F4.5).
|
|
64
|
+
* This is what we EMIT — `arguments` is always present on a ToolCall we return. */
|
|
48
65
|
interface ToolCall {
|
|
49
66
|
id: string;
|
|
50
67
|
name: string;
|
|
51
68
|
arguments: Record<string, unknown>;
|
|
52
69
|
}
|
|
70
|
+
/** What we ACCEPT when a tool call is handed BACK to us in a message history.
|
|
71
|
+
*
|
|
72
|
+
* F043. `@broberg/chat` spells the same field `args`, consistently across its
|
|
73
|
+
* ModelEvent / ChatFrame / ChatTool.run. Two packages describing the same thing
|
|
74
|
+
* with two names cost cms a production outage: a rename living in a workaround was
|
|
75
|
+
* deleted along with the workaround, and the next call failed with
|
|
76
|
+
* `messages.1.toolCalls.0.arguments — Required`.
|
|
77
|
+
*
|
|
78
|
+
* So we read both and keep emitting `arguments`. Accepting is cheap and cannot
|
|
79
|
+
* break anyone; renaming would have broken a consumer who adopted the same day, and
|
|
80
|
+
* asking every consumer that bridges the two packages to remember a translation is
|
|
81
|
+
* how this happens again. Precedence is `arguments` when both are set. */
|
|
82
|
+
type ToolCallLike = {
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
arguments?: Record<string, unknown>;
|
|
86
|
+
args?: Record<string, unknown>;
|
|
87
|
+
};
|
|
53
88
|
/** Per-call usage. Fields mirror the upmetrics `agent_runs` schema 1:1 so the
|
|
54
89
|
* upmetricsSink (F3.7) forwards without re-mapping. `costUsd` is 0 for
|
|
55
90
|
* subprocess (Max plan); `subprocess:true` lets dashboards split free vs paid. */
|
|
@@ -57,6 +92,14 @@ interface Usage {
|
|
|
57
92
|
provider: string;
|
|
58
93
|
model: string;
|
|
59
94
|
tier?: Tier;
|
|
95
|
+
/** Data-residency of the endpoint that ACTUALLY answered (F042). Derived from the
|
|
96
|
+
* host/region the request used, not from the provider's name — vertex, azure and
|
|
97
|
+
* bfl are EU by default but a consumer can point them elsewhere.
|
|
98
|
+
*
|
|
99
|
+
* **Only `"eu"` is a positive residency claim.** `"unknown"` means we cannot say
|
|
100
|
+
* (an aggregator picked its own upstream, or the region string is one we do not
|
|
101
|
+
* recognise) — it is NOT a synonym for safe, so `region !== "us"` is not an EU check. */
|
|
102
|
+
region: Region;
|
|
60
103
|
transport: Transport;
|
|
61
104
|
inputTokens: number;
|
|
62
105
|
outputTokens: number;
|
|
@@ -2103,8 +2146,8 @@ declare const falStubAdapter: ProviderAdapter;
|
|
|
2103
2146
|
* wires the live adapters. */
|
|
2104
2147
|
declare const stubProviders: Record<string, ProviderAdapter>;
|
|
2105
2148
|
|
|
2106
|
-
declare const VERSION: "0.
|
|
2107
|
-
declare const SDK_TAG: "@broberg/ai-sdk@0.
|
|
2149
|
+
declare const VERSION: "0.35.0";
|
|
2150
|
+
declare const SDK_TAG: "@broberg/ai-sdk@0.35.0";
|
|
2108
2151
|
|
|
2109
2152
|
/** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
|
|
2110
2153
|
* per-call override.
|
|
@@ -2125,7 +2168,7 @@ declare const DEFAULT_TIER_MAP: Record<Tier, TierSpec>;
|
|
|
2125
2168
|
* - `configMap` is the client-level AiConfig.defaults (per-tier full specs).
|
|
2126
2169
|
* - `override` is a per-call Partial<TierSpec> — only the fields it sets win.
|
|
2127
2170
|
*/
|
|
2128
|
-
declare function resolveTier(tier: Tier, override?: Partial<TierSpec>, configMap?: Partial<Record<Tier, TierSpec
|
|
2171
|
+
declare function resolveTier(tier: Tier, override?: Partial<TierSpec>, configMap?: Partial<Record<Tier, TierSpec>>, knownProviders?: readonly string[]): TierSpec;
|
|
2129
2172
|
|
|
2130
2173
|
interface RefreshOptions {
|
|
2131
2174
|
/** Only "anthropic" is wired in v1 (where the incident hit). */
|
|
@@ -2257,6 +2300,11 @@ declare function freshUsage(args: {
|
|
|
2257
2300
|
outputTokens: number;
|
|
2258
2301
|
cacheReadTokens?: number;
|
|
2259
2302
|
cacheCreationTokens?: number;
|
|
2303
|
+
/** Data residency of the endpoint this call ACTUALLY used (F042). Pass it from any
|
|
2304
|
+
* adapter whose region a consumer can change — vertex, azure, bfl. Omitting it
|
|
2305
|
+
* falls back to the fixed provider table, which does not list those three, so a
|
|
2306
|
+
* forgetful adapter degrades to "unknown" rather than to a false "eu". */
|
|
2307
|
+
region?: Region;
|
|
2260
2308
|
subprocess?: boolean;
|
|
2261
2309
|
}): Usage;
|
|
2262
2310
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveModel,
|
|
8
8
|
resolveTier,
|
|
9
9
|
setAvailability
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-Y3EMGZFH.js";
|
|
11
11
|
import {
|
|
12
12
|
getPrice
|
|
13
13
|
} from "./chunk-TENEIW7I.js";
|
|
@@ -26,6 +26,15 @@ async function httpTransport(req) {
|
|
|
26
26
|
const json = await res.json().catch(() => void 0);
|
|
27
27
|
return { ok: res.ok, status: res.status, json };
|
|
28
28
|
}
|
|
29
|
+
function errorBody(json, fallback = "(no body)") {
|
|
30
|
+
if (json === void 0 || json === null) return fallback;
|
|
31
|
+
if (typeof json === "string") return json.slice(0, 300);
|
|
32
|
+
try {
|
|
33
|
+
return (JSON.stringify(json) ?? fallback).slice(0, 300);
|
|
34
|
+
} catch {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
29
38
|
|
|
30
39
|
// src/transport/subprocess.ts
|
|
31
40
|
function parseClaudeCliJson(raw) {
|
|
@@ -202,6 +211,74 @@ function parseArgs(raw) {
|
|
|
202
211
|
}
|
|
203
212
|
return {};
|
|
204
213
|
}
|
|
214
|
+
function toolCallArgs(tc) {
|
|
215
|
+
return tc.arguments ?? tc.args ?? {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/cost/region.ts
|
|
219
|
+
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",
|
|
224
|
+
openai: "us",
|
|
225
|
+
anthropic: "us",
|
|
226
|
+
// generativelanguage.googleapis.com is Google's GLOBAL endpoint, not a US-pinned one.
|
|
227
|
+
// We report "us" rather than "unknown" because it is certainly not EU-resident, and
|
|
228
|
+
// the honest error direction for a residency field is away from an EU claim. Vertex
|
|
229
|
+
// (below, region-pinned) is the route to use when EU residency is required.
|
|
230
|
+
gemini: "us",
|
|
231
|
+
deepinfra: "us",
|
|
232
|
+
fal: "us",
|
|
233
|
+
elevenlabs: "us",
|
|
234
|
+
// api.deepseek.com — People's Republic of China. No EU adequacy decision at all,
|
|
235
|
+
// which is a materially different position from the US, hence its own value.
|
|
236
|
+
deepseek: "cn",
|
|
237
|
+
// Aggregators: they choose the upstream provider per request, so the region is not
|
|
238
|
+
// ours to know. Reporting anything else here would be inventing a fact.
|
|
239
|
+
openrouter: "unknown",
|
|
240
|
+
requesty: "unknown"
|
|
241
|
+
};
|
|
242
|
+
var EU_REGION_NAMES = /* @__PURE__ */ new Set([
|
|
243
|
+
// Azure
|
|
244
|
+
"westeurope",
|
|
245
|
+
"northeurope",
|
|
246
|
+
"swedencentral",
|
|
247
|
+
"francecentral",
|
|
248
|
+
"francesouth",
|
|
249
|
+
"germanywestcentral",
|
|
250
|
+
"germanynorth",
|
|
251
|
+
"norwayeast",
|
|
252
|
+
"norwaywest",
|
|
253
|
+
"polandcentral",
|
|
254
|
+
"italynorth",
|
|
255
|
+
"spaincentral"
|
|
256
|
+
]);
|
|
257
|
+
var US_REGION_NAMES = /* @__PURE__ */ new Set([
|
|
258
|
+
// Azure
|
|
259
|
+
"eastus",
|
|
260
|
+
"eastus2",
|
|
261
|
+
"westus",
|
|
262
|
+
"westus2",
|
|
263
|
+
"westus3",
|
|
264
|
+
"centralus",
|
|
265
|
+
"northcentralus",
|
|
266
|
+
"southcentralus",
|
|
267
|
+
"westcentralus"
|
|
268
|
+
]);
|
|
269
|
+
function classifyRegionName(name) {
|
|
270
|
+
if (!name) return "unknown";
|
|
271
|
+
const n = name.trim().toLowerCase();
|
|
272
|
+
if (!n) return "unknown";
|
|
273
|
+
if (n.startsWith("europe-")) return "eu";
|
|
274
|
+
if (n.startsWith("us-")) return "us";
|
|
275
|
+
if (EU_REGION_NAMES.has(n)) return "eu";
|
|
276
|
+
if (US_REGION_NAMES.has(n)) return "us";
|
|
277
|
+
return "unknown";
|
|
278
|
+
}
|
|
279
|
+
function regionOfProvider(provider) {
|
|
280
|
+
return FIXED_PROVIDER_REGION[provider] ?? "unknown";
|
|
281
|
+
}
|
|
205
282
|
|
|
206
283
|
// src/cost/usage.ts
|
|
207
284
|
function computeCost(provider, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheCreationTokens = 0) {
|
|
@@ -228,6 +305,7 @@ function freshUsage(args) {
|
|
|
228
305
|
const usage = {
|
|
229
306
|
provider: args.provider,
|
|
230
307
|
model: args.model,
|
|
308
|
+
region: args.region ?? regionOfProvider(args.provider),
|
|
231
309
|
transport: args.transport,
|
|
232
310
|
inputTokens: args.inputTokens,
|
|
233
311
|
outputTokens: args.outputTokens,
|
|
@@ -302,7 +380,7 @@ function anthropicAdapter(config = {}) {
|
|
|
302
380
|
blocks.push(...contentBlocks(m.content));
|
|
303
381
|
}
|
|
304
382
|
for (const tc of m.toolCalls) {
|
|
305
|
-
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc
|
|
383
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: toolCallArgs(tc) });
|
|
306
384
|
}
|
|
307
385
|
messages.push({ role: "assistant", content: blocks });
|
|
308
386
|
continue;
|
|
@@ -340,7 +418,7 @@ function anthropicAdapter(config = {}) {
|
|
|
340
418
|
body
|
|
341
419
|
}
|
|
342
420
|
});
|
|
343
|
-
if (!res.ok) throw new Error(`anthropic ${res.status}: ${
|
|
421
|
+
if (!res.ok) throw new Error(`anthropic ${res.status}: ${errorBody(res.json)}`);
|
|
344
422
|
const data = res.json;
|
|
345
423
|
const blocks = data.content ?? [];
|
|
346
424
|
const text = blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
@@ -503,7 +581,7 @@ function toOpenAIMessage(m) {
|
|
|
503
581
|
base.tool_calls = m.toolCalls.map((tc) => ({
|
|
504
582
|
id: tc.id,
|
|
505
583
|
type: "function",
|
|
506
|
-
function: { name: tc.name, arguments: JSON.stringify(tc
|
|
584
|
+
function: { name: tc.name, arguments: JSON.stringify(toolCallArgs(tc)) }
|
|
507
585
|
}));
|
|
508
586
|
}
|
|
509
587
|
return base;
|
|
@@ -519,25 +597,29 @@ function toOpenAIMessage(m) {
|
|
|
519
597
|
});
|
|
520
598
|
return { role: m.role, content };
|
|
521
599
|
}
|
|
600
|
+
function buildChatBody(req, config) {
|
|
601
|
+
const body = {
|
|
602
|
+
model: req.spec.model,
|
|
603
|
+
messages: req.messages.map(toOpenAIMessage)
|
|
604
|
+
};
|
|
605
|
+
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
606
|
+
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
607
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
608
|
+
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
609
|
+
if (config.supportsPromptCacheKey && req.promptCache !== false) {
|
|
610
|
+
const k = req.promptCacheKey ?? autoCacheKey(req.messages);
|
|
611
|
+
if (k !== void 0) body.prompt_cache_key = k;
|
|
612
|
+
}
|
|
613
|
+
if (config.costFromResponseField) body.usage = { include: true };
|
|
614
|
+
return body;
|
|
615
|
+
}
|
|
522
616
|
function makeOpenAICompatibleAdapter(config) {
|
|
523
617
|
async function chat(req) {
|
|
524
618
|
const apiKey = config.apiKey ?? process.env[`${config.name.toUpperCase()}_API_KEY`];
|
|
525
619
|
if (!apiKey) {
|
|
526
620
|
throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
|
|
527
621
|
}
|
|
528
|
-
const body =
|
|
529
|
-
model: req.spec.model,
|
|
530
|
-
messages: req.messages.map(toOpenAIMessage)
|
|
531
|
-
};
|
|
532
|
-
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
533
|
-
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
534
|
-
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
535
|
-
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
536
|
-
if (config.supportsPromptCacheKey && req.promptCache !== false) {
|
|
537
|
-
const k = req.promptCacheKey ?? autoCacheKey(req.messages);
|
|
538
|
-
if (k !== void 0) body.prompt_cache_key = k;
|
|
539
|
-
}
|
|
540
|
-
if (config.costFromResponseField) body.usage = { include: true };
|
|
622
|
+
const body = buildChatBody(req, config);
|
|
541
623
|
const res = await httpTransport({
|
|
542
624
|
spec: req.spec,
|
|
543
625
|
http: {
|
|
@@ -551,7 +633,7 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
551
633
|
}
|
|
552
634
|
});
|
|
553
635
|
if (!res.ok) {
|
|
554
|
-
throw new Error(`${config.name} ${res.status}: ${
|
|
636
|
+
throw new Error(`${config.name} ${res.status}: ${errorBody(res.json)}`);
|
|
555
637
|
}
|
|
556
638
|
const data = res.json;
|
|
557
639
|
const msg = data.choices?.[0]?.message;
|
|
@@ -586,16 +668,10 @@ function makeOpenAICompatibleAdapter(config) {
|
|
|
586
668
|
throw new Error(`${config.name} adapter: API key not set (env ${config.name.toUpperCase()}_API_KEY)`);
|
|
587
669
|
}
|
|
588
670
|
const body = {
|
|
589
|
-
|
|
590
|
-
messages: req.messages.map(toOpenAIMessage),
|
|
671
|
+
...buildChatBody(req, config),
|
|
591
672
|
stream: true,
|
|
592
673
|
stream_options: { include_usage: true }
|
|
593
674
|
};
|
|
594
|
-
if (req.tools) body.tools = toProviderTools(req.tools, "openai");
|
|
595
|
-
if (req.maxTokens !== void 0) body.max_tokens = req.maxTokens;
|
|
596
|
-
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
597
|
-
if (req.responseFormat === "json") body.response_format = { type: "json_object" };
|
|
598
|
-
if (config.costFromResponseField) body.usage = { include: true };
|
|
599
675
|
const stream = streamTransport({
|
|
600
676
|
spec: req.spec,
|
|
601
677
|
fetch: config.fetch,
|
|
@@ -711,7 +787,7 @@ function openaiAdapter(config = {}) {
|
|
|
711
787
|
}
|
|
712
788
|
});
|
|
713
789
|
if (!res.ok) {
|
|
714
|
-
throw new Error(`openai ${res.status}: ${
|
|
790
|
+
throw new Error(`openai ${res.status}: ${errorBody(res.json)}`);
|
|
715
791
|
}
|
|
716
792
|
const data = res.json;
|
|
717
793
|
const vectors = (data.data ?? []).map((d) => d.embedding);
|
|
@@ -876,7 +952,7 @@ function geminiAdapter(config = {}) {
|
|
|
876
952
|
}
|
|
877
953
|
});
|
|
878
954
|
if (!res.ok) {
|
|
879
|
-
throw new Error(`gemini ${res.status}: ${
|
|
955
|
+
throw new Error(`gemini ${res.status}: ${errorBody(res.json)}`);
|
|
880
956
|
}
|
|
881
957
|
const data = res.json;
|
|
882
958
|
const parts = data.candidates?.[0]?.content?.parts ?? [];
|
|
@@ -1480,6 +1556,9 @@ function azureAdapter(config = {}) {
|
|
|
1480
1556
|
const usage = freshUsage({
|
|
1481
1557
|
provider: "azure",
|
|
1482
1558
|
model,
|
|
1559
|
+
// Same region() that picked the host. westeurope by default, but
|
|
1560
|
+
// config.region / AZURE_SPEECH_REGION can point this at eastus.
|
|
1561
|
+
region: classifyRegionName(region()),
|
|
1483
1562
|
transport: "http",
|
|
1484
1563
|
capability: "tts",
|
|
1485
1564
|
inputTokens: 0,
|
|
@@ -1540,6 +1619,7 @@ function azureAdapter(config = {}) {
|
|
|
1540
1619
|
const usage = freshUsage({
|
|
1541
1620
|
provider: "azure",
|
|
1542
1621
|
model: req.spec.model,
|
|
1622
|
+
region: classifyRegionName(region()),
|
|
1543
1623
|
transport: "http",
|
|
1544
1624
|
capability: "transcribe",
|
|
1545
1625
|
inputTokens: 0,
|
|
@@ -1713,6 +1793,10 @@ function vertexAdapter(config = {}) {
|
|
|
1713
1793
|
const usage = freshUsage({
|
|
1714
1794
|
provider: "vertex",
|
|
1715
1795
|
model: req.spec.model,
|
|
1796
|
+
// Read from the SAME region() that built the URL above — this adapter is EU by
|
|
1797
|
+
// default, but config.region / GOOGLE_VERTEX_REGION can move it, and a residency
|
|
1798
|
+
// field that ignores the override is worse than none.
|
|
1799
|
+
region: classifyRegionName(region()),
|
|
1716
1800
|
transport: "http",
|
|
1717
1801
|
capability: "animate",
|
|
1718
1802
|
inputTokens: 0,
|
|
@@ -1754,6 +1838,10 @@ function vertexAdapter(config = {}) {
|
|
|
1754
1838
|
const usage = freshUsage({
|
|
1755
1839
|
provider: "vertex",
|
|
1756
1840
|
model: req.spec.model,
|
|
1841
|
+
// Read from the SAME region() that built the URL above — this adapter is EU by
|
|
1842
|
+
// default, but config.region / GOOGLE_VERTEX_REGION can move it, and a residency
|
|
1843
|
+
// field that ignores the override is worse than none.
|
|
1844
|
+
region: classifyRegionName(region()),
|
|
1757
1845
|
transport: "http",
|
|
1758
1846
|
capability: "vision",
|
|
1759
1847
|
inputTokens,
|
|
@@ -2145,6 +2233,13 @@ async function bflCredits(opts = {}) {
|
|
|
2145
2233
|
const credits = typeof data.credits === "number" ? data.credits : 0;
|
|
2146
2234
|
return { credits, usd: credits * BFL_CREDIT_USD };
|
|
2147
2235
|
}
|
|
2236
|
+
function bflRegion(base) {
|
|
2237
|
+
try {
|
|
2238
|
+
return new URL(base).host.toLowerCase() === "api.eu.bfl.ai" ? "eu" : "unknown";
|
|
2239
|
+
} catch {
|
|
2240
|
+
return "unknown";
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2148
2243
|
function bflAdapter(config = {}) {
|
|
2149
2244
|
const doFetch = config.fetch ?? fetch;
|
|
2150
2245
|
const base = config.baseUrl ?? EU_BASE2;
|
|
@@ -2191,6 +2286,7 @@ function bflAdapter(config = {}) {
|
|
|
2191
2286
|
const usage = freshUsage({
|
|
2192
2287
|
provider: "bfl",
|
|
2193
2288
|
model: req.spec.model,
|
|
2289
|
+
region: bflRegion(base),
|
|
2194
2290
|
transport: "http",
|
|
2195
2291
|
capability: "image",
|
|
2196
2292
|
inputTokens: 0,
|
|
@@ -2822,8 +2918,8 @@ var aiConfigSchema = z.object({
|
|
|
2822
2918
|
});
|
|
2823
2919
|
|
|
2824
2920
|
// src/version.ts
|
|
2825
|
-
var VERSION = "0.
|
|
2826
|
-
var SDK_TAG = "@broberg/ai-sdk@0.
|
|
2921
|
+
var VERSION = "0.35.0";
|
|
2922
|
+
var SDK_TAG = "@broberg/ai-sdk@0.35.0";
|
|
2827
2923
|
|
|
2828
2924
|
// src/cost/sinks/upmetrics.ts
|
|
2829
2925
|
function upmetricsSink(config) {
|
|
@@ -2953,6 +3049,7 @@ function createAI(config = {}) {
|
|
|
2953
3049
|
async function settle(usage) {
|
|
2954
3050
|
if (budget) await budget.record(usage.costUsd);
|
|
2955
3051
|
}
|
|
3052
|
+
const providerNames = Object.keys(providers);
|
|
2956
3053
|
function pickProvider(name) {
|
|
2957
3054
|
const adapter = providers[name];
|
|
2958
3055
|
if (!adapter) {
|
|
@@ -3043,7 +3140,7 @@ function createAI(config = {}) {
|
|
|
3043
3140
|
);
|
|
3044
3141
|
const estOut = input.maxTokens ?? 512;
|
|
3045
3142
|
const routes = [
|
|
3046
|
-
resolveTier(tier, input.override, cfg.defaults),
|
|
3143
|
+
resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3047
3144
|
...(input.fallback ?? []).map(
|
|
3048
3145
|
(f) => typeof f === "string" ? resolveTier(f, void 0, cfg.defaults) : f
|
|
3049
3146
|
)
|
|
@@ -3065,7 +3162,13 @@ function createAI(config = {}) {
|
|
|
3065
3162
|
tools: input.tools,
|
|
3066
3163
|
maxTokens: input.maxTokens,
|
|
3067
3164
|
temperature: input.temperature,
|
|
3068
|
-
responseFormat: input.responseFormat
|
|
3165
|
+
responseFormat: input.responseFormat,
|
|
3166
|
+
// F043: these two were dropped here, so a streamed call could not cache even
|
|
3167
|
+
// after the adapter learned how. A chat UI streams every turn and repeats the
|
|
3168
|
+
// same system prompt each time — the call shape with the most to gain was the
|
|
3169
|
+
// one paying full price. Kept identical to the chat branch above on purpose.
|
|
3170
|
+
promptCacheKey: input.promptCacheKey,
|
|
3171
|
+
promptCache: input.promptCache ?? cfg.promptCache
|
|
3069
3172
|
})) {
|
|
3070
3173
|
if (ev.type === "text" || ev.type === "tool_call") emitted = true;
|
|
3071
3174
|
if (ev.type === "usage") {
|
|
@@ -3096,7 +3199,7 @@ function createAI(config = {}) {
|
|
|
3096
3199
|
0
|
|
3097
3200
|
);
|
|
3098
3201
|
return runCapability({
|
|
3099
|
-
primary: resolveTier(tier, input.override, cfg.defaults),
|
|
3202
|
+
primary: resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3100
3203
|
fallback: input.fallback,
|
|
3101
3204
|
capability: "chat",
|
|
3102
3205
|
tier,
|
|
@@ -3117,7 +3220,7 @@ function createAI(config = {}) {
|
|
|
3117
3220
|
const tier = input.tier ?? VISION_DEFAULT_TIER;
|
|
3118
3221
|
const messages = buildVisionMessages(input);
|
|
3119
3222
|
return runCapability({
|
|
3120
|
-
primary: resolveTier(tier, input.override, cfg.defaults),
|
|
3223
|
+
primary: resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3121
3224
|
fallback: input.fallback,
|
|
3122
3225
|
capability: "vision",
|
|
3123
3226
|
tier,
|
|
@@ -3138,7 +3241,7 @@ function createAI(config = {}) {
|
|
|
3138
3241
|
const tier = input.tier ?? VIDEO_DEFAULT_TIER;
|
|
3139
3242
|
const messages = buildVideoMessages(input);
|
|
3140
3243
|
return runCapability({
|
|
3141
|
-
primary: resolveTier(tier, input.override, cfg.defaults),
|
|
3244
|
+
primary: resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3142
3245
|
fallback: input.fallback,
|
|
3143
3246
|
capability: "video",
|
|
3144
3247
|
tier,
|
|
@@ -3160,7 +3263,7 @@ function createAI(config = {}) {
|
|
|
3160
3263
|
const messages = buildTranslateMessages(input);
|
|
3161
3264
|
const estIn = estTokens(input.text) + 40;
|
|
3162
3265
|
const res = await runCapability({
|
|
3163
|
-
primary: resolveTier(tier, input.override, cfg.defaults),
|
|
3266
|
+
primary: resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3164
3267
|
fallback: input.fallback,
|
|
3165
3268
|
capability: "translate",
|
|
3166
3269
|
tier,
|
|
@@ -3350,7 +3453,7 @@ function createAI(config = {}) {
|
|
|
3350
3453
|
const tier = input.tier ?? EMBEDDING_DEFAULT_TIER;
|
|
3351
3454
|
const text = Array.isArray(input.text) ? input.text : [input.text];
|
|
3352
3455
|
return runCapability({
|
|
3353
|
-
primary: resolveTier(tier, input.override, cfg.defaults),
|
|
3456
|
+
primary: resolveTier(tier, input.override, cfg.defaults, providerNames),
|
|
3354
3457
|
fallback: input.fallback,
|
|
3355
3458
|
capability: "embedding",
|
|
3356
3459
|
tier,
|
|
@@ -3416,6 +3519,9 @@ function stubUsage(provider, model, transport, capability) {
|
|
|
3416
3519
|
return {
|
|
3417
3520
|
provider,
|
|
3418
3521
|
model,
|
|
3522
|
+
// Stubs answer no network, so there is no endpoint whose residency we could
|
|
3523
|
+
// report. "unknown" is the truthful reading — not a placeholder "eu".
|
|
3524
|
+
region: "unknown",
|
|
3419
3525
|
transport,
|
|
3420
3526
|
inputTokens: 0,
|
|
3421
3527
|
outputTokens: 0,
|