@oh-my-pi/pi-ai 3.15.0 → 3.20.0
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/package.json +1 -1
- package/src/bun-imports.d.ts +14 -0
- package/src/cli.ts +16 -1
- package/src/index.ts +2 -0
- package/src/models.generated.ts +37 -20
- package/src/models.ts +16 -9
- package/src/providers/google-shared.ts +1 -1
- package/src/providers/google-vertex.ts +355 -0
- package/src/providers/openai-codex/constants.ts +25 -0
- package/src/providers/openai-codex/prompts/codex-instructions.md +105 -0
- package/src/providers/openai-codex/prompts/codex.ts +217 -0
- package/src/providers/openai-codex/prompts/pi-codex-bridge.ts +48 -0
- package/src/providers/openai-codex/request-transformer.ts +328 -0
- package/src/providers/openai-codex/response-handler.ts +133 -0
- package/src/providers/openai-codex-responses.ts +619 -0
- package/src/stream.ts +116 -7
- package/src/types.ts +9 -1
- package/src/utils/oauth/index.ts +14 -0
- package/src/utils/oauth/openai-codex.ts +334 -0
- package/src/utils/oauth/types.ts +7 -1
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ResponseFunctionToolCall,
|
|
3
|
+
ResponseInput,
|
|
4
|
+
ResponseInputContent,
|
|
5
|
+
ResponseInputImage,
|
|
6
|
+
ResponseInputText,
|
|
7
|
+
ResponseOutputMessage,
|
|
8
|
+
ResponseReasoningItem,
|
|
9
|
+
} from "openai/resources/responses/responses.js";
|
|
10
|
+
import { calculateCost } from "../models";
|
|
11
|
+
import { getEnvApiKey } from "../stream";
|
|
12
|
+
import type {
|
|
13
|
+
Api,
|
|
14
|
+
AssistantMessage,
|
|
15
|
+
Context,
|
|
16
|
+
Model,
|
|
17
|
+
StopReason,
|
|
18
|
+
StreamFunction,
|
|
19
|
+
StreamOptions,
|
|
20
|
+
TextContent,
|
|
21
|
+
ThinkingContent,
|
|
22
|
+
Tool,
|
|
23
|
+
ToolCall,
|
|
24
|
+
} from "../types";
|
|
25
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
26
|
+
import { parseStreamingJson } from "../utils/json-parse";
|
|
27
|
+
import { sanitizeSurrogates } from "../utils/sanitize-unicode";
|
|
28
|
+
import {
|
|
29
|
+
CODEX_BASE_URL,
|
|
30
|
+
JWT_CLAIM_PATH,
|
|
31
|
+
OPENAI_HEADER_VALUES,
|
|
32
|
+
OPENAI_HEADERS,
|
|
33
|
+
URL_PATHS,
|
|
34
|
+
} from "./openai-codex/constants";
|
|
35
|
+
import { getCodexInstructions } from "./openai-codex/prompts/codex";
|
|
36
|
+
import {
|
|
37
|
+
type CodexRequestOptions,
|
|
38
|
+
normalizeModel,
|
|
39
|
+
type RequestBody,
|
|
40
|
+
transformRequestBody,
|
|
41
|
+
} from "./openai-codex/request-transformer";
|
|
42
|
+
import { parseCodexError, parseCodexSseStream } from "./openai-codex/response-handler";
|
|
43
|
+
import { transformMessages } from "./transorm-messages";
|
|
44
|
+
|
|
45
|
+
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
46
|
+
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
47
|
+
reasoningSummary?: "auto" | "concise" | "detailed" | "off" | "on" | null;
|
|
48
|
+
textVerbosity?: "low" | "medium" | "high";
|
|
49
|
+
include?: string[];
|
|
50
|
+
codexMode?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CODEX_DEBUG = process.env.PI_CODEX_DEBUG === "1" || process.env.PI_CODEX_DEBUG === "true";
|
|
54
|
+
|
|
55
|
+
export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
|
|
56
|
+
model: Model<"openai-codex-responses">,
|
|
57
|
+
context: Context,
|
|
58
|
+
options?: OpenAICodexResponsesOptions,
|
|
59
|
+
): AssistantMessageEventStream => {
|
|
60
|
+
const stream = new AssistantMessageEventStream();
|
|
61
|
+
|
|
62
|
+
(async () => {
|
|
63
|
+
const output: AssistantMessage = {
|
|
64
|
+
role: "assistant",
|
|
65
|
+
content: [],
|
|
66
|
+
api: "openai-codex-responses" as Api,
|
|
67
|
+
provider: model.provider,
|
|
68
|
+
model: model.id,
|
|
69
|
+
usage: {
|
|
70
|
+
input: 0,
|
|
71
|
+
output: 0,
|
|
72
|
+
cacheRead: 0,
|
|
73
|
+
cacheWrite: 0,
|
|
74
|
+
totalTokens: 0,
|
|
75
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
76
|
+
},
|
|
77
|
+
stopReason: "stop",
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
throw new Error(`No API key for provider: ${model.provider}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const accountId = getAccountId(apiKey);
|
|
88
|
+
const baseUrl = model.baseUrl || CODEX_BASE_URL;
|
|
89
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
90
|
+
const url = rewriteUrlForCodex(new URL(URL_PATHS.RESPONSES.slice(1), baseWithSlash).toString());
|
|
91
|
+
|
|
92
|
+
const messages = convertMessages(model, context);
|
|
93
|
+
const params: RequestBody = {
|
|
94
|
+
model: model.id,
|
|
95
|
+
input: messages,
|
|
96
|
+
stream: true,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (options?.maxTokens) {
|
|
100
|
+
params.max_output_tokens = options.maxTokens;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options?.temperature !== undefined) {
|
|
104
|
+
params.temperature = options.temperature;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (context.tools) {
|
|
108
|
+
params.tools = convertTools(context.tools);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const normalizedModel = normalizeModel(params.model);
|
|
112
|
+
const codexInstructions = await getCodexInstructions(normalizedModel);
|
|
113
|
+
|
|
114
|
+
const codexOptions: CodexRequestOptions = {
|
|
115
|
+
reasoningEffort: options?.reasoningEffort,
|
|
116
|
+
reasoningSummary: options?.reasoningSummary ?? undefined,
|
|
117
|
+
textVerbosity: options?.textVerbosity,
|
|
118
|
+
include: options?.include,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const transformedBody = await transformRequestBody(
|
|
122
|
+
params,
|
|
123
|
+
codexInstructions,
|
|
124
|
+
codexOptions,
|
|
125
|
+
options?.codexMode ?? true,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const headers = createCodexHeaders(model.headers, accountId, apiKey, transformedBody.prompt_cache_key);
|
|
129
|
+
logCodexDebug("codex request", {
|
|
130
|
+
url,
|
|
131
|
+
model: params.model,
|
|
132
|
+
headers: redactHeaders(headers),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers,
|
|
138
|
+
body: JSON.stringify(transformedBody),
|
|
139
|
+
signal: options?.signal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
logCodexDebug("codex response", {
|
|
143
|
+
url: response.url,
|
|
144
|
+
status: response.status,
|
|
145
|
+
statusText: response.statusText,
|
|
146
|
+
contentType: response.headers.get("content-type") || null,
|
|
147
|
+
cfRay: response.headers.get("cf-ray") || null,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const info = await parseCodexError(response);
|
|
152
|
+
throw new Error(info.friendlyMessage || info.message);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!response.body) {
|
|
156
|
+
throw new Error("No response body");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
stream.push({ type: "start", partial: output });
|
|
160
|
+
|
|
161
|
+
let currentItem: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | null = null;
|
|
162
|
+
let currentBlock: ThinkingContent | TextContent | (ToolCall & { partialJson: string }) | null = null;
|
|
163
|
+
const blocks = output.content;
|
|
164
|
+
const blockIndex = () => blocks.length - 1;
|
|
165
|
+
|
|
166
|
+
for await (const rawEvent of parseCodexSseStream(response)) {
|
|
167
|
+
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
168
|
+
if (!eventType) continue;
|
|
169
|
+
|
|
170
|
+
if (eventType === "response.output_item.added") {
|
|
171
|
+
const item = rawEvent.item as ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall;
|
|
172
|
+
if (item.type === "reasoning") {
|
|
173
|
+
currentItem = item;
|
|
174
|
+
currentBlock = { type: "thinking", thinking: "" };
|
|
175
|
+
output.content.push(currentBlock);
|
|
176
|
+
stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
|
|
177
|
+
} else if (item.type === "message") {
|
|
178
|
+
currentItem = item;
|
|
179
|
+
currentBlock = { type: "text", text: "" };
|
|
180
|
+
output.content.push(currentBlock);
|
|
181
|
+
stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
|
|
182
|
+
} else if (item.type === "function_call") {
|
|
183
|
+
currentItem = item;
|
|
184
|
+
currentBlock = {
|
|
185
|
+
type: "toolCall",
|
|
186
|
+
id: `${item.call_id}|${item.id}`,
|
|
187
|
+
name: item.name,
|
|
188
|
+
arguments: {},
|
|
189
|
+
partialJson: item.arguments || "",
|
|
190
|
+
};
|
|
191
|
+
output.content.push(currentBlock);
|
|
192
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
193
|
+
}
|
|
194
|
+
} else if (eventType === "response.reasoning_summary_part.added") {
|
|
195
|
+
if (currentItem && currentItem.type === "reasoning") {
|
|
196
|
+
currentItem.summary = currentItem.summary || [];
|
|
197
|
+
currentItem.summary.push((rawEvent as { part: ResponseReasoningItem["summary"][number] }).part);
|
|
198
|
+
}
|
|
199
|
+
} else if (eventType === "response.reasoning_summary_text.delta") {
|
|
200
|
+
if (currentItem && currentItem.type === "reasoning" && currentBlock?.type === "thinking") {
|
|
201
|
+
currentItem.summary = currentItem.summary || [];
|
|
202
|
+
const lastPart = currentItem.summary[currentItem.summary.length - 1];
|
|
203
|
+
if (lastPart) {
|
|
204
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
205
|
+
currentBlock.thinking += delta;
|
|
206
|
+
lastPart.text += delta;
|
|
207
|
+
stream.push({
|
|
208
|
+
type: "thinking_delta",
|
|
209
|
+
contentIndex: blockIndex(),
|
|
210
|
+
delta,
|
|
211
|
+
partial: output,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else if (eventType === "response.reasoning_summary_part.done") {
|
|
216
|
+
if (currentItem && currentItem.type === "reasoning" && currentBlock?.type === "thinking") {
|
|
217
|
+
currentItem.summary = currentItem.summary || [];
|
|
218
|
+
const lastPart = currentItem.summary[currentItem.summary.length - 1];
|
|
219
|
+
if (lastPart) {
|
|
220
|
+
currentBlock.thinking += "\n\n";
|
|
221
|
+
lastPart.text += "\n\n";
|
|
222
|
+
stream.push({
|
|
223
|
+
type: "thinking_delta",
|
|
224
|
+
contentIndex: blockIndex(),
|
|
225
|
+
delta: "\n\n",
|
|
226
|
+
partial: output,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else if (eventType === "response.content_part.added") {
|
|
231
|
+
if (currentItem && currentItem.type === "message") {
|
|
232
|
+
currentItem.content = currentItem.content || [];
|
|
233
|
+
const part = (rawEvent as { part?: ResponseOutputMessage["content"][number] }).part;
|
|
234
|
+
if (part && (part.type === "output_text" || part.type === "refusal")) {
|
|
235
|
+
currentItem.content.push(part);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if (eventType === "response.output_text.delta") {
|
|
239
|
+
if (currentItem && currentItem.type === "message" && currentBlock?.type === "text") {
|
|
240
|
+
const lastPart = currentItem.content[currentItem.content.length - 1];
|
|
241
|
+
if (lastPart && lastPart.type === "output_text") {
|
|
242
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
243
|
+
currentBlock.text += delta;
|
|
244
|
+
lastPart.text += delta;
|
|
245
|
+
stream.push({
|
|
246
|
+
type: "text_delta",
|
|
247
|
+
contentIndex: blockIndex(),
|
|
248
|
+
delta,
|
|
249
|
+
partial: output,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (eventType === "response.refusal.delta") {
|
|
254
|
+
if (currentItem && currentItem.type === "message" && currentBlock?.type === "text") {
|
|
255
|
+
const lastPart = currentItem.content[currentItem.content.length - 1];
|
|
256
|
+
if (lastPart && lastPart.type === "refusal") {
|
|
257
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
258
|
+
currentBlock.text += delta;
|
|
259
|
+
lastPart.refusal += delta;
|
|
260
|
+
stream.push({
|
|
261
|
+
type: "text_delta",
|
|
262
|
+
contentIndex: blockIndex(),
|
|
263
|
+
delta,
|
|
264
|
+
partial: output,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} else if (eventType === "response.function_call_arguments.delta") {
|
|
269
|
+
if (currentItem && currentItem.type === "function_call" && currentBlock?.type === "toolCall") {
|
|
270
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
271
|
+
currentBlock.partialJson += delta;
|
|
272
|
+
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
273
|
+
stream.push({
|
|
274
|
+
type: "toolcall_delta",
|
|
275
|
+
contentIndex: blockIndex(),
|
|
276
|
+
delta,
|
|
277
|
+
partial: output,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} else if (eventType === "response.output_item.done") {
|
|
281
|
+
const item = rawEvent.item as ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall;
|
|
282
|
+
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
|
|
283
|
+
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
|
|
284
|
+
currentBlock.thinkingSignature = JSON.stringify(item);
|
|
285
|
+
stream.push({
|
|
286
|
+
type: "thinking_end",
|
|
287
|
+
contentIndex: blockIndex(),
|
|
288
|
+
content: currentBlock.thinking,
|
|
289
|
+
partial: output,
|
|
290
|
+
});
|
|
291
|
+
currentBlock = null;
|
|
292
|
+
} else if (item.type === "message" && currentBlock?.type === "text") {
|
|
293
|
+
currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
|
|
294
|
+
currentBlock.textSignature = item.id;
|
|
295
|
+
stream.push({
|
|
296
|
+
type: "text_end",
|
|
297
|
+
contentIndex: blockIndex(),
|
|
298
|
+
content: currentBlock.text,
|
|
299
|
+
partial: output,
|
|
300
|
+
});
|
|
301
|
+
currentBlock = null;
|
|
302
|
+
} else if (item.type === "function_call") {
|
|
303
|
+
const toolCall: ToolCall = {
|
|
304
|
+
type: "toolCall",
|
|
305
|
+
id: `${item.call_id}|${item.id}`,
|
|
306
|
+
name: item.name,
|
|
307
|
+
arguments: JSON.parse(item.arguments),
|
|
308
|
+
};
|
|
309
|
+
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
310
|
+
}
|
|
311
|
+
} else if (eventType === "response.completed" || eventType === "response.done") {
|
|
312
|
+
const response = (
|
|
313
|
+
rawEvent as {
|
|
314
|
+
response?: {
|
|
315
|
+
usage?: {
|
|
316
|
+
input_tokens?: number;
|
|
317
|
+
output_tokens?: number;
|
|
318
|
+
total_tokens?: number;
|
|
319
|
+
input_tokens_details?: { cached_tokens?: number };
|
|
320
|
+
};
|
|
321
|
+
status?: string;
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
).response;
|
|
325
|
+
if (response?.usage) {
|
|
326
|
+
const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
|
|
327
|
+
output.usage = {
|
|
328
|
+
input: (response.usage.input_tokens || 0) - cachedTokens,
|
|
329
|
+
output: response.usage.output_tokens || 0,
|
|
330
|
+
cacheRead: cachedTokens,
|
|
331
|
+
cacheWrite: 0,
|
|
332
|
+
totalTokens: response.usage.total_tokens || 0,
|
|
333
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
calculateCost(model, output.usage);
|
|
337
|
+
output.stopReason = mapStopReason(response?.status);
|
|
338
|
+
if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
|
|
339
|
+
output.stopReason = "toolUse";
|
|
340
|
+
}
|
|
341
|
+
} else if (eventType === "error") {
|
|
342
|
+
const code = (rawEvent as { code?: string }).code || "";
|
|
343
|
+
const message = (rawEvent as { message?: string }).message || "Unknown error";
|
|
344
|
+
throw new Error(code ? `Error Code ${code}: ${message}` : message);
|
|
345
|
+
} else if (eventType === "response.failed") {
|
|
346
|
+
throw new Error("Unknown error");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (options?.signal?.aborted) {
|
|
351
|
+
throw new Error("Request was aborted");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
355
|
+
throw new Error("An unknown error occurred");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
359
|
+
stream.end();
|
|
360
|
+
} catch (error) {
|
|
361
|
+
for (const block of output.content) delete (block as { index?: number }).index;
|
|
362
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
363
|
+
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
364
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
365
|
+
stream.end();
|
|
366
|
+
}
|
|
367
|
+
})();
|
|
368
|
+
|
|
369
|
+
return stream;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
function createCodexHeaders(
|
|
373
|
+
initHeaders: Record<string, string> | undefined,
|
|
374
|
+
accountId: string,
|
|
375
|
+
accessToken: string,
|
|
376
|
+
promptCacheKey?: string,
|
|
377
|
+
): Headers {
|
|
378
|
+
const headers = new Headers(initHeaders ?? {});
|
|
379
|
+
headers.delete("x-api-key");
|
|
380
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
381
|
+
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
382
|
+
headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
383
|
+
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
384
|
+
|
|
385
|
+
if (promptCacheKey) {
|
|
386
|
+
headers.set(OPENAI_HEADERS.CONVERSATION_ID, promptCacheKey);
|
|
387
|
+
headers.set(OPENAI_HEADERS.SESSION_ID, promptCacheKey);
|
|
388
|
+
} else {
|
|
389
|
+
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
|
390
|
+
headers.delete(OPENAI_HEADERS.SESSION_ID);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
headers.set("accept", "text/event-stream");
|
|
394
|
+
headers.set("content-type", "application/json");
|
|
395
|
+
return headers;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function logCodexDebug(message: string, details?: Record<string, unknown>): void {
|
|
399
|
+
if (!CODEX_DEBUG) return;
|
|
400
|
+
if (details) {
|
|
401
|
+
console.error(`[codex] ${message}`, details);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
console.error(`[codex] ${message}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function redactHeaders(headers: Headers): Record<string, string> {
|
|
408
|
+
const redacted: Record<string, string> = {};
|
|
409
|
+
headers.forEach((value, key) => {
|
|
410
|
+
const lower = key.toLowerCase();
|
|
411
|
+
if (lower === "authorization") {
|
|
412
|
+
redacted[key] = "Bearer [redacted]";
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
lower.includes("account") ||
|
|
417
|
+
lower.includes("session") ||
|
|
418
|
+
lower.includes("conversation") ||
|
|
419
|
+
lower === "cookie"
|
|
420
|
+
) {
|
|
421
|
+
redacted[key] = "[redacted]";
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
redacted[key] = value;
|
|
425
|
+
});
|
|
426
|
+
return redacted;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function rewriteUrlForCodex(url: string): string {
|
|
430
|
+
return url.replace(URL_PATHS.RESPONSES, URL_PATHS.CODEX_RESPONSES);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
type JwtPayload = {
|
|
434
|
+
[JWT_CLAIM_PATH]?: {
|
|
435
|
+
chatgpt_account_id?: string;
|
|
436
|
+
};
|
|
437
|
+
[key: string]: unknown;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
function decodeJwt(token: string): JwtPayload | null {
|
|
441
|
+
try {
|
|
442
|
+
const parts = token.split(".");
|
|
443
|
+
if (parts.length !== 3) return null;
|
|
444
|
+
const payload = parts[1] ?? "";
|
|
445
|
+
const decoded = Buffer.from(payload, "base64").toString("utf-8");
|
|
446
|
+
return JSON.parse(decoded) as JwtPayload;
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getAccountId(accessToken: string): string {
|
|
453
|
+
const payload = decodeJwt(accessToken);
|
|
454
|
+
const auth = payload?.[JWT_CLAIM_PATH];
|
|
455
|
+
const accountId = auth?.chatgpt_account_id;
|
|
456
|
+
if (!accountId) {
|
|
457
|
+
throw new Error("Failed to extract accountId from token");
|
|
458
|
+
}
|
|
459
|
+
return accountId;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function shortHash(str: string): string {
|
|
463
|
+
let h1 = 0xdeadbeef;
|
|
464
|
+
let h2 = 0x41c6ce57;
|
|
465
|
+
for (let i = 0; i < str.length; i++) {
|
|
466
|
+
const ch = str.charCodeAt(i);
|
|
467
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
468
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
469
|
+
}
|
|
470
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
471
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
472
|
+
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
|
|
476
|
+
const messages: ResponseInput = [];
|
|
477
|
+
|
|
478
|
+
const transformedMessages = transformMessages(context.messages, model);
|
|
479
|
+
|
|
480
|
+
let msgIndex = 0;
|
|
481
|
+
for (const msg of transformedMessages) {
|
|
482
|
+
if (msg.role === "user") {
|
|
483
|
+
if (typeof msg.content === "string") {
|
|
484
|
+
messages.push({
|
|
485
|
+
role: "user",
|
|
486
|
+
content: [{ type: "input_text", text: sanitizeSurrogates(msg.content) }],
|
|
487
|
+
});
|
|
488
|
+
} else {
|
|
489
|
+
const content: ResponseInputContent[] = msg.content.map((item): ResponseInputContent => {
|
|
490
|
+
if (item.type === "text") {
|
|
491
|
+
return {
|
|
492
|
+
type: "input_text",
|
|
493
|
+
text: sanitizeSurrogates(item.text),
|
|
494
|
+
} satisfies ResponseInputText;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
type: "input_image",
|
|
498
|
+
detail: "auto",
|
|
499
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
500
|
+
} satisfies ResponseInputImage;
|
|
501
|
+
});
|
|
502
|
+
const filteredContent = !model.input.includes("image")
|
|
503
|
+
? content.filter((c) => c.type !== "input_image")
|
|
504
|
+
: content;
|
|
505
|
+
if (filteredContent.length === 0) continue;
|
|
506
|
+
messages.push({
|
|
507
|
+
role: "user",
|
|
508
|
+
content: filteredContent,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} else if (msg.role === "assistant") {
|
|
512
|
+
const output: ResponseInput = [];
|
|
513
|
+
|
|
514
|
+
for (const block of msg.content) {
|
|
515
|
+
if (block.type === "thinking" && msg.stopReason !== "error") {
|
|
516
|
+
if (block.thinkingSignature) {
|
|
517
|
+
const reasoningItem = JSON.parse(block.thinkingSignature) as ResponseReasoningItem;
|
|
518
|
+
output.push(reasoningItem);
|
|
519
|
+
}
|
|
520
|
+
} else if (block.type === "text") {
|
|
521
|
+
const textBlock = block as TextContent;
|
|
522
|
+
let msgId = textBlock.textSignature;
|
|
523
|
+
if (!msgId) {
|
|
524
|
+
msgId = `msg_${msgIndex}`;
|
|
525
|
+
} else if (msgId.length > 64) {
|
|
526
|
+
msgId = `msg_${shortHash(msgId)}`;
|
|
527
|
+
}
|
|
528
|
+
output.push({
|
|
529
|
+
type: "message",
|
|
530
|
+
role: "assistant",
|
|
531
|
+
content: [{ type: "output_text", text: sanitizeSurrogates(textBlock.text), annotations: [] }],
|
|
532
|
+
status: "completed",
|
|
533
|
+
id: msgId,
|
|
534
|
+
} satisfies ResponseOutputMessage);
|
|
535
|
+
} else if (block.type === "toolCall" && msg.stopReason !== "error") {
|
|
536
|
+
const toolCall = block as ToolCall;
|
|
537
|
+
output.push({
|
|
538
|
+
type: "function_call",
|
|
539
|
+
id: toolCall.id.split("|")[1],
|
|
540
|
+
call_id: toolCall.id.split("|")[0],
|
|
541
|
+
name: toolCall.name,
|
|
542
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (output.length === 0) continue;
|
|
547
|
+
messages.push(...output);
|
|
548
|
+
} else if (msg.role === "toolResult") {
|
|
549
|
+
const textResult = msg.content
|
|
550
|
+
.filter((c) => c.type === "text")
|
|
551
|
+
.map((c) => (c as { text: string }).text)
|
|
552
|
+
.join("\n");
|
|
553
|
+
const hasImages = msg.content.some((c) => c.type === "image");
|
|
554
|
+
|
|
555
|
+
const hasText = textResult.length > 0;
|
|
556
|
+
messages.push({
|
|
557
|
+
type: "function_call_output",
|
|
558
|
+
call_id: msg.toolCallId.split("|")[0],
|
|
559
|
+
output: sanitizeSurrogates(hasText ? textResult : "(see attached image)"),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
if (hasImages && model.input.includes("image")) {
|
|
563
|
+
const contentParts: ResponseInputContent[] = [];
|
|
564
|
+
contentParts.push({
|
|
565
|
+
type: "input_text",
|
|
566
|
+
text: "Attached image(s) from tool result:",
|
|
567
|
+
} satisfies ResponseInputText);
|
|
568
|
+
|
|
569
|
+
for (const block of msg.content) {
|
|
570
|
+
if (block.type === "image") {
|
|
571
|
+
contentParts.push({
|
|
572
|
+
type: "input_image",
|
|
573
|
+
detail: "auto",
|
|
574
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
575
|
+
} satisfies ResponseInputImage);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
messages.push({
|
|
580
|
+
role: "user",
|
|
581
|
+
content: contentParts,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
msgIndex++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return messages;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function convertTools(
|
|
592
|
+
tools: Tool[],
|
|
593
|
+
): Array<{ type: "function"; name: string; description: string; parameters: Record<string, unknown>; strict: null }> {
|
|
594
|
+
return tools.map((tool) => ({
|
|
595
|
+
type: "function",
|
|
596
|
+
name: tool.name,
|
|
597
|
+
description: tool.description,
|
|
598
|
+
parameters: tool.parameters as unknown as Record<string, unknown>,
|
|
599
|
+
strict: null,
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function mapStopReason(status: string | undefined): StopReason {
|
|
604
|
+
if (!status) return "stop";
|
|
605
|
+
switch (status) {
|
|
606
|
+
case "completed":
|
|
607
|
+
return "stop";
|
|
608
|
+
case "incomplete":
|
|
609
|
+
return "length";
|
|
610
|
+
case "failed":
|
|
611
|
+
case "cancelled":
|
|
612
|
+
return "error";
|
|
613
|
+
case "in_progress":
|
|
614
|
+
case "queued":
|
|
615
|
+
return "stop";
|
|
616
|
+
default:
|
|
617
|
+
return "stop";
|
|
618
|
+
}
|
|
619
|
+
}
|