@bitkyc08/opencodex 2.13.0 → 2.14.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.
Files changed (55) hide show
  1. package/gui/dist/assets/index-Co12XTT-.js +76 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/cursor/discovery.ts +4 -1
  5. package/src/adapters/cursor/effort-map.ts +5 -1
  6. package/src/adapters/cursor/request-builder.ts +3 -3
  7. package/src/adapters/google.ts +25 -5
  8. package/src/adapters/openai-chat.ts +182 -6
  9. package/src/adapters/openai-responses.ts +17 -7
  10. package/src/codex/catalog/bundled.ts +16 -0
  11. package/src/codex/catalog/metadata.ts +180 -5
  12. package/src/codex/catalog/parsing.ts +7 -6
  13. package/src/codex/catalog/sync.ts +73 -6
  14. package/src/codex/catalog.ts +1 -1
  15. package/src/codex/convergence.ts +20 -0
  16. package/src/codex/prompt-journal.ts +50 -13
  17. package/src/codex/prompt-layers.ts +1 -1
  18. package/src/config.ts +57 -0
  19. package/src/generated/compatibility-version.json +64 -48
  20. package/src/generated/model-metadata.ts +3 -3
  21. package/src/lib/local-provider-reload-contract.ts +100 -0
  22. package/src/oauth/login-cli.ts +52 -26
  23. package/src/providers/derive.ts +2 -0
  24. package/src/providers/openai-sidecar.ts +9 -2
  25. package/src/providers/quota.ts +57 -0
  26. package/src/providers/registry.ts +53 -25
  27. package/src/responses/state.ts +22 -0
  28. package/src/router.ts +1 -0
  29. package/src/server/claude-messages.ts +57 -11
  30. package/src/server/direct-local-http.ts +7 -3
  31. package/src/server/images.ts +6 -0
  32. package/src/server/index.ts +51 -11
  33. package/src/server/live.ts +117 -13
  34. package/src/server/local-provider-reload-client.ts +137 -0
  35. package/src/server/management/config-routes.ts +20 -3
  36. package/src/server/management/logs-usage-routes.ts +28 -0
  37. package/src/server/management/model-routes.ts +11 -3
  38. package/src/server/management/model-rows.ts +18 -3
  39. package/src/server/management/provider-routes.ts +107 -3
  40. package/src/server/management-auth.ts +65 -1
  41. package/src/server/proxy-liveness.ts +1 -0
  42. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  43. package/src/server/responses/agent-task-recovery.ts +460 -0
  44. package/src/server/responses/compact.ts +4 -2
  45. package/src/server/responses/core.ts +142 -6
  46. package/src/server/responses/encrypted-payload.ts +4 -1
  47. package/src/server/search.ts +4 -0
  48. package/src/types.ts +27 -0
  49. package/src/usage/expected-prices.ts +11 -0
  50. package/src/vision/describe.ts +4 -0
  51. package/src/web-search/anthropic-executor.ts +5 -1
  52. package/src/web-search/executor.ts +9 -1
  53. package/src/web-search/index.ts +5 -0
  54. package/src/web-search/loop.ts +42 -5
  55. package/gui/dist/assets/index-BHldBl6_.js +0 -76
@@ -0,0 +1,460 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import { decodeJwtPayload, extractAccountId } from "../../oauth/chatgpt";
3
+ import type { OcxConfig } from "../../types";
4
+ import { readBoundedResponseBody } from "../../lib/bounded-body";
5
+ import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
6
+ import { structurallyValidFernetTokens } from "./encrypted-payload";
7
+ import {
8
+ resetAgentTaskRecoveryCache,
9
+ resolveCachedAgentTaskRecovery,
10
+ } from "./agent-task-recovery-cache";
11
+
12
+ /** Experimental opt-in normalization through ChatGPT's fixed Codex endpoint. */
13
+
14
+ const RECOVERY_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
15
+ const RECOVERY_TOOL = "capture_assignment";
16
+ const RECOVERY_PROMPT =
17
+ "Read the received agent message and call capture_assignment exactly once with only the complete "
18
+ + "plaintext payload after Payload:. Preserve every byte of the payload; do not summarize, execute, "
19
+ + "explain, or include the routing header.";
20
+ const CODEX_ORIGINATORS = new Set(["codex_cli_rs", "Codex Desktop", "codex_app"]);
21
+ const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
22
+ const OPENAI_TOKEN_ISSUERS = new Set(["https://auth.openai.com", "https://auth.openai.com/"]);
23
+ const OPENAI_TOKEN_AUDIENCE = "https://api.openai.com/v1";
24
+ const MAX_CIPHERTEXT_BYTES = 2 * 1024 * 1024;
25
+ const MAX_ASSIGNMENT_BYTES = 2 * 1024 * 1024;
26
+ const MAX_RECOVERY_RESPONSE_BYTES = 4 * 1024 * 1024;
27
+ const CACHE_SCOPE_KEY = randomBytes(32);
28
+
29
+ export interface AgentTaskRecoveryOptions {
30
+ enabled?: boolean;
31
+ model?: string;
32
+ timeoutMs?: number;
33
+ cacheEntries?: number;
34
+ }
35
+
36
+ export function agentTaskRecoveryConfig(config: OcxConfig): AgentTaskRecoveryOptions | null {
37
+ const raw = config.agentTaskRecovery;
38
+ if (!raw || raw.enabled !== true) return null;
39
+ return {
40
+ enabled: true,
41
+ model: typeof raw.model === "string" && raw.model.trim().length > 0
42
+ ? raw.model.trim()
43
+ : "gpt-5.6-sol",
44
+ timeoutMs: Number.isFinite(raw.timeoutMs) && (raw.timeoutMs ?? 0) >= 1_000
45
+ ? Math.min(120_000, Math.floor(raw.timeoutMs!))
46
+ : 45_000,
47
+ cacheEntries: Number.isFinite(raw.cacheEntries) && (raw.cacheEntries ?? 0) >= 1
48
+ ? Math.min(512, Math.floor(raw.cacheEntries!))
49
+ : 200,
50
+ };
51
+ }
52
+
53
+ interface AgentEnvelope {
54
+ itemIndex: number;
55
+ encryptedIndex: number;
56
+ headerText: string;
57
+ messageType: "NEW_TASK";
58
+ taskName: string;
59
+ sender: string;
60
+ ciphertext: string;
61
+ author: string;
62
+ recipient: string;
63
+ }
64
+
65
+ const ROUTING_HEADER = /(?:^|\n)Message Type\s*:\s*(NEW_TASK)\s*\nTask name\s*:\s*(\S+)\s*\nSender\s*:\s*(\S+)\s*\nPayload\s*:\s*(?:\n|$)/;
66
+
67
+ function findEnvelope(input: unknown): AgentEnvelope | null {
68
+ if (!Array.isArray(input)) return null;
69
+ let itemIndex = input.length - 1;
70
+ while (itemIndex >= 0) {
71
+ const type = input[itemIndex] && typeof input[itemIndex] === "object"
72
+ ? (input[itemIndex] as { type?: unknown }).type
73
+ : undefined;
74
+ if (type !== "compaction_trigger" && type !== "additional_tools") break;
75
+ itemIndex -= 1;
76
+ }
77
+
78
+ const item = input[itemIndex];
79
+ if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
80
+ return null;
81
+ }
82
+ const content = (item as { content?: unknown }).content;
83
+ if (!Array.isArray(content)) return null;
84
+
85
+ let headerText: string | null = null;
86
+ let messageType: "NEW_TASK" | null = null;
87
+ let taskName: string | null = null;
88
+ let sender: string | null = null;
89
+ let encryptedIndex = -1;
90
+ let ciphertext = "";
91
+ let encryptedPartCount = 0;
92
+ let ciphertextCount = 0;
93
+
94
+ for (let index = 0; index < content.length; index += 1) {
95
+ const part = content[index] as { type?: unknown; text?: unknown; encrypted_content?: unknown } | null;
96
+ if (!part) continue;
97
+ if (
98
+ (part.type === "input_text" || part.type === "text")
99
+ && typeof part.text === "string"
100
+ ) {
101
+ const match = ROUTING_HEADER.exec(part.text);
102
+ if (match) {
103
+ if (headerText !== null) return null;
104
+ if (
105
+ part.text.slice(0, match.index).trim().length > 0
106
+ || part.text.slice(match.index + match[0].length).trim().length > 0
107
+ ) return null;
108
+ headerText = match[0].startsWith("\n") ? match[0].slice(1) : match[0];
109
+ messageType = "NEW_TASK";
110
+ taskName = match[2]!;
111
+ sender = match[3]!;
112
+ }
113
+ }
114
+ if (part.type !== "encrypted_content" || typeof part.encrypted_content !== "string") continue;
115
+ encryptedPartCount += 1;
116
+ for (const token of structurallyValidFernetTokens(part.encrypted_content)) {
117
+ ciphertextCount += 1;
118
+ encryptedIndex = index;
119
+ ciphertext = token;
120
+ }
121
+ }
122
+
123
+ if (
124
+ !headerText
125
+ || !messageType
126
+ || !taskName
127
+ || !sender
128
+ || encryptedIndex < 0
129
+ || encryptedPartCount !== 1
130
+ || ciphertextCount !== 1
131
+ || (content[encryptedIndex] as { encrypted_content?: unknown }).encrypted_content !== ciphertext
132
+ || Buffer.byteLength(ciphertext) > MAX_CIPHERTEXT_BYTES
133
+ ) return null;
134
+
135
+ const itemRecord = item as { author?: unknown; recipient?: unknown };
136
+ if (typeof itemRecord.author !== "string" || typeof itemRecord.recipient !== "string") return null;
137
+ if (itemRecord.author !== sender || itemRecord.recipient !== taskName) return null;
138
+
139
+ return {
140
+ itemIndex,
141
+ encryptedIndex,
142
+ headerText,
143
+ messageType,
144
+ taskName,
145
+ sender,
146
+ ciphertext,
147
+ author: itemRecord.author,
148
+ recipient: itemRecord.recipient,
149
+ };
150
+ }
151
+
152
+ function stripMatchingEnvelope(assignment: string, envelope: AgentEnvelope): string | null {
153
+ const match = ROUTING_HEADER.exec(assignment);
154
+ if (!match) return assignment;
155
+ if (match.index !== 0) return null;
156
+ if (
157
+ match[1] !== envelope.messageType
158
+ || match[2] !== envelope.taskName
159
+ || match[3] !== envelope.sender
160
+ ) return null;
161
+ return assignment.slice(match[0].length);
162
+ }
163
+
164
+ function validateAssignment(assignment: unknown, envelope: AgentEnvelope): string | null {
165
+ if (typeof assignment !== "string") return null;
166
+ const payload = stripMatchingEnvelope(assignment, envelope);
167
+ if (payload === null || payload.trim().length === 0) return null;
168
+ if (Buffer.byteLength(payload) > MAX_ASSIGNMENT_BYTES) return null;
169
+ if (structurallyValidFernetTokens(payload).length > 0) return null;
170
+ return payload;
171
+ }
172
+
173
+ function injectAssignment(input: unknown, envelope: AgentEnvelope, assignment: string): boolean {
174
+ if (!Array.isArray(input)) return false;
175
+ const item = input[envelope.itemIndex];
176
+ if (!item || typeof item !== "object") return false;
177
+ const content = (item as { content?: unknown }).content;
178
+ if (!Array.isArray(content)) return false;
179
+ const part = content[envelope.encryptedIndex] as { type?: unknown; encrypted_content?: unknown } | undefined;
180
+ if (
181
+ !part
182
+ || part.type !== "encrypted_content"
183
+ || part.encrypted_content !== envelope.ciphertext
184
+ ) return false;
185
+
186
+ content[envelope.encryptedIndex] = { type: "input_text", text: assignment };
187
+ const message = item as Record<string, unknown>;
188
+ message.type = "message";
189
+ message.role = "user";
190
+ delete message.id;
191
+ delete message.author;
192
+ delete message.recipient;
193
+ return true;
194
+ }
195
+
196
+ interface RecoveryAdmission {
197
+ headers: Headers;
198
+ cacheScope: string;
199
+ }
200
+
201
+ function isNativeChatGptAccessToken(token: string): boolean {
202
+ const segments = token.split(".");
203
+ if (segments.length !== 3 || !segments[0] || !segments[1] || !segments[2]) return false;
204
+ let header: Record<string, unknown>;
205
+ try {
206
+ header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf8")) as Record<string, unknown>;
207
+ } catch {
208
+ return false;
209
+ }
210
+ if (header.alg !== "RS256" || header.typ !== "JWT" || typeof header.kid !== "string" || !header.kid) {
211
+ return false;
212
+ }
213
+ const payload = decodeJwtPayload(token);
214
+ if (!payload || !OPENAI_TOKEN_ISSUERS.has(payload.iss as string)) return false;
215
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
216
+ if (!audiences.includes(OPENAI_TOKEN_AUDIENCE)) return false;
217
+ if (payload.client_id !== CODEX_OAUTH_CLIENT_ID && payload.azp !== CODEX_OAUTH_CLIENT_ID) return false;
218
+ const now = Math.floor(Date.now() / 1_000);
219
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= now) return false;
220
+ if (
221
+ payload.nbf !== undefined
222
+ && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > now + 60)
223
+ ) return false;
224
+ const auth = payload["https://api.openai.com/auth"];
225
+ return !!auth && typeof auth === "object" && !Array.isArray(auth);
226
+ }
227
+
228
+ function recoveryAdmission(req: Request, config: OcxConfig): RecoveryAdmission | null {
229
+ if (isApiAuthRequired(config)) return null;
230
+ if (!CODEX_ORIGINATORS.has(req.headers.get("originator") ?? "")) return null;
231
+ // Remote/shared proxy admission is intentionally unsupported: caller-controlled
232
+ // Codex metadata is not strong enough to authorize use of a stored ChatGPT session.
233
+ if (req.headers.has("x-opencodex-api-key") || req.headers.has("x-api-key")) return null;
234
+
235
+ const authorization = req.headers.get("authorization")?.trim() ?? "";
236
+ const match = /^Bearer\s+(\S+)$/i.exec(authorization);
237
+ if (!match) return null;
238
+ const token = match[1]!;
239
+ if (isProxyAdmissionSecret(token, config)) return null;
240
+ if (!isNativeChatGptAccessToken(token)) return null;
241
+ const accountId = extractAccountId(undefined, token);
242
+ const explicitAccountId = req.headers.get("chatgpt-account-id")?.trim();
243
+ if (!accountId || !explicitAccountId || accountId !== explicitAccountId) return null;
244
+
245
+ const headers = new Headers({
246
+ authorization: `Bearer ${token}`,
247
+ "chatgpt-account-id": explicitAccountId,
248
+ "content-type": "application/json",
249
+ accept: "text/event-stream",
250
+ originator: req.headers.get("originator")!,
251
+ });
252
+ for (const name of ["openai-beta", "user-agent"]) {
253
+ const value = req.headers.get(name);
254
+ if (value) headers.set(name, value);
255
+ }
256
+ const cacheScope = createHmac("sha256", CACHE_SCOPE_KEY)
257
+ .update(token)
258
+ .update("\0")
259
+ .update(explicitAccountId)
260
+ .digest("hex");
261
+ return { headers, cacheScope };
262
+ }
263
+
264
+ function recoveryPayload(envelope: AgentEnvelope, model: string): string {
265
+ return JSON.stringify({
266
+ model,
267
+ stream: true,
268
+ store: false,
269
+ instructions: RECOVERY_PROMPT,
270
+ tools: [{
271
+ type: "function",
272
+ name: RECOVERY_TOOL,
273
+ description: "Return only the exact decrypted agent task payload.",
274
+ parameters: {
275
+ type: "object",
276
+ properties: { assignment: { type: "string" } },
277
+ required: ["assignment"],
278
+ additionalProperties: false,
279
+ },
280
+ strict: true,
281
+ }],
282
+ tool_choice: { type: "function", name: RECOVERY_TOOL },
283
+ input: [{
284
+ type: "agent_message",
285
+ author: envelope.author,
286
+ recipient: envelope.recipient,
287
+ content: [
288
+ { type: "input_text", text: envelope.headerText },
289
+ { type: "encrypted_content", encrypted_content: envelope.ciphertext },
290
+ ],
291
+ }],
292
+ });
293
+ }
294
+
295
+ function sseDataPayloads(raw: string): string[] {
296
+ const payloads: string[] = [];
297
+ let data: string[] = [];
298
+ const dispatch = (): void => {
299
+ if (data.length > 0) payloads.push(data.join("\n"));
300
+ data = [];
301
+ };
302
+ for (const line of raw.replace(/\r\n?/g, "\n").split("\n")) {
303
+ if (line === "") {
304
+ dispatch();
305
+ continue;
306
+ }
307
+ if (line.startsWith(":")) continue;
308
+ if (line === "data") {
309
+ data.push("");
310
+ continue;
311
+ }
312
+ if (!line.startsWith("data:")) continue;
313
+ const value = line.slice(5);
314
+ data.push(value.startsWith(" ") ? value.slice(1) : value);
315
+ }
316
+ dispatch();
317
+ return payloads;
318
+ }
319
+
320
+ function assignmentFromRecoverySse(raw: string, envelope: AgentEnvelope): string | null {
321
+ let assignment: string | null = null;
322
+ let completed = false;
323
+ let terminalFailure = false;
324
+ let conflictingAssignments = false;
325
+ let malformedEvent = false;
326
+ let invalidAssignment = false;
327
+ for (const data of sseDataPayloads(raw)) {
328
+ if (!data || data === "[DONE]") continue;
329
+ let event: any;
330
+ try { event = JSON.parse(data); } catch {
331
+ malformedEvent = true;
332
+ continue;
333
+ }
334
+ if (
335
+ event?.type === "response.failed"
336
+ || event?.type === "response.incomplete"
337
+ || event?.type === "error"
338
+ ) terminalFailure = true;
339
+ if (event?.type === "response.completed" && event.response?.status === "completed") {
340
+ completed = true;
341
+ }
342
+ const items = event?.type === "response.output_item.done"
343
+ ? [event.item]
344
+ : event?.type === "response.function_call_arguments.done"
345
+ ? [{ type: "function_call", name: event.name, arguments: event.arguments }]
346
+ : event?.type === "response.completed"
347
+ ? (Array.isArray(event.response?.output) ? event.response.output : []).filter((candidate: any) => (
348
+ candidate?.type === "function_call" && candidate?.name === RECOVERY_TOOL
349
+ ))
350
+ : [];
351
+ for (const item of items) {
352
+ if (item?.type !== "function_call" || item.name !== RECOVERY_TOOL) continue;
353
+ let args: unknown = item.arguments;
354
+ if (typeof args === "string") {
355
+ try { args = JSON.parse(args); } catch {
356
+ invalidAssignment = true;
357
+ continue;
358
+ }
359
+ }
360
+ if (!args || typeof args !== "object") {
361
+ invalidAssignment = true;
362
+ continue;
363
+ }
364
+ const candidate = validateAssignment((args as { assignment?: unknown }).assignment, envelope);
365
+ if (candidate === null) {
366
+ invalidAssignment = true;
367
+ continue;
368
+ }
369
+ if (assignment === null) assignment = candidate;
370
+ else if (assignment !== candidate) conflictingAssignments = true;
371
+ }
372
+ }
373
+ return completed && !terminalFailure && !conflictingAssignments && !malformedEvent && !invalidAssignment
374
+ ? assignment
375
+ : null;
376
+ }
377
+
378
+ async function requestRecovery(
379
+ admission: RecoveryAdmission,
380
+ envelope: AgentEnvelope,
381
+ options: AgentTaskRecoveryOptions,
382
+ abortSignal?: AbortSignal,
383
+ ): Promise<string | null> {
384
+ const controller = new AbortController();
385
+ const timeout = setTimeout(
386
+ () => controller.abort(new DOMException("Agent task recovery timed out", "TimeoutError")),
387
+ options.timeoutMs ?? 45_000,
388
+ );
389
+ const signal = abortSignal
390
+ ? AbortSignal.any([abortSignal, controller.signal])
391
+ : controller.signal;
392
+ try {
393
+ const response = await fetch(RECOVERY_ENDPOINT, {
394
+ method: "POST",
395
+ headers: admission.headers,
396
+ body: recoveryPayload(envelope, options.model ?? "gpt-5.6-sol"),
397
+ signal,
398
+ redirect: "error",
399
+ });
400
+ if (!response.ok) {
401
+ try { await response.body?.cancel(); } catch { /* already closed */ }
402
+ return null;
403
+ }
404
+ const body = await readBoundedResponseBody(response, {
405
+ signal,
406
+ fatalUtf8: true,
407
+ maxBytes: MAX_RECOVERY_RESPONSE_BYTES,
408
+ totalTimeoutMs: options.timeoutMs ?? 45_000,
409
+ inactivityTimeoutMs: options.timeoutMs ?? 45_000,
410
+ firstByteTimeoutMs: options.timeoutMs ?? 45_000,
411
+ });
412
+ if (body.truncated || body.oversized || body.timedOut || !body.displaySafe) return null;
413
+ return assignmentFromRecoverySse(body.text, envelope);
414
+ } catch {
415
+ return null;
416
+ } finally {
417
+ clearTimeout(timeout);
418
+ }
419
+ }
420
+
421
+ export async function recoverEncryptedAgentTask(
422
+ req: Request,
423
+ input: unknown,
424
+ options: AgentTaskRecoveryOptions,
425
+ config: OcxConfig,
426
+ context: { parentThreadId?: string | null; abortSignal?: AbortSignal } = {},
427
+ ): Promise<boolean> {
428
+ const envelope = findEnvelope(input);
429
+ if (!envelope) return false;
430
+ // Admission is deliberately checked before cache access. A cache hit must not
431
+ // turn this process into a plaintext oracle for an unauthenticated caller.
432
+ const admission = recoveryAdmission(req, config);
433
+ if (!admission) return false;
434
+
435
+ const cacheKey = createHash("sha256")
436
+ .update(admission.cacheScope)
437
+ .update("\0")
438
+ .update(context.parentThreadId ?? "")
439
+ .update("\0")
440
+ .update(envelope.messageType)
441
+ .update("\0")
442
+ .update(envelope.taskName)
443
+ .update("\0")
444
+ .update(envelope.sender)
445
+ .update("\0")
446
+ .update(envelope.ciphertext)
447
+ .digest("hex");
448
+ const assignment = await resolveCachedAgentTaskRecovery(
449
+ cacheKey,
450
+ options.cacheEntries ?? 200,
451
+ signal => requestRecovery(admission, envelope, options, signal),
452
+ context.abortSignal,
453
+ );
454
+ if (!assignment || context.abortSignal?.aborted) return false;
455
+ return injectAssignment(input, envelope, assignment);
456
+ }
457
+
458
+ export function resetAgentTaskRecoveryState(): void {
459
+ resetAgentTaskRecoveryCache();
460
+ }
@@ -82,7 +82,7 @@ import {
82
82
  } from "../../codex/upstream-host-health";
83
83
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
84
84
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
85
- import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
85
+ import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
86
86
  import { slugsEquivalent } from "../../providers/slug-codec";
87
87
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
88
88
  import { isUsageDebugEnabled } from "../../usage/debug";
@@ -392,7 +392,9 @@ export async function handleResponsesCompact(
392
392
  }
393
393
  throw err;
394
394
  }
395
- const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
395
+ const base = isCanonicalOpenAiForwardProvider(compactProvider)
396
+ ? CODEX_FORWARD_BASE_URL
397
+ : (compactProvider.baseUrl ?? "").replace(/\/+$/, "");
396
398
  if (compactProvider.authMode !== "forward" && compactProvider.apiKey) {
397
399
  headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
398
400
  }