@broberg/ai-sdk 0.58.1 → 0.59.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/index.d.ts CHANGED
@@ -52,13 +52,28 @@ interface SearchCache {
52
52
  get(key: string): SearchResult | undefined;
53
53
  set(key: string, value: SearchResult, ttlMs: number): void;
54
54
  }
55
+ /** Running spend per key (tenant + day). Async allowed, so it can live in a database. */
56
+ interface SearchCapStore {
57
+ getSpent(key: string): number | Promise<number>;
58
+ addSpent(key: string, usd: number): void | Promise<void>;
59
+ }
55
60
  interface SearchOptions {
56
61
  /** false = no cache. Default: one in-process memory cache shared by all calls. */
57
62
  cache?: SearchCache | false;
58
63
  /** Where each call (and each failed attempt) is recorded. ai.search() passes the client's sink. */
59
64
  costSink?: CostSink;
60
- /** Explicit keys (BYOK); missing fields fall back to env. */
65
+ /** Explicit keys (BYOK); missing fields fall back to env — unless `byok` is set. */
61
66
  credentials?: SearchCredentials;
67
+ /** F078.3 — customer-key mode: keys come ONLY from `credentials`, env is never read,
68
+ * so a tenant whose key is missing cannot run on the fleet's account. */
69
+ byok?: boolean;
70
+ /** F078.3 — hard ceiling in USD per tenant (`labels.tenantId`) per calendar day in
71
+ * Europe/Copenhagen. A call that would cross it throws before any request. */
72
+ dailyCapUsd?: number;
73
+ /** Where the day's spend is kept. Default: in-process memory shared by all calls. */
74
+ capStore?: SearchCapStore;
75
+ /** Clock for the day boundary (tests). */
76
+ now?: () => Date;
62
77
  /** Injectable fetch (tests, proxies) — same contract as the model adapters (F073). */
63
78
  fetch?: typeof fetch;
64
79
  }
@@ -68,6 +83,15 @@ declare class SearchKeyMissingError extends Error {
68
83
  readonly envVar: string;
69
84
  constructor(provider: SearchProviderId, envVar: string);
70
85
  }
86
+ /** Thrown before any request when the tenant's daily ceiling would be crossed. */
87
+ declare class SearchBudgetExceededError extends Error {
88
+ readonly tenant: string;
89
+ readonly day: string;
90
+ readonly capUsd: number;
91
+ readonly spentUsd: number;
92
+ readonly requestedUsd: number;
93
+ constructor(tenant: string, day: string, capUsd: number, spentUsd: number, requestedUsd: number);
94
+ }
71
95
 
72
96
  interface MockupInput {
73
97
  description: string;
@@ -2301,8 +2325,8 @@ declare const falStubAdapter: ProviderAdapter;
2301
2325
  * wires the live adapters. */
2302
2326
  declare const stubProviders: Record<string, ProviderAdapter>;
2303
2327
 
2304
- declare const VERSION: "0.58.1";
2305
- declare const SDK_TAG: "@broberg/ai-sdk@0.58.1";
2328
+ declare const VERSION: "0.59.0";
2329
+ declare const SDK_TAG: "@broberg/ai-sdk@0.59.0";
2306
2330
 
2307
2331
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
2308
2332
  * per-call override.
@@ -2849,4 +2873,4 @@ declare function memorySearchCache(max?: number): SearchCache;
2849
2873
 
2850
2874
  declare function search(req: SearchRequest, opts?: SearchOptions): Promise<SearchResult>;
2851
2875
 
2852
- export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AzureVoiceInfo, BatchJob, BatchRequestItem, BatchResultItem, type BflAdapterConfig, type BflCredits, BudgetConfig, BudgetExceededError, BudgetGuard, BudgetStore, CONFIG_DERIVED_HOSTS, Capability, type ChatInput, ChatResult, ChatStreamEvent, type CheckVoiceOptions, type ClassifyInput, type ClassifyResult, type Contracts, type CostQuery, CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_BASE_URLS, DEFAULT_SEARCH_ROUTES, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type FixedHostProvider, type HttpResponse, type ImageInput, ImageResult, type JudgeInput, JudgeResult, LOCALLY_PINNED_HOSTS, type MockupInput, type MockupResult, type ModerationInput, ModerationResult, type OcrInput, OcrResult, type OpenAICompatibleConfig, type PodcastInput, PodcastResult, type PriceDriftResult, type PriceDriftRow, type PricingEntry, ProviderAdapter, type RefreshOptions, type RefreshResult, Region, type RerankInput, type RerankResult, type RouteForecast, SDK_TAG, SEARCH_CACHE_TTL_MS, SEARCH_PRICE_USD, type SearchCache, type SearchCredentials, type SearchItem, SearchKeyMissingError, type SearchOptions, type SearchProviderId, type SearchPurpose, type SearchRequest, type SearchResult, type SearchRoute, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, Tier, TierSpec, Tool, ToolCall, type TrainStyleInput, TrainStyleResult, type TranscribeInput, TranscribeResult, type TranslateInput, TranslateResult, Transport, type TransportRequest, type TransportResponse, type TtsInput, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSink, type UpmetricsSinkConfig, type UpmetricsSinkStats, Usage, VERSION, type VideoInput, type VisionInput, type VoiceInfo, type VoiceProvider, type VoiceResolveResult, type VoiceStatus, VoiceUnavailableError, ZDR_PROVIDERS, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, byokAdapter, chatInputSchema, checkPriceDrift, checkVoice, 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, memorySearchCache, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, routeSearch, search, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, typesafeAdapter, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema, wouldProviderRouteTo, wouldRouteTo };
2876
+ export { AZURE_DANISH_VOICES, AZURE_DANISH_VOICE_LIST, type AiClient, type AiConfig, type AzureVoiceInfo, BatchJob, BatchRequestItem, BatchResultItem, type BflAdapterConfig, type BflCredits, BudgetConfig, BudgetExceededError, BudgetGuard, BudgetStore, CONFIG_DERIVED_HOSTS, Capability, type ChatInput, ChatResult, ChatStreamEvent, type CheckVoiceOptions, type ClassifyInput, type ClassifyResult, type Contracts, type CostQuery, CostSink, type CostSummary, type CostSummaryQuery, type CostTimeseriesQuery, DEFAULT_BASE_URLS, DEFAULT_SEARCH_ROUTES, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type FixedHostProvider, type HttpResponse, type ImageInput, ImageResult, type JudgeInput, JudgeResult, LOCALLY_PINNED_HOSTS, type MockupInput, type MockupResult, type ModerationInput, ModerationResult, type OcrInput, OcrResult, type OpenAICompatibleConfig, type PodcastInput, PodcastResult, type PriceDriftResult, type PriceDriftRow, type PricingEntry, ProviderAdapter, type RefreshOptions, type RefreshResult, Region, type RerankInput, type RerankResult, type RouteForecast, SDK_TAG, SEARCH_CACHE_TTL_MS, SEARCH_PRICE_USD, SearchBudgetExceededError, type SearchCache, type SearchCapStore, type SearchCredentials, type SearchItem, SearchKeyMissingError, type SearchOptions, type SearchProviderId, type SearchPurpose, type SearchRequest, type SearchResult, type SearchRoute, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, Tier, TierSpec, Tool, ToolCall, type TrainStyleInput, TrainStyleResult, type TranscribeInput, TranscribeResult, type TranslateInput, TranslateResult, Transport, type TransportRequest, type TransportResponse, type TtsInput, type UpmetricsCostClientConfig, UpmetricsCostError, type UpmetricsCostRow, type UpmetricsCostSummary, type UpmetricsCostTimeseries, type UpmetricsSink, type UpmetricsSinkConfig, type UpmetricsSinkStats, Usage, VERSION, type VideoInput, type VisionInput, type VoiceInfo, type VoiceProvider, type VoiceResolveResult, type VoiceStatus, VoiceUnavailableError, ZDR_PROVIDERS, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, azureAdapter, bflAdapter, bflCredits, byokAdapter, chatInputSchema, checkPriceDrift, checkVoice, 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, memorySearchCache, messageSchema, mistralAdapter, mistralStubAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, refreshAvailability, requestyAdapter, resetRefreshState, resetRegistry, resolveAzureVoice, resolveTier, resolveVoice, routeSearch, search, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, typesafeAdapter, upmetricsCostClient, upmetricsSink, usdFromMicro, vertexAdapter, visionInputSchema, wouldProviderRouteTo, wouldRouteTo };
package/dist/index.js CHANGED
@@ -171,20 +171,44 @@ var SearchKeyMissingError = class extends Error {
171
171
  provider;
172
172
  envVar;
173
173
  };
174
+ var SearchBudgetExceededError = class extends Error {
175
+ constructor(tenant, day, capUsd, spentUsd, requestedUsd) {
176
+ super(`search: daily cap $${capUsd} for tenant "${tenant}" on ${day} reached ($${spentUsd.toFixed(5)} spent, this call $${requestedUsd})`);
177
+ this.tenant = tenant;
178
+ this.day = day;
179
+ this.capUsd = capUsd;
180
+ this.spentUsd = spentUsd;
181
+ this.requestedUsd = requestedUsd;
182
+ this.name = "SearchBudgetExceededError";
183
+ }
184
+ tenant;
185
+ day;
186
+ capUsd;
187
+ spentUsd;
188
+ requestedUsd;
189
+ };
174
190
 
175
191
  // src/search/index.ts
176
192
  var sharedCache = memorySearchCache();
193
+ var sharedCapStore = /* @__PURE__ */ (() => {
194
+ const m = /* @__PURE__ */ new Map();
195
+ return { getSpent: (k) => m.get(k) ?? 0, addSpent: (k, usd) => {
196
+ m.set(k, (m.get(k) ?? 0) + usd);
197
+ } };
198
+ })();
199
+ var copenhagenDay = (d) => new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Copenhagen" }).format(d);
177
200
  async function callProvider(provider, req, opts) {
178
201
  const f = opts.fetch ?? fetch;
179
202
  const c = opts.credentials ?? {};
203
+ const env = opts.byok ? {} : process.env;
180
204
  if (provider === "brave") {
181
- const key = c.braveApiKey ?? process.env.BRAVE_API_KEY;
205
+ const key = c.braveApiKey ?? env.BRAVE_API_KEY;
182
206
  if (!key) throw new SearchKeyMissingError(provider, "BRAVE_API_KEY");
183
207
  return braveSearch({ ...req, provider }, key, f);
184
208
  }
185
209
  if (provider.startsWith("cloudflare:")) {
186
- const accountId = c.cloudflareAccountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
187
- const apiToken = c.cloudflareApiToken ?? process.env.CLOUDFLARE_API_TOKEN;
210
+ const accountId = c.cloudflareAccountId ?? env.CLOUDFLARE_ACCOUNT_ID;
211
+ const apiToken = c.cloudflareApiToken ?? env.CLOUDFLARE_API_TOKEN;
188
212
  if (!accountId) throw new SearchKeyMissingError(provider, "CLOUDFLARE_ACCOUNT_ID");
189
213
  if (!apiToken) throw new SearchKeyMissingError(provider, "CLOUDFLARE_API_TOKEN");
190
214
  return cloudflareSearch({ ...req, provider }, { accountId, apiToken }, f);
@@ -217,11 +241,20 @@ async function search(req, opts = {}) {
217
241
  const cache = opts.cache === false ? void 0 : opts.cache ?? sharedCache;
218
242
  const ttl = SEARCH_CACHE_TTL_MS[req.purpose ?? "agent"];
219
243
  const sink = opts.costSink;
244
+ const capStore = opts.capStore ?? sharedCapStore;
245
+ const tenant = req.labels?.tenantId ?? "_";
246
+ const day = copenhagenDay((opts.now ?? (() => /* @__PURE__ */ new Date()))());
247
+ const capKey = JSON.stringify([tenant, day]);
220
248
  let lastErr;
221
249
  for (const provider of chain) {
222
250
  const key = searchCacheKey(provider, req);
223
251
  const hit = cache?.get(key);
224
252
  if (hit) return { items: hit.items, meta: { ...hit.meta, cached: true, costUsd: 0, latencyMs: 0 } };
253
+ if (opts.dailyCapUsd !== void 0) {
254
+ const spent = await capStore.getSpent(capKey);
255
+ const price = SEARCH_PRICE_USD[provider];
256
+ if (spent + price > opts.dailyCapUsd) throw new SearchBudgetExceededError(tenant, day, opts.dailyCapUsd, spent, price);
257
+ }
225
258
  const t0 = performance.now();
226
259
  try {
227
260
  const out = await callProvider(provider, req, opts);
@@ -231,6 +264,7 @@ async function search(req, opts = {}) {
231
264
  meta: { provider, latencyMs, cached: false, costUsd: SEARCH_PRICE_USD[provider], ...out.requestId ? { requestId: out.requestId } : {} }
232
265
  };
233
266
  cache?.set(key, result, ttl);
267
+ await capStore.addSpent(capKey, result.meta.costUsd);
234
268
  try {
235
269
  await sink?.record(usageRow(provider, req, result.meta.costUsd, latencyMs));
236
270
  } catch {
@@ -3785,8 +3819,8 @@ var aiConfigSchema = z.object({
3785
3819
  import { randomUUID } from "crypto";
3786
3820
 
3787
3821
  // src/version.ts
3788
- var VERSION = "0.58.1";
3789
- var SDK_TAG = "@broberg/ai-sdk@0.58.1";
3822
+ var VERSION = "0.59.0";
3823
+ var SDK_TAG = "@broberg/ai-sdk@0.59.0";
3790
3824
 
3791
3825
  // src/cost/sinks/upmetrics.ts
3792
3826
  function isRetryableStatus(status) {
@@ -4580,7 +4614,7 @@ function createAI(config = {}) {
4580
4614
  },
4581
4615
  async search(req, opts) {
4582
4616
  const labels = { ...cfg.labels, ...req.labels };
4583
- return search({ ...req, ...Object.keys(labels).length ? { labels } : {} }, { ...opts, ...costSink ? { costSink } : {} });
4617
+ return search({ ...req, ...Object.keys(labels).length ? { labels } : {} }, { ...opts, ...cfg.byok ? { byok: true } : {}, ...costSink ? { costSink } : {} });
4584
4618
  },
4585
4619
  async judge(input) {
4586
4620
  input = judgeInputSchema.parse(input);
@@ -5330,6 +5364,7 @@ export {
5330
5364
  SDK_TAG,
5331
5365
  SEARCH_CACHE_TTL_MS,
5332
5366
  SEARCH_PRICE_USD,
5367
+ SearchBudgetExceededError,
5333
5368
  SearchKeyMissingError,
5334
5369
  StreamHttpError,
5335
5370
  UpmetricsCostError,