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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodexError,
|
|
3
|
+
classifyEventErrorMessage,
|
|
4
|
+
classifyHttpStatus,
|
|
5
|
+
formatHttpErrorBody,
|
|
6
|
+
} from "../errors.ts";
|
|
7
|
+
import type { CodexTransport } from "../transport.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CodexWebSearchResult,
|
|
10
|
+
CodexCitation,
|
|
11
|
+
CodexSearchCall,
|
|
12
|
+
SearchContextSize,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
export interface ResponsesSearchOptions {
|
|
16
|
+
query: string;
|
|
17
|
+
model: string;
|
|
18
|
+
transport: CodexTransport;
|
|
19
|
+
externalWebAccess: boolean;
|
|
20
|
+
indexGatedWebAccess?: true;
|
|
21
|
+
searchContextSize?: SearchContextSize;
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
threadId?: string;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
onTextDelta?: (delta: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SseEvent {
|
|
29
|
+
type: string;
|
|
30
|
+
data?: unknown;
|
|
31
|
+
raw?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ResponseOutputText {
|
|
35
|
+
type?: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
annotations?: Array<{
|
|
38
|
+
type?: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
start_index?: number;
|
|
42
|
+
end_index?: number;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ResponseOutputItem {
|
|
47
|
+
id?: string;
|
|
48
|
+
type?: string;
|
|
49
|
+
status?: string;
|
|
50
|
+
role?: string;
|
|
51
|
+
action?: {
|
|
52
|
+
type?: string;
|
|
53
|
+
query?: string;
|
|
54
|
+
queries?: string[];
|
|
55
|
+
url?: string;
|
|
56
|
+
};
|
|
57
|
+
content?: ResponseOutputText[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ResponseUsage {
|
|
61
|
+
input_tokens?: number;
|
|
62
|
+
output_tokens?: number;
|
|
63
|
+
total_tokens?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ResponseEnvelope {
|
|
67
|
+
id?: string;
|
|
68
|
+
usage?: ResponseUsage;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ResponseEventData {
|
|
72
|
+
response?: ResponseEnvelope;
|
|
73
|
+
item?: ResponseOutputItem;
|
|
74
|
+
delta?: string;
|
|
75
|
+
error?: {
|
|
76
|
+
message?: string;
|
|
77
|
+
code?: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runResponsesSearch(
|
|
82
|
+
options: ResponsesSearchOptions,
|
|
83
|
+
): Promise<CodexWebSearchResult> {
|
|
84
|
+
const {
|
|
85
|
+
transport,
|
|
86
|
+
query,
|
|
87
|
+
model,
|
|
88
|
+
externalWebAccess,
|
|
89
|
+
searchContextSize,
|
|
90
|
+
sessionId,
|
|
91
|
+
threadId,
|
|
92
|
+
signal,
|
|
93
|
+
onTextDelta,
|
|
94
|
+
} = options;
|
|
95
|
+
const headers = transport.buildHeaders("text/event-stream");
|
|
96
|
+
if (sessionId) headers.set("session-id", sessionId);
|
|
97
|
+
if (threadId) {
|
|
98
|
+
headers.set("thread-id", threadId);
|
|
99
|
+
headers.set("x-client-request-id", threadId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const webSearchTool: Record<string, unknown> = {
|
|
103
|
+
type: "web_search",
|
|
104
|
+
external_web_access: externalWebAccess,
|
|
105
|
+
search_context_size: searchContextSize ?? "medium",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const response = await transport.fetch(transport.resolveEndpoint("responses"), {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
model,
|
|
113
|
+
instructions:
|
|
114
|
+
"You are a concise web search assistant. Use web search, answer the query, and preserve source citations from annotations.",
|
|
115
|
+
input: [
|
|
116
|
+
{
|
|
117
|
+
type: "message",
|
|
118
|
+
role: "user",
|
|
119
|
+
content: [{ type: "input_text", text: query }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
tools: [webSearchTool],
|
|
123
|
+
tool_choice: "required",
|
|
124
|
+
parallel_tool_calls: true,
|
|
125
|
+
store: false,
|
|
126
|
+
stream: true,
|
|
127
|
+
include: [],
|
|
128
|
+
}),
|
|
129
|
+
signal,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const status = response.status;
|
|
134
|
+
const text = formatHttpErrorBody(await response.text(), "responses");
|
|
135
|
+
throw new CodexError(
|
|
136
|
+
classifyHttpStatus(status),
|
|
137
|
+
`Codex responses request failed: HTTP ${status}: ${text}`,
|
|
138
|
+
status,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!response.body) {
|
|
142
|
+
throw new Error("Codex responses response did not include a body");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let responseId: string | undefined;
|
|
146
|
+
let usage: ResponseUsage | undefined;
|
|
147
|
+
let streamedText = "";
|
|
148
|
+
const messageTextParts: string[] = [];
|
|
149
|
+
const searchCalls = new Map<string, CodexSearchCall>();
|
|
150
|
+
const citations = new Map<string, CodexCitation>();
|
|
151
|
+
|
|
152
|
+
for await (const event of parseSse(response.body)) {
|
|
153
|
+
const data = event.data as ResponseEventData | undefined;
|
|
154
|
+
if (!data) continue;
|
|
155
|
+
|
|
156
|
+
if (event.type === "response.created") {
|
|
157
|
+
responseId = data.response?.id;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (event.type === "response.output_text.delta") {
|
|
162
|
+
const delta = data.delta ?? "";
|
|
163
|
+
streamedText += delta;
|
|
164
|
+
onTextDelta?.(delta);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.type === "response.output_item.added" && data.item?.type === "web_search_call") {
|
|
169
|
+
const item = data.item;
|
|
170
|
+
if (item.id) {
|
|
171
|
+
searchCalls.set(item.id, {
|
|
172
|
+
id: item.id,
|
|
173
|
+
status: item.status,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (event.type === "response.output_item.done") {
|
|
180
|
+
collectOutputItem(data.item, searchCalls, messageTextParts, citations);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (event.type === "response.completed") {
|
|
185
|
+
usage = data.response?.usage;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (event.type === "response.failed") {
|
|
190
|
+
const message = data.error?.message ?? data.error?.code ?? "Codex web search failed";
|
|
191
|
+
throw new CodexError(classifyEventErrorMessage(message), message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
responseId,
|
|
197
|
+
model,
|
|
198
|
+
text: messageTextParts.join("") || streamedText,
|
|
199
|
+
searchCalls: [...searchCalls.values()],
|
|
200
|
+
citations: [...citations.values()],
|
|
201
|
+
usage: usage
|
|
202
|
+
? {
|
|
203
|
+
inputTokens: usage.input_tokens,
|
|
204
|
+
outputTokens: usage.output_tokens,
|
|
205
|
+
totalTokens: usage.total_tokens,
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function* parseSse(body: ReadableStream<Uint8Array>): AsyncGenerator<SseEvent> {
|
|
212
|
+
const reader = body.getReader();
|
|
213
|
+
const decoder = new TextDecoder();
|
|
214
|
+
let buffer = "";
|
|
215
|
+
|
|
216
|
+
let doneReading = false;
|
|
217
|
+
try {
|
|
218
|
+
while (true) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done) {
|
|
221
|
+
doneReading = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
buffer += decoder.decode(value, { stream: true });
|
|
225
|
+
|
|
226
|
+
let separator = findSseSeparator(buffer);
|
|
227
|
+
while (separator) {
|
|
228
|
+
const frame = buffer.slice(0, separator.index);
|
|
229
|
+
buffer = buffer.slice(separator.index + separator.length);
|
|
230
|
+
const event = parseSseFrame(frame);
|
|
231
|
+
if (event) yield event;
|
|
232
|
+
separator = findSseSeparator(buffer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
if (!doneReading) await reader.cancel().catch(() => undefined);
|
|
237
|
+
reader.releaseLock();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
buffer += decoder.decode();
|
|
241
|
+
const event = parseSseFrame(buffer);
|
|
242
|
+
if (event) yield event;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findSseSeparator(buffer: string): { index: number; length: number } | undefined {
|
|
246
|
+
const match = /\r?\n\r?\n/.exec(buffer);
|
|
247
|
+
return match?.index === undefined ? undefined : { index: match.index, length: match[0].length };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseSseFrame(frame: string): SseEvent | undefined {
|
|
251
|
+
const lines = frame.split(/\r?\n/);
|
|
252
|
+
let type = "";
|
|
253
|
+
const dataLines: string[] = [];
|
|
254
|
+
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
if (line.startsWith("event:")) {
|
|
257
|
+
type = line.slice("event:".length).trim();
|
|
258
|
+
} else if (line.startsWith("data:")) {
|
|
259
|
+
dataLines.push(line.slice("data:".length).trimStart());
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (dataLines.length === 0) return undefined;
|
|
264
|
+
const raw = dataLines.join("\n");
|
|
265
|
+
if (raw === "[DONE]") return undefined;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
return { type, data: JSON.parse(raw) };
|
|
269
|
+
} catch {
|
|
270
|
+
return { type, raw };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function collectOutputItem(
|
|
275
|
+
item: ResponseOutputItem | undefined,
|
|
276
|
+
searchCalls: Map<string, CodexSearchCall>,
|
|
277
|
+
messageTextParts: string[],
|
|
278
|
+
citations: Map<string, CodexCitation>,
|
|
279
|
+
): void {
|
|
280
|
+
if (!item) return;
|
|
281
|
+
|
|
282
|
+
if (item.type === "web_search_call") {
|
|
283
|
+
const key = item.id ?? `search-${searchCalls.size + 1}`;
|
|
284
|
+
const query = item.action?.query ?? item.action?.queries?.join(", ");
|
|
285
|
+
searchCalls.set(key, {
|
|
286
|
+
id: item.id,
|
|
287
|
+
status: item.status,
|
|
288
|
+
query,
|
|
289
|
+
url: item.action?.url,
|
|
290
|
+
actionType: item.action?.type,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (item.type !== "message" || item.role !== "assistant") return;
|
|
296
|
+
|
|
297
|
+
for (const part of item.content ?? []) {
|
|
298
|
+
if (part.type !== "output_text") continue;
|
|
299
|
+
messageTextParts.push(part.text ?? "");
|
|
300
|
+
for (const annotation of part.annotations ?? []) {
|
|
301
|
+
if (annotation.type !== "url_citation" || !annotation.url) continue;
|
|
302
|
+
citations.set(annotation.url, {
|
|
303
|
+
title: annotation.title,
|
|
304
|
+
url: annotation.url,
|
|
305
|
+
startIndex: annotation.start_index,
|
|
306
|
+
endIndex: annotation.end_index,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodexError,
|
|
3
|
+
classifyHttpStatus,
|
|
4
|
+
formatHttpErrorBody,
|
|
5
|
+
isCloudflareChallenge,
|
|
6
|
+
} from "../errors.ts";
|
|
7
|
+
import type { CodexTransport } from "../transport.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CodexWebSearchResult,
|
|
10
|
+
CodexCitation,
|
|
11
|
+
CodexSearchCall,
|
|
12
|
+
SearchContextSize,
|
|
13
|
+
Freshness,
|
|
14
|
+
StandaloneExternalWebAccess,
|
|
15
|
+
ResponseLength,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export interface SearchQuery {
|
|
19
|
+
q: string;
|
|
20
|
+
recency?: number;
|
|
21
|
+
domains?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OpenCommand {
|
|
25
|
+
refId: string;
|
|
26
|
+
lineno?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FindCommand {
|
|
30
|
+
refId: string;
|
|
31
|
+
pattern: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ClickCommand {
|
|
35
|
+
refId: string;
|
|
36
|
+
id: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ScreenshotCommand {
|
|
40
|
+
refId: string;
|
|
41
|
+
pageno: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FinanceCommand {
|
|
45
|
+
ticker: string;
|
|
46
|
+
type: "equity" | "fund" | "crypto" | "index";
|
|
47
|
+
market?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WeatherCommand {
|
|
51
|
+
location: string;
|
|
52
|
+
start?: string;
|
|
53
|
+
duration?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type SportsLeague =
|
|
57
|
+
| "nba"
|
|
58
|
+
| "wnba"
|
|
59
|
+
| "nfl"
|
|
60
|
+
| "nhl"
|
|
61
|
+
| "mlb"
|
|
62
|
+
| "epl"
|
|
63
|
+
| "ncaamb"
|
|
64
|
+
| "ncaawb"
|
|
65
|
+
| "ipl";
|
|
66
|
+
|
|
67
|
+
export interface SportsCommand {
|
|
68
|
+
fn: "schedule" | "standings";
|
|
69
|
+
league: SportsLeague;
|
|
70
|
+
team?: string;
|
|
71
|
+
opponent?: string;
|
|
72
|
+
date_from?: string;
|
|
73
|
+
date_to?: string;
|
|
74
|
+
num_games?: number;
|
|
75
|
+
locale?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TimeCommand {
|
|
79
|
+
utc_offset: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StandaloneCommandsOptions {
|
|
83
|
+
model: string;
|
|
84
|
+
transport: CodexTransport;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
searchQuery?: SearchQuery[];
|
|
87
|
+
imageQuery?: SearchQuery[];
|
|
88
|
+
open?: OpenCommand[];
|
|
89
|
+
find?: FindCommand[];
|
|
90
|
+
click?: ClickCommand[];
|
|
91
|
+
screenshot?: ScreenshotCommand[];
|
|
92
|
+
finance?: FinanceCommand[];
|
|
93
|
+
weather?: WeatherCommand[];
|
|
94
|
+
sports?: SportsCommand[];
|
|
95
|
+
time?: TimeCommand[];
|
|
96
|
+
freshness: Freshness;
|
|
97
|
+
searchContextSize?: SearchContextSize;
|
|
98
|
+
responseLength?: ResponseLength;
|
|
99
|
+
maxOutputTokens?: number;
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface StandaloneSearchResponse {
|
|
104
|
+
encrypted_output?: string;
|
|
105
|
+
output?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function externalWebAccessForFreshness(freshness: Freshness): StandaloneExternalWebAccess {
|
|
109
|
+
if (freshness === "cached") return false;
|
|
110
|
+
if (freshness === "indexed") return "indexed";
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function hasAnyCommand(options: StandaloneCommandsOptions): boolean {
|
|
115
|
+
return countCommands(options) > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isUnsupportedStandaloneCombination(
|
|
119
|
+
searchContextSize: SearchContextSize | undefined,
|
|
120
|
+
_freshness: Freshness,
|
|
121
|
+
): boolean {
|
|
122
|
+
return (searchContextSize ?? "medium") === "low";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function assertSupportedStandaloneCombination(
|
|
126
|
+
searchContextSize: SearchContextSize | undefined,
|
|
127
|
+
freshness: Freshness,
|
|
128
|
+
): void {
|
|
129
|
+
if (isUnsupportedStandaloneCombination(searchContextSize, freshness)) {
|
|
130
|
+
throw new CodexError(
|
|
131
|
+
"schema",
|
|
132
|
+
'standalone/low is disabled because Codex returns Cloudflare challenges for low-context standalone requests. Use search_context_size "medium" or "high".',
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function countCommands(options: StandaloneCommandsOptions): number {
|
|
138
|
+
return (
|
|
139
|
+
(options.searchQuery?.length ?? 0) +
|
|
140
|
+
(options.imageQuery?.length ?? 0) +
|
|
141
|
+
(options.open?.length ?? 0) +
|
|
142
|
+
(options.find?.length ?? 0) +
|
|
143
|
+
(options.click?.length ?? 0) +
|
|
144
|
+
(options.screenshot?.length ?? 0) +
|
|
145
|
+
(options.finance?.length ?? 0) +
|
|
146
|
+
(options.weather?.length ?? 0) +
|
|
147
|
+
(options.sports?.length ?? 0) +
|
|
148
|
+
(options.time?.length ?? 0)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function runStandaloneCommands(
|
|
153
|
+
options: StandaloneCommandsOptions,
|
|
154
|
+
): Promise<CodexWebSearchResult> {
|
|
155
|
+
if (!hasAnyCommand(options)) {
|
|
156
|
+
throw new CodexError("schema", "Codex standalone commands require at least one command");
|
|
157
|
+
}
|
|
158
|
+
if (countCommands(options) > 1) {
|
|
159
|
+
throw new CodexError("schema", "Codex standalone actions must be sent one per request");
|
|
160
|
+
}
|
|
161
|
+
assertSupportedStandaloneCombination(options.searchContextSize, options.freshness);
|
|
162
|
+
|
|
163
|
+
const {
|
|
164
|
+
transport,
|
|
165
|
+
model,
|
|
166
|
+
sessionId,
|
|
167
|
+
freshness,
|
|
168
|
+
searchContextSize,
|
|
169
|
+
responseLength,
|
|
170
|
+
maxOutputTokens,
|
|
171
|
+
signal,
|
|
172
|
+
} = options;
|
|
173
|
+
const headers = transport.buildHeaders("application/json");
|
|
174
|
+
headers.set("OpenAI-Beta", "responses=experimental");
|
|
175
|
+
headers.set("content-type", "application/json");
|
|
176
|
+
|
|
177
|
+
const commands: Record<string, unknown> = {};
|
|
178
|
+
if (options.searchQuery?.length) commands.search_query = options.searchQuery;
|
|
179
|
+
if (options.imageQuery?.length) commands.image_query = options.imageQuery;
|
|
180
|
+
if (options.open?.length)
|
|
181
|
+
commands.open = options.open.map((c) => ({ ref_id: c.refId, lineno: c.lineno }));
|
|
182
|
+
if (options.find?.length)
|
|
183
|
+
commands.find = options.find.map((c) => ({ ref_id: c.refId, pattern: c.pattern }));
|
|
184
|
+
if (options.click?.length)
|
|
185
|
+
commands.click = options.click.map((c) => ({ ref_id: c.refId, id: c.id }));
|
|
186
|
+
if (options.screenshot?.length) {
|
|
187
|
+
commands.screenshot = options.screenshot.map((c) => ({ ref_id: c.refId, pageno: c.pageno }));
|
|
188
|
+
}
|
|
189
|
+
if (options.finance?.length) {
|
|
190
|
+
commands.finance = options.finance.map((c) => ({
|
|
191
|
+
ticker: c.ticker,
|
|
192
|
+
type: c.type,
|
|
193
|
+
market: c.market,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
if (options.weather?.length) {
|
|
197
|
+
commands.weather = options.weather.map((c) => ({
|
|
198
|
+
location: c.location,
|
|
199
|
+
start: c.start,
|
|
200
|
+
duration: c.duration,
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
if (options.sports?.length) {
|
|
204
|
+
commands.sports = options.sports.map((c) => ({
|
|
205
|
+
fn: c.fn,
|
|
206
|
+
league: c.league,
|
|
207
|
+
team: c.team,
|
|
208
|
+
opponent: c.opponent,
|
|
209
|
+
date_from: c.date_from,
|
|
210
|
+
date_to: c.date_to,
|
|
211
|
+
num_games: c.num_games,
|
|
212
|
+
locale: c.locale,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
if (options.time?.length) commands.time = options.time.map((c) => ({ utc_offset: c.utc_offset }));
|
|
216
|
+
if (responseLength) commands.response_length = responseLength;
|
|
217
|
+
|
|
218
|
+
const body: Record<string, unknown> = {
|
|
219
|
+
id: sessionId,
|
|
220
|
+
model,
|
|
221
|
+
input: buildInput(options),
|
|
222
|
+
commands,
|
|
223
|
+
settings: {
|
|
224
|
+
search_context_size: searchContextSize ?? "medium",
|
|
225
|
+
allowed_callers: ["direct"],
|
|
226
|
+
external_web_access: externalWebAccessForFreshness(freshness),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
body.max_output_tokens = maxOutputTokens ?? 8000;
|
|
230
|
+
|
|
231
|
+
const bodyText = JSON.stringify(body);
|
|
232
|
+
let response = await transport.fetch(transport.resolveSearchEndpoint(), {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers,
|
|
235
|
+
body: bodyText,
|
|
236
|
+
signal,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
let status = response.status;
|
|
241
|
+
let rawText = await response.text();
|
|
242
|
+
if (status === 403 && isCloudflareChallenge(rawText) && !signal?.aborted) {
|
|
243
|
+
await delay(750, signal);
|
|
244
|
+
if (signal?.aborted) {
|
|
245
|
+
throw new CodexError("timeout", "Codex standalone request was aborted before retry.");
|
|
246
|
+
}
|
|
247
|
+
response = await transport.fetch(transport.resolveSearchEndpoint(), {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers,
|
|
250
|
+
body: bodyText,
|
|
251
|
+
signal,
|
|
252
|
+
});
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
status = response.status;
|
|
255
|
+
rawText = await response.text();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const text = formatHttpErrorBody(rawText, "standalone");
|
|
260
|
+
throw new CodexError(
|
|
261
|
+
classifyHttpStatus(status),
|
|
262
|
+
`Codex standalone search request failed: HTTP ${status}: ${text}`,
|
|
263
|
+
status,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const data = (await response.json()) as StandaloneSearchResponse;
|
|
269
|
+
const text = typeof data.output === "string" ? data.output : "";
|
|
270
|
+
const refIds = extractRefIds(text);
|
|
271
|
+
const searchCalls = inferSearchCalls(options);
|
|
272
|
+
|
|
273
|
+
const result: CodexWebSearchResult = {
|
|
274
|
+
model,
|
|
275
|
+
text,
|
|
276
|
+
searchCalls,
|
|
277
|
+
citations: extractMarkdownCitations(text),
|
|
278
|
+
refIds,
|
|
279
|
+
};
|
|
280
|
+
if (data.encrypted_output !== undefined) result.encryptedOutput = data.encrypted_output;
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function delay(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
285
|
+
if (signal?.aborted) return;
|
|
286
|
+
await new Promise<void>((resolve) => {
|
|
287
|
+
const timeout = setTimeout(resolve, ms);
|
|
288
|
+
signal?.addEventListener(
|
|
289
|
+
"abort",
|
|
290
|
+
() => {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
resolve();
|
|
293
|
+
},
|
|
294
|
+
{ once: true },
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildInput(options: StandaloneCommandsOptions): unknown[] {
|
|
300
|
+
const texts: string[] = [];
|
|
301
|
+
options.searchQuery?.forEach((q) => texts.push(q.q));
|
|
302
|
+
options.imageQuery?.forEach((q) => texts.push(q.q));
|
|
303
|
+
options.open?.forEach((c) => texts.push(c.refId));
|
|
304
|
+
options.find?.forEach((c) => texts.push(`find "${c.pattern}" in ${c.refId}`));
|
|
305
|
+
options.click?.forEach((c) => texts.push(`click ${c.id} in ${c.refId}`));
|
|
306
|
+
options.screenshot?.forEach((c) => texts.push(`screenshot ${c.pageno} of ${c.refId}`));
|
|
307
|
+
options.finance?.forEach((c) => texts.push(`finance ${c.ticker} ${c.type} ${c.market ?? ""}`));
|
|
308
|
+
options.weather?.forEach((c) => texts.push(`weather ${c.location}`));
|
|
309
|
+
options.sports?.forEach((c) => texts.push(`sports ${c.fn} ${c.league}`));
|
|
310
|
+
options.time?.forEach((c) => texts.push(`time ${c.utc_offset}`));
|
|
311
|
+
|
|
312
|
+
const prompt = texts.filter(Boolean).join("\n");
|
|
313
|
+
return [
|
|
314
|
+
{
|
|
315
|
+
type: "message",
|
|
316
|
+
role: "user",
|
|
317
|
+
content: [{ type: "input_text", text: prompt }],
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function inferSearchCalls(options: StandaloneCommandsOptions): CodexSearchCall[] {
|
|
323
|
+
const calls: CodexSearchCall[] = [];
|
|
324
|
+
options.searchQuery?.forEach((q) =>
|
|
325
|
+
calls.push({ status: "completed", query: q.q, actionType: "search_query" }),
|
|
326
|
+
);
|
|
327
|
+
options.imageQuery?.forEach((q) =>
|
|
328
|
+
calls.push({ status: "completed", query: q.q, actionType: "image_query" }),
|
|
329
|
+
);
|
|
330
|
+
options.open?.forEach((c) =>
|
|
331
|
+
calls.push({ status: "completed", refId: c.refId, actionType: "open_page" }),
|
|
332
|
+
);
|
|
333
|
+
options.find?.forEach((c) =>
|
|
334
|
+
calls.push({ status: "completed", refId: c.refId, actionType: "find_in_page" }),
|
|
335
|
+
);
|
|
336
|
+
options.click?.forEach((c) =>
|
|
337
|
+
calls.push({ status: "completed", refId: c.refId, actionType: "click" }),
|
|
338
|
+
);
|
|
339
|
+
options.screenshot?.forEach((c) =>
|
|
340
|
+
calls.push({ status: "completed", refId: c.refId, actionType: "screenshot" }),
|
|
341
|
+
);
|
|
342
|
+
options.finance?.forEach((c) =>
|
|
343
|
+
calls.push({ status: "completed", query: `${c.ticker}`, actionType: "finance" }),
|
|
344
|
+
);
|
|
345
|
+
options.weather?.forEach((c) =>
|
|
346
|
+
calls.push({ status: "completed", query: c.location, actionType: "weather" }),
|
|
347
|
+
);
|
|
348
|
+
options.sports?.forEach((c) =>
|
|
349
|
+
calls.push({ status: "completed", query: `${c.fn} ${c.league}`, actionType: "sports" }),
|
|
350
|
+
);
|
|
351
|
+
options.time?.forEach((c) =>
|
|
352
|
+
calls.push({ status: "completed", query: c.utc_offset, actionType: "time" }),
|
|
353
|
+
);
|
|
354
|
+
return calls;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const REF_ID_PATTERN = /\b(turn\d+(?:search|fetch|view)\d+)\b/g;
|
|
358
|
+
|
|
359
|
+
function extractRefIds(text: string): Record<string, string> {
|
|
360
|
+
const refs: Record<string, string> = {};
|
|
361
|
+
for (const match of text.matchAll(REF_ID_PATTERN)) {
|
|
362
|
+
const refId = match[1];
|
|
363
|
+
if (refId) refs[refId] = refId;
|
|
364
|
+
}
|
|
365
|
+
return refs;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function extractMarkdownCitations(text: string): CodexCitation[] {
|
|
369
|
+
const citations = new Map<string, CodexCitation>();
|
|
370
|
+
const markdownLinkPattern = /\[([^\]\n]{1,200})\]\((https?:\/\/[^)\s]+)\)/g;
|
|
371
|
+
for (const match of text.matchAll(markdownLinkPattern)) {
|
|
372
|
+
const title = match[1]?.trim();
|
|
373
|
+
const url = match[2]?.trim();
|
|
374
|
+
if (!url || citations.has(url)) continue;
|
|
375
|
+
citations.set(url, { title: title || url, url, startIndex: match.index });
|
|
376
|
+
}
|
|
377
|
+
return [...citations.values()];
|
|
378
|
+
}
|