@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/codex.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Provider,
|
|
3
|
+
ProviderCapabilities,
|
|
4
|
+
ProviderContext,
|
|
5
|
+
ProviderModel,
|
|
6
|
+
ProviderProfile,
|
|
7
|
+
ProviderUsage,
|
|
8
|
+
SearchOption,
|
|
9
|
+
SearchRequest,
|
|
10
|
+
SearchResponse,
|
|
11
|
+
SearchResult,
|
|
12
|
+
SearchWarning,
|
|
13
|
+
} from "./contracts";
|
|
14
|
+
import { createProviderError } from "./errors";
|
|
15
|
+
import { executeGroundedSearch } from "./grounding";
|
|
16
|
+
import { httpSource, objectValue, optionalString, type SearchHttpFetch } from "./provider-http";
|
|
17
|
+
import { modelAuthHeaders, type ModelExecution } from "./model-selection";
|
|
18
|
+
import { validateSearchRequest } from "./search";
|
|
19
|
+
|
|
20
|
+
export const CODEX_SEARCH_ENDPOINT = "https://chatgpt.com/backend-api/codex/alpha/search";
|
|
21
|
+
export const DEFAULT_CODEX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
const MAX_CODEX_ANSWER_LENGTH = 8_000;
|
|
23
|
+
const MAX_CODEX_SOURCE_LENGTH = 8_192;
|
|
24
|
+
|
|
25
|
+
export interface CodexAdapterOptions {
|
|
26
|
+
readonly endpoint?: string;
|
|
27
|
+
readonly fetchImpl?: SearchHttpFetch;
|
|
28
|
+
readonly maxResponseBytes?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const capabilities: ProviderCapabilities = {
|
|
32
|
+
semantic: true,
|
|
33
|
+
freshness: true,
|
|
34
|
+
excerpts: true,
|
|
35
|
+
domainFilter: true,
|
|
36
|
+
nativeGrounding: true,
|
|
37
|
+
searchContextSize: true,
|
|
38
|
+
externalWebAccess: true,
|
|
39
|
+
userLocation: true,
|
|
40
|
+
searchContentTypes: true,
|
|
41
|
+
imageSettings: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const profile: ProviderProfile = {
|
|
45
|
+
auth: "modelRegistry",
|
|
46
|
+
costModel: "unknown",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export interface CodexRequestPlan {
|
|
50
|
+
readonly body: Record<string, unknown>;
|
|
51
|
+
readonly appliedOptions: readonly SearchOption[];
|
|
52
|
+
readonly warnings: readonly SearchWarning[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function endpointFor(model: ProviderModel, override?: string): string {
|
|
56
|
+
const candidate = override ?? (model.baseUrl.trim().length > 0 ? model.baseUrl : CODEX_SEARCH_ENDPOINT);
|
|
57
|
+
let url: URL;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(candidate);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw createProviderError({ provider: "openai-codex", kind: "badRequest", message: "Codex search endpoint is not a valid URL", retryable: false, cause: error });
|
|
62
|
+
}
|
|
63
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
64
|
+
throw createProviderError({ provider: "openai-codex", kind: "badRequest", message: "Codex search endpoint must use HTTP or HTTPS", retryable: false });
|
|
65
|
+
}
|
|
66
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
67
|
+
if (path.endsWith("/alpha/search")) return url.toString();
|
|
68
|
+
const basePath = path.endsWith("/responses") ? path.slice(0, -"/responses".length) : path;
|
|
69
|
+
if (basePath.endsWith("/codex")) url.pathname = `${basePath}/alpha/search`;
|
|
70
|
+
else if (basePath.endsWith("/backend-api")) url.pathname = `${basePath}/codex/alpha/search`;
|
|
71
|
+
else url.pathname = `${basePath}/alpha/search`;
|
|
72
|
+
return url.toString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function userLocation(value: SearchRequest["userLocation"]): Record<string, unknown> | undefined {
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
return {
|
|
78
|
+
type: value.type,
|
|
79
|
+
...(value.country === undefined ? {} : { country: value.country }),
|
|
80
|
+
...(value.region === undefined ? {} : { region: value.region }),
|
|
81
|
+
...(value.city === undefined ? {} : { city: value.city }),
|
|
82
|
+
...(value.timezone === undefined ? {} : { timezone: value.timezone }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildSettings(request: SearchRequest): Record<string, unknown> | undefined {
|
|
87
|
+
const filters = {
|
|
88
|
+
...(request.domains?.include?.length ? { allowed_domains: [...request.domains.include] } : {}),
|
|
89
|
+
...(request.domains?.exclude?.length ? { blocked_domains: [...request.domains.exclude] } : {}),
|
|
90
|
+
};
|
|
91
|
+
const imageSettings = request.imageSettings === undefined ? undefined : {
|
|
92
|
+
...(request.imageSettings.maxResults === undefined ? {} : { max_results: request.imageSettings.maxResults }),
|
|
93
|
+
...(request.imageSettings.caption === undefined ? {} : { caption: request.imageSettings.caption }),
|
|
94
|
+
};
|
|
95
|
+
const location = userLocation(request.userLocation);
|
|
96
|
+
const settings = {
|
|
97
|
+
...(request.searchContextSize === undefined ? {} : { search_context_size: request.searchContextSize }),
|
|
98
|
+
...(Object.keys(filters).length === 0 ? {} : { filters }),
|
|
99
|
+
...(location === undefined ? {} : { user_location: location }),
|
|
100
|
+
...(imageSettings === undefined ? {} : { image_settings: imageSettings }),
|
|
101
|
+
...(request.externalWebAccess === undefined ? {} : { external_web_access: request.externalWebAccess }),
|
|
102
|
+
};
|
|
103
|
+
return Object.keys(settings).length === 0 ? undefined : settings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildCodexRequest(request: SearchRequest): CodexRequestPlan {
|
|
107
|
+
const normalized = validateSearchRequest(request);
|
|
108
|
+
if (normalized.dateRange !== undefined || normalized.social !== undefined) {
|
|
109
|
+
throw createProviderError({ provider: "openai-codex", kind: "unsupported", message: "Codex standalone search does not expose exact date-range or dedicated social/X constraints", retryable: false });
|
|
110
|
+
}
|
|
111
|
+
if (normalized.returnTokenBudget !== undefined) {
|
|
112
|
+
throw createProviderError({ provider: "openai-codex", kind: "unsupported", message: "Codex standalone search does not expose return-token-budget control", retryable: false });
|
|
113
|
+
}
|
|
114
|
+
const warnings: SearchWarning[] = [];
|
|
115
|
+
if (normalized.mode === "keyword") warnings.push({ code: "unsupported-option", option: "mode", message: "Codex standalone search is semantic and does not guarantee keyword-only ranking" });
|
|
116
|
+
if (normalized.mode === "fresh") warnings.push({ code: "unsupported-option", option: "mode", message: "Codex standalone search can use current sources but does not guarantee a freshness-only ranking" });
|
|
117
|
+
const contentTypes = normalized.searchContentTypes ?? ["text"];
|
|
118
|
+
const commands: Record<string, unknown> = {};
|
|
119
|
+
if (contentTypes.includes("text")) commands.search_query = [{
|
|
120
|
+
q: normalized.query,
|
|
121
|
+
...(normalized.mode === "fresh" ? { recency: 7 } : {}),
|
|
122
|
+
...(normalized.domains?.include?.length ? { domains: [...normalized.domains.include] } : {}),
|
|
123
|
+
}];
|
|
124
|
+
if (contentTypes.includes("image")) commands.image_query = [{ q: normalized.query }];
|
|
125
|
+
const settings = buildSettings(normalized);
|
|
126
|
+
const appliedOptions: SearchOption[] = ["maxResults", "mode"];
|
|
127
|
+
if (normalized.domains?.include?.length || normalized.domains?.exclude?.length) appliedOptions.push("domains");
|
|
128
|
+
if (normalized.searchContextSize !== undefined) appliedOptions.push("searchContextSize");
|
|
129
|
+
if (normalized.userLocation !== undefined) appliedOptions.push("userLocation");
|
|
130
|
+
if (normalized.searchContentTypes !== undefined) appliedOptions.push("searchContentTypes");
|
|
131
|
+
if (normalized.imageSettings !== undefined) appliedOptions.push("imageSettings");
|
|
132
|
+
if (normalized.externalWebAccess !== undefined) appliedOptions.push("externalWebAccess");
|
|
133
|
+
return {
|
|
134
|
+
body: {
|
|
135
|
+
id: `pi-search-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
136
|
+
model: "",
|
|
137
|
+
input: normalized.query,
|
|
138
|
+
commands,
|
|
139
|
+
...(settings === undefined ? {} : { settings }),
|
|
140
|
+
max_output_tokens: 2_048,
|
|
141
|
+
},
|
|
142
|
+
appliedOptions,
|
|
143
|
+
warnings,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseUsage(value: unknown): ProviderUsage | undefined {
|
|
148
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
149
|
+
const usage = value as Record<string, unknown>;
|
|
150
|
+
const number = (key: string): number | undefined => typeof usage[key] === "number" && Number.isFinite(usage[key]) && (usage[key] as number) >= 0 ? usage[key] as number : undefined;
|
|
151
|
+
const inputTokens = number("input_tokens");
|
|
152
|
+
const outputTokens = number("output_tokens");
|
|
153
|
+
const totalTokens = number("total_tokens");
|
|
154
|
+
if (inputTokens === undefined && outputTokens === undefined && totalTokens === undefined) return undefined;
|
|
155
|
+
return {
|
|
156
|
+
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
157
|
+
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
158
|
+
...(totalTokens === undefined ? {} : { totalTokens, billedUnits: totalTokens, billedUnit: "tokens" }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sourceFromRecord(value: unknown, query: string): SearchResult | undefined {
|
|
163
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
164
|
+
const record = value as Record<string, unknown>;
|
|
165
|
+
const rawUrl = record.url ?? record.image_url ?? record.source_website_url;
|
|
166
|
+
const parsed = httpSource(rawUrl, "openai-codex");
|
|
167
|
+
if (parsed === undefined || parsed.url.length > MAX_CODEX_SOURCE_LENGTH) return undefined;
|
|
168
|
+
const sourcePage = httpSource(record.source_website_url, "openai-codex");
|
|
169
|
+
const title = optionalString(record.title ?? record.caption, 500);
|
|
170
|
+
const excerpt = optionalString(record.snippet ?? record.text ?? record.description ?? record.caption, 4_000);
|
|
171
|
+
const publishedAt = optionalString(record.published_at ?? record.published_date, 100);
|
|
172
|
+
const sourceId = optionalString(record.ref_id ?? record.id ?? record.source_id, 500);
|
|
173
|
+
return {
|
|
174
|
+
url: parsed.url,
|
|
175
|
+
...(title === undefined ? {} : { title }),
|
|
176
|
+
domain: parsed.domain,
|
|
177
|
+
...(excerpt === undefined ? {} : { excerpt }),
|
|
178
|
+
...(publishedAt === undefined ? {} : { publishedAt }),
|
|
179
|
+
provider: "openai-codex",
|
|
180
|
+
searchQuery: query,
|
|
181
|
+
...(sourceId === undefined ? {} : { sourceId }),
|
|
182
|
+
...(sourcePage === undefined || sourcePage.url === parsed.url ? {} : { sourcePageUrl: sourcePage.url }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function trimExtractedUrl(value: string): string {
|
|
187
|
+
let candidate = value;
|
|
188
|
+
while (candidate.length > 0) {
|
|
189
|
+
const trailing = candidate.match(/[)\]}>.,;:!?\u0027"`]+\/$/);
|
|
190
|
+
if (trailing !== null) {
|
|
191
|
+
candidate = candidate.slice(0, -trailing[0].length);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const stripped = candidate.replace(/[)\]}>.,;:!?\u0027"`]+$/, "");
|
|
195
|
+
if (stripped === candidate) break;
|
|
196
|
+
candidate = stripped;
|
|
197
|
+
}
|
|
198
|
+
return candidate;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function urlsFromText(text: string): readonly string[] {
|
|
202
|
+
return [...text.matchAll(/https?:\/\/[^\s<>\]]+/gi)]
|
|
203
|
+
.map((match) => trimExtractedUrl(match[0]!))
|
|
204
|
+
.filter((url) => url.length > 0)
|
|
205
|
+
.slice(0, 20);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeCodexResponse(payload: unknown, request: SearchRequest): SearchResponse {
|
|
209
|
+
const normalized = validateSearchRequest(request);
|
|
210
|
+
const root = objectValue(payload, "response", "openai-codex");
|
|
211
|
+
const answerText = optionalString(root.output, MAX_CODEX_ANSWER_LENGTH) ?? "";
|
|
212
|
+
const rawResults = Array.isArray(root.results) ? root.results : [];
|
|
213
|
+
const results: SearchResult[] = [];
|
|
214
|
+
const seen = new Set<string>();
|
|
215
|
+
for (const value of rawResults) {
|
|
216
|
+
const result = sourceFromRecord(value, normalized.query);
|
|
217
|
+
if (result !== undefined && !seen.has(result.url)) {
|
|
218
|
+
seen.add(result.url);
|
|
219
|
+
results.push(result);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const url of urlsFromText(answerText)) {
|
|
223
|
+
const parsed = httpSource(url, "openai-codex");
|
|
224
|
+
if (parsed === undefined || seen.has(parsed.url)) continue;
|
|
225
|
+
seen.add(parsed.url);
|
|
226
|
+
results.push({ url: parsed.url, domain: parsed.domain, provider: "openai-codex", searchQuery: normalized.query });
|
|
227
|
+
}
|
|
228
|
+
const limitedResults = results.slice(0, normalized.maxResults ?? 10);
|
|
229
|
+
if (limitedResults.length === 0) throw createProviderError({ provider: "openai-codex", kind: "malformed", message: "Codex standalone search returned no inspectable HTTP sources", retryable: false });
|
|
230
|
+
const byRef = new Map(limitedResults.flatMap((result) => result.sourceId === undefined ? [] : [[result.sourceId, result] as const]));
|
|
231
|
+
const cited = new Set<string>();
|
|
232
|
+
for (const ref of answerText.matchAll(/\bturn\d+(?:search|image)\d+\b/g)) {
|
|
233
|
+
const result = byRef.get(ref[0]);
|
|
234
|
+
if (result !== undefined) cited.add(result.url);
|
|
235
|
+
}
|
|
236
|
+
for (const result of limitedResults) if (answerText.includes(result.url)) cited.add(result.url);
|
|
237
|
+
const citations = limitedResults.filter((result) => cited.has(result.url)).map((result) => ({ url: result.url, ...(result.title === undefined ? {} : { title: result.title }), ...(result.sourceId === undefined ? {} : { sourceId: result.sourceId }) }));
|
|
238
|
+
const answer = normalized.answerMode !== "evidence" && answerText.length > 0 && citations.length > 0
|
|
239
|
+
? { text: answerText, contentTrust: "untrusted" as const, provider: "openai-codex" as const, citations }
|
|
240
|
+
: undefined;
|
|
241
|
+
return {
|
|
242
|
+
query: normalized.query,
|
|
243
|
+
results: limitedResults,
|
|
244
|
+
...(answer === undefined ? {} : { answer }),
|
|
245
|
+
provider: "openai-codex",
|
|
246
|
+
appliedOptions: [],
|
|
247
|
+
warnings: [],
|
|
248
|
+
...(optionalString(root.id, 500) === undefined ? {} : { requestId: optionalString(root.id, 500) }),
|
|
249
|
+
...(parseUsage(root.usage) === undefined ? {} : { usage: parseUsage(root.usage) }),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function codexHeaders(execution: ModelExecution): Readonly<Record<string, string>> {
|
|
254
|
+
const headers = modelAuthHeaders(execution);
|
|
255
|
+
if (execution.auth.apiKey === undefined || execution.auth.apiKey.trim().length === 0) {
|
|
256
|
+
throw createProviderError({ provider: "openai-codex", kind: "auth", message: "Codex authentication returned no token", retryable: false });
|
|
257
|
+
}
|
|
258
|
+
const parts = execution.auth.apiKey.split(".");
|
|
259
|
+
if (parts.length === 3 && parts[1] !== undefined) {
|
|
260
|
+
try {
|
|
261
|
+
const decoded = JSON.parse(Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(parts[1].length / 4) * 4, "="), "base64").toString("utf8")) as Record<string, unknown>;
|
|
262
|
+
const auth = decoded["https://api.openai.com/auth"];
|
|
263
|
+
if (auth !== null && typeof auth === "object" && !Array.isArray(auth)) {
|
|
264
|
+
const accountId = optionalString((auth as Record<string, unknown>).chatgpt_account_id, 500);
|
|
265
|
+
if (accountId !== undefined) headers.set("chatgpt-account-id", accountId);
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// The backend may authenticate non-JWT tokens through headers alone.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
headers.set("originator", "pi");
|
|
272
|
+
headers.set("accept", "application/json");
|
|
273
|
+
headers.set("content-type", "application/json");
|
|
274
|
+
return Object.fromEntries(headers.entries());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export class CodexProvider implements Provider {
|
|
278
|
+
readonly id = "openai-codex" as const;
|
|
279
|
+
readonly capabilities = capabilities;
|
|
280
|
+
readonly profile = profile;
|
|
281
|
+
private readonly endpoint?: string;
|
|
282
|
+
private readonly fetchImpl: SearchHttpFetch;
|
|
283
|
+
private readonly maxResponseBytes: number;
|
|
284
|
+
|
|
285
|
+
constructor(options: CodexAdapterOptions = {}) {
|
|
286
|
+
this.endpoint = options.endpoint;
|
|
287
|
+
this.fetchImpl = options.fetchImpl ?? (fetch as SearchHttpFetch);
|
|
288
|
+
this.maxResponseBytes = options.maxResponseBytes ?? DEFAULT_CODEX_RESPONSE_BYTES;
|
|
289
|
+
if (!Number.isInteger(this.maxResponseBytes) || this.maxResponseBytes < 1) throw new Error("Codex maxResponseBytes must be a positive integer");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async search(request: SearchRequest, signal: AbortSignal, context: ProviderContext): Promise<SearchResponse> {
|
|
293
|
+
const normalized = validateSearchRequest(request);
|
|
294
|
+
const plan = buildCodexRequest(normalized);
|
|
295
|
+
return executeGroundedSearch({
|
|
296
|
+
provider: this.id,
|
|
297
|
+
modelProvider: "openai-codex",
|
|
298
|
+
api: "openai-codex-responses",
|
|
299
|
+
request: normalized,
|
|
300
|
+
signal,
|
|
301
|
+
context,
|
|
302
|
+
fetchImpl: this.fetchImpl,
|
|
303
|
+
maxResponseBytes: this.maxResponseBytes,
|
|
304
|
+
endpointFor: (model) => endpointFor(model, this.endpoint),
|
|
305
|
+
headersFor: codexHeaders,
|
|
306
|
+
plan,
|
|
307
|
+
normalize: normalizeCodexResponse,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createCodexProvider(options: CodexAdapterOptions = {}): CodexProvider {
|
|
313
|
+
return new CodexProvider(options);
|
|
314
|
+
}
|