@bitkyc08/opencodex 2.18.2 → 2.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.
Files changed (131) hide show
  1. package/gui/dist/assets/index-DF_UFrGS.css +1 -0
  2. package/gui/dist/assets/index-DSK3S5HY.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/google-antigravity-wire.ts +4 -3
  7. package/src/adapters/google.ts +1 -1
  8. package/src/adapters/mimo-free.ts +17 -0
  9. package/src/adapters/openai-chat.ts +367 -32
  10. package/src/adapters/registry.ts +144 -0
  11. package/src/chat/inbound.ts +37 -8
  12. package/src/cli/claude.ts +2 -1
  13. package/src/cli/combo.ts +3 -0
  14. package/src/cli/dispatch.ts +8 -0
  15. package/src/cli/export-command.ts +2 -2
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +3 -2
  18. package/src/cli/lab.ts +135 -1
  19. package/src/cli/minimax.ts +491 -0
  20. package/src/cli/models-runtime.ts +22 -1
  21. package/src/cli/models.ts +67 -2
  22. package/src/cli/opencode.ts +2 -1
  23. package/src/cli/registry.ts +22 -2
  24. package/src/clients/config-export.ts +125 -7
  25. package/src/codex/app-server-processes.ts +57 -2
  26. package/src/codex/app-server-restart-service.ts +232 -0
  27. package/src/codex/catalog/aggregation.ts +10 -1
  28. package/src/codex/catalog/effort.ts +15 -3
  29. package/src/codex/catalog/parsing.ts +3 -1
  30. package/src/codex/catalog/provider-fetch.ts +45 -5
  31. package/src/codex/catalog/sync.ts +74 -4
  32. package/src/codex/convergence.ts +2 -0
  33. package/src/codex/shim.ts +100 -5
  34. package/src/combos/index.ts +1 -0
  35. package/src/combos/request.ts +30 -0
  36. package/src/combos/types.ts +6 -0
  37. package/src/config.ts +52 -0
  38. package/src/generated/compatibility-version.json +239 -87
  39. package/src/images/loop.ts +11 -1
  40. package/src/integrations/registry.ts +7 -0
  41. package/src/lab/conformance/jcs.ts +42 -2
  42. package/src/lab/conformance/negative-controls.ts +6 -2
  43. package/src/lab/conformance/runner.ts +16 -5
  44. package/src/lab/fabric/observe.ts +49 -14
  45. package/src/lab/index.ts +16 -0
  46. package/src/lab/ledger/purge.ts +152 -83
  47. package/src/lab/ledger/store.ts +168 -54
  48. package/src/lab/observe/from-conformance.ts +8 -6
  49. package/src/lab/observe/from-live.ts +8 -2
  50. package/src/lab/paths.ts +23 -0
  51. package/src/lab/public/bundle.ts +217 -0
  52. package/src/lab/public/community-authority.ts +175 -0
  53. package/src/lab/public/community-files.ts +29 -0
  54. package/src/lab/public/community.ts +479 -0
  55. package/src/lab/public/file-safety.ts +155 -0
  56. package/src/lab/public/ids.ts +26 -0
  57. package/src/lab/public/index.ts +16 -0
  58. package/src/lab/public/mutation-lock.ts +424 -0
  59. package/src/lab/public/operator.ts +353 -0
  60. package/src/lab/public/origin-purge.ts +79 -0
  61. package/src/lab/public/origin.ts +203 -0
  62. package/src/lab/public/privacy.ts +143 -0
  63. package/src/lab/public/private-file.ts +261 -0
  64. package/src/lab/public/project.ts +124 -0
  65. package/src/lab/public/purge-test-fault.ts +21 -0
  66. package/src/lab/public/purge.ts +223 -0
  67. package/src/lab/public/registry.ts +44 -0
  68. package/src/lab/public/revocation.ts +252 -0
  69. package/src/lab/public/signature.ts +219 -0
  70. package/src/lab/public/storage.ts +105 -0
  71. package/src/lab/public/strict-json.ts +206 -0
  72. package/src/lab/public/time.ts +26 -0
  73. package/src/lab/public/types.ts +172 -0
  74. package/src/lab/public/validate.ts +391 -0
  75. package/src/lib/codex-restart-contract.ts +120 -0
  76. package/src/lib/errors.ts +27 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +9 -3
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/spill-store.ts +20 -1
  91. package/src/responses/state.ts +159 -3
  92. package/src/router.ts +23 -6
  93. package/src/routing/capability.ts +4 -2
  94. package/src/routing/compatibility/behavior.ts +5 -1
  95. package/src/server/adapter-resolve.ts +2 -32
  96. package/src/server/auth-cors.ts +8 -0
  97. package/src/server/chat-completions.ts +74 -34
  98. package/src/server/chat-native-sse.ts +331 -0
  99. package/src/server/chat-native.ts +371 -0
  100. package/src/server/effort-policy.ts +18 -0
  101. package/src/server/management/combo-routes.ts +16 -2
  102. package/src/server/management/config-routes.ts +6 -4
  103. package/src/server/management/context.ts +17 -0
  104. package/src/server/management/lab-routes.ts +181 -19
  105. package/src/server/management/model-routes.ts +76 -2
  106. package/src/server/management/model-rows.ts +8 -0
  107. package/src/server/management/provider-capability-config.ts +48 -0
  108. package/src/server/management/provider-routes.ts +76 -4
  109. package/src/server/management/system-restart.ts +4 -2
  110. package/src/server/management/system-routes.ts +38 -0
  111. package/src/server/relay.ts +17 -3
  112. package/src/server/responses/compact.ts +4 -1
  113. package/src/server/responses/core.ts +278 -48
  114. package/src/server/responses/empty-completion-guard.ts +276 -0
  115. package/src/server/responses/fetch-helpers.ts +35 -4
  116. package/src/server/responses/input-admission.ts +169 -0
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/service-manager-probe.ts +2 -3
  123. package/src/types.ts +76 -6
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/vision/index.ts +21 -4
  129. package/src/web-search/index.ts +2 -1
  130. package/gui/dist/assets/index-CXI1262_.js +0 -76
  131. package/gui/dist/assets/index-DUCH59lJ.css +0 -1
@@ -0,0 +1,331 @@
1
+ import { chatCompletionsErrorBody } from "../chat/outbound";
2
+ import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
3
+ import { redactSecretString } from "../lib/redact";
4
+ import {
5
+ isTranslatorBudgetExceededError,
6
+ TRANSLATOR_MAX_SSE_EVENT_BYTES,
7
+ type TranslatorBudget,
8
+ } from "../lib/translator-budget";
9
+ import type { OcxUsage } from "../types";
10
+ import { nextSseBlock, replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
11
+
12
+ type Rec = Record<string, unknown>;
13
+
14
+ function isRec(value: unknown): value is Rec {
15
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ export function usageFromChat(value: unknown): OcxUsage | undefined {
19
+ if (!isRec(value)) return undefined;
20
+ const prompt = typeof value.prompt_tokens === "number" ? value.prompt_tokens : undefined;
21
+ const completion = typeof value.completion_tokens === "number" ? value.completion_tokens : undefined;
22
+ if (prompt === undefined && completion === undefined) return undefined;
23
+ const promptDetails = isRec(value.prompt_tokens_details) ? value.prompt_tokens_details : undefined;
24
+ const completionDetails = isRec(value.completion_tokens_details) ? value.completion_tokens_details : undefined;
25
+ return {
26
+ inputTokens: prompt ?? 0,
27
+ outputTokens: completion ?? 0,
28
+ ...(typeof promptDetails?.cached_tokens === "number" ? { cachedInputTokens: promptDetails.cached_tokens } : {}),
29
+ ...(typeof completionDetails?.reasoning_tokens === "number"
30
+ ? { reasoningOutputTokens: completionDetails.reasoning_tokens }
31
+ : {}),
32
+ };
33
+ }
34
+
35
+ export function structuredError(value: unknown): {
36
+ message: string;
37
+ type?: string;
38
+ code?: string | null;
39
+ status?: number;
40
+ } | null {
41
+ if (!isRec(value) || value.error === undefined || value.error === null) return null;
42
+ const error = value.error;
43
+ if (typeof error === "string") return { message: redactSecretString(error) };
44
+ if (!isRec(error)) return { message: "upstream error" };
45
+ const rawMessage = typeof error.message === "string" ? error.message : "upstream error";
46
+ return {
47
+ message: redactSecretString(rawMessage),
48
+ ...(typeof error.type === "string" ? { type: error.type } : {}),
49
+ ...(error.code === null || typeof error.code === "string" ? { code: error.code } : {}),
50
+ ...(typeof error.status === "number" && Number.isInteger(error.status) ? { status: error.status } : {}),
51
+ };
52
+ }
53
+
54
+ function normalizedChunk(value: Rec, requestedModel: string): Rec {
55
+ return {
56
+ ...value,
57
+ id: typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`,
58
+ object: "chat.completion.chunk",
59
+ created: typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000),
60
+ model: requestedModel,
61
+ };
62
+ }
63
+
64
+ export function jsonCompletionSse(value: Rec, requestedModel: string): string {
65
+ const id = typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
66
+ const created = typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000);
67
+ const model = requestedModel;
68
+ const choices = Array.isArray(value.choices) ? value.choices : [];
69
+ const choice = isRec(choices[0]) ? choices[0] : {};
70
+ const message = isRec(choice.message) ? choice.message : {};
71
+ const frames: Rec[] = [{
72
+ id,
73
+ object: "chat.completion.chunk",
74
+ created,
75
+ model,
76
+ choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
77
+ }];
78
+ const delta: Rec = {};
79
+ if (typeof message.content === "string" && message.content.length > 0) delta.content = message.content;
80
+ if (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0) {
81
+ delta.reasoning_content = message.reasoning_content;
82
+ }
83
+ if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
84
+ delta.tool_calls = message.tool_calls.map((tool, index) => isRec(tool) ? { index, ...tool } : tool);
85
+ }
86
+ if (Object.keys(delta).length > 0) {
87
+ frames.push({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] });
88
+ }
89
+ frames.push({
90
+ id,
91
+ object: "chat.completion.chunk",
92
+ created,
93
+ model,
94
+ choices: [{ index: 0, delta: {}, finish_reason: typeof choice.finish_reason === "string" ? choice.finish_reason : "stop" }],
95
+ ...(value.usage !== undefined ? { usage: value.usage } : {}),
96
+ });
97
+ return `${frames.map(frame => `data: ${JSON.stringify(frame)}\n\n`).join("")}data: [DONE]\n\n`;
98
+ }
99
+
100
+ interface NativeChatSseOptions {
101
+ requestedModel: string;
102
+ translatorBudget: TranslatorBudget;
103
+ signal: AbortSignal;
104
+ onFirstOutput?: () => void;
105
+ onUsage: (usage: OcxUsage) => void;
106
+ onTerminal?: (status: number, message?: string) => void;
107
+ onCancel?: () => void;
108
+ }
109
+
110
+ export function nativeChatSse(
111
+ body: ReadableStream<Uint8Array>,
112
+ options: NativeChatSseOptions,
113
+ ): ReadableStream<Uint8Array> {
114
+ const reader = body.getReader();
115
+ const decoder = new TextDecoder();
116
+ const encoder = new TextEncoder();
117
+ const scope = { kind: "live_transient" as const };
118
+ let buffer = "";
119
+ let bufferBytes = 0;
120
+ let queuedBytes = 0;
121
+ let sawFinish = false;
122
+ let sawDone = false;
123
+ let firstOutput = false;
124
+ let settled = false;
125
+ let cancelled = false;
126
+ let cancelledBySignal = false;
127
+
128
+ const releaseQueued = () => {
129
+ if (queuedBytes === 0) return;
130
+ options.translatorBudget.releaseRetained(queuedBytes, scope);
131
+ queuedBytes = 0;
132
+ };
133
+ const replaceBuffer = (next: string) => {
134
+ const nextBytes = encoder.encode(next).byteLength;
135
+ const reservation = options.translatorBudget.reserveTransient(nextBytes, scope);
136
+ reservation.commitRetained();
137
+ options.translatorBudget.releaseRetained(bufferBytes, scope);
138
+ buffer = next;
139
+ bufferBytes = nextBytes;
140
+ };
141
+ const appendBuffer = (fragment: string) => {
142
+ if (!fragment) return;
143
+ const fragmentBytes = encoder.encode(fragment).byteLength;
144
+ const nextBytes = bufferBytes + fragmentBytes;
145
+ if (nextBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
146
+ throw new Error("upstream SSE event exceeded the safe limit", { cause: { code: "translation_buffer_limit" } });
147
+ }
148
+ const reservation = options.translatorBudget.reserveTransient(nextBytes, scope);
149
+ try {
150
+ buffer += fragment;
151
+ reservation.commitRetained();
152
+ options.translatorBudget.releaseRetained(bufferBytes, scope);
153
+ bufferBytes = nextBytes;
154
+ } catch (error) {
155
+ reservation.release();
156
+ throw error;
157
+ }
158
+ };
159
+ const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
160
+ const bytes = encoder.encode(text);
161
+ const reservation = options.translatorBudget.reserveTransient(bytes.byteLength, scope);
162
+ controller.enqueue(bytes);
163
+ reservation.commitRetained();
164
+ queuedBytes += bytes.byteLength;
165
+ };
166
+ const releaseBuffer = () => {
167
+ options.translatorBudget.releaseRetained(bufferBytes, scope);
168
+ buffer = "";
169
+ bufferBytes = 0;
170
+ };
171
+ const settle = (status: number, message?: string) => {
172
+ if (settled) return;
173
+ settled = true;
174
+ options.signal.removeEventListener("abort", onAbort);
175
+ options.onTerminal?.(status, message);
176
+ };
177
+ const fail = (
178
+ controller: ReadableStreamDefaultController<Uint8Array>,
179
+ message: string,
180
+ code: string,
181
+ status = 502,
182
+ type = "upstream_error",
183
+ ) => {
184
+ const safeMessage = redactSecretString(message);
185
+ try {
186
+ const frame = `data: ${JSON.stringify(chatCompletionsErrorBody(status, safeMessage, type, code))}\n\n`;
187
+ if (code === "translation_buffer_limit") controller.enqueue(encoder.encode(frame));
188
+ else enqueue(controller, frame);
189
+ } catch { /* controller may already be closed */ }
190
+ releaseBuffer();
191
+ settle(status, safeMessage);
192
+ try { void reader.cancel(new Error(safeMessage)).catch(() => {}); } catch { /* already closed */ }
193
+ try { controller.close(); } catch { /* already closed */ }
194
+ };
195
+ const processBlock = (
196
+ controller: ReadableStreamDefaultController<Uint8Array>,
197
+ block: string,
198
+ delimiter: string,
199
+ ): void => {
200
+ const payload = sseDataPayload(block);
201
+ if (payload === null) {
202
+ enqueue(controller, block + delimiter);
203
+ return;
204
+ }
205
+ const trimmed = payload.trim();
206
+ if (trimmed === "[DONE]") {
207
+ sawDone = true;
208
+ enqueue(controller, replaceSseDataPayload(block, "[DONE]") + delimiter);
209
+ settle(200);
210
+ try { void reader.cancel().catch(() => {}); } catch { /* already closed */ }
211
+ controller.close();
212
+ return;
213
+ }
214
+ let parsed: unknown;
215
+ try {
216
+ parsed = JSON.parse(payload);
217
+ } catch {
218
+ fail(controller, "upstream returned malformed SSE JSON", "upstream_sse_invalid");
219
+ return;
220
+ }
221
+ if (!isRec(parsed)) {
222
+ fail(controller, "upstream returned an invalid SSE event", "upstream_sse_invalid");
223
+ return;
224
+ }
225
+ const error = structuredError(parsed);
226
+ if (error) {
227
+ const status = error.status ?? 502;
228
+ const classified = classifyError(status, error.type ?? "upstream_error", error.message);
229
+ if (isCyberPolicyCode(error.code)) {
230
+ classified.code = CYBER_POLICY_ERROR_CODE;
231
+ classified.type = "invalid_request_error";
232
+ } else if (error.code !== undefined && error.code !== null) {
233
+ classified.code = error.code;
234
+ }
235
+ const safe = chatCompletionsErrorBody(status, classified.message, classified.type, classified.code);
236
+ enqueue(controller, replaceSseDataPayload(block, JSON.stringify(safe)) + delimiter);
237
+ settle(isCyberPolicyCode(classified.code) ? 400 : status, classified.message);
238
+ try { void reader.cancel(new Error(classified.message)).catch(() => {}); } catch { /* already closed */ }
239
+ controller.close();
240
+ return;
241
+ }
242
+ const usage = usageFromChat(parsed.usage);
243
+ if (usage) options.onUsage(usage);
244
+ const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
245
+ if (choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0)) {
246
+ sawFinish = true;
247
+ }
248
+ if (!firstOutput && choices.some(choice => {
249
+ if (!isRec(choice) || !isRec(choice.delta)) return false;
250
+ const delta = choice.delta;
251
+ return (typeof delta.content === "string" && delta.content.length > 0)
252
+ || (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0)
253
+ || (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0);
254
+ })) {
255
+ firstOutput = true;
256
+ options.onFirstOutput?.();
257
+ }
258
+ enqueue(controller, replaceSseDataPayload(block, JSON.stringify(normalizedChunk(parsed, options.requestedModel))) + delimiter);
259
+ };
260
+
261
+ function onAbort() {
262
+ cancelledBySignal = true;
263
+ if (!settled) {
264
+ settled = true;
265
+ options.onCancel?.();
266
+ }
267
+ try { void reader.cancel(options.signal.reason).catch(() => {}); } catch { /* already closed */ }
268
+ }
269
+ if (options.signal.aborted) onAbort();
270
+ else options.signal.addEventListener("abort", onAbort, { once: true });
271
+
272
+ return new ReadableStream<Uint8Array>({
273
+ async pull(controller) {
274
+ releaseQueued();
275
+ try {
276
+ for (;;) {
277
+ const next = nextSseBlock(buffer);
278
+ if (next) {
279
+ replaceBuffer(next.rest);
280
+ processBlock(controller, next.block, next.delimiter);
281
+ return;
282
+ }
283
+ const { done, value } = await reader.read();
284
+ if (cancelled) return;
285
+ if (cancelledBySignal) {
286
+ releaseBuffer();
287
+ controller.close();
288
+ return;
289
+ }
290
+ if (!done) {
291
+ appendBuffer(decoder.decode(value, { stream: true }));
292
+ continue;
293
+ }
294
+ appendBuffer(decoder.decode());
295
+ if (buffer.trim().length > 0) {
296
+ fail(controller, "upstream SSE ended with an unterminated event", "upstream_sse_unterminated");
297
+ return;
298
+ }
299
+ releaseBuffer();
300
+ if (sawDone || sawFinish) {
301
+ if (!sawDone) enqueue(controller, "data: [DONE]\n\n");
302
+ settle(200);
303
+ controller.close();
304
+ } else {
305
+ fail(controller, "upstream SSE ended before a terminal event", "upstream_sse_truncated");
306
+ }
307
+ return;
308
+ }
309
+ } catch (error) {
310
+ const overflow = isTranslatorBudgetExceededError(error)
311
+ || (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit");
312
+ fail(
313
+ controller,
314
+ overflow ? "upstream SSE event exceeded the safe limit" : error instanceof Error ? error.message : String(error),
315
+ overflow ? "translation_buffer_limit" : "upstream_sse_error",
316
+ );
317
+ }
318
+ },
319
+ cancel(reason) {
320
+ cancelled = true;
321
+ releaseQueued();
322
+ releaseBuffer();
323
+ options.signal.removeEventListener("abort", onAbort);
324
+ if (!settled) {
325
+ settled = true;
326
+ options.onCancel?.();
327
+ }
328
+ try { return reader.cancel(reason).then(() => undefined, () => undefined); } catch { return Promise.resolve(); }
329
+ },
330
+ });
331
+ }
@@ -0,0 +1,371 @@
1
+ import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
2
+ import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
3
+ import {
4
+ chatCompletionsErrorBody,
5
+ chatCompletionsErrorResponse,
6
+ collectChatCompletion,
7
+ isChatCompletionsStreamError,
8
+ } from "../chat/outbound";
9
+ import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
10
+ import { readBoundedResponseBody } from "../lib/bounded-body";
11
+ import { redactSecretString } from "../lib/redact";
12
+ import { resolveClientRetryAfter } from "../lib/retry-after";
13
+ import {
14
+ applyUpstreamRecoveryInit,
15
+ fetchWithResetRetry,
16
+ prepareSameTarget429Wait,
17
+ type UpstreamSendRecovery,
18
+ } from "../lib/upstream-retry";
19
+ import {
20
+ isTranslatorBudgetExceededError,
21
+ type TranslatorBudget,
22
+ } from "../lib/translator-budget";
23
+ import {
24
+ hasKeyPoolFailover,
25
+ rateLimitRetryDelayMs,
26
+ rateLimitRetryPolicyFor,
27
+ rotateProviderTransportOn429,
28
+ } from "../providers/key-failover";
29
+ import type { RouteResult } from "../router";
30
+ import type { OcxConfig, OcxProviderConfig } from "../types";
31
+ import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
32
+ import { linkAbortSignal } from "./responses";
33
+ import {
34
+ addFinalRequestLog,
35
+ beginRequestAttempt,
36
+ noteAttemptSend,
37
+ recordFirstOutput,
38
+ sealRequestAttemptIdentity,
39
+ type RequestLogContext,
40
+ } from "./request-log";
41
+ import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
42
+
43
+ type Rec = Record<string, unknown>;
44
+
45
+ const MAX_NATIVE_CHAT_JSON_BYTES = 32 * 1024 * 1024;
46
+ const MAX_NATIVE_CHAT_ERROR_BYTES = 64 * 1024;
47
+
48
+ function isRec(value: unknown): value is Rec {
49
+ return value !== null && typeof value === "object" && !Array.isArray(value);
50
+ }
51
+
52
+ export function isNativeChatRouteEligible(route: RouteResult, rawBody: Rec): boolean {
53
+ const provider = route.provider;
54
+ if (provider.adapter !== "openai-chat") return false;
55
+ if (provider.authMode !== undefined && provider.authMode !== "key" && provider.authMode !== "local") return false;
56
+ // Combo and policy execution own multi-candidate retries in the Responses pipeline.
57
+ if (route.combo || route.routeKind === "combo" || route.routeKind === "policy") return false;
58
+ if (rawBody.store === true || rawBody.background === true) return false;
59
+ if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id.length > 0) return false;
60
+ if (rawBody.compaction_trigger !== undefined) return false;
61
+ if (Array.isArray(rawBody.tools)) {
62
+ for (const tool of rawBody.tools) {
63
+ if (!isRec(tool)) continue;
64
+ if (tool.type === "web_search" || tool.type === "web_search_preview" || tool.type === "image_generation") {
65
+ return false;
66
+ }
67
+ }
68
+ }
69
+ return true;
70
+ }
71
+
72
+ function chatCompletionJson(value: unknown): Rec | null {
73
+ if (!isRec(value) || !Array.isArray(value.choices) || value.choices.length === 0) return null;
74
+ return value;
75
+ }
76
+
77
+ interface HandleNativeChatOptions {
78
+ req: Request;
79
+ config: OcxConfig;
80
+ logCtx: RequestLogContext;
81
+ logIds?: { requestId: string; start: number };
82
+ route: RouteResult;
83
+ chatBody: Rec;
84
+ requestedModel: string;
85
+ requestedStream: boolean;
86
+ translatorBudget: TranslatorBudget;
87
+ }
88
+
89
+ export async function handleNativeChatCompletions(options: HandleNativeChatOptions): Promise<Response> {
90
+ const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget } = options;
91
+ logCtx.inboundProtocol = "chat";
92
+ const attempt = beginRequestAttempt(
93
+ (logCtx.attempts?.length ?? 0) + 1,
94
+ route.providerName,
95
+ route.modelId,
96
+ "openai-chat",
97
+ );
98
+ logCtx.activeAttempt = attempt;
99
+ logCtx.activeAttemptStartedAt = Date.now();
100
+ (logCtx.attempts ??= []).push(attempt);
101
+ sealRequestAttemptIdentity(attempt, route.providerName, "openai-chat", logCtx.accountLogLabel);
102
+
103
+ let logged = false;
104
+ const finishLog = (status: number, message?: string, closeReason: "non_stream" | "terminal" | "client_cancel" = "non_stream") => {
105
+ if (logged) return;
106
+ logged = true;
107
+ if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
108
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason });
109
+ };
110
+ const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
111
+ const safeMessage = redactSecretString(message);
112
+ finishLog(status, safeMessage);
113
+ return chatCompletionsErrorResponse(status, safeMessage, type, code);
114
+ };
115
+
116
+ logCtx.requestedEffort = typeof options.chatBody.reasoning_effort === "string"
117
+ ? options.chatBody.reasoning_effort
118
+ : undefined;
119
+ logCtx.requestedServiceTier = typeof options.chatBody.service_tier === "string"
120
+ ? options.chatBody.service_tier
121
+ : undefined;
122
+
123
+ const upstream = new AbortController();
124
+ const cleanupAbort = linkAbortSignal(upstream, req.signal);
125
+ const connectMs = config.connectTimeoutMs ?? 200_000;
126
+ let activeProvider: OcxProviderConfig = route.provider;
127
+ let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
128
+ let activeRequest: AdapterRequest;
129
+ let retainedRequestBytes = 0;
130
+ const releaseRetainedRequest = () => {
131
+ if (retainedRequestBytes === 0) return;
132
+ translatorBudget.releaseRetained(retainedRequestBytes, { kind: "request_copies" });
133
+ retainedRequestBytes = 0;
134
+ };
135
+ const retainRequest = (request: AdapterRequest) => {
136
+ const bytes = new TextEncoder().encode(request.body).byteLength;
137
+ translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
138
+ retainedRequestBytes = bytes;
139
+ };
140
+ try {
141
+ activeRequest = buildOpenAIChatPassthroughRequest(activeProvider, options.chatBody, route.modelId, requestedStream);
142
+ retainRequest(activeRequest);
143
+ } catch (error) {
144
+ releaseRetainedRequest();
145
+ cleanupAbort();
146
+ upstream.abort();
147
+ if (isTranslatorBudgetExceededError(error)) {
148
+ return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
149
+ }
150
+ return fail(400, error instanceof Error ? error.message : String(error), "invalid_request_error");
151
+ }
152
+
153
+ const send = async (request: AdapterRequest, recovery?: "rate-limit-429" | "key-429"): Promise<Response> => {
154
+ try {
155
+ return await fetchWithResetRetry(
156
+ (transportRecovery?: UpstreamSendRecovery) => {
157
+ noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
158
+ return fetchWithHeaderTimeout(
159
+ request.url,
160
+ applyUpstreamRecoveryInit({
161
+ method: request.method,
162
+ headers: request.headers,
163
+ body: request.body,
164
+ }, transportRecovery),
165
+ upstream.signal,
166
+ connectMs,
167
+ requestedStream,
168
+ providerFetch(activeProvider, undefined, {
169
+ providerName: route.providerName,
170
+ modelId: route.modelId,
171
+ }),
172
+ );
173
+ },
174
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
175
+ );
176
+ } finally {
177
+ request.releaseBodyObservation?.();
178
+ }
179
+ };
180
+
181
+ let response: Response;
182
+ try {
183
+ response = await send(activeRequest);
184
+ const retryPolicy = rateLimitRetryPolicyFor(activeProvider);
185
+ let retries = 0;
186
+ while (response.status === 429 && retryPolicy && retries < retryPolicy.attempts) {
187
+ retries += 1;
188
+ for await (const _ of prepareSameTarget429Wait({
189
+ body: response.body,
190
+ signal: upstream.signal,
191
+ delayMs: rateLimitRetryDelayMs(retryPolicy, response.headers.get("retry-after"), Date.now()),
192
+ })) { /* pre-stream wait */ }
193
+ if (upstream.signal.aborted) throw upstream.signal.reason;
194
+ response = await send(activeRequest, "rate-limit-429");
195
+ }
196
+ while (response.status === 429 && hasKeyPoolFailover(activeProvider)) {
197
+ const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
198
+ retryAfter: response.headers.get("retry-after"),
199
+ now: Date.now(),
200
+ attemptedKey: activeProvider.apiKey,
201
+ promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
202
+ });
203
+ if (!rotated) break;
204
+ try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
205
+ activeProvider = rotated;
206
+ activeAdapter = createOpenAIChatAdapter(activeProvider);
207
+ releaseRetainedRequest();
208
+ activeRequest = buildOpenAIChatPassthroughRequest(activeProvider, options.chatBody, route.modelId, requestedStream);
209
+ retainRequest(activeRequest);
210
+ response = await send(activeRequest, "key-429");
211
+ }
212
+ } catch (error) {
213
+ releaseRetainedRequest();
214
+ cleanupAbort();
215
+ upstream.abort();
216
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
217
+ return fail(502, error instanceof Error ? error.message : String(error), "server_error");
218
+ }
219
+ releaseRetainedRequest();
220
+
221
+ if (!response.ok) {
222
+ let bodyText = "";
223
+ try {
224
+ const body = await readBoundedResponseBody(response, {
225
+ signal: upstream.signal,
226
+ maxBytes: MAX_NATIVE_CHAT_ERROR_BYTES,
227
+ });
228
+ if (body.displaySafe) bodyText = body.text;
229
+ } catch { /* status-only fallback */ }
230
+ cleanupAbort();
231
+ if (req.signal.aborted) {
232
+ upstream.abort();
233
+ return fail(499, "Client cancelled request", "client_cancelled");
234
+ }
235
+ const detail = activeAdapter.formatErrorBody?.(response.status, response.headers, bodyText) ?? "";
236
+ let upstreamType: string | undefined;
237
+ let upstreamCode: string | null | undefined;
238
+ try {
239
+ const parsedError = JSON.parse(bodyText) as Rec;
240
+ const nested = isRec(parsedError.error) ? parsedError.error : undefined;
241
+ if (typeof nested?.type === "string") upstreamType = nested.type;
242
+ if (nested?.code === null || typeof nested?.code === "string") upstreamCode = nested.code;
243
+ } catch { /* keep generic classification */ }
244
+ const message = detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
245
+ const classified = classifyError(
246
+ response.status,
247
+ upstreamType ?? (response.status === 401 ? "authentication_error"
248
+ : response.status === 429 ? "rate_limit_error"
249
+ : response.status >= 500 ? "server_error" : "invalid_request_error"),
250
+ message,
251
+ );
252
+ if (isCyberPolicyCode(upstreamCode)) {
253
+ classified.code = CYBER_POLICY_ERROR_CODE;
254
+ classified.type = "invalid_request_error";
255
+ } else if (upstreamCode === "model_not_found") {
256
+ classified.code = "model_not_found";
257
+ classified.type = "invalid_request_error";
258
+ } else if (upstreamCode !== undefined && upstreamCode !== null && classified.code == null) {
259
+ classified.code = upstreamCode;
260
+ }
261
+ const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
262
+ const retryAfter = resolveClientRetryAfter({
263
+ status: response.status,
264
+ message: classified.message,
265
+ upstreamRetryAfter: response.headers.get("retry-after"),
266
+ });
267
+ finishLog(status, classified.message);
268
+ return new Response(JSON.stringify(chatCompletionsErrorBody(status, classified.message, classified.type, classified.code)), {
269
+ status,
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ ...(retryAfter ? { "Retry-After": retryAfter } : {}),
273
+ },
274
+ });
275
+ }
276
+
277
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
278
+ if (contentType.includes("text/event-stream") && response.body) {
279
+ const stream = nativeChatSse(response.body, {
280
+ requestedModel,
281
+ translatorBudget,
282
+ signal: upstream.signal,
283
+ onFirstOutput: logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined,
284
+ onUsage: usage => {
285
+ logCtx.usage = usage;
286
+ attempt.usage = usage;
287
+ },
288
+ ...(requestedStream ? {
289
+ onTerminal: (status: number, message?: string) => {
290
+ cleanupAbort();
291
+ finishLog(status, message, "terminal");
292
+ },
293
+ onCancel: () => {
294
+ cleanupAbort();
295
+ upstream.abort();
296
+ finishLog(499, undefined, "client_cancel");
297
+ },
298
+ } : {}),
299
+ });
300
+ if (requestedStream) {
301
+ return new Response(stream, {
302
+ status: 200,
303
+ headers: {
304
+ "Content-Type": "text/event-stream; charset=utf-8",
305
+ "Cache-Control": "no-cache",
306
+ Connection: "keep-alive",
307
+ },
308
+ });
309
+ }
310
+ try {
311
+ const completion = await collectChatCompletion(stream, requestedModel, translatorBudget);
312
+ cleanupAbort();
313
+ finishLog(200);
314
+ return Response.json(completion);
315
+ } catch (error) {
316
+ cleanupAbort();
317
+ upstream.abort();
318
+ if (isChatCompletionsStreamError(error)) {
319
+ return fail(error.status, error.message, error.type, error.code);
320
+ }
321
+ return fail(502, error instanceof Error ? error.message : String(error), "upstream_error");
322
+ }
323
+ }
324
+
325
+ let body;
326
+ try {
327
+ body = await readBoundedResponseBody(response, {
328
+ signal: upstream.signal,
329
+ maxBytes: MAX_NATIVE_CHAT_JSON_BYTES,
330
+ totalTimeoutMs: Math.max(connectMs, 5_000),
331
+ inactivityTimeoutMs: Math.max(connectMs, 5_000),
332
+ });
333
+ } catch (error) {
334
+ cleanupAbort();
335
+ upstream.abort();
336
+ if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
337
+ return fail(502, error instanceof Error ? error.message : String(error), "upstream_error");
338
+ }
339
+ cleanupAbort();
340
+ if (body.oversized) {
341
+ upstream.abort();
342
+ return fail(502, "upstream response exceeded the safe limit", "upstream_error", "translation_buffer_limit");
343
+ }
344
+ let parsedJson: unknown;
345
+ try {
346
+ parsedJson = JSON.parse(body.text);
347
+ } catch {
348
+ return fail(502, "upstream returned malformed Chat Completions JSON", "upstream_error");
349
+ }
350
+ const error = structuredError(parsedJson);
351
+ if (error) return fail(error.status ?? 502, error.message, error.type, error.code);
352
+ const completion = chatCompletionJson(parsedJson);
353
+ if (!completion) return fail(502, "upstream response contained no choices", "upstream_error");
354
+ const usage = usageFromChat(completion.usage);
355
+ if (usage) {
356
+ logCtx.usage = usage;
357
+ attempt.usage = usage;
358
+ }
359
+ if (logIds) recordFirstOutput(logCtx, logIds.start);
360
+ finishLog(200);
361
+ if (requestedStream) {
362
+ return new Response(jsonCompletionSse(completion, requestedModel), {
363
+ status: 200,
364
+ headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
365
+ });
366
+ }
367
+ return new Response(JSON.stringify(completion), {
368
+ status: 200,
369
+ headers: { "Content-Type": "application/json" },
370
+ });
371
+ }