@nijaru/pi-search 0.0.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/LICENSE +21 -0
- package/README.md +165 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/package.json +69 -0
- package/src/anthropic.ts +350 -0
- package/src/anydoc-worker.mjs +35 -0
- package/src/anydoc.ts +161 -0
- package/src/brave-answers.ts +396 -0
- package/src/brave.ts +441 -0
- package/src/codex.ts +314 -0
- package/src/contracts.ts +644 -0
- package/src/direct-transport.ts +141 -0
- package/src/errors.ts +185 -0
- package/src/exa.ts +204 -0
- package/src/fetch-errors.ts +134 -0
- package/src/fetch-extractor-worker.mjs +85 -0
- package/src/fetch-tool.ts +166 -0
- package/src/fetcher.ts +451 -0
- package/src/fixtures/sample.csv +2 -0
- package/src/fixtures/sample.docx +0 -0
- package/src/fixtures/sample.epub +0 -0
- package/src/fixtures/sample.odt +0 -0
- package/src/fixtures/sample.pdf +32 -0
- package/src/fixtures/sample.pptx +0 -0
- package/src/fixtures/sample.rtf +1 -0
- package/src/fixtures/sample.xlsx +0 -0
- package/src/gemini.ts +298 -0
- package/src/grounding.ts +167 -0
- package/src/http.ts +78 -0
- package/src/index.ts +101 -0
- package/src/local-llama-compat.ts +134 -0
- package/src/meta.ts +137 -0
- package/src/model-selection.ts +134 -0
- package/src/openai.ts +850 -0
- package/src/parallel-responses.ts +260 -0
- package/src/parallel.ts +173 -0
- package/src/pdf.ts +153 -0
- package/src/provider-availability.ts +35 -0
- package/src/provider-http.ts +203 -0
- package/src/render-text.ts +8 -0
- package/src/research-tool.ts +487 -0
- package/src/router.ts +302 -0
- package/src/search-cleanup.ts +219 -0
- package/src/search-tool.ts +551 -0
- package/src/search.ts +420 -0
- package/src/ssrf.ts +302 -0
- package/src/url-rendering.ts +21 -0
- package/src/x.ts +188 -0
- package/src/xai.ts +254 -0
- package/src/youtube.ts +224 -0
package/src/brave.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Provider,
|
|
3
|
+
ProviderCapabilities,
|
|
4
|
+
ProviderContext,
|
|
5
|
+
ProviderRateLimitInfo,
|
|
6
|
+
ProviderRateLimitWindow,
|
|
7
|
+
ProviderProfile,
|
|
8
|
+
SearchOption,
|
|
9
|
+
SearchRequest,
|
|
10
|
+
SearchResponse,
|
|
11
|
+
SearchResult,
|
|
12
|
+
SearchWarning,
|
|
13
|
+
} from "./contracts";
|
|
14
|
+
import { createProviderError, isProviderError } from "./errors";
|
|
15
|
+
import { cancelResponseBody, readBoundedResponseText } from "./http";
|
|
16
|
+
import { validateSearchRequest } from "./search";
|
|
17
|
+
import { matchesSearchDomain, normalizeSearchUrl } from "./search-cleanup";
|
|
18
|
+
|
|
19
|
+
export const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
|
20
|
+
export const DEFAULT_BRAVE_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
export const BRAVE_MAX_RESULTS = 20;
|
|
22
|
+
|
|
23
|
+
export type BraveFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
|
+
|
|
25
|
+
export interface BraveCapacityTracker {
|
|
26
|
+
/** True when no known finite quota window currently blocks a call. */
|
|
27
|
+
readonly canAttempt: () => boolean;
|
|
28
|
+
/** Reserve a local request slot, returning false when the caller canceled. */
|
|
29
|
+
readonly waitForTurn?: (signal: AbortSignal) => Promise<boolean>;
|
|
30
|
+
/** Record provider observations without inventing or persisting counters. */
|
|
31
|
+
readonly observe: (info: ProviderRateLimitInfo | undefined) => void;
|
|
32
|
+
/** Return the latest immutable observation for diagnostics and errors. */
|
|
33
|
+
readonly snapshot: () => ProviderRateLimitInfo | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BraveQuotaTrackerOptions {
|
|
37
|
+
/** Minimum spacing between request starts for a conservative local policy. */
|
|
38
|
+
readonly minimumIntervalMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tracks provider-reported windows and, when configured, serializes local
|
|
43
|
+
* request starts. The provider remains authoritative; this is an admission
|
|
44
|
+
* guard, not a quota counter or billing ledger.
|
|
45
|
+
*/
|
|
46
|
+
export class BraveQuotaTracker implements BraveCapacityTracker {
|
|
47
|
+
private latest: ProviderRateLimitInfo | undefined;
|
|
48
|
+
private observedAtMs = 0;
|
|
49
|
+
private readonly minimumIntervalMs: number;
|
|
50
|
+
private nextRequestAtMs = 0;
|
|
51
|
+
|
|
52
|
+
constructor(options: BraveQuotaTrackerOptions = {}) {
|
|
53
|
+
const minimumIntervalMs = options.minimumIntervalMs ?? 0;
|
|
54
|
+
if (!Number.isInteger(minimumIntervalMs) || minimumIntervalMs < 0) {
|
|
55
|
+
throw new Error("Brave minimumIntervalMs must be a non-negative integer");
|
|
56
|
+
}
|
|
57
|
+
this.minimumIntervalMs = minimumIntervalMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async waitForTurn(signal: AbortSignal): Promise<boolean> {
|
|
61
|
+
if (signal.aborted) return false;
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const startAt = Math.max(now, this.nextRequestAtMs);
|
|
64
|
+
this.nextRequestAtMs = startAt + this.minimumIntervalMs;
|
|
65
|
+
const delayMs = startAt - now;
|
|
66
|
+
if (delayMs <= 0) return true;
|
|
67
|
+
return new Promise<boolean>((resolve) => {
|
|
68
|
+
let settled = false;
|
|
69
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
const finish = (admitted: boolean): void => {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
74
|
+
signal.removeEventListener("abort", onAbort);
|
|
75
|
+
resolve(admitted);
|
|
76
|
+
};
|
|
77
|
+
const onAbort = (): void => finish(false);
|
|
78
|
+
timer = setTimeout(() => finish(true), delayMs);
|
|
79
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
if (signal.aborted) onAbort();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
canAttempt(): boolean {
|
|
85
|
+
const info = this.snapshot();
|
|
86
|
+
if (info === undefined) return true;
|
|
87
|
+
if (info.retryAfterMs !== undefined && info.retryAfterMs > 0) return false;
|
|
88
|
+
return !info.windows.some((window) => {
|
|
89
|
+
// Some providers use a zero limit to mean that this window is not
|
|
90
|
+
// enforced. It must not become a permanent local denial.
|
|
91
|
+
if (window.limit === 0 || window.remaining !== 0) return false;
|
|
92
|
+
if (window.resetAfterMs !== undefined) return window.resetAfterMs > 0;
|
|
93
|
+
if (window.resetAt !== undefined) {
|
|
94
|
+
const resetAt = Date.parse(window.resetAt);
|
|
95
|
+
return Number.isFinite(resetAt) && resetAt > Date.now();
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
observe(info: ProviderRateLimitInfo | undefined): void {
|
|
102
|
+
if (info !== undefined && (info.windows.length > 0 || info.retryAfterMs !== undefined)) {
|
|
103
|
+
this.latest = info;
|
|
104
|
+
this.observedAtMs = Date.now();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
snapshot(): ProviderRateLimitInfo | undefined {
|
|
109
|
+
if (this.latest === undefined) return undefined;
|
|
110
|
+
const elapsedMs = Math.max(0, Date.now() - this.observedAtMs);
|
|
111
|
+
return {
|
|
112
|
+
windows: this.latest.windows.map((window) => ({
|
|
113
|
+
...window,
|
|
114
|
+
...(window.resetAfterMs === undefined ? {} : { resetAfterMs: Math.max(0, window.resetAfterMs - elapsedMs) }),
|
|
115
|
+
})),
|
|
116
|
+
...(this.latest.retryAfterMs === undefined ? {} : { retryAfterMs: Math.max(0, this.latest.retryAfterMs - elapsedMs) }),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface BraveAdapterOptions {
|
|
122
|
+
/** The key is supplied by the construction boundary; never read globally here. */
|
|
123
|
+
readonly apiKey?: string;
|
|
124
|
+
readonly endpoint?: string;
|
|
125
|
+
readonly fetchImpl?: BraveFetch;
|
|
126
|
+
readonly maxResponseBytes?: number;
|
|
127
|
+
readonly capacityTracker?: BraveCapacityTracker;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface BraveResultPayload {
|
|
131
|
+
readonly title?: unknown;
|
|
132
|
+
readonly url?: unknown;
|
|
133
|
+
readonly description?: unknown;
|
|
134
|
+
readonly snippet?: unknown;
|
|
135
|
+
readonly published?: unknown;
|
|
136
|
+
readonly page_age?: unknown;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const capabilities: ProviderCapabilities = {
|
|
140
|
+
keyword: true,
|
|
141
|
+
freshness: true,
|
|
142
|
+
excerpts: true,
|
|
143
|
+
domainFilter: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const profile: ProviderProfile = {
|
|
147
|
+
auth: "environment",
|
|
148
|
+
costModel: "per-request",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function malformed(message: string, cause?: unknown): never {
|
|
152
|
+
throw createProviderError({
|
|
153
|
+
provider: "brave",
|
|
154
|
+
kind: "malformed",
|
|
155
|
+
message: `Brave returned a malformed response (${message})`,
|
|
156
|
+
retryable: false,
|
|
157
|
+
cause,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
|
162
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
163
|
+
return malformed(`${label} is not an object`);
|
|
164
|
+
}
|
|
165
|
+
return value as Record<string, unknown>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function optionalString(value: unknown, label: string, maxLength = 8_192): string | undefined {
|
|
169
|
+
if (value === undefined || value === null) return undefined;
|
|
170
|
+
if (typeof value !== "string") return malformed(`${label} is not a string`);
|
|
171
|
+
return value.slice(0, maxLength);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function optionalTimestamp(value: unknown, label: string): string | undefined {
|
|
175
|
+
const candidate = optionalString(value, label, 100);
|
|
176
|
+
if (candidate === undefined) return undefined;
|
|
177
|
+
return Number.isFinite(Date.parse(candidate)) ? candidate : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizedUrl(value: unknown): { url: string; domain: string } {
|
|
181
|
+
if (typeof value !== "string" || value.trim().length === 0) return malformed("web.results[].url is missing");
|
|
182
|
+
if (value.length > 8_192) return malformed("web.results[].url exceeds the supported length limit");
|
|
183
|
+
const normalized = normalizeSearchUrl(value);
|
|
184
|
+
if (normalized === undefined) return malformed("web.results[].url is not a valid HTTP(S) URL");
|
|
185
|
+
return { url: normalized.url, domain: normalized.domain };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resultAllowed(result: SearchResult, request: SearchRequest): boolean {
|
|
189
|
+
const include = request.domains?.include;
|
|
190
|
+
const exclude = request.domains?.exclude;
|
|
191
|
+
const domain = result.domain ?? new URL(result.url).hostname.toLowerCase();
|
|
192
|
+
if (include !== undefined && include.length > 0 && !include.some((candidate) => matchesSearchDomain(domain, candidate))) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return exclude === undefined || !exclude.some((candidate) => matchesSearchDomain(domain, candidate));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resultFromPayload(value: unknown, query: string): SearchResult {
|
|
199
|
+
const result = objectValue(value, "web.results[]") as BraveResultPayload;
|
|
200
|
+
const parsed = normalizedUrl(result.url);
|
|
201
|
+
const title = optionalString(result.title, "web.results[].title", 500);
|
|
202
|
+
const description = optionalString(result.description ?? result.snippet, "web.results[].description", 4_000);
|
|
203
|
+
const publishedAt = optionalTimestamp(result.published ?? result.page_age, "web.results[].published");
|
|
204
|
+
return {
|
|
205
|
+
url: parsed.url,
|
|
206
|
+
...(title === undefined ? {} : { title }),
|
|
207
|
+
domain: parsed.domain,
|
|
208
|
+
...(publishedAt === undefined ? {} : { publishedAt }),
|
|
209
|
+
...(description === undefined ? {} : { excerpt: description }),
|
|
210
|
+
provider: "brave",
|
|
211
|
+
searchQuery: query,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseNonNegative(value: string | null): number | undefined {
|
|
216
|
+
if (value === null || !/^\d+(?:\.\d+)?$/.test(value.trim())) return undefined;
|
|
217
|
+
const parsed = Number(value);
|
|
218
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function splitHeader(value: string | null): string[] {
|
|
222
|
+
return value === null ? [] : value.split(",").map((part) => part.trim());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseReset(value: string | undefined): Pick<ProviderRateLimitWindow, "resetAt" | "resetAfterMs"> {
|
|
226
|
+
if (value === undefined) return {};
|
|
227
|
+
const numeric = parseNonNegative(value);
|
|
228
|
+
if (numeric === undefined) return {};
|
|
229
|
+
if (numeric >= 1_000_000_000_000) {
|
|
230
|
+
return { resetAt: new Date(numeric).toISOString() };
|
|
231
|
+
}
|
|
232
|
+
if (numeric >= 1_000_000_000) {
|
|
233
|
+
return { resetAt: new Date(numeric * 1_000).toISOString() };
|
|
234
|
+
}
|
|
235
|
+
return { resetAfterMs: Math.round(numeric * 1_000) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Parse common Brave quota headers without assuming a single plan window. */
|
|
239
|
+
export function parseBraveRateLimits(headers: Headers): ProviderRateLimitInfo | undefined {
|
|
240
|
+
const limits = splitHeader(headers.get("x-ratelimit-limit"));
|
|
241
|
+
const remaining = splitHeader(headers.get("x-ratelimit-remaining"));
|
|
242
|
+
const resets = splitHeader(headers.get("x-ratelimit-reset"));
|
|
243
|
+
const count = Math.max(limits.length, remaining.length, resets.length);
|
|
244
|
+
const windows: ProviderRateLimitWindow[] = [];
|
|
245
|
+
for (let index = 0; index < count; index += 1) {
|
|
246
|
+
const limit = parseNonNegative(limits[index] ?? "");
|
|
247
|
+
const left = parseNonNegative(remaining[index] ?? "");
|
|
248
|
+
const reset = parseReset(resets[index]);
|
|
249
|
+
if (limit === undefined && left === undefined && Object.keys(reset).length === 0) continue;
|
|
250
|
+
windows.push({
|
|
251
|
+
...(limit === undefined ? {} : { limit }),
|
|
252
|
+
...(left === undefined ? {} : { remaining: left }),
|
|
253
|
+
...reset,
|
|
254
|
+
scope: `window-${index}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const retryAfter = headers.get("retry-after");
|
|
258
|
+
let retryAfterMs: number | undefined;
|
|
259
|
+
if (retryAfter !== null) {
|
|
260
|
+
const seconds = parseNonNegative(retryAfter);
|
|
261
|
+
if (seconds !== undefined) retryAfterMs = Math.round(seconds * 1_000);
|
|
262
|
+
else {
|
|
263
|
+
const date = Date.parse(retryAfter);
|
|
264
|
+
if (Number.isFinite(date)) retryAfterMs = Math.max(0, date - Date.now());
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (windows.length === 0 && retryAfterMs === undefined) return undefined;
|
|
268
|
+
return { windows, ...(retryAfterMs === undefined ? {} : { retryAfterMs }) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function domainQuery(request: SearchRequest): string {
|
|
272
|
+
const include = request.domains?.include ?? [];
|
|
273
|
+
const exclude = request.domains?.exclude ?? [];
|
|
274
|
+
const includeQuery = include.length === 0 ? undefined : include.length === 1 ? `site:${include[0]}` : `(${include.map((domain) => `site:${domain}`).join(" OR ")})`;
|
|
275
|
+
const excludeQuery = exclude.map((domain) => `-site:${domain}`);
|
|
276
|
+
return [includeQuery, request.query, ...excludeQuery].filter((part): part is string => part !== undefined && part.length > 0).join(" ");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface BraveRequestPlan {
|
|
280
|
+
readonly url: string;
|
|
281
|
+
readonly appliedOptions: readonly SearchOption[];
|
|
282
|
+
readonly warnings: readonly SearchWarning[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Build one bounded Brave request; no pagination or hidden follow-up calls. */
|
|
286
|
+
export function buildBraveRequest(request: SearchRequest, endpoint = BRAVE_SEARCH_ENDPOINT): BraveRequestPlan {
|
|
287
|
+
const normalized = validateSearchRequest(request);
|
|
288
|
+
if (normalized.dateRange !== undefined || normalized.social !== undefined) {
|
|
289
|
+
throw createProviderError({ provider: "brave", kind: "unsupported", message: "Brave Search does not provide exact date-range or dedicated social/X constraints", retryable: false });
|
|
290
|
+
}
|
|
291
|
+
const appliedOptions: SearchOption[] = ["maxResults"];
|
|
292
|
+
const warnings: SearchWarning[] = [];
|
|
293
|
+
const params = new URLSearchParams({
|
|
294
|
+
q: domainQuery(normalized),
|
|
295
|
+
count: String(Math.min(normalized.maxResults ?? 10, BRAVE_MAX_RESULTS)),
|
|
296
|
+
});
|
|
297
|
+
if (normalized.domains?.include?.length || normalized.domains?.exclude?.length) appliedOptions.push("domains");
|
|
298
|
+
switch (normalized.mode) {
|
|
299
|
+
case "auto":
|
|
300
|
+
case "keyword":
|
|
301
|
+
appliedOptions.push("mode");
|
|
302
|
+
break;
|
|
303
|
+
case "fresh":
|
|
304
|
+
params.set("freshness", "pm");
|
|
305
|
+
appliedOptions.push("mode");
|
|
306
|
+
break;
|
|
307
|
+
default:
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
return { url: `${endpoint}?${params.toString()}`, appliedOptions, warnings };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function normalizeBraveResponse(
|
|
314
|
+
payload: unknown,
|
|
315
|
+
request: SearchRequest,
|
|
316
|
+
options: { readonly requestId?: string; readonly rateLimits?: ProviderRateLimitInfo } = {},
|
|
317
|
+
): SearchResponse {
|
|
318
|
+
const normalized = validateSearchRequest(request);
|
|
319
|
+
const root = objectValue(payload, "response");
|
|
320
|
+
const web = objectValue(root.web, "web");
|
|
321
|
+
if (!Array.isArray(web.results)) return malformed("web.results is not an array");
|
|
322
|
+
const results = web.results
|
|
323
|
+
.map((value) => resultFromPayload(value, normalized.query))
|
|
324
|
+
.filter((result) => resultAllowed(result, normalized))
|
|
325
|
+
.slice(0, normalized.maxResults);
|
|
326
|
+
return {
|
|
327
|
+
query: normalized.query,
|
|
328
|
+
results,
|
|
329
|
+
provider: "brave",
|
|
330
|
+
appliedOptions: [],
|
|
331
|
+
warnings: [],
|
|
332
|
+
...(options.requestId === undefined ? {} : { requestId: options.requestId }),
|
|
333
|
+
...(options.rateLimits === undefined ? {} : { usage: { rateLimits: options.rateLimits } }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export class BraveProvider implements Provider {
|
|
338
|
+
readonly id = "brave" as const;
|
|
339
|
+
readonly capabilities = capabilities;
|
|
340
|
+
readonly profile = profile;
|
|
341
|
+
private readonly apiKey?: string;
|
|
342
|
+
private readonly endpoint: string;
|
|
343
|
+
private readonly fetchImpl: BraveFetch;
|
|
344
|
+
private readonly maxResponseBytes: number;
|
|
345
|
+
private readonly capacityTracker?: BraveCapacityTracker;
|
|
346
|
+
|
|
347
|
+
constructor(options: BraveAdapterOptions) {
|
|
348
|
+
this.apiKey = options.apiKey;
|
|
349
|
+
this.endpoint = options.endpoint ?? BRAVE_SEARCH_ENDPOINT;
|
|
350
|
+
this.fetchImpl = options.fetchImpl ?? (fetch as BraveFetch);
|
|
351
|
+
this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_BRAVE_RESPONSE_BYTES;
|
|
352
|
+
this.capacityTracker = options.capacityTracker;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async search(request: SearchRequest, signal: AbortSignal, _context: ProviderContext): Promise<SearchResponse> {
|
|
356
|
+
const normalized = validateSearchRequest(request);
|
|
357
|
+
const plan = buildBraveRequest(normalized, this.endpoint);
|
|
358
|
+
if (signal.aborted) {
|
|
359
|
+
throw createProviderError({ provider: this.id, kind: "canceled", message: "Search canceled", retryable: false });
|
|
360
|
+
}
|
|
361
|
+
const observed = this.capacityTracker?.snapshot();
|
|
362
|
+
if (this.capacityTracker !== undefined && !this.capacityTracker.canAttempt()) {
|
|
363
|
+
throw createProviderError({
|
|
364
|
+
provider: this.id,
|
|
365
|
+
kind: "rateLimit",
|
|
366
|
+
message: "Brave quota window is exhausted",
|
|
367
|
+
retryable: true,
|
|
368
|
+
rateLimits: observed,
|
|
369
|
+
retryAfterMs: observed?.retryAfterMs,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (this.capacityTracker?.waitForTurn !== undefined) {
|
|
373
|
+
const admitted = await this.capacityTracker.waitForTurn(signal);
|
|
374
|
+
if (!admitted) {
|
|
375
|
+
throw createProviderError({ provider: this.id, kind: "canceled", message: "Search canceled", retryable: false });
|
|
376
|
+
}
|
|
377
|
+
if (!this.capacityTracker.canAttempt()) {
|
|
378
|
+
const latest = this.capacityTracker.snapshot();
|
|
379
|
+
throw createProviderError({
|
|
380
|
+
provider: this.id,
|
|
381
|
+
kind: "rateLimit",
|
|
382
|
+
message: "Brave quota window is exhausted",
|
|
383
|
+
retryable: true,
|
|
384
|
+
rateLimits: latest,
|
|
385
|
+
retryAfterMs: latest?.retryAfterMs,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const apiKey = this.apiKey;
|
|
390
|
+
if (apiKey === undefined || apiKey.trim().length === 0) {
|
|
391
|
+
throw createProviderError({ provider: this.id, kind: "auth", message: "Brave API key is not configured", retryable: false });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let response: Response;
|
|
395
|
+
try {
|
|
396
|
+
response = await this.fetchImpl(plan.url, {
|
|
397
|
+
method: "GET",
|
|
398
|
+
headers: { accept: "application/json", "x-subscription-token": apiKey },
|
|
399
|
+
signal,
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (signal.aborted) {
|
|
403
|
+
throw createProviderError({ provider: this.id, kind: "canceled", message: "Search canceled", retryable: false, cause: error });
|
|
404
|
+
}
|
|
405
|
+
if (isProviderError(error)) throw error;
|
|
406
|
+
throw createProviderError({ provider: this.id, kind: "network", message: "Brave network request failed", retryable: true, cause: error });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const rateLimits = parseBraveRateLimits(response.headers);
|
|
410
|
+
this.capacityTracker?.observe(rateLimits);
|
|
411
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-brave-request-id") ?? undefined;
|
|
412
|
+
if (response.status === 401 || response.status === 403) {
|
|
413
|
+
await cancelResponseBody(response);
|
|
414
|
+
throw createProviderError({ provider: this.id, kind: "auth", message: `Brave rejected the API key (HTTP ${response.status})`, status: response.status, retryable: false, requestId, rateLimits });
|
|
415
|
+
}
|
|
416
|
+
if (response.status === 429) {
|
|
417
|
+
await cancelResponseBody(response);
|
|
418
|
+
throw createProviderError({ provider: this.id, kind: "rateLimit", message: "Brave rate limit exceeded", status: response.status, retryable: true, requestId, retryAfterMs: rateLimits?.retryAfterMs, rateLimits });
|
|
419
|
+
}
|
|
420
|
+
if (response.status < 200 || response.status >= 300) {
|
|
421
|
+
await cancelResponseBody(response);
|
|
422
|
+
throw createProviderError({ provider: this.id, kind: response.status === 400 || response.status === 422 ? "badRequest" : "http", message: `Brave search failed with HTTP ${response.status}`, status: response.status, retryable: response.status === 408 || response.status === 425 || response.status >= 500, requestId, rateLimits });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let payload: unknown;
|
|
426
|
+
try {
|
|
427
|
+
payload = JSON.parse(await readBoundedResponseText(response, this.maxResponseBytes, signal));
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (signal.aborted) {
|
|
430
|
+
throw createProviderError({ provider: this.id, kind: "canceled", message: "Search canceled", retryable: false, cause: error });
|
|
431
|
+
}
|
|
432
|
+
throw createProviderError({ provider: this.id, kind: "malformed", message: "Brave returned a malformed or oversized JSON response", retryable: false, requestId, rateLimits, cause: error });
|
|
433
|
+
}
|
|
434
|
+
const normalizedResponse = normalizeBraveResponse(payload, normalized, { requestId, rateLimits });
|
|
435
|
+
return { ...normalizedResponse, appliedOptions: plan.appliedOptions, warnings: plan.warnings };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function createBraveProvider(options: BraveAdapterOptions): BraveProvider {
|
|
440
|
+
return new BraveProvider(options);
|
|
441
|
+
}
|