@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/THIRD_PARTY_NOTICES.md +31 -0
  4. package/package.json +69 -0
  5. package/src/anthropic.ts +350 -0
  6. package/src/anydoc-worker.mjs +35 -0
  7. package/src/anydoc.ts +161 -0
  8. package/src/brave-answers.ts +396 -0
  9. package/src/brave.ts +441 -0
  10. package/src/codex.ts +314 -0
  11. package/src/contracts.ts +644 -0
  12. package/src/direct-transport.ts +141 -0
  13. package/src/errors.ts +185 -0
  14. package/src/exa.ts +204 -0
  15. package/src/fetch-errors.ts +134 -0
  16. package/src/fetch-extractor-worker.mjs +85 -0
  17. package/src/fetch-tool.ts +166 -0
  18. package/src/fetcher.ts +451 -0
  19. package/src/fixtures/sample.csv +2 -0
  20. package/src/fixtures/sample.docx +0 -0
  21. package/src/fixtures/sample.epub +0 -0
  22. package/src/fixtures/sample.odt +0 -0
  23. package/src/fixtures/sample.pdf +32 -0
  24. package/src/fixtures/sample.pptx +0 -0
  25. package/src/fixtures/sample.rtf +1 -0
  26. package/src/fixtures/sample.xlsx +0 -0
  27. package/src/gemini.ts +298 -0
  28. package/src/grounding.ts +167 -0
  29. package/src/http.ts +78 -0
  30. package/src/index.ts +101 -0
  31. package/src/local-llama-compat.ts +134 -0
  32. package/src/meta.ts +137 -0
  33. package/src/model-selection.ts +134 -0
  34. package/src/openai.ts +850 -0
  35. package/src/parallel-responses.ts +260 -0
  36. package/src/parallel.ts +173 -0
  37. package/src/pdf.ts +153 -0
  38. package/src/provider-availability.ts +35 -0
  39. package/src/provider-http.ts +203 -0
  40. package/src/render-text.ts +8 -0
  41. package/src/research-tool.ts +487 -0
  42. package/src/router.ts +302 -0
  43. package/src/search-cleanup.ts +219 -0
  44. package/src/search-tool.ts +551 -0
  45. package/src/search.ts +420 -0
  46. package/src/ssrf.ts +302 -0
  47. package/src/url-rendering.ts +21 -0
  48. package/src/x.ts +188 -0
  49. package/src/xai.ts +254 -0
  50. package/src/youtube.ts +224 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Provider-neutral contracts for pi-search.
3
+ *
4
+ * These types are the foundation of the extension. They are intentionally
5
+ * agnostic of how a provider fulfills a request — whether by a direct HTTP
6
+ * search endpoint (Brave, Exa, or Parallel) or by a model-mediated grounding
7
+ * call (OpenAI/Codex, Gemini, or xAI). See `docs/DESIGN.md` for the shipped
8
+ * boundary.
9
+ *
10
+ * Nothing here performs I/O or depends on Pi runtime APIs, so it can be
11
+ * unit-tested deterministically and offline. Provider adapters build on these
12
+ * contracts and keep their transport and payload handling in separate modules.
13
+ */
14
+
15
+ // ─── Common ────────────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * A normalized, canonical URL string (already resolved against redirects,
19
+ * with fragment and tracking-noise stripped where a provider exposes it).
20
+ * The original URL the user/agent supplied is preserved separately as
21
+ * `source_url` when it differs.
22
+ */
23
+ export type Url = string;
24
+
25
+ /**
26
+ * ISO-8601 timestamp string when a result was published, fetched, or
27
+ * indexed. Omitted when the provider does not surface one.
28
+ */
29
+ export type IsoTimestamp = string;
30
+
31
+ /**
32
+ * Opaque identifier a provider returns for a single search call, useful for
33
+ * debugging, cost attribution, and provider-side replay. Optional.
34
+ */
35
+ export type RequestId = string;
36
+
37
+ /** Pi's header override contract: null deletes a provider/default header. */
38
+ export type ProviderHeaders = Readonly<Record<string, string | null>>;
39
+ /**
40
+ * The immutable subset of Pi's active model needed by model-mediated
41
+ * providers. Keeping this structural avoids coupling provider adapters to Pi's
42
+ * runtime types while still making the routing and auth boundary explicit.
43
+ */
44
+ export interface ProviderModel {
45
+ readonly id: string;
46
+ readonly provider: string;
47
+ readonly api: string;
48
+ readonly baseUrl: string;
49
+ readonly headers?: ProviderHeaders;
50
+ }
51
+
52
+ /** The result of resolving credentials through Pi's model registry. */
53
+ export type ProviderAuthResult =
54
+ | {
55
+ readonly ok: true;
56
+ readonly apiKey?: string;
57
+ readonly headers?: ProviderHeaders;
58
+ }
59
+ | {
60
+ readonly ok: false;
61
+ readonly error: string;
62
+ };
63
+
64
+ /**
65
+ * The narrow model-auth surface needed by model-mediated providers. The tool
66
+ * boundary binds this function to Pi's model registry, so adapters can resolve
67
+ * the active model or a same-provider execution model without reading Pi auth
68
+ * state or environment credentials globally.
69
+ */
70
+ export interface ProviderModelRegistry {
71
+ readonly getApiKeyAndHeaders: (model: ProviderModel) => Promise<ProviderAuthResult>;
72
+ /** Available authenticated-model metadata for adapters that need a provider-compatible execution model. */
73
+ readonly getModels?: () => readonly ProviderModel[];
74
+ }
75
+
76
+ /**
77
+ * Runtime services supplied by the Pi tool boundary to a provider adapter.
78
+ * Direct HTTP providers may ignore this context; model-mediated providers
79
+ * must use `modelRegistry` rather than reading Pi credentials globally.
80
+ */
81
+ export interface ProviderContext {
82
+ readonly model?: ProviderModel;
83
+ readonly modelRegistry?: ProviderModelRegistry;
84
+ }
85
+
86
+ /**
87
+ * Operational information used by the router for bounded provider selection.
88
+ * Estimates are advisory; actual usage belongs in `SearchResponse.usage`.
89
+ */
90
+ export interface ProviderProfile {
91
+ readonly typicalLatencyMs?: number;
92
+ readonly estimatedCostUsd?: number;
93
+ readonly costModel: "free" | "per-request" | "usage-based" | "unknown";
94
+ readonly auth: "none" | "environment" | "modelRegistry";
95
+ }
96
+
97
+ /** A single provider-reported quota or rate-limit window. */
98
+ export interface ProviderRateLimitWindow {
99
+ /** Maximum calls in this window, when the provider reports it. */
100
+ readonly limit?: number;
101
+ /** Calls remaining in this window, when the provider reports it. */
102
+ readonly remaining?: number;
103
+ /** Absolute reset time, when the provider reports one. */
104
+ readonly resetAt?: IsoTimestamp;
105
+ /** Relative reset delay, when the provider reports one. */
106
+ readonly resetAfterMs?: number;
107
+ /** Provider-defined scope such as `second` or `month`. */
108
+ readonly scope?: string;
109
+ }
110
+
111
+ /** Provider-reported rate-limit metadata. It is observational, never a budget reservation. */
112
+ export interface ProviderRateLimitInfo {
113
+ readonly windows: readonly ProviderRateLimitWindow[];
114
+ readonly retryAfterMs?: number;
115
+ }
116
+
117
+ /**
118
+ * Provider-reported billing and capacity information when available.
119
+ */
120
+ export interface ProviderUsage {
121
+ readonly costUsd?: number;
122
+ readonly billedUnits?: number;
123
+ readonly billedUnit?: string;
124
+ /** Number of provider-side search queries when exposed separately from token usage. */
125
+ readonly searchQueries?: number;
126
+ /** Provider-reported input/prompt token count, when available. */
127
+ readonly inputTokens?: number;
128
+ /** Provider-reported output/completion token count, when available. */
129
+ readonly outputTokens?: number;
130
+ /** Provider-reported total token count, when available. */
131
+ readonly totalTokens?: number;
132
+ readonly rateLimits?: ProviderRateLimitInfo;
133
+ }
134
+
135
+ /**
136
+ * Free-form capability flags describing what a provider can do. The router
137
+ * uses these to select a provider by task rather than by a fixed vendor
138
+ * ranking (D3).
139
+ */
140
+ export interface ProviderCapabilities {
141
+ /** Classic keyword / exact-match retrieval. */
142
+ readonly keyword?: boolean;
143
+ /** Freshness-sensitive retrieval. */
144
+ readonly freshness?: boolean;
145
+ /** Semantic or natural-language retrieval. */
146
+ readonly semantic?: boolean;
147
+ /** Returns source excerpts alongside the URL. */
148
+ readonly excerpts?: boolean;
149
+ /** Supports domain include/exclude filters. */
150
+ readonly domainFilter?: boolean;
151
+ /** Supports provider-enforced date ranges. */
152
+ readonly dateFilter?: boolean;
153
+ /** Searches social sources such as X. */
154
+ readonly social?: boolean;
155
+ /** Supports provider-enforced social-handle filters. */
156
+ readonly handleFilter?: boolean;
157
+ /** Supports provider-side image/video understanding during search. */
158
+ readonly mediaUnderstanding?: boolean;
159
+ /** Uses a model's native grounding/search tool. */
160
+ readonly nativeGrounding?: boolean;
161
+ /** Supports provider-native search context sizing. */
162
+ readonly searchContextSize?: boolean;
163
+ /** Supports provider-native returned-token budgets. */
164
+ readonly returnTokenBudget?: boolean;
165
+ /** Supports live/cache-only search selection. */
166
+ readonly externalWebAccess?: boolean;
167
+ /** Supports approximate user location. */
168
+ readonly userLocation?: boolean;
169
+ /** Supports provider-native content-type selection. */
170
+ readonly searchContentTypes?: boolean;
171
+ /** Supports image result settings. */
172
+ readonly imageSettings?: boolean;
173
+ }
174
+
175
+ // ─── Search ────────────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * How results should be ranked/retrieved. Mirrors the capability axes so the
179
+ * router can pick a provider whose capabilities match the requested mode.
180
+ */
181
+ export type SearchMode = "auto" | "keyword" | "fresh";
182
+
183
+ /**
184
+ * Domain filter: restrict or exclude specific hosts.
185
+ *
186
+ * Empty by default. An entry in `include` restricts results to those hosts;
187
+ * an entry in `exclude` removes them. A host should not appear in both.
188
+ */
189
+ export interface DomainFilter {
190
+ readonly include?: readonly string[];
191
+ readonly exclude?: readonly string[];
192
+ }
193
+
194
+ /** Provider-enforced temporal bounds for providers that support them. */
195
+ export interface SearchDateRange {
196
+ readonly from?: string;
197
+ readonly to?: string;
198
+ }
199
+
200
+ /** Provider-enforced social/X constraints. */
201
+ export interface SocialSearchOptions {
202
+ readonly includeHandles?: readonly string[];
203
+ readonly excludeHandles?: readonly string[];
204
+ readonly understandImages?: boolean;
205
+ readonly understandVideos?: boolean;
206
+ }
207
+
208
+ export type SearchContextSize = "low" | "medium" | "high";
209
+ export type SearchReturnTokenBudget = "default" | "unlimited";
210
+ export type SearchContentType = "text" | "image";
211
+
212
+ export interface SearchUserLocation {
213
+ readonly type: "approximate";
214
+ readonly country?: string;
215
+ readonly region?: string;
216
+ readonly city?: string;
217
+ readonly timezone?: string;
218
+ }
219
+
220
+ export interface SearchImageSettings {
221
+ readonly maxResults?: number;
222
+ readonly caption?: boolean;
223
+ }
224
+
225
+ /**
226
+ * Provider-neutral search request. Every field is optional except `query`
227
+ * so that a caller can start with a bare string and refine as needed.
228
+ *
229
+ * Adapters map this to their provider-specific payload. Unsupported options
230
+ * must be surfaced in `SearchResponse.warnings`; hard constraints must never
231
+ * disappear silently.
232
+ */
233
+ export type SearchAnswerMode = "auto" | "evidence";
234
+
235
+ export interface SearchRequest {
236
+ /** Natural-language or keyword query. Required. */
237
+ readonly query: string;
238
+ /** Retrieval bias; default `auto`. */
239
+ readonly mode?: SearchMode;
240
+ /** Maximum results to return. Adapters may return fewer. */
241
+ readonly maxResults?: number;
242
+ /** Restrict/exclude hosts. */
243
+ readonly domains?: DomainFilter;
244
+ /** Provider-enforced date range, when supported. */
245
+ readonly dateRange?: SearchDateRange;
246
+ /** Provider-enforced social/X constraints, when supported. */
247
+ readonly social?: SocialSearchOptions;
248
+ /** Select a model-mediated execution model; requires a compatible provider hint when cross-provider. */
249
+ readonly executionModel?: string;
250
+ /** Include a provider-grounded answer when the backend supplies one. */
251
+ readonly answerMode?: SearchAnswerMode;
252
+ /** Provider-native amount of search context supplied to the model. */
253
+ readonly searchContextSize?: SearchContextSize;
254
+ /** Provider-native returned-token budget for long research runs. */
255
+ readonly returnTokenBudget?: SearchReturnTokenBudget;
256
+ /** Whether provider search may use live internet access. */
257
+ readonly externalWebAccess?: boolean;
258
+ /** Approximate location used to localize provider search results. */
259
+ readonly userLocation?: SearchUserLocation;
260
+ /** Provider-native content types to retrieve. */
261
+ readonly searchContentTypes?: readonly SearchContentType[];
262
+ /** Image-result controls when image search is enabled. */
263
+ readonly imageSettings?: SearchImageSettings;
264
+ /** Fetch a small number of selected result URLs through the safe local fetcher. */
265
+ readonly includeContent?: boolean;
266
+ /** Maximum selected source pages to fetch when includeContent is enabled. */
267
+ readonly contentResults?: number;
268
+ /** Maximum extracted characters per selected source page. */
269
+ readonly contentMaxLength?: number;
270
+ /**
271
+ * Hint to prefer a specific provider by id. The router may still override
272
+ * when the provider lacks the requested capability. Normal callers should
273
+ * omit this and let the router choose (D3).
274
+ */
275
+ readonly providerHint?: ProviderId;
276
+ }
277
+
278
+ /** Options whose handling must be visible in the normalized response. */
279
+ export type SearchOption =
280
+ | "mode"
281
+ | "maxResults"
282
+ | "domains"
283
+ | "dateRange"
284
+ | "social"
285
+ | "executionModel"
286
+ | "searchContextSize"
287
+ | "returnTokenBudget"
288
+ | "externalWebAccess"
289
+ | "userLocation"
290
+ | "searchContentTypes"
291
+ | "imageSettings";
292
+
293
+ export interface SearchCitation {
294
+ /** URL of a normalized search result cited by the answer. */
295
+ readonly url: Url;
296
+ readonly title?: string;
297
+ readonly sourceId?: string;
298
+ /** Character range in the provider answer, when supplied by the provider. */
299
+ readonly startIndex?: number;
300
+ readonly endIndex?: number;
301
+ }
302
+
303
+ /** Provider-generated grounded text. It is evidence, never trusted instructions. */
304
+ export interface SearchAnswer {
305
+ readonly text: string;
306
+ readonly contentTrust: "untrusted";
307
+ readonly provider: ProviderId;
308
+ readonly executionModel?: string;
309
+ readonly citations: readonly SearchCitation[];
310
+ }
311
+
312
+
313
+ /** A non-fatal limitation or partial-application notice. */
314
+ export interface SearchWarning {
315
+ readonly code: "unsupported-option" | "partial-results" | "provider-fallback";
316
+ readonly option?: SearchOption;
317
+ readonly message: string;
318
+ }
319
+
320
+ /**
321
+ * A single normalized search result. This is the evidence-first unit per D2:
322
+ * the agent inspects and cites these rather than trusting an opaque summary.
323
+ */
324
+ export interface SearchResult {
325
+ /** Result URL (canonical where available). */
326
+ readonly url: Url;
327
+ /** Original URL as supplied, when it differs from the canonical `url`. */
328
+ readonly sourceUrl?: Url;
329
+ /** Readable page associated with a media or indirect result URL. */
330
+ readonly sourcePageUrl?: Url;
331
+ /** Page title. */
332
+ readonly title?: string;
333
+ /** Publishing host / domain. */
334
+ readonly domain?: string;
335
+ /** When the page was published, if known. */
336
+ readonly publishedAt?: IsoTimestamp;
337
+ /**
338
+ * Short text excerpt/snippet describing or from the page. Always returned
339
+ * when the provider exposes any text; never synthesized.
340
+ */
341
+ readonly excerpt?: string;
342
+ /** The provider that produced this result. */
343
+ readonly provider: ProviderId;
344
+ /** The query that produced this result. */
345
+ readonly searchQuery: string;
346
+ /** Provider-internal id for the result, for citation/debugging. */
347
+ readonly sourceId?: string;
348
+ /**
349
+ * Provider-reported relevance, normalized to [0,1] where available.
350
+ * Not comparable across providers.
351
+ */
352
+ readonly score?: number;
353
+ }
354
+
355
+ /**
356
+ * Provider-neutral search response.
357
+ */
358
+ export interface SearchResponse {
359
+ /** The query that was executed (after any normalization). */
360
+ readonly query: string;
361
+ /** Results, best-first. May be empty. */
362
+ readonly results: readonly SearchResult[];
363
+ /** Optional provider-grounded answer aligned with `results`. */
364
+ readonly answer?: SearchAnswer;
365
+ /** Bounded source pages fetched after search when requested. */
366
+ readonly sourceContents?: readonly FetchedContent[];
367
+ /** Which provider actually served the request. */
368
+ readonly provider: ProviderId;
369
+ /** Model used by a model-mediated provider, when available. */
370
+ readonly executionModel?: string;
371
+ /** Providers attempted by bounded automatic routing, in order. */
372
+ readonly attemptedProviders?: readonly ProviderId[];
373
+ /** Options the provider applied, including post-filtered constraints. */
374
+ readonly appliedOptions: readonly SearchOption[];
375
+ /** Explicit warnings for options that were unsupported or only partial. */
376
+ readonly warnings: readonly SearchWarning[];
377
+ /** Provider request id for debugging/attribution. */
378
+ readonly requestId?: RequestId;
379
+ /** Wall-clock latency in milliseconds. */
380
+ readonly latencyMs?: number;
381
+ /** Provider-reported billing information, when available. */
382
+ readonly usage?: ProviderUsage;
383
+ }
384
+
385
+ // ─── Research ──────────────────────────────────────────────────────────────
386
+
387
+ /**
388
+ * Required limits for the multi-step research tool. The orchestrator must
389
+ * reject invalid budgets before making a provider call and stop at every
390
+ * limit, including the cost limit when usage is reported.
391
+ */
392
+ export interface ResearchBudget {
393
+ readonly maxSteps: number;
394
+ readonly maxProviderCalls: number;
395
+ readonly maxFetches: number;
396
+ readonly timeoutMs: number;
397
+ readonly maxOutputChars: number;
398
+ readonly maxCostUsd?: number;
399
+ }
400
+
401
+ export interface ResearchRequest {
402
+ readonly question: string;
403
+ /** The caller supplies the query plan; defaults to [question]. */
404
+ readonly queries?: readonly string[];
405
+ readonly budget: ResearchBudget;
406
+ /** Select one provider strictly for the whole invocation. */
407
+ readonly provider?: "native" | SearchProviderHintId;
408
+ /** Explicit model id for a model-mediated provider. */
409
+ readonly executionModel?: string;
410
+ /** Number of result URLs to fetch after search, bounded by budget.maxFetches. */
411
+ readonly fetchResults?: number;
412
+ }
413
+
414
+ export type ResearchStopReason = "completed" | "partial" | "budget" | "deadline" | "canceled" | "provider-error";
415
+
416
+ export interface ResearchResponse {
417
+ readonly question: string;
418
+ /** The single provider selected for the whole research invocation. */
419
+ readonly provider: ProviderId;
420
+ /** Model used when the selected provider is model-mediated. */
421
+ readonly executionModel?: string;
422
+ readonly results: readonly SearchResult[];
423
+ readonly fetched: readonly FetchedContent[];
424
+ readonly stepsCompleted: number;
425
+ readonly providerCalls: number;
426
+ readonly fetchesCompleted: number;
427
+ readonly fetchAttempts: number;
428
+ readonly stopReason: ResearchStopReason;
429
+ readonly usage?: ProviderUsage;
430
+ readonly warnings: readonly SearchWarning[];
431
+ }
432
+
433
+ /** Validate the hard limits before the research orchestrator starts. */
434
+ export function validateResearchBudget(budget: ResearchBudget): void {
435
+ if (!Number.isInteger(budget.maxSteps) || budget.maxSteps < 1) {
436
+ throw new Error("Research budget maxSteps must be a positive integer");
437
+ }
438
+ if (!Number.isInteger(budget.maxProviderCalls) || budget.maxProviderCalls < 1) {
439
+ throw new Error("Research budget maxProviderCalls must be a positive integer");
440
+ }
441
+ if (!Number.isInteger(budget.maxFetches) || budget.maxFetches < 0) {
442
+ throw new Error("Research budget maxFetches must be a non-negative integer");
443
+ }
444
+ if (!Number.isFinite(budget.timeoutMs) || budget.timeoutMs <= 0) {
445
+ throw new Error("Research budget timeoutMs must be positive");
446
+ }
447
+ if (!Number.isInteger(budget.maxOutputChars) || budget.maxOutputChars < 1) {
448
+ throw new Error("Research budget maxOutputChars must be a positive integer");
449
+ }
450
+ if (budget.maxCostUsd !== undefined && (!Number.isFinite(budget.maxCostUsd) || budget.maxCostUsd < 0)) {
451
+ throw new Error("Research budget maxCostUsd must be non-negative");
452
+ }
453
+ }
454
+
455
+ // ─── Fetch ─────────────────────────────────────────────────────────────────
456
+
457
+ /**
458
+ * Provider-neutral fetch request for a known URL.
459
+ *
460
+ * Only user-supplied or search-returned URLs may be fetched by default (D6).
461
+ * The fetcher revalidates redirects, blocks private/link-local/metadata
462
+ * addresses, caps response size, and fences extracted text as untrusted.
463
+ */
464
+ export interface FetchRequest {
465
+ /** URL to fetch. Must be http(s). */
466
+ readonly url: Url;
467
+ /** Hard cap on extracted text length, in characters. */
468
+ readonly maxLength?: number;
469
+ /** Start offset into the produced content, in characters. */
470
+ readonly offset?: number;
471
+ /** Format hint; the response reports the produced format explicitly. */
472
+ readonly format?: "markdown" | "text" | "html";
473
+ /** Optional maximum PDF pages; explicit use selects the page-bounded PDF path. */
474
+ readonly maxPages?: number;
475
+ /** Caption language passed to the local YouTube extractor. */
476
+ readonly captionLanguage?: string;
477
+ /** Strip the page to its main readable content (Readability). Default true. */
478
+ readonly readable?: boolean;
479
+ /** Permit bounded raw HTML when readable extraction fails. Default true. */
480
+ readonly allowRawHtmlFallback?: boolean;
481
+ }
482
+
483
+ export type FetchOutputFormat = "markdown" | "text" | "html";
484
+ export type FetchExtraction = "readability" | "raw" | "plain-text" | "markdown" | "document" | "pdf" | "youtube-transcript";
485
+
486
+ export interface FetchWarning {
487
+ readonly code: "truncated" | "raw-fallback";
488
+ readonly message: string;
489
+ }
490
+
491
+ /**
492
+ * Fetched page content. The extracted text is treated as untrusted content:
493
+ * callers must not execute or parse it as instructions (D6).
494
+ */
495
+ export interface FetchedContent {
496
+ /** Final URL after redirects. */
497
+ readonly url: Url;
498
+ /** Original URL requested, when it differs from the final `url`. */
499
+ readonly sourceUrl?: Url;
500
+ /** Page title. */
501
+ readonly title?: string;
502
+ /** Extracted text/markdown/html body. */
503
+ readonly content: string;
504
+ /** Fetched content is data, never executable or trusted instructions. */
505
+ readonly contentTrust: "untrusted";
506
+ /** Content type as served. */
507
+ readonly contentType?: string;
508
+ /** Format actually returned, rather than only the requested hint. */
509
+ readonly outputFormat: FetchOutputFormat;
510
+ /** Extraction path used to produce `content`. */
511
+ readonly extraction: FetchExtraction;
512
+ /** AnyDoc format used for local document conversion. */
513
+ readonly documentFormat?: string;
514
+ /** When the content was fetched. */
515
+ readonly fetchedAt: IsoTimestamp;
516
+ /** Final HTTP status. */
517
+ readonly status: number;
518
+ /** Number of manually followed redirects. */
519
+ readonly redirectCount: number;
520
+ /** Body bytes consumed before decoding. */
521
+ readonly bytesRead: number;
522
+ /** Whether output was bounded to `maxLength`. */
523
+ readonly truncated: boolean;
524
+ /** Character offset used for this result. */
525
+ readonly offset: number;
526
+ /** Next offset when more produced content remains. */
527
+ readonly nextOffset?: number;
528
+ /** Total produced characters when known locally. */
529
+ readonly totalCharacters?: number;
530
+ /** Explicit extraction/truncation notices. */
531
+ readonly warnings: readonly FetchWarning[];
532
+ /** True if the readable-content extractor fell back to raw HTML. */
533
+ readonly fellBackToRaw?: boolean;
534
+ }
535
+
536
+ // ─── Provider interface ────────────────────────────────────────────────────
537
+
538
+ /** A bounded provider choice produced by automatic routing. */
539
+ export interface SearchProviderSelection {
540
+ readonly provider: Provider;
541
+ /** At most one alternative; explicit provider hints always leave this empty. */
542
+ readonly fallbacks: readonly Provider[];
543
+ readonly automatic: boolean;
544
+ }
545
+
546
+ /**
547
+ * Identifier for a provider. Used in results and capability descriptors.
548
+ * The concrete union grows as adapters land; string allows forward-compat.
549
+ */
550
+ export type ProviderId =
551
+ | "native"
552
+ | "openai"
553
+ | "openai-codex"
554
+ | "gemini"
555
+ | "brave"
556
+ | "exa"
557
+ | "parallel"
558
+ | "parallel-responses"
559
+ | "brave-answers"
560
+ | "x"
561
+ | "xai"
562
+ | "xai-x"
563
+ | "anthropic"
564
+ | "meta"
565
+ | (string & {});
566
+
567
+ /** Concrete providers accepted as an explicit `provider` hint (no `native` alias). */
568
+ export const SEARCH_PROVIDER_HINT_IDS = [
569
+ "openai",
570
+ "openai-codex",
571
+ "gemini",
572
+ "brave",
573
+ "exa",
574
+ "parallel",
575
+ "parallel-responses",
576
+ "brave-answers",
577
+ "x",
578
+ "xai",
579
+ "xai-x",
580
+ "anthropic",
581
+ "meta",
582
+ ] as const;
583
+ export type SearchProviderHintId = (typeof SEARCH_PROVIDER_HINT_IDS)[number];
584
+
585
+ /**
586
+ * A normalized error from a provider call. Carries enough to route retries
587
+ * or report to the agent without leaking provider-specific payload shapes.
588
+ */
589
+ export interface ProviderError extends Error {
590
+ readonly provider: ProviderId;
591
+ /** Network failure, auth failure, rate limit, bad request, or availability. */
592
+ readonly kind:
593
+ | "network"
594
+ | "auth"
595
+ | "rateLimit"
596
+ | "badRequest"
597
+ | "malformed"
598
+ | "unsupported"
599
+ | "timeout"
600
+ | "canceled"
601
+ | "http"
602
+ | "unavailable"
603
+ | "unknown";
604
+ /** HTTP status if applicable. */
605
+ readonly status?: number;
606
+ /** Provider request identifier when one exists. */
607
+ readonly requestId?: RequestId;
608
+ /** Provider-advised delay before another call. */
609
+ readonly retryAfterMs?: number;
610
+ /** Provider-reported quota metadata. */
611
+ readonly rateLimits?: ProviderRateLimitInfo;
612
+ /** Whether retrying the same provider could help. */
613
+ readonly retryable: boolean;
614
+ }
615
+
616
+ /**
617
+ * The provider-neutral adapter contract. Both adapter families (D10) implement
618
+ * this: a direct search API maps the request to an HTTP call and parses the
619
+ * response; a model-mediated grounding provider maps it to a grounded model
620
+ * call. The contract is identical to the caller.
621
+ *
622
+ * `fetch` is optional because not every search provider also fetches pages;
623
+ * direct HTTP fetch is handled separately in the `web_fetch` tool.
624
+ */
625
+ export interface Provider {
626
+ readonly id: ProviderId;
627
+ readonly capabilities: ProviderCapabilities;
628
+ readonly profile: ProviderProfile;
629
+ /**
630
+ * Execute a search. Adapters must:
631
+ * - apply supported options and list them in `appliedOptions`;
632
+ * - report unsupported options in `warnings` or throw `ProviderError` with
633
+ * kind `unsupported`;
634
+ * - normalize results to {@link SearchResult};
635
+ * - preserve `url`, `excerpt`, `publishedAt`, `provider`, `searchQuery`;
636
+ * - return evidence and, when supplied by the backend, a typed untrusted
637
+ * answer whose citations align with returned result URLs.
638
+ */
639
+ readonly search: (
640
+ request: SearchRequest,
641
+ signal: AbortSignal,
642
+ context: ProviderContext,
643
+ ) => Promise<SearchResponse>;
644
+ }