pi-codex-search 0.1.2 → 0.1.3

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/src/codex.ts CHANGED
@@ -1,203 +1,95 @@
1
- export type SearchContextSize = "low" | "medium" | "high";
2
-
3
- export type CodexErrorKind = "auth" | "rate_limit" | "transport" | "timeout" | "schema" | "unknown";
4
-
5
- export class CodexError extends Error {
6
- readonly kind: CodexErrorKind;
7
- readonly status?: number;
8
-
9
- constructor(kind: CodexErrorKind, message: string, status?: number) {
10
- super(message);
11
- this.name = "CodexError";
12
- this.kind = kind;
13
- if (status !== undefined) this.status = status;
14
- }
15
- }
16
-
17
- export function classifyError(error: unknown): CodexErrorKind {
18
- if (error instanceof CodexError) return error.kind;
19
- if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
20
- return "timeout";
21
- }
22
- return "unknown";
23
- }
24
-
25
- function classifyHttpStatus(status: number): CodexErrorKind {
26
- if (status === 401 || status === 403) return "auth";
27
- if (status === 429) return "rate_limit";
28
- return "transport";
29
- }
30
-
31
- function classifyEventErrorMessage(message: string): CodexErrorKind {
32
- const lower = message.toLowerCase();
33
- if (/rate[- ]?limit|too many requests|quota|429/.test(lower)) return "rate_limit";
34
- if (/auth|unauthori[sz]ed|forbidden|401|403/.test(lower)) return "auth";
35
- if (/timeout|timed out/.test(lower)) return "timeout";
36
- if (/network|connection|disconnect|transport|fetch failed/.test(lower)) return "transport";
37
- return "unknown";
38
- }
39
-
40
- export interface CodexModel {
41
- id: string;
42
- name?: string;
43
- isDefault?: boolean;
44
- }
45
-
46
- export interface CodexWebSearchOptions {
47
- query: string;
1
+ export {
2
+ CodexError,
3
+ classifyError,
4
+ classifyHttpStatus,
5
+ classifyEventErrorMessage,
6
+ } from "./errors.ts";
7
+ export type { CodexErrorKind } from "./errors.ts";
8
+ export type {
9
+ CodexCitation,
10
+ CodexSearchCall,
11
+ CodexWebSearchResult,
12
+ Freshness,
13
+ ResponseLength,
14
+ SearchContextSize,
15
+ StandaloneExternalWebAccess,
16
+ } from "./modes/types.ts";
17
+ export type { CodexModel } from "./modes/types.ts";
18
+ export { runResponsesSearch } from "./modes/responses.ts";
19
+ export {
20
+ runStandaloneCommands,
21
+ externalWebAccessForFreshness,
22
+ hasAnyCommand,
23
+ assertSupportedStandaloneCombination,
24
+ isUnsupportedStandaloneCombination,
25
+ } from "./modes/standalone.ts";
26
+ export type {
27
+ SearchQuery,
28
+ OpenCommand,
29
+ FindCommand,
30
+ ClickCommand,
31
+ ScreenshotCommand,
32
+ FinanceCommand,
33
+ WeatherCommand,
34
+ SportsCommand,
35
+ TimeCommand,
36
+ StandaloneCommandsOptions,
37
+ } from "./modes/standalone.ts";
38
+ export {
39
+ createTransport,
40
+ normalizeCodexBaseUrl,
41
+ resolveCodexEndpoint,
42
+ resolveCodexSearchEndpoint,
43
+ } from "./transport.ts";
44
+ export type { CodexTransport } from "./transport.ts";
45
+ export { createRefStore } from "./ref-store.ts";
46
+ export type { RefStore } from "./ref-store.ts";
47
+ export { buildCodexUserAgent, getCodexOriginator } from "./ua.ts";
48
+ export {
49
+ getSharedCookieStore,
50
+ wrapFetchWithCookies,
51
+ ChatGptCloudflareCookieStore,
52
+ } from "./cookies.ts";
53
+ export type { FetchLike } from "./cookies.ts";
54
+
55
+ export interface FetchCodexModelsOptions {
48
56
  token: string;
49
57
  accountId: string;
50
- model: string;
51
58
  baseUrl?: string;
52
- externalWebAccess?: boolean;
53
- searchContextSize?: SearchContextSize;
59
+ clientVersion?: string;
54
60
  signal?: AbortSignal;
55
- onTextDelta?: (delta: string) => void;
56
61
  fetchImpl?: typeof fetch;
57
62
  }
58
63
 
59
- export interface CodexCitation {
60
- title?: string;
61
- url: string;
62
- startIndex?: number;
63
- endIndex?: number;
64
- }
65
-
66
- export interface CodexSearchCall {
67
- id?: string;
68
- status?: string;
69
- query?: string;
70
- url?: string;
71
- actionType?: string;
72
- }
73
-
74
- export interface CodexWebSearchResult {
75
- responseId?: string;
76
- model: string;
77
- text: string;
78
- searchCalls: CodexSearchCall[];
79
- citations: CodexCitation[];
80
- usage?: {
81
- inputTokens?: number;
82
- outputTokens?: number;
83
- totalTokens?: number;
84
- };
85
- }
86
-
87
- interface SseEvent {
88
- type: string;
89
- data?: unknown;
90
- raw?: string;
91
- }
92
-
93
- interface ResponseOutputText {
94
- type?: string;
95
- text?: string;
96
- annotations?: Array<{
97
- type?: string;
98
- title?: string;
99
- url?: string;
100
- start_index?: number;
101
- end_index?: number;
102
- }>;
103
- }
104
-
105
- interface ResponseOutputItem {
106
- id?: string;
107
- type?: string;
108
- status?: string;
109
- role?: string;
110
- action?: {
111
- type?: string;
112
- query?: string;
113
- queries?: string[];
114
- url?: string;
115
- };
116
- content?: ResponseOutputText[];
117
- }
118
-
119
- interface ResponseUsage {
120
- input_tokens?: number;
121
- output_tokens?: number;
122
- total_tokens?: number;
123
- }
124
-
125
- interface ResponseEnvelope {
126
- id?: string;
127
- usage?: ResponseUsage;
128
- }
129
-
130
- interface ResponseEventData {
131
- response?: ResponseEnvelope;
132
- item?: ResponseOutputItem;
133
- delta?: string;
134
- error?: {
135
- message?: string;
136
- code?: string;
137
- };
138
- }
139
-
140
- const DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
141
- const DEFAULT_CLIENT_VERSION = "1.0.0";
142
- const ACCOUNT_ID_CLAIM = "https://api.openai.com/auth";
143
-
144
- export function normalizeCodexBaseUrl(baseUrl: string | undefined): string {
145
- const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_BASE_URL;
146
- const normalized = raw.replace(/\/+$/, "");
147
- if (normalized.endsWith("/codex/responses"))
148
- return normalized.slice(0, -"/codex/responses".length);
149
- if (normalized.endsWith("/codex")) return normalized.slice(0, -"/codex".length);
150
- return normalized;
151
- }
152
-
153
- export function resolveCodexEndpoint(
154
- baseUrl: string | undefined,
155
- path: "models" | "responses",
156
- ): string {
157
- return `${normalizeCodexBaseUrl(baseUrl)}/codex/${path}`;
158
- }
159
-
160
- export function extractAccountIdFromToken(token: string): string | undefined {
161
- const parts = token.split(".");
162
- if (parts.length !== 3) return undefined;
163
-
164
- try {
165
- const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8")) as {
166
- [ACCOUNT_ID_CLAIM]?: { chatgpt_account_id?: unknown };
167
- };
168
- const accountId = payload[ACCOUNT_ID_CLAIM]?.chatgpt_account_id;
169
- return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
170
- } catch {
171
- return undefined;
172
- }
173
- }
64
+ export async function fetchCodexModels(
65
+ options: FetchCodexModelsOptions,
66
+ ): Promise<import("./modes/types.ts").CodexModel[]> {
67
+ const { CodexError, classifyHttpStatus } = await import("./errors.ts");
68
+ const { createTransport } = await import("./transport.ts");
69
+ const transport = createTransport({
70
+ token: options.token,
71
+ accountId: options.accountId,
72
+ baseUrl: options.baseUrl,
73
+ fetchImpl: options.fetchImpl as typeof fetch,
74
+ });
174
75
 
175
- export async function fetchCodexModels(options: {
176
- token: string;
177
- accountId: string;
178
- baseUrl?: string;
179
- clientVersion?: string;
180
- fetchImpl?: typeof fetch;
181
- signal?: AbortSignal;
182
- }): Promise<CodexModel[]> {
183
- const fetcher = options.fetchImpl ?? fetch;
184
- const endpoint = new URL(resolveCodexEndpoint(options.baseUrl, "models"));
76
+ const endpoint = new URL(transport.resolveEndpoint("models"));
185
77
  endpoint.searchParams.set(
186
78
  "client_version",
187
- options.clientVersion ??
188
- process.env.PI_CODEX_WEB_SEARCH_CLIENT_VERSION ??
189
- DEFAULT_CLIENT_VERSION,
79
+ options.clientVersion ?? process.env.PI_CODEX_WEB_SEARCH_CLIENT_VERSION ?? "1.0.0",
190
80
  );
191
81
 
192
- const response = await fetcher(endpoint.toString(), {
193
- headers: buildCodexHeaders(options.token, options.accountId, "application/json"),
82
+ const response = await transport.fetch(endpoint.toString(), {
83
+ headers: transport.buildHeaders("application/json"),
194
84
  signal: options.signal,
195
85
  });
86
+
196
87
  if (!response.ok) {
197
88
  const status = response.status;
89
+ const text = await response.text();
198
90
  throw new CodexError(
199
91
  classifyHttpStatus(status),
200
- `Codex models request failed: HTTP ${status} ${await response.text()}`,
92
+ `Codex models request failed: HTTP ${status}: ${text}`,
201
93
  status,
202
94
  );
203
95
  }
@@ -220,221 +112,23 @@ export async function fetchCodexModels(options: {
220
112
  .filter((model) => model.id.length > 0);
221
113
  }
222
114
 
223
- export function selectDefaultModel(models: CodexModel[]): string | undefined {
115
+ export function selectDefaultModel(
116
+ models: import("./modes/types.ts").CodexModel[],
117
+ ): string | undefined {
224
118
  return (models.find((model) => model.isDefault) ?? models[0])?.id;
225
119
  }
226
120
 
227
- export async function fetchCodexWebSearch(
228
- options: CodexWebSearchOptions,
229
- ): Promise<CodexWebSearchResult> {
230
- const fetcher = options.fetchImpl ?? fetch;
231
- const response = await fetcher(resolveCodexEndpoint(options.baseUrl, "responses"), {
232
- method: "POST",
233
- headers: buildCodexHeaders(options.token, options.accountId, "text/event-stream"),
234
- body: JSON.stringify(buildWebSearchRequestBody(options)),
235
- signal: options.signal,
236
- });
237
-
238
- if (!response.ok) {
239
- const status = response.status;
240
- throw new CodexError(
241
- classifyHttpStatus(status),
242
- `Codex web search request failed: HTTP ${status} ${await response.text()}`,
243
- status,
244
- );
245
- }
246
- if (!response.body) {
247
- throw new CodexError("transport", "Codex web search response did not include a body");
248
- }
249
-
250
- let responseId: string | undefined;
251
- let usage: ResponseUsage | undefined;
252
- let streamedText = "";
253
- const messageTextParts: string[] = [];
254
- const searchCalls = new Map<string, CodexSearchCall>();
255
- const citations = new Map<string, CodexCitation>();
256
-
257
- for await (const event of parseSse(response.body)) {
258
- const data = event.data as ResponseEventData | undefined;
259
- if (!data) continue;
260
-
261
- if (event.type === "response.created") {
262
- responseId = data.response?.id;
263
- continue;
264
- }
265
-
266
- if (event.type === "response.output_text.delta") {
267
- const delta = data.delta ?? "";
268
- streamedText += delta;
269
- options.onTextDelta?.(delta);
270
- continue;
271
- }
272
-
273
- if (event.type === "response.output_item.added" && data.item?.type === "web_search_call") {
274
- const item = data.item;
275
- searchCalls.set(item.id ?? `search-${searchCalls.size + 1}`, {
276
- id: item.id,
277
- status: item.status,
278
- });
279
- continue;
280
- }
281
-
282
- if (event.type === "response.output_item.done") {
283
- collectOutputItem(data.item, searchCalls, messageTextParts, citations);
284
- continue;
285
- }
286
-
287
- if (event.type === "response.completed") {
288
- usage = data.response?.usage;
289
- continue;
290
- }
291
-
292
- if (event.type === "response.failed") {
293
- const message = data.error?.message ?? data.error?.code ?? "Codex web search failed";
294
- throw new CodexError(classifyEventErrorMessage(message), message);
295
- }
296
- }
297
-
298
- return {
299
- responseId,
300
- model: options.model,
301
- text: messageTextParts.join("") || streamedText,
302
- searchCalls: [...searchCalls.values()],
303
- citations: [...citations.values()],
304
- usage: usage
305
- ? {
306
- inputTokens: usage.input_tokens,
307
- outputTokens: usage.output_tokens,
308
- totalTokens: usage.total_tokens,
309
- }
310
- : undefined,
311
- };
312
- }
313
-
314
- function buildCodexHeaders(token: string, accountId: string, accept: string): Headers {
315
- const headers = new Headers();
316
- headers.set("Authorization", `Bearer ${token}`);
317
- headers.set("chatgpt-account-id", accountId);
318
- headers.set("originator", "pi");
319
- headers.set("OpenAI-Beta", "responses=experimental");
320
- headers.set("accept", accept);
321
- if (accept === "text/event-stream") {
322
- headers.set("content-type", "application/json");
323
- }
324
- headers.set("User-Agent", "pi-codex-search");
325
- return headers;
326
- }
327
-
328
- function buildWebSearchRequestBody(options: CodexWebSearchOptions) {
329
- return {
330
- model: options.model,
331
- instructions:
332
- "You are a concise web search assistant. Use web search, answer the query, and preserve source citations from annotations.",
333
- input: [
334
- {
335
- type: "message",
336
- role: "user",
337
- content: [{ type: "input_text", text: options.query }],
338
- },
339
- ],
340
- tools: [
341
- {
342
- type: "web_search",
343
- external_web_access: options.externalWebAccess ?? true,
344
- search_context_size: options.searchContextSize ?? "medium",
345
- },
346
- ],
347
- tool_choice: "required",
348
- parallel_tool_calls: true,
349
- store: false,
350
- stream: true,
351
- include: [],
352
- };
353
- }
354
-
355
- async function* parseSse(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent> {
356
- const reader = body.getReader();
357
- const decoder = new TextDecoder();
358
- let buffer = "";
359
-
360
- while (true) {
361
- const { done, value } = await reader.read();
362
- if (done) break;
363
- buffer += decoder.decode(value, { stream: true });
364
-
365
- let separatorIndex = buffer.indexOf("\n\n");
366
- while (separatorIndex !== -1) {
367
- const frame = buffer.slice(0, separatorIndex);
368
- buffer = buffer.slice(separatorIndex + 2);
369
- const event = parseSseFrame(frame);
370
- if (event) yield event;
371
- separatorIndex = buffer.indexOf("\n\n");
372
- }
373
- }
374
-
375
- buffer += decoder.decode();
376
- const event = parseSseFrame(buffer);
377
- if (event) yield event;
378
- }
379
-
380
- function parseSseFrame(frame: string): SseEvent | undefined {
381
- const lines = frame.split(/\r?\n/);
382
- let type = "";
383
- const dataLines: string[] = [];
384
-
385
- for (const line of lines) {
386
- if (line.startsWith("event:")) {
387
- type = line.slice("event:".length).trim();
388
- } else if (line.startsWith("data:")) {
389
- dataLines.push(line.slice("data:".length).trimStart());
390
- }
391
- }
392
-
393
- if (dataLines.length === 0) return undefined;
394
- const raw = dataLines.join("\n");
395
- if (raw === "[DONE]") return undefined;
121
+ export function extractAccountIdFromToken(token: string): string | undefined {
122
+ const parts = token.split(".");
123
+ if (parts.length !== 3) return undefined;
396
124
 
397
125
  try {
398
- return { type, data: JSON.parse(raw) };
126
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8")) as {
127
+ "https://api.openai.com/auth"?: { chatgpt_account_id?: unknown };
128
+ };
129
+ const accountId = payload["https://api.openai.com/auth"]?.chatgpt_account_id;
130
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
399
131
  } catch {
400
- return { type, raw };
401
- }
402
- }
403
-
404
- function collectOutputItem(
405
- item: ResponseOutputItem | undefined,
406
- searchCalls: Map<string, CodexSearchCall>,
407
- messageTextParts: string[],
408
- citations: Map<string, CodexCitation>,
409
- ): void {
410
- if (!item) return;
411
-
412
- if (item.type === "web_search_call") {
413
- const key = item.id ?? `search-${searchCalls.size + 1}`;
414
- const query = item.action?.query ?? item.action?.queries?.join(", ");
415
- searchCalls.set(key, {
416
- id: item.id,
417
- status: item.status,
418
- query,
419
- url: item.action?.url,
420
- actionType: item.action?.type,
421
- });
422
- return;
423
- }
424
-
425
- if (item.type !== "message" || item.role !== "assistant") return;
426
-
427
- for (const part of item.content ?? []) {
428
- if (part.type !== "output_text") continue;
429
- messageTextParts.push(part.text ?? "");
430
- for (const annotation of part.annotations ?? []) {
431
- if (annotation.type !== "url_citation" || !annotation.url) continue;
432
- citations.set(annotation.url, {
433
- title: annotation.title,
434
- url: annotation.url,
435
- startIndex: annotation.start_index,
436
- endIndex: annotation.end_index,
437
- });
438
- }
132
+ return undefined;
439
133
  }
440
134
  }