@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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ProviderId, ProviderRateLimitInfo, ProviderRateLimitWindow } from "./contracts";
|
|
2
|
+
import { createProviderError, isProviderError } from "./errors";
|
|
3
|
+
import { cancelResponseBody, readBoundedResponseText } from "./http";
|
|
4
|
+
import { normalizeSearchUrl } from "./search-cleanup";
|
|
5
|
+
|
|
6
|
+
export type SearchHttpFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_SEARCH_PROVIDER_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export interface JsonRequestOptions {
|
|
11
|
+
readonly provider: ProviderId;
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
14
|
+
readonly body?: unknown;
|
|
15
|
+
readonly method?: "GET" | "POST";
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
readonly fetchImpl: SearchHttpFetch;
|
|
18
|
+
readonly maxResponseBytes?: number;
|
|
19
|
+
readonly rateLimits?: ProviderRateLimitInfo;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface JsonResponse {
|
|
23
|
+
readonly payload: unknown;
|
|
24
|
+
readonly requestId?: string;
|
|
25
|
+
readonly retryAfterMs?: number;
|
|
26
|
+
readonly rateLimits?: ProviderRateLimitInfo;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function retryAfterMs(headers: Headers): number | undefined {
|
|
30
|
+
const milliseconds = headers.get("retry-after-ms");
|
|
31
|
+
if (milliseconds !== null && /^\d+(?:\.\d+)?$/.test(milliseconds.trim())) {
|
|
32
|
+
const value = Number(milliseconds);
|
|
33
|
+
if (Number.isFinite(value)) return Math.max(0, Math.round(value));
|
|
34
|
+
}
|
|
35
|
+
const retryAfter = headers.get("retry-after");
|
|
36
|
+
if (retryAfter === null) return undefined;
|
|
37
|
+
if (/^\d+(?:\.\d+)?$/.test(retryAfter.trim())) {
|
|
38
|
+
const value = Number(retryAfter);
|
|
39
|
+
return Number.isFinite(value) ? Math.max(0, Math.round(value * 1_000)) : undefined;
|
|
40
|
+
}
|
|
41
|
+
const date = Date.parse(retryAfter);
|
|
42
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Retry delay from provider headers, in milliseconds, when present. */
|
|
46
|
+
export function retryAfterMsFromHeaders(headers: Headers): number | undefined {
|
|
47
|
+
return retryAfterMs(headers);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nonNegativeHeaderNumber(value: string | null): number | undefined {
|
|
51
|
+
if (value === null || !/^\d+(?:\.\d+)?$/.test(value.trim())) return undefined;
|
|
52
|
+
const parsed = Number(value);
|
|
53
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resetFromHeader(value: string | null): Pick<ProviderRateLimitWindow, "resetAt" | "resetAfterMs"> {
|
|
57
|
+
const parsed = nonNegativeHeaderNumber(value);
|
|
58
|
+
if (parsed === undefined) return {};
|
|
59
|
+
if (parsed >= 1_000_000_000_000) {
|
|
60
|
+
const date = new Date(parsed);
|
|
61
|
+
return Number.isFinite(date.getTime()) ? { resetAt: date.toISOString() } : {};
|
|
62
|
+
}
|
|
63
|
+
if (parsed >= 1_000_000_000) {
|
|
64
|
+
const date = new Date(parsed * 1_000);
|
|
65
|
+
return Number.isFinite(date.getTime()) ? { resetAt: date.toISOString() } : {};
|
|
66
|
+
}
|
|
67
|
+
return { resetAfterMs: Math.round(parsed * 1_000) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parse standard quota headers without assuming provider-specific plans. */
|
|
71
|
+
export function parseProviderRateLimits(headers: Headers): ProviderRateLimitInfo | undefined {
|
|
72
|
+
const header = (standard: string, xApi: string): string => headers.get(standard) ?? headers.get(xApi) ?? "";
|
|
73
|
+
const limits = header("x-ratelimit-limit", "x-rate-limit-limit").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
|
|
74
|
+
const remaining = header("x-ratelimit-remaining", "x-rate-limit-remaining").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
|
|
75
|
+
const resets = header("x-ratelimit-reset", "x-rate-limit-reset").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
|
|
76
|
+
const windows: ProviderRateLimitWindow[] = [];
|
|
77
|
+
const count = Math.max(limits.length, remaining.length, resets.length);
|
|
78
|
+
for (let index = 0; index < count; index += 1) {
|
|
79
|
+
const limit = nonNegativeHeaderNumber(limits[index] ?? null);
|
|
80
|
+
const left = nonNegativeHeaderNumber(remaining[index] ?? null);
|
|
81
|
+
const reset = resetFromHeader(resets[index] ?? null);
|
|
82
|
+
if (limit === undefined && left === undefined && Object.keys(reset).length === 0) continue;
|
|
83
|
+
windows.push({
|
|
84
|
+
...(limit === undefined ? {} : { limit }),
|
|
85
|
+
...(left === undefined ? {} : { remaining: left }),
|
|
86
|
+
...reset,
|
|
87
|
+
scope: `window-${index}`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const retryAfter = retryAfterMs(headers);
|
|
91
|
+
if (windows.length === 0 && retryAfter === undefined) return undefined;
|
|
92
|
+
return { windows, ...(retryAfter === undefined ? {} : { retryAfterMs: retryAfter }) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requestMetadata(response: Response): Pick<JsonResponse, "requestId" | "retryAfterMs" | "rateLimits"> {
|
|
96
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined;
|
|
97
|
+
const delay = retryAfterMs(response.headers);
|
|
98
|
+
const rateLimits = parseProviderRateLimits(response.headers);
|
|
99
|
+
return {
|
|
100
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
101
|
+
...(delay === undefined ? {} : { retryAfterMs: delay }),
|
|
102
|
+
...(rateLimits === undefined ? {} : { rateLimits }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Perform one bounded JSON request. It never retries or follows provider fallbacks. */
|
|
107
|
+
export async function postJson(options: JsonRequestOptions): Promise<JsonResponse> {
|
|
108
|
+
if (options.signal.aborted) {
|
|
109
|
+
throw createProviderError({ provider: options.provider, kind: "canceled", message: "Search canceled", retryable: false });
|
|
110
|
+
}
|
|
111
|
+
let response: Response;
|
|
112
|
+
const method = options.method ?? "POST";
|
|
113
|
+
try {
|
|
114
|
+
response = await options.fetchImpl(options.url, {
|
|
115
|
+
method,
|
|
116
|
+
headers: {
|
|
117
|
+
...options.headers,
|
|
118
|
+
accept: "application/json",
|
|
119
|
+
...(method === "POST" ? { "content-type": "application/json" } : {}),
|
|
120
|
+
},
|
|
121
|
+
...(options.body === undefined ? {} : { body: method === "POST" ? JSON.stringify(options.body) : undefined }),
|
|
122
|
+
signal: options.signal,
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (options.signal.aborted) {
|
|
126
|
+
throw createProviderError({ provider: options.provider, kind: "canceled", message: "Search canceled", retryable: false, cause: error });
|
|
127
|
+
}
|
|
128
|
+
if (isProviderError(error)) throw error;
|
|
129
|
+
throw createProviderError({ provider: options.provider, kind: "network", message: `${options.provider} search network request failed`, retryable: true, cause: error });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const metadata = requestMetadata(response);
|
|
133
|
+
if (response.status === 401 || response.status === 403) {
|
|
134
|
+
await cancelResponseBody(response);
|
|
135
|
+
throw createProviderError({ provider: options.provider, kind: "auth", message: `${options.provider} search authentication failed (HTTP ${response.status})`, status: response.status, retryable: false, ...metadata, rateLimits: options.rateLimits ?? metadata.rateLimits });
|
|
136
|
+
}
|
|
137
|
+
if (response.status === 429) {
|
|
138
|
+
await cancelResponseBody(response);
|
|
139
|
+
throw createProviderError({ provider: options.provider, kind: "rateLimit", message: `${options.provider} search rate limit exceeded`, status: response.status, retryable: true, ...metadata, rateLimits: options.rateLimits ?? metadata.rateLimits });
|
|
140
|
+
}
|
|
141
|
+
if (response.status < 200 || response.status >= 300) {
|
|
142
|
+
await cancelResponseBody(response);
|
|
143
|
+
throw createProviderError({
|
|
144
|
+
provider: options.provider,
|
|
145
|
+
kind: response.status === 400 || response.status === 422 ? "badRequest" : "http",
|
|
146
|
+
message: `${options.provider} search failed with HTTP ${response.status}`,
|
|
147
|
+
status: response.status,
|
|
148
|
+
retryable: response.status === 408 || response.status === 425 || response.status >= 500,
|
|
149
|
+
...metadata,
|
|
150
|
+
rateLimits: options.rateLimits ?? metadata.rateLimits,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const text = await readBoundedResponseText(response, options.maxResponseBytes ?? DEFAULT_SEARCH_PROVIDER_RESPONSE_BYTES, options.signal);
|
|
156
|
+
if (text.trim().length === 0) {
|
|
157
|
+
throw createProviderError({ provider: options.provider, kind: "malformed", message: `${options.provider} returned an empty response`, retryable: false, ...metadata });
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
payload: JSON.parse(text) as unknown,
|
|
161
|
+
...metadata,
|
|
162
|
+
...(options.rateLimits === undefined && metadata.rateLimits === undefined ? {} : { rateLimits: options.rateLimits ?? metadata.rateLimits }),
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (options.signal.aborted) {
|
|
166
|
+
throw createProviderError({ provider: options.provider, kind: "canceled", message: "Search canceled", retryable: false, cause: error, ...metadata });
|
|
167
|
+
}
|
|
168
|
+
if (isProviderError(error)) throw error;
|
|
169
|
+
throw createProviderError({ provider: options.provider, kind: "malformed", message: `${options.provider} returned malformed or oversized JSON`, retryable: false, cause: error, ...metadata });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getJson(options: Omit<JsonRequestOptions, "body" | "method">): Promise<JsonResponse> {
|
|
174
|
+
return postJson({ ...options, method: "GET" });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function requireApiKey(provider: ProviderId, apiKey: string | undefined): string {
|
|
178
|
+
if (apiKey === undefined || apiKey.trim().length === 0) {
|
|
179
|
+
throw createProviderError({ provider, kind: "auth", message: `${provider} API key is not configured`, retryable: false });
|
|
180
|
+
}
|
|
181
|
+
return apiKey;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function objectValue(value: unknown, label: string, provider: ProviderId): Record<string, unknown> {
|
|
185
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
186
|
+
throw createProviderError({ provider, kind: "malformed", message: `${provider} returned malformed response (${label} is not an object)`, retryable: false });
|
|
187
|
+
}
|
|
188
|
+
return value as Record<string, unknown>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function optionalString(value: unknown, maxLength = Number.POSITIVE_INFINITY): string | undefined {
|
|
192
|
+
return typeof value === "string" && value.trim().length > 0 ? value.slice(0, maxLength) : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function httpSource(value: unknown, _provider: ProviderId): { url: string; domain: string } | undefined {
|
|
196
|
+
const normalized = normalizeSearchUrl(value);
|
|
197
|
+
return normalized === undefined ? undefined : { url: normalized.url, domain: normalized.domain };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function optionalTimestamp(value: unknown, maxLength = 100): string | undefined {
|
|
201
|
+
const candidate = optionalString(value, maxLength);
|
|
202
|
+
return candidate !== undefined && Number.isFinite(Date.parse(candidate)) ? candidate : undefined;
|
|
203
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared whitespace-compaction for model-visible tool output. Bounded text is
|
|
3
|
+
* the trust fence for untrusted provider content; every renderer uses this so
|
|
4
|
+
* bounds stay consistent across tools.
|
|
5
|
+
*/
|
|
6
|
+
export function compactText(value: string, maxLength: number): string {
|
|
7
|
+
return value.replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
8
|
+
}
|