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