@broberg/ai-sdk 0.53.0 → 0.54.1

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.
@@ -225,10 +225,11 @@ export {
225
225
  assertOverrideProvider,
226
226
  resolveTier,
227
227
  resetRegistry,
228
+ allEntries,
228
229
  providerIds,
229
230
  setAvailability,
230
231
  ModelUnavailableError,
231
232
  listModels,
232
233
  resolveModel
233
234
  };
234
- //# sourceMappingURL=chunk-XDBN6QJ4.js.map
235
+ //# sourceMappingURL=chunk-PDGFAD66.js.map
@@ -460,8 +460,10 @@ function resetPricingWarningForTests() {
460
460
  }
461
461
 
462
462
  export {
463
+ PRICING,
463
464
  getPrice,
464
465
  MEDIA_PRICING_CHECKED_AT,
466
+ MEDIA_PRICING,
465
467
  DEFAULT_CLIP_SEC,
466
468
  getMediaPrice,
467
469
  listMediaPrices,
@@ -475,4 +477,4 @@ export {
475
477
  warnIfPricingStale,
476
478
  resetPricingWarningForTests
477
479
  };
478
- //# sourceMappingURL=chunk-ZSLRCOYM.js.map
480
+ //# sourceMappingURL=chunk-TSNJ5N4F.js.map
package/dist/index.d.ts CHANGED
@@ -2850,7 +2850,7 @@ interface OpenAICompatibleConfig {
2850
2850
  apiKey?: string;
2851
2851
  /** Extra headers (e.g. OpenRouter's HTTP-Referer / X-Title). */
2852
2852
  extraHeaders?: Record<string, string>;
2853
- /** Injectable fetch for the streaming path (tests). */
2853
+ /** Injectable fetch — used by BOTH the streaming and the non-streaming path (F073). */
2854
2854
  fetch?: typeof fetch;
2855
2855
  /** F049 — the provider understands `prefix: true` on a trailing assistant message.
2856
2856
  * Mistral only. A prefix sent anywhere else is REFUSED rather than dropped: a
@@ -2884,6 +2884,43 @@ declare function byokAdapter<C extends KeyedConfig>(factory: (config: C) => Prov
2884
2884
  /** True when the adapter was built by byokAdapter with a real key. */
2885
2885
  declare function isByokKeyed(adapter: ProviderAdapter): boolean;
2886
2886
 
2887
+ interface PriceDriftRow {
2888
+ /** `${provider}:${model}` — the key in ai-sdk's price table. */
2889
+ key: string;
2890
+ provider: string;
2891
+ model: string;
2892
+ ours: {
2893
+ inputPer1M: number;
2894
+ outputPer1M: number;
2895
+ };
2896
+ upstream: {
2897
+ inputPer1M?: number;
2898
+ outputPer1M?: number;
2899
+ };
2900
+ /** Relative change, upstream vs ours, in percent (+20 = 20 % dearer). Undefined when
2901
+ * upstream omits that side. */
2902
+ inputChangePct?: number;
2903
+ outputChangePct?: number;
2904
+ }
2905
+ type PriceDriftResult = {
2906
+ ok: true;
2907
+ measuredAt: string;
2908
+ source: "openrouter";
2909
+ checked: number;
2910
+ drift: PriceDriftRow[];
2911
+ coverage: string;
2912
+ } | {
2913
+ ok: false;
2914
+ measuredAt: string;
2915
+ error: string;
2916
+ };
2917
+ /** Compare ai-sdk's price table with OpenRouter's live prices. Never throws: an outage
2918
+ * returns `{ ok: false, error }`, so it cannot look like "no price changes". */
2919
+ declare function checkPriceDrift(opts?: {
2920
+ fetch?: typeof fetch;
2921
+ threshold?: number;
2922
+ }): Promise<PriceDriftResult>;
2923
+
2887
2924
  declare const defaultProviders: Record<string, ProviderAdapter>;
2888
2925
 
2889
2926
  /** Anthropic adapter stub (HTTP path). */
@@ -2902,8 +2939,8 @@ declare const falStubAdapter: ProviderAdapter;
2902
2939
  * wires the live adapters. */
2903
2940
  declare const stubProviders: Record<string, ProviderAdapter>;
2904
2941
 
2905
- declare const VERSION: "0.53.0";
2906
- declare const SDK_TAG: "@broberg/ai-sdk@0.53.0";
2942
+ declare const VERSION: "0.54.1";
2943
+ declare const SDK_TAG: "@broberg/ai-sdk@0.54.1";
2907
2944
 
2908
2945
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2909
2946
  * per-call override.
@@ -3354,6 +3391,10 @@ interface TransportRequest {
3354
3391
  spec: TierSpec;
3355
3392
  /** HTTP details — required when spec.transport === "http". The adapter has
3356
3393
  * already built the provider-specific URL/headers/body. */
3394
+ /** F073 — the fetch to use. Absent → the global fetch. An adapter given `fetch` in its
3395
+ * config MUST pass it here, or a test's fake fetch is silently bypassed and the call
3396
+ * reaches the real provider with whatever key is in the environment. */
3397
+ fetch?: typeof fetch;
3357
3398
  http?: {
3358
3399
  url: string;
3359
3400
  method?: string;
@@ -3414,4 +3455,4 @@ interface StreamTransportRequest extends TransportRequest {
3414
3455
  */
3415
3456
  declare function streamTransport(req: StreamTransportRequest): AsyncIterable<string>;
3416
3457
 
3417
- export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AlignedWordTimings, type AzureVoiceInfo, type AzureWordBoundary, type BatchJob, type BatchRequestItem, type BatchResultItem, type BflAdapterConfig, type BflCredits, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, CONFIG_DERIVED_HOSTS, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type CheckVoiceOptions, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostQuery, type CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_BASE_URLS, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type FixedHostProvider, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type JudgeAnswer, type JudgeChoiceAnswer, type JudgeInput, type JudgeNoulAnswer, type JudgeQuestion, type JudgeResult, type JudgeScoreAnswer, LOCALLY_PINNED_HOSTS, type LoraWeight, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PricingEntry, type ProviderAdapter, type RefreshOptions, type RefreshResult, type Region, type RerankInput, type RerankResult, type Role, type RouteForecast, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TrainStyleInput, type TrainStyleRequest, type TrainStyleResult, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSink, type UpmetricsSinkConfig, type UpmetricsSinkStats, type Usage, VERSION, type VideoInput, type VisionInput, type VoiceInfo, type VoiceProvider, type VoiceResolveResult, type VoiceStatus, VoiceUnavailableError, type WordTiming, aiConfigSchema, alignWordTimings, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, byokAdapter, chatInputSchema, checkVoice, classifyRegionName, computeCost, createAI, deepinfraAdapter, deeplAdapter, deepseekAdapter, defaultBaseUrl, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, isByokKeyed, listAzureDanishVoices, listVoices, makeContracts, makeOpenAICompatibleAdapter, matchLabel, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, regionOfHost, regionOfProvider, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, typesafeAdapter, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema, wouldProviderRouteTo, wouldRouteTo };
3458
+ export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AlignedWordTimings, type AzureVoiceInfo, type AzureWordBoundary, type BatchJob, type BatchRequestItem, type BatchResultItem, type BflAdapterConfig, type BflCredits, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, CONFIG_DERIVED_HOSTS, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type CheckVoiceOptions, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostQuery, type CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_BASE_URLS, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type FixedHostProvider, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type JudgeAnswer, type JudgeChoiceAnswer, type JudgeInput, type JudgeNoulAnswer, type JudgeQuestion, type JudgeResult, type JudgeScoreAnswer, LOCALLY_PINNED_HOSTS, type LoraWeight, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PriceDriftResult, type PriceDriftRow, type PricingEntry, type ProviderAdapter, type RefreshOptions, type RefreshResult, type Region, type RerankInput, type RerankResult, type Role, type RouteForecast, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TrainStyleInput, type TrainStyleRequest, type TrainStyleResult, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSink, type UpmetricsSinkConfig, type UpmetricsSinkStats, type Usage, VERSION, type VideoInput, type VisionInput, type VoiceInfo, type VoiceProvider, type VoiceResolveResult, type VoiceStatus, VoiceUnavailableError, type WordTiming, aiConfigSchema, alignWordTimings, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, byokAdapter, chatInputSchema, checkPriceDrift, checkVoice, classifyRegionName, computeCost, createAI, deepinfraAdapter, deeplAdapter, deepseekAdapter, defaultBaseUrl, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, isByokKeyed, listAzureDanishVoices, listVoices, makeContracts, makeOpenAICompatibleAdapter, matchLabel, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, regionOfHost, regionOfProvider, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, typesafeAdapter, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema, wouldProviderRouteTo, wouldRouteTo };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DEFAULT_TIER_MAP,
3
3
  ModelUnavailableError,
4
+ allEntries,
4
5
  assertOverrideProvider,
5
6
  listModels,
6
7
  providerIds,
@@ -8,17 +9,19 @@ import {
8
9
  resolveModel,
9
10
  resolveTier,
10
11
  setAvailability
11
- } from "./chunk-XDBN6QJ4.js";
12
+ } from "./chunk-PDGFAD66.js";
12
13
  import {
13
14
  DEFAULT_CLIP_SEC,
15
+ MEDIA_PRICING,
14
16
  MEDIA_PRICING_CHECKED_AT,
17
+ PRICING,
15
18
  PRICING_STALE_AFTER_DAYS,
16
19
  getMediaPrice,
17
20
  getPrice,
18
21
  listMediaPrices,
19
22
  pricingFreshness,
20
23
  pricingGeneratedAt
21
- } from "./chunk-ZSLRCOYM.js";
24
+ } from "./chunk-TSNJ5N4F.js";
22
25
 
23
26
  // src/cost/default-hosts.ts
24
27
  var DEFAULT_BASE_URLS = {
@@ -53,7 +56,7 @@ async function httpTransport(req) {
53
56
  throw new Error("httpTransport: req.http is required for http transport");
54
57
  }
55
58
  const { url, method = "POST", headers, body } = req.http;
56
- const res = await fetch(url, {
59
+ const res = await (req.fetch ?? fetch)(url, {
57
60
  method,
58
61
  headers,
59
62
  body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body)
@@ -363,7 +366,7 @@ function regionOfProvider(provider) {
363
366
  function computeCost(provider, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheCreationTokens = 0) {
364
367
  const price = getPrice(provider, model);
365
368
  if (!price) return 0;
366
- const perToken = (per1M) => per1M / 1e6;
369
+ const perToken = (per1M2) => per1M2 / 1e6;
367
370
  const inRate = perToken(price.inputPer1M);
368
371
  const outRate = perToken(price.outputPer1M);
369
372
  const cacheReadRate = price.cacheReadPer1M !== void 0 ? perToken(price.cacheReadPer1M) : inRate;
@@ -492,6 +495,7 @@ function anthropicAdapter(config = {}) {
492
495
  const body = buildBody(req);
493
496
  const res = await httpTransport({
494
497
  spec: req.spec,
498
+ fetch: config.fetch,
495
499
  http: {
496
500
  url: `${baseUrl}/v1/messages`,
497
501
  headers: {
@@ -771,6 +775,7 @@ function makeOpenAICompatibleAdapter(config) {
771
775
  const body = buildChatBody(req, config);
772
776
  const res = await httpTransport({
773
777
  spec: req.spec,
778
+ fetch: config.fetch,
774
779
  http: {
775
780
  url: `${config.baseUrl}/chat/completions`,
776
781
  headers: {
@@ -928,12 +933,13 @@ function mapFinishReason(reason) {
928
933
  // src/providers/openai.ts
929
934
  function openaiAdapter(config = {}) {
930
935
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URLS.openai;
931
- const base = makeOpenAICompatibleAdapter({ name: "openai", baseUrl, apiKey: config.apiKey });
936
+ const base = makeOpenAICompatibleAdapter({ name: "openai", baseUrl, apiKey: config.apiKey, fetch: config.fetch });
932
937
  async function embedding(req) {
933
938
  const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
934
939
  if (!apiKey) throw new Error("openai adapter: API key not set (env OPENAI_API_KEY)");
935
940
  const res = await httpTransport({
936
941
  spec: req.spec,
942
+ fetch: config.fetch,
937
943
  http: {
938
944
  url: `${baseUrl}/embeddings`,
939
945
  headers: { "content-type": "application/json", Authorization: `Bearer ${apiKey}` },
@@ -1078,6 +1084,7 @@ function geminiAdapter(config = {}) {
1078
1084
  const body = buildBody(req);
1079
1085
  const res = await httpTransport({
1080
1086
  spec: req.spec,
1087
+ fetch: config.fetch,
1081
1088
  http: {
1082
1089
  url: `${baseUrl}/models/${req.spec.model}:generateContent?key=${encodeURIComponent(apiKey)}`,
1083
1090
  headers: { "content-type": "application/json" },
@@ -1373,7 +1380,7 @@ function deepseekAdapter(config = {}) {
1373
1380
  // src/providers/mistral.ts
1374
1381
  function mistralAdapter(config = {}) {
1375
1382
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URLS.mistral;
1376
- const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey, supportsPromptCacheKey: true, supportsPrefix: true });
1383
+ const base = makeOpenAICompatibleAdapter({ name: "mistral", baseUrl, apiKey: config.apiKey, fetch: config.fetch, supportsPromptCacheKey: true, supportsPrefix: true });
1377
1384
  function key() {
1378
1385
  const k = config.apiKey ?? process.env.MISTRAL_API_KEY;
1379
1386
  if (!k) throw new Error("mistral adapter: API key not set (env MISTRAL_API_KEY)");
@@ -3523,8 +3530,8 @@ var aiConfigSchema = z.object({
3523
3530
  import { randomUUID } from "crypto";
3524
3531
 
3525
3532
  // src/version.ts
3526
- var VERSION = "0.53.0";
3527
- var SDK_TAG = "@broberg/ai-sdk@0.53.0";
3533
+ var VERSION = "0.54.1";
3534
+ var SDK_TAG = "@broberg/ai-sdk@0.54.1";
3528
3535
 
3529
3536
  // src/cost/sinks/upmetrics.ts
3530
3537
  function isRetryableStatus(status) {
@@ -3871,6 +3878,21 @@ var DEFAULT_JUDGE_SPEC = { provider: "typesafe", model: "jev-latest", transport:
3871
3878
  var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
3872
3879
  var DEFAULT_TTS_SPEC = { provider: "elevenlabs", model: "eleven_multilingual_v2", transport: "http" };
3873
3880
  var DEFAULT_BATCH_SPEC = { provider: "mistral", model: "mistral-small-latest", transport: "http" };
3881
+ var ROUTED_SPECS = [
3882
+ DEFAULT_IMAGE_SPEC,
3883
+ DEFAULT_LORA_IMAGE_SPEC,
3884
+ DEFAULT_TRAINSTYLE_SPEC,
3885
+ DEFAULT_ANIMATE_SPEC,
3886
+ DEFAULT_BFL_FINETUNE_SPEC,
3887
+ DEFAULT_BFL_REFERENCE_SPEC,
3888
+ DEFAULT_OCR_SPEC,
3889
+ DEFAULT_MODERATION_SPEC,
3890
+ DEFAULT_JUDGE_SPEC,
3891
+ DEFAULT_PODCAST_SPEC,
3892
+ DEFAULT_TTS_SPEC,
3893
+ DEFAULT_BATCH_SPEC,
3894
+ DEFAULT_TRANSCRIBE_SPEC
3895
+ ];
3874
3896
  function defaultCostSink() {
3875
3897
  const apiKey = process.env.UPMETRICS_API_KEY;
3876
3898
  if (!apiKey) return void 0;
@@ -4432,6 +4454,175 @@ function createAI(config = {}) {
4432
4454
  return client;
4433
4455
  }
4434
4456
 
4457
+ // src/catalogue/fetchers.ts
4458
+ async function getJson(f, url, headers) {
4459
+ const res = await f(url, { headers: { accept: "application/json", ...headers } });
4460
+ if (!res.ok) {
4461
+ const body = await res.text().catch(() => "");
4462
+ throw new Error(`${res.status} ${body.slice(0, 200)}`);
4463
+ }
4464
+ return res.json();
4465
+ }
4466
+ function per1M(v) {
4467
+ if (v == null) return void 0;
4468
+ const n = Number(v) * 1e6;
4469
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
4470
+ }
4471
+ async function fetchOpenRouterCatalogue(opts = {}) {
4472
+ const f = opts.fetch ?? fetch;
4473
+ const base = opts.baseUrl ?? "https://openrouter.ai/api/v1";
4474
+ const json = await getJson(f, `${base}/models`, {});
4475
+ return (json.data ?? []).map((m) => {
4476
+ const inputPer1M = per1M(m.pricing?.prompt);
4477
+ const outputPer1M = per1M(m.pricing?.completion);
4478
+ return {
4479
+ provider: "openrouter",
4480
+ model: m.id,
4481
+ ...inputPer1M !== void 0 ? { inputPer1M } : {},
4482
+ ...outputPer1M !== void 0 ? { outputPer1M } : {},
4483
+ ...m.context_length ? { contextLength: m.context_length } : {}
4484
+ };
4485
+ });
4486
+ }
4487
+
4488
+ // src/catalogue/types.ts
4489
+ function catalogueKey(m) {
4490
+ return `${m.provider}:${m.model}`;
4491
+ }
4492
+
4493
+ // src/catalogue/diff.ts
4494
+ function brandToken(model) {
4495
+ const tail = model.split("/").pop() ?? model;
4496
+ return tail.split("-")[0] ?? tail;
4497
+ }
4498
+ var DIRECT_PROVIDERS = /* @__PURE__ */ new Set(["openai", "anthropic", "gemini"]);
4499
+ var FULL_LIST_PROVIDERS = /* @__PURE__ */ new Set(["mistral", "deepseek", "elevenlabs"]);
4500
+ function undatedBase(id) {
4501
+ return id.replace(/-(\d{4}-\d{2}-\d{2}|\d{8}|\d{4})$/, "");
4502
+ }
4503
+ function knownKeys() {
4504
+ const known = /* @__PURE__ */ new Set([...Object.keys(PRICING), ...Object.keys(MEDIA_PRICING)]);
4505
+ for (const e of allEntries()) known.add(`${e.provider}:${e.id}`);
4506
+ for (const spec of [...Object.values(DEFAULT_TIER_MAP), ...ROUTED_SPECS]) known.add(`${spec.provider}:${spec.model}`);
4507
+ return known;
4508
+ }
4509
+ function diffCatalogue(fetched, opts = {}) {
4510
+ const threshold = opts.priceThreshold ?? 0.01;
4511
+ const fetchedProviders = new Set(opts.fetchedProviders ?? fetched.map((m) => m.provider));
4512
+ const fetchedByKey = /* @__PURE__ */ new Map();
4513
+ for (const m of fetched) {
4514
+ fetchedByKey.set(catalogueKey(m), m);
4515
+ for (const a of m.aliases ?? []) {
4516
+ const k = catalogueKey({ provider: m.provider, model: a });
4517
+ if (!fetchedByKey.has(k)) fetchedByKey.set(k, m);
4518
+ }
4519
+ }
4520
+ const known = knownKeys();
4521
+ const trackedBrands = /* @__PURE__ */ new Map();
4522
+ for (const key of Object.keys(PRICING)) {
4523
+ const [provider, model] = splitKey(key);
4524
+ if (!trackedBrands.has(provider)) trackedBrands.set(provider, /* @__PURE__ */ new Set());
4525
+ trackedBrands.get(provider).add(brandToken(model));
4526
+ }
4527
+ const added = [];
4528
+ const reportedGroups = /* @__PURE__ */ new Set();
4529
+ const listedByProvider = /* @__PURE__ */ new Map();
4530
+ for (const m of fetched) {
4531
+ if (!listedByProvider.has(m.provider)) listedByProvider.set(m.provider, /* @__PURE__ */ new Set());
4532
+ listedByProvider.get(m.provider).add(m.model);
4533
+ }
4534
+ const isSnapshotOfListed = (m) => {
4535
+ const base = undatedBase(m.model);
4536
+ if (base === m.model) return false;
4537
+ return listedByProvider.get(m.provider)?.has(base) === true || known.has(catalogueKey({ provider: m.provider, model: base }));
4538
+ };
4539
+ for (const m of fetched) {
4540
+ if (FULL_LIST_PROVIDERS.has(m.provider)) {
4541
+ if (m.deprecated) continue;
4542
+ const group = [m.model, ...m.aliases ?? []].map((id) => catalogueKey({ provider: m.provider, model: id }));
4543
+ if (group.some((k) => known.has(k) || reportedGroups.has(k))) continue;
4544
+ for (const k of group) reportedGroups.add(k);
4545
+ added.push(m);
4546
+ continue;
4547
+ }
4548
+ if (!DIRECT_PROVIDERS.has(m.provider)) continue;
4549
+ if (isSnapshotOfListed(m)) continue;
4550
+ const brands = trackedBrands.get(m.provider);
4551
+ if (!brands?.has(brandToken(m.model))) continue;
4552
+ if (PRICING[catalogueKey(m)]) continue;
4553
+ added.push(m);
4554
+ }
4555
+ const missingPrice = [];
4556
+ for (const spec of Object.values(DEFAULT_TIER_MAP)) {
4557
+ if (!getPrice(spec.provider, spec.model)) missingPrice.push(`${spec.provider}:${spec.model}`);
4558
+ }
4559
+ const priceChanged = [];
4560
+ const removedUpstream = [];
4561
+ for (const [key, entry] of Object.entries(PRICING)) {
4562
+ const [provider] = splitKey(key);
4563
+ if (!fetchedProviders.has(provider)) continue;
4564
+ const up = fetchedByKey.get(key);
4565
+ if (!up) {
4566
+ removedUpstream.push(key);
4567
+ continue;
4568
+ }
4569
+ if (up.inputPer1M === void 0 && up.outputPer1M === void 0) continue;
4570
+ const inDrift = relDrift(entry.inputPer1M, up.inputPer1M);
4571
+ const outDrift = relDrift(entry.outputPer1M, up.outputPer1M);
4572
+ if (inDrift > threshold || outDrift > threshold) {
4573
+ priceChanged.push({
4574
+ key,
4575
+ ourInputPer1M: entry.inputPer1M,
4576
+ ourOutputPer1M: entry.outputPer1M,
4577
+ ...up.inputPer1M !== void 0 ? { upstreamInputPer1M: up.inputPer1M } : {},
4578
+ ...up.outputPer1M !== void 0 ? { upstreamOutputPer1M: up.outputPer1M } : {}
4579
+ });
4580
+ }
4581
+ }
4582
+ return { added, missingPrice, priceChanged, removedUpstream };
4583
+ }
4584
+ function relDrift(ours, upstream) {
4585
+ if (upstream === void 0) return 0;
4586
+ if (upstream === 0) return ours === 0 ? 0 : Infinity;
4587
+ return Math.abs(ours - upstream) / upstream;
4588
+ }
4589
+ function splitKey(key) {
4590
+ const i = key.indexOf(":");
4591
+ return [key.slice(0, i), key.slice(i + 1)];
4592
+ }
4593
+
4594
+ // src/catalogue/price-drift.ts
4595
+ var COVERAGE = "OpenRouter catalogue only. Not covered: direct-provider prices (no price API) and speech/image/video/OCR (hand-checked, F050).";
4596
+ function pct(ours, up) {
4597
+ if (up === void 0) return void 0;
4598
+ if (ours === 0) return up === 0 ? 0 : Infinity;
4599
+ return Math.round((up - ours) / ours * 1e3) / 10;
4600
+ }
4601
+ async function checkPriceDrift(opts = {}) {
4602
+ const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
4603
+ try {
4604
+ const models = await fetchOpenRouterCatalogue({ fetch: opts.fetch });
4605
+ if (models.length === 0) return { ok: false, measuredAt, error: "OpenRouter returned an empty catalogue" };
4606
+ const diff = diffCatalogue(models, { fetchedProviders: ["openrouter"], priceThreshold: opts.threshold ?? 0.01 });
4607
+ const drift = diff.priceChanged.map((p) => {
4608
+ const i = p.key.indexOf(":");
4609
+ return {
4610
+ key: p.key,
4611
+ provider: p.key.slice(0, i),
4612
+ model: p.key.slice(i + 1),
4613
+ ours: { inputPer1M: p.ourInputPer1M, outputPer1M: p.ourOutputPer1M },
4614
+ upstream: { inputPer1M: p.upstreamInputPer1M, outputPer1M: p.upstreamOutputPer1M },
4615
+ inputChangePct: pct(p.ourInputPer1M, p.upstreamInputPer1M),
4616
+ outputChangePct: pct(p.ourOutputPer1M, p.upstreamOutputPer1M)
4617
+ };
4618
+ });
4619
+ const checked = models.filter((m) => m.inputPer1M !== void 0 || m.outputPer1M !== void 0).length;
4620
+ return { ok: true, measuredAt, source: "openrouter", checked, drift, coverage: COVERAGE };
4621
+ } catch (e) {
4622
+ return { ok: false, measuredAt, error: e instanceof Error ? e.message : String(e) };
4623
+ }
4624
+ }
4625
+
4435
4626
  // src/providers/stub.ts
4436
4627
  function stubUsage(provider, model, transport, capability) {
4437
4628
  return {
@@ -4874,6 +5065,7 @@ export {
4874
5065
  bflCredits,
4875
5066
  byokAdapter,
4876
5067
  chatInputSchema,
5068
+ checkPriceDrift,
4877
5069
  checkVoice,
4878
5070
  classifyRegionName,
4879
5071
  computeCost,