@broberg/ai-sdk 0.57.0 → 0.58.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.
package/dist/index.js CHANGED
@@ -26,6 +26,243 @@ import {
26
26
  getPrice
27
27
  } from "./chunk-QVMGFHCF.js";
28
28
 
29
+ // src/search/brave.ts
30
+ async function braveSearch(req, apiKey, f) {
31
+ const params = new URLSearchParams({ q: req.query, count: String(Math.min(req.limit ?? 10, 20)) });
32
+ if (req.lang) params.set("search_lang", req.lang);
33
+ if (req.country) params.set("country", req.country);
34
+ const res = await f(`https://api.search.brave.com/res/v1/web/search?${params}`, {
35
+ headers: { accept: "application/json", "X-Subscription-Token": apiKey }
36
+ });
37
+ if (!res.ok) throw Object.assign(new Error(`brave search: HTTP ${res.status} ${(await res.text()).slice(0, 200)}`), { status: res.status });
38
+ const body = await res.json();
39
+ const items = (body.web?.results ?? []).map((r) => ({
40
+ title: r.title ?? "",
41
+ url: r.url ?? "",
42
+ description: r.description ?? "",
43
+ provider: "brave",
44
+ ...r.language ? { lang: r.language } : {},
45
+ ...r.page_age ? { publishedAt: new Date(r.page_age).toISOString() } : {}
46
+ }));
47
+ return { items, requestId: res.headers.get("x-request-id") ?? void 0 };
48
+ }
49
+
50
+ // src/search/cloudflare.ts
51
+ var CLOUDFLARE_MAX_QUERY = 1024;
52
+ var CLOUDFLARE_MAX_LIMIT = 10;
53
+ async function cloudflareSearch(req, creds, f) {
54
+ if (req.query.length < 1 || req.query.length > CLOUDFLARE_MAX_QUERY) {
55
+ throw new Error(`search: Cloudflare takes a query of 1\u2013${CLOUDFLARE_MAX_QUERY} characters, got ${req.query.length}`);
56
+ }
57
+ const provider = req.provider.split(":")[1];
58
+ const res = await f(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(creds.accountId)}/ai/websearch/`, {
59
+ method: "POST",
60
+ headers: { authorization: `Bearer ${creds.apiToken}`, "content-type": "application/json" },
61
+ body: JSON.stringify({ query: req.query, provider, limit: Math.min(req.limit ?? CLOUDFLARE_MAX_LIMIT, CLOUDFLARE_MAX_LIMIT) })
62
+ });
63
+ if (!res.ok) throw Object.assign(new Error(`cloudflare search (${provider}): HTTP ${res.status} ${(await res.text()).slice(0, 200)}`), { status: res.status });
64
+ const body = await res.json();
65
+ const inner = body.result ?? body;
66
+ const items = (inner.items ?? []).map((r) => ({
67
+ title: r.title ?? "",
68
+ url: r.url ?? "",
69
+ description: r.description ?? "",
70
+ provider: req.provider
71
+ }));
72
+ return { items, requestId: inner.metadata?.requestId };
73
+ }
74
+
75
+ // src/search/prices.ts
76
+ var SEARCH_PRICE_USD = {
77
+ "cloudflare:ceramic": 25e-5,
78
+ "cloudflare:linkup": 5e-3,
79
+ "cloudflare:exa": 7e-3,
80
+ brave: 5e-3
81
+ };
82
+
83
+ // src/search/router.ts
84
+ var DEFAULT_SEARCH_ROUTES = [
85
+ { name: "fresh-or-country", when: { fresh: true }, chain: ["brave", "cloudflare:linkup"] },
86
+ { name: "english-agent", when: { english: true, purpose: ["agent", "grounding"] }, chain: ["cloudflare:ceramic", "cloudflare:linkup", "brave"] },
87
+ { name: "english-other", when: { english: true }, chain: ["cloudflare:linkup", "cloudflare:ceramic", "brave"] },
88
+ { name: "non-english", when: {}, chain: ["cloudflare:linkup", "brave"] }
89
+ ];
90
+ var ZDR_PROVIDERS = /* @__PURE__ */ new Set(["cloudflare:ceramic", "cloudflare:linkup"]);
91
+ var isEnglish = (lang) => !lang || /^en\b/i.test(lang);
92
+ function routeSearch(req, routes = DEFAULT_SEARCH_ROUTES) {
93
+ if (req.provider) {
94
+ if (req.zdr && !ZDR_PROVIDERS.has(req.provider)) {
95
+ throw new Error(`search: zdr:true but provider "${req.provider}" keeps query data \u2014 use cloudflare:ceramic or cloudflare:linkup`);
96
+ }
97
+ return [req.provider];
98
+ }
99
+ const fresh = Boolean(req.fresh || req.country);
100
+ const english = isEnglish(req.lang);
101
+ const route = routes.find(
102
+ (r) => (r.when.fresh === void 0 || r.when.fresh === fresh) && (r.when.english === void 0 || r.when.english === english) && (r.when.purpose === void 0 || r.when.purpose.includes(req.purpose ?? "agent"))
103
+ ) ?? routes[routes.length - 1];
104
+ let chain = route.chain.filter((p) => english || p !== "cloudflare:ceramic");
105
+ chain = chain.filter((p) => p !== "cloudflare:exa");
106
+ if (req.zdr) chain = chain.filter((p) => ZDR_PROVIDERS.has(p));
107
+ return chain;
108
+ }
109
+
110
+ // src/search/cache.ts
111
+ var SEARCH_CACHE_TTL_MS = {
112
+ monitor: 5 * 6e4,
113
+ agent: 60 * 6e4,
114
+ grounding: 60 * 6e4,
115
+ discovery: 7 * 24 * 60 * 6e4
116
+ };
117
+ function searchCacheKey(provider, req) {
118
+ const q = req.query.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ");
119
+ return JSON.stringify([provider, req.lang ?? "", req.country ?? "", req.limit ?? 10, q]);
120
+ }
121
+ function memorySearchCache(max = 1e3) {
122
+ const m = /* @__PURE__ */ new Map();
123
+ return {
124
+ get(k) {
125
+ const e = m.get(k);
126
+ if (!e) return void 0;
127
+ if (Date.now() > e.until) {
128
+ m.delete(k);
129
+ return void 0;
130
+ }
131
+ return e.v;
132
+ },
133
+ set(k, v, ttlMs) {
134
+ if (m.size >= max) m.delete(m.keys().next().value);
135
+ m.set(k, { v, until: Date.now() + ttlMs });
136
+ }
137
+ };
138
+ }
139
+
140
+ // src/cost/failure.ts
141
+ function classifyFailure(err) {
142
+ const e = err;
143
+ const name = e?.name ?? "";
144
+ const message = String(e?.message ?? "");
145
+ if (name === "AbortError" || name === "TimeoutError" || /timed? ?out/i.test(message.split("\n")[0] ?? "")) {
146
+ return { errorCode: "timeout", errorKind: "timeout" };
147
+ }
148
+ let status = typeof e?.status === "number" ? e.status : void 0;
149
+ if (status === void 0) {
150
+ const m = /(?:^|\s)([45]\d{2})(?=[:\s]|$)/.exec(message.split("\n")[0] ?? "");
151
+ if (m) status = Number(m[1]);
152
+ }
153
+ if (status !== void 0) {
154
+ const kind = status === 401 || status === 403 ? "auth" : status === 404 ? "not_found" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "other";
155
+ return { errorCode: String(status), errorKind: kind };
156
+ }
157
+ if (name === "TypeError" && /fetch failed|network|ECONN|ENOTFOUND/i.test(message + (e?.cause?.code ?? ""))) {
158
+ return { errorCode: "network", errorKind: "network" };
159
+ }
160
+ return { errorCode: "unknown", errorKind: "other" };
161
+ }
162
+
163
+ // src/search/types.ts
164
+ var SearchKeyMissingError = class extends Error {
165
+ constructor(provider, envVar) {
166
+ super(`search: ${provider} needs ${envVar} (env or options.credentials)`);
167
+ this.provider = provider;
168
+ this.envVar = envVar;
169
+ this.name = "SearchKeyMissingError";
170
+ }
171
+ provider;
172
+ envVar;
173
+ };
174
+
175
+ // src/search/index.ts
176
+ var sharedCache = memorySearchCache();
177
+ async function callProvider(provider, req, opts) {
178
+ const f = opts.fetch ?? fetch;
179
+ const c = opts.credentials ?? {};
180
+ if (provider === "brave") {
181
+ const key = c.braveApiKey ?? process.env.BRAVE_API_KEY;
182
+ if (!key) throw new SearchKeyMissingError(provider, "BRAVE_API_KEY");
183
+ return braveSearch({ ...req, provider }, key, f);
184
+ }
185
+ if (provider.startsWith("cloudflare:")) {
186
+ const accountId = c.cloudflareAccountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
187
+ const apiToken = c.cloudflareApiToken ?? process.env.CLOUDFLARE_API_TOKEN;
188
+ if (!accountId) throw new SearchKeyMissingError(provider, "CLOUDFLARE_ACCOUNT_ID");
189
+ if (!apiToken) throw new SearchKeyMissingError(provider, "CLOUDFLARE_API_TOKEN");
190
+ return cloudflareSearch({ ...req, provider }, { accountId, apiToken }, f);
191
+ }
192
+ throw new Error(`search: unknown provider "${String(provider)}"`);
193
+ }
194
+ function usageRow(provider, req, costUsd, latencyMs) {
195
+ return {
196
+ provider,
197
+ model: "web-search",
198
+ region: "unknown",
199
+ // Brave and Cloudflare's gateway do not let us say where a query is served
200
+ transport: "http",
201
+ inputTokens: 0,
202
+ outputTokens: 0,
203
+ cacheReadTokens: 0,
204
+ cacheCreationTokens: 0,
205
+ costUsd,
206
+ costBasis: "computed",
207
+ latencyMs,
208
+ capability: "search",
209
+ ...req.purpose ? { purpose: req.purpose } : {},
210
+ ...req.labels ? { labels: req.labels } : {},
211
+ ts: (/* @__PURE__ */ new Date()).toISOString()
212
+ };
213
+ }
214
+ async function search(req, opts = {}) {
215
+ const chain = routeSearch(req);
216
+ if (chain.length === 0) throw new Error("search: no provider fits this request (zdr excludes every candidate)");
217
+ const cache = opts.cache === false ? void 0 : opts.cache ?? sharedCache;
218
+ const ttl = SEARCH_CACHE_TTL_MS[req.purpose ?? "agent"];
219
+ const sink = opts.costSink;
220
+ let lastErr;
221
+ for (const provider of chain) {
222
+ const key = searchCacheKey(provider, req);
223
+ const hit = cache?.get(key);
224
+ if (hit) return { items: hit.items, meta: { ...hit.meta, cached: true, costUsd: 0, latencyMs: 0 } };
225
+ const t0 = performance.now();
226
+ try {
227
+ const out = await callProvider(provider, req, opts);
228
+ const latencyMs = Math.round(performance.now() - t0);
229
+ const result = {
230
+ items: dedupe(out.items),
231
+ meta: { provider, latencyMs, cached: false, costUsd: SEARCH_PRICE_USD[provider], ...out.requestId ? { requestId: out.requestId } : {} }
232
+ };
233
+ cache?.set(key, result, ttl);
234
+ try {
235
+ await sink?.record(usageRow(provider, req, result.meta.costUsd, latencyMs));
236
+ } catch {
237
+ }
238
+ return result;
239
+ } catch (e) {
240
+ lastErr = e;
241
+ if (sink?.recordFailure) {
242
+ try {
243
+ await sink.recordFailure({
244
+ provider,
245
+ model: "web-search",
246
+ transport: "http",
247
+ capability: "search",
248
+ ...req.purpose ? { purpose: req.purpose } : {},
249
+ ...req.labels ? { labels: req.labels } : {},
250
+ latencyMs: Math.round(performance.now() - t0),
251
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
252
+ ...classifyFailure(e)
253
+ });
254
+ } catch {
255
+ }
256
+ }
257
+ }
258
+ }
259
+ throw lastErr;
260
+ }
261
+ function dedupe(items) {
262
+ const seen = /* @__PURE__ */ new Set();
263
+ return items.filter((i) => seen.has(i.url) ? false : (seen.add(i.url), true));
264
+ }
265
+
29
266
  // src/cost/default-hosts.ts
30
267
  var DEFAULT_BASE_URLS = {
31
268
  anthropic: "https://api.anthropic.com",
@@ -3548,8 +3785,8 @@ var aiConfigSchema = z.object({
3548
3785
  import { randomUUID } from "crypto";
3549
3786
 
3550
3787
  // src/version.ts
3551
- var VERSION = "0.57.0";
3552
- var SDK_TAG = "@broberg/ai-sdk@0.57.0";
3788
+ var VERSION = "0.58.1";
3789
+ var SDK_TAG = "@broberg/ai-sdk@0.58.1";
3553
3790
 
3554
3791
  // src/cost/sinks/upmetrics.ts
3555
3792
  function isRetryableStatus(status) {
@@ -3830,29 +4067,6 @@ function assertByokProviders(providers) {
3830
4067
  }
3831
4068
  }
3832
4069
 
3833
- // src/cost/failure.ts
3834
- function classifyFailure(err) {
3835
- const e = err;
3836
- const name = e?.name ?? "";
3837
- const message = String(e?.message ?? "");
3838
- if (name === "AbortError" || name === "TimeoutError" || /timed? ?out/i.test(message.split("\n")[0] ?? "")) {
3839
- return { errorCode: "timeout", errorKind: "timeout" };
3840
- }
3841
- let status = typeof e?.status === "number" ? e.status : void 0;
3842
- if (status === void 0) {
3843
- const m = /(?:^|\s)([45]\d{2})(?=[:\s]|$)/.exec(message.split("\n")[0] ?? "");
3844
- if (m) status = Number(m[1]);
3845
- }
3846
- if (status !== void 0) {
3847
- const kind = status === 401 || status === 403 ? "auth" : status === 404 ? "not_found" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "other";
3848
- return { errorCode: String(status), errorKind: kind };
3849
- }
3850
- if (name === "TypeError" && /fetch failed|network|ECONN|ENOTFOUND/i.test(message + (e?.cause?.code ?? ""))) {
3851
- return { errorCode: "network", errorKind: "network" };
3852
- }
3853
- return { errorCode: "unknown", errorKind: "other" };
3854
- }
3855
-
3856
4070
  // src/client.ts
3857
4071
  var DEFAULT_IMAGE_SPEC = {
3858
4072
  provider: "fal",
@@ -4364,6 +4578,10 @@ function createAI(config = {}) {
4364
4578
  }
4365
4579
  });
4366
4580
  },
4581
+ async search(req, opts) {
4582
+ const labels = { ...cfg.labels, ...req.labels };
4583
+ return search({ ...req, ...Object.keys(labels).length ? { labels } : {} }, { ...opts, ...costSink ? { costSink } : {} });
4584
+ },
4367
4585
  async judge(input) {
4368
4586
  input = judgeInputSchema.parse(input);
4369
4587
  const stateText = typeof input.state === "string" ? input.state : JSON.stringify(input.state);
@@ -5094,103 +5312,6 @@ function wouldRouteTo(tier) {
5094
5312
  if (!spec) return { kind: "unknown-provider", provider: String(tier), onlyIf: [...ASSUMPTIONS] };
5095
5313
  return wouldProviderRouteTo(spec.provider);
5096
5314
  }
5097
-
5098
- // src/search/brave.ts
5099
- async function braveSearch(req, apiKey, f) {
5100
- const params = new URLSearchParams({ q: req.query, count: String(Math.min(req.limit ?? 10, 20)) });
5101
- if (req.lang) params.set("search_lang", req.lang);
5102
- if (req.country) params.set("country", req.country);
5103
- const res = await f(`https://api.search.brave.com/res/v1/web/search?${params}`, {
5104
- headers: { accept: "application/json", "X-Subscription-Token": apiKey }
5105
- });
5106
- if (!res.ok) throw Object.assign(new Error(`brave search: HTTP ${res.status} ${(await res.text()).slice(0, 200)}`), { status: res.status });
5107
- const body = await res.json();
5108
- const items = (body.web?.results ?? []).map((r) => ({
5109
- title: r.title ?? "",
5110
- url: r.url ?? "",
5111
- description: r.description ?? "",
5112
- provider: "brave",
5113
- ...r.language ? { lang: r.language } : {},
5114
- ...r.page_age ? { publishedAt: new Date(r.page_age).toISOString() } : {}
5115
- }));
5116
- return { items, requestId: res.headers.get("x-request-id") ?? void 0 };
5117
- }
5118
-
5119
- // src/search/cloudflare.ts
5120
- var CLOUDFLARE_MAX_QUERY = 1024;
5121
- var CLOUDFLARE_MAX_LIMIT = 10;
5122
- async function cloudflareSearch(req, creds, f) {
5123
- if (req.query.length < 1 || req.query.length > CLOUDFLARE_MAX_QUERY) {
5124
- throw new Error(`search: Cloudflare takes a query of 1\u2013${CLOUDFLARE_MAX_QUERY} characters, got ${req.query.length}`);
5125
- }
5126
- const provider = req.provider.split(":")[1];
5127
- const res = await f(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(creds.accountId)}/ai/websearch/`, {
5128
- method: "POST",
5129
- headers: { authorization: `Bearer ${creds.apiToken}`, "content-type": "application/json" },
5130
- body: JSON.stringify({ query: req.query, provider, limit: Math.min(req.limit ?? CLOUDFLARE_MAX_LIMIT, CLOUDFLARE_MAX_LIMIT) })
5131
- });
5132
- if (!res.ok) throw Object.assign(new Error(`cloudflare search (${provider}): HTTP ${res.status} ${(await res.text()).slice(0, 200)}`), { status: res.status });
5133
- const body = await res.json();
5134
- const inner = body.result ?? body;
5135
- const items = (inner.items ?? []).map((r) => ({
5136
- title: r.title ?? "",
5137
- url: r.url ?? "",
5138
- description: r.description ?? "",
5139
- provider: req.provider
5140
- }));
5141
- return { items, requestId: inner.metadata?.requestId };
5142
- }
5143
-
5144
- // src/search/prices.ts
5145
- var SEARCH_PRICE_USD = {
5146
- "cloudflare:ceramic": 25e-5,
5147
- "cloudflare:linkup": 5e-3,
5148
- "cloudflare:exa": 7e-3,
5149
- brave: 5e-3
5150
- };
5151
-
5152
- // src/search/types.ts
5153
- var SearchKeyMissingError = class extends Error {
5154
- constructor(provider, envVar) {
5155
- super(`search: ${provider} needs ${envVar} (env or options.credentials)`);
5156
- this.provider = provider;
5157
- this.envVar = envVar;
5158
- this.name = "SearchKeyMissingError";
5159
- }
5160
- provider;
5161
- envVar;
5162
- };
5163
-
5164
- // src/search/index.ts
5165
- async function search(req, opts = {}) {
5166
- const f = opts.fetch ?? fetch;
5167
- const c = opts.credentials ?? {};
5168
- const t0 = performance.now();
5169
- let out;
5170
- if (req.provider === "brave") {
5171
- const key = c.braveApiKey ?? process.env.BRAVE_API_KEY;
5172
- if (!key) throw new SearchKeyMissingError(req.provider, "BRAVE_API_KEY");
5173
- out = await braveSearch(req, key, f);
5174
- } else if (req.provider.startsWith("cloudflare:")) {
5175
- const accountId = c.cloudflareAccountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
5176
- const apiToken = c.cloudflareApiToken ?? process.env.CLOUDFLARE_API_TOKEN;
5177
- if (!accountId) throw new SearchKeyMissingError(req.provider, "CLOUDFLARE_ACCOUNT_ID");
5178
- if (!apiToken) throw new SearchKeyMissingError(req.provider, "CLOUDFLARE_API_TOKEN");
5179
- out = await cloudflareSearch(req, { accountId, apiToken }, f);
5180
- } else {
5181
- throw new Error(`search: unknown provider "${String(req.provider)}"`);
5182
- }
5183
- return {
5184
- items: out.items,
5185
- meta: {
5186
- provider: req.provider,
5187
- latencyMs: Math.round(performance.now() - t0),
5188
- cached: false,
5189
- costUsd: SEARCH_PRICE_USD[req.provider],
5190
- ...out.requestId ? { requestId: out.requestId } : {}
5191
- }
5192
- };
5193
- }
5194
5315
  export {
5195
5316
  AZURE_DANISH_VOICES,
5196
5317
  AZURE_DANISH_VOICE_LIST,
@@ -5199,6 +5320,7 @@ export {
5199
5320
  CONFIG_DERIVED_HOSTS,
5200
5321
  DEFAULT_BASE_URLS,
5201
5322
  DEFAULT_CLIP_SEC,
5323
+ DEFAULT_SEARCH_ROUTES,
5202
5324
  DEFAULT_TIER_MAP,
5203
5325
  ELEVENLABS_DANISH_VOICES,
5204
5326
  LOCALLY_PINNED_HOSTS,
@@ -5206,12 +5328,14 @@ export {
5206
5328
  ModelUnavailableError,
5207
5329
  PRICING_STALE_AFTER_DAYS,
5208
5330
  SDK_TAG,
5331
+ SEARCH_CACHE_TTL_MS,
5209
5332
  SEARCH_PRICE_USD,
5210
5333
  SearchKeyMissingError,
5211
5334
  StreamHttpError,
5212
5335
  UpmetricsCostError,
5213
5336
  VERSION,
5214
5337
  VoiceUnavailableError,
5338
+ ZDR_PROVIDERS,
5215
5339
  aiConfigSchema,
5216
5340
  alignWordTimings,
5217
5341
  anthropicAdapter,
@@ -5254,6 +5378,7 @@ export {
5254
5378
  makeContracts,
5255
5379
  makeOpenAICompatibleAdapter,
5256
5380
  matchLabel,
5381
+ memorySearchCache,
5257
5382
  messageSchema,
5258
5383
  mistralAdapter,
5259
5384
  mistralStubAdapter,
@@ -5276,6 +5401,7 @@ export {
5276
5401
  resolveModel,
5277
5402
  resolveTier,
5278
5403
  resolveVoice,
5404
+ routeSearch,
5279
5405
  search,
5280
5406
  sqliteBudgetStore,
5281
5407
  sqliteSink,