@broberg/ai-sdk 0.56.1 → 0.58.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.js CHANGED
@@ -26,6 +26,238 @@ 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) return [req.provider];
94
+ const fresh = Boolean(req.fresh || req.country);
95
+ const english = isEnglish(req.lang);
96
+ const route = routes.find(
97
+ (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"))
98
+ ) ?? routes[routes.length - 1];
99
+ let chain = route.chain.filter((p) => english || p !== "cloudflare:ceramic");
100
+ chain = chain.filter((p) => p !== "cloudflare:exa");
101
+ if (req.zdr) chain = chain.filter((p) => ZDR_PROVIDERS.has(p));
102
+ return chain;
103
+ }
104
+
105
+ // src/search/cache.ts
106
+ var SEARCH_CACHE_TTL_MS = {
107
+ monitor: 5 * 6e4,
108
+ agent: 60 * 6e4,
109
+ grounding: 60 * 6e4,
110
+ discovery: 7 * 24 * 60 * 6e4
111
+ };
112
+ function searchCacheKey(provider, req) {
113
+ const q = req.query.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ");
114
+ return JSON.stringify([provider, req.lang ?? "", req.country ?? "", req.limit ?? 10, q]);
115
+ }
116
+ function memorySearchCache(max = 1e3) {
117
+ const m = /* @__PURE__ */ new Map();
118
+ return {
119
+ get(k) {
120
+ const e = m.get(k);
121
+ if (!e) return void 0;
122
+ if (Date.now() > e.until) {
123
+ m.delete(k);
124
+ return void 0;
125
+ }
126
+ return e.v;
127
+ },
128
+ set(k, v, ttlMs) {
129
+ if (m.size >= max) m.delete(m.keys().next().value);
130
+ m.set(k, { v, until: Date.now() + ttlMs });
131
+ }
132
+ };
133
+ }
134
+
135
+ // src/cost/failure.ts
136
+ function classifyFailure(err) {
137
+ const e = err;
138
+ const name = e?.name ?? "";
139
+ const message = String(e?.message ?? "");
140
+ if (name === "AbortError" || name === "TimeoutError" || /timed? ?out/i.test(message.split("\n")[0] ?? "")) {
141
+ return { errorCode: "timeout", errorKind: "timeout" };
142
+ }
143
+ let status = typeof e?.status === "number" ? e.status : void 0;
144
+ if (status === void 0) {
145
+ const m = /(?:^|\s)([45]\d{2})(?=[:\s]|$)/.exec(message.split("\n")[0] ?? "");
146
+ if (m) status = Number(m[1]);
147
+ }
148
+ if (status !== void 0) {
149
+ const kind = status === 401 || status === 403 ? "auth" : status === 404 ? "not_found" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "other";
150
+ return { errorCode: String(status), errorKind: kind };
151
+ }
152
+ if (name === "TypeError" && /fetch failed|network|ECONN|ENOTFOUND/i.test(message + (e?.cause?.code ?? ""))) {
153
+ return { errorCode: "network", errorKind: "network" };
154
+ }
155
+ return { errorCode: "unknown", errorKind: "other" };
156
+ }
157
+
158
+ // src/search/types.ts
159
+ var SearchKeyMissingError = class extends Error {
160
+ constructor(provider, envVar) {
161
+ super(`search: ${provider} needs ${envVar} (env or options.credentials)`);
162
+ this.provider = provider;
163
+ this.envVar = envVar;
164
+ this.name = "SearchKeyMissingError";
165
+ }
166
+ provider;
167
+ envVar;
168
+ };
169
+
170
+ // src/search/index.ts
171
+ var sharedCache = memorySearchCache();
172
+ async function callProvider(provider, req, opts) {
173
+ const f = opts.fetch ?? fetch;
174
+ const c = opts.credentials ?? {};
175
+ if (provider === "brave") {
176
+ const key = c.braveApiKey ?? process.env.BRAVE_API_KEY;
177
+ if (!key) throw new SearchKeyMissingError(provider, "BRAVE_API_KEY");
178
+ return braveSearch({ ...req, provider }, key, f);
179
+ }
180
+ if (provider.startsWith("cloudflare:")) {
181
+ const accountId = c.cloudflareAccountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
182
+ const apiToken = c.cloudflareApiToken ?? process.env.CLOUDFLARE_API_TOKEN;
183
+ if (!accountId) throw new SearchKeyMissingError(provider, "CLOUDFLARE_ACCOUNT_ID");
184
+ if (!apiToken) throw new SearchKeyMissingError(provider, "CLOUDFLARE_API_TOKEN");
185
+ return cloudflareSearch({ ...req, provider }, { accountId, apiToken }, f);
186
+ }
187
+ throw new Error(`search: unknown provider "${String(provider)}"`);
188
+ }
189
+ function usageRow(provider, req, costUsd, latencyMs) {
190
+ return {
191
+ provider,
192
+ model: "web-search",
193
+ region: "unknown",
194
+ // Brave and Cloudflare's gateway do not let us say where a query is served
195
+ transport: "http",
196
+ inputTokens: 0,
197
+ outputTokens: 0,
198
+ cacheReadTokens: 0,
199
+ cacheCreationTokens: 0,
200
+ costUsd,
201
+ costBasis: "computed",
202
+ latencyMs,
203
+ capability: "search",
204
+ ...req.purpose ? { purpose: req.purpose } : {},
205
+ ...req.labels ? { labels: req.labels } : {},
206
+ ts: (/* @__PURE__ */ new Date()).toISOString()
207
+ };
208
+ }
209
+ async function search(req, opts = {}) {
210
+ const chain = routeSearch(req);
211
+ if (chain.length === 0) throw new Error("search: no provider fits this request (zdr excludes every candidate)");
212
+ const cache = opts.cache === false ? void 0 : opts.cache ?? sharedCache;
213
+ const ttl = SEARCH_CACHE_TTL_MS[req.purpose ?? "agent"];
214
+ const sink = opts.costSink;
215
+ let lastErr;
216
+ for (const provider of chain) {
217
+ const key = searchCacheKey(provider, req);
218
+ const hit = cache?.get(key);
219
+ if (hit) return { items: hit.items, meta: { ...hit.meta, cached: true, costUsd: 0, latencyMs: 0 } };
220
+ const t0 = performance.now();
221
+ try {
222
+ const out = await callProvider(provider, req, opts);
223
+ const latencyMs = Math.round(performance.now() - t0);
224
+ const result = {
225
+ items: dedupe(out.items),
226
+ meta: { provider, latencyMs, cached: false, costUsd: SEARCH_PRICE_USD[provider], ...out.requestId ? { requestId: out.requestId } : {} }
227
+ };
228
+ cache?.set(key, result, ttl);
229
+ try {
230
+ await sink?.record(usageRow(provider, req, result.meta.costUsd, latencyMs));
231
+ } catch {
232
+ }
233
+ return result;
234
+ } catch (e) {
235
+ lastErr = e;
236
+ if (sink?.recordFailure) {
237
+ try {
238
+ await sink.recordFailure({
239
+ provider,
240
+ model: "web-search",
241
+ transport: "http",
242
+ capability: "search",
243
+ ...req.purpose ? { purpose: req.purpose } : {},
244
+ ...req.labels ? { labels: req.labels } : {},
245
+ latencyMs: Math.round(performance.now() - t0),
246
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
247
+ ...classifyFailure(e)
248
+ });
249
+ } catch {
250
+ }
251
+ }
252
+ }
253
+ }
254
+ throw lastErr;
255
+ }
256
+ function dedupe(items) {
257
+ const seen = /* @__PURE__ */ new Set();
258
+ return items.filter((i) => seen.has(i.url) ? false : (seen.add(i.url), true));
259
+ }
260
+
29
261
  // src/cost/default-hosts.ts
30
262
  var DEFAULT_BASE_URLS = {
31
263
  anthropic: "https://api.anthropic.com",
@@ -3548,8 +3780,8 @@ var aiConfigSchema = z.object({
3548
3780
  import { randomUUID } from "crypto";
3549
3781
 
3550
3782
  // src/version.ts
3551
- var VERSION = "0.56.1";
3552
- var SDK_TAG = "@broberg/ai-sdk@0.56.1";
3783
+ var VERSION = "0.58.0";
3784
+ var SDK_TAG = "@broberg/ai-sdk@0.58.0";
3553
3785
 
3554
3786
  // src/cost/sinks/upmetrics.ts
3555
3787
  function isRetryableStatus(status) {
@@ -3830,29 +4062,6 @@ function assertByokProviders(providers) {
3830
4062
  }
3831
4063
  }
3832
4064
 
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
4065
  // src/client.ts
3857
4066
  var DEFAULT_IMAGE_SPEC = {
3858
4067
  provider: "fal",
@@ -4364,6 +4573,10 @@ function createAI(config = {}) {
4364
4573
  }
4365
4574
  });
4366
4575
  },
4576
+ async search(req, opts) {
4577
+ const labels = { ...cfg.labels, ...req.labels };
4578
+ return search({ ...req, ...Object.keys(labels).length ? { labels } : {} }, { ...opts, ...costSink ? { costSink } : {} });
4579
+ },
4367
4580
  async judge(input) {
4368
4581
  input = judgeInputSchema.parse(input);
4369
4582
  const stateText = typeof input.state === "string" ? input.state : JSON.stringify(input.state);
@@ -5102,6 +5315,7 @@ export {
5102
5315
  CONFIG_DERIVED_HOSTS,
5103
5316
  DEFAULT_BASE_URLS,
5104
5317
  DEFAULT_CLIP_SEC,
5318
+ DEFAULT_SEARCH_ROUTES,
5105
5319
  DEFAULT_TIER_MAP,
5106
5320
  ELEVENLABS_DANISH_VOICES,
5107
5321
  LOCALLY_PINNED_HOSTS,
@@ -5109,10 +5323,14 @@ export {
5109
5323
  ModelUnavailableError,
5110
5324
  PRICING_STALE_AFTER_DAYS,
5111
5325
  SDK_TAG,
5326
+ SEARCH_CACHE_TTL_MS,
5327
+ SEARCH_PRICE_USD,
5328
+ SearchKeyMissingError,
5112
5329
  StreamHttpError,
5113
5330
  UpmetricsCostError,
5114
5331
  VERSION,
5115
5332
  VoiceUnavailableError,
5333
+ ZDR_PROVIDERS,
5116
5334
  aiConfigSchema,
5117
5335
  alignWordTimings,
5118
5336
  anthropicAdapter,
@@ -5155,6 +5373,7 @@ export {
5155
5373
  makeContracts,
5156
5374
  makeOpenAICompatibleAdapter,
5157
5375
  matchLabel,
5376
+ memorySearchCache,
5158
5377
  messageSchema,
5159
5378
  mistralAdapter,
5160
5379
  mistralStubAdapter,
@@ -5177,6 +5396,8 @@ export {
5177
5396
  resolveModel,
5178
5397
  resolveTier,
5179
5398
  resolveVoice,
5399
+ routeSearch,
5400
+ search,
5180
5401
  sqliteBudgetStore,
5181
5402
  sqliteSink,
5182
5403
  streamTransport,