@yansigit/opencodex 2.33.1 → 2.35.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/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +156 -65
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -37
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +229 -153
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +73 -14
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +152 -80
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +15 -4
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalizeApplyPatchDelimiters } from "./apply-patch-envelope";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function unwrapPatchInput(value: string): string {
|
|
8
|
+
try {
|
|
9
|
+
const parsed: unknown = JSON.parse(value);
|
|
10
|
+
if (isPlainObject(parsed)) {
|
|
11
|
+
if (typeof parsed.input === "string") return parsed.input;
|
|
12
|
+
if (typeof parsed.patch === "string") return parsed.patch;
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// Native custom calls carry the patch body directly.
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert a nested Code Mode helper call into unified-exec JavaScript.
|
|
22
|
+
*
|
|
23
|
+
* Parsed values are serialized as data, never interpolated as source, so command and patch text
|
|
24
|
+
* cannot escape the generated call. Invalid structured shell payloads are also passed as data so
|
|
25
|
+
* nested-tool validation can reject them without evaluating provider text as JavaScript.
|
|
26
|
+
*/
|
|
27
|
+
export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
|
|
28
|
+
if (typeof argumentsText !== "string") return "";
|
|
29
|
+
if (toolName === "apply_patch") {
|
|
30
|
+
const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
|
|
31
|
+
return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
|
|
32
|
+
}
|
|
33
|
+
let parsed: unknown = argumentsText;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(argumentsText);
|
|
36
|
+
} catch {
|
|
37
|
+
// Keep malformed provider text as data rather than executable source.
|
|
38
|
+
}
|
|
39
|
+
const args: unknown = isPlainObject(parsed) ? { ...parsed } : parsed;
|
|
40
|
+
if (
|
|
41
|
+
toolName === "shell_command"
|
|
42
|
+
&& isPlainObject(args)
|
|
43
|
+
&& typeof args.command === "string"
|
|
44
|
+
&& args.cmd === undefined
|
|
45
|
+
) {
|
|
46
|
+
args.cmd = args.command;
|
|
47
|
+
delete args.command;
|
|
48
|
+
}
|
|
49
|
+
return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
|
|
50
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { namespacedToolName } from "../types";
|
|
1
|
+
import { namespacedToolName, normalizeDeclaredToolName } from "../types";
|
|
2
2
|
import {
|
|
3
3
|
normalizeApplyPatchDelimiters,
|
|
4
4
|
repairFreeformToolInput,
|
|
5
5
|
unwrapFreeformToolInput,
|
|
6
6
|
} from "./apply-patch-envelope";
|
|
7
|
+
import { compileCodeModeHelperInput } from "./code-mode-helper-compat";
|
|
7
8
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
8
9
|
|
|
9
10
|
const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
|
|
@@ -65,6 +66,20 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** Resolve a provider-emitted wire name to the routed custom tool the client declared. */
|
|
70
|
+
export function routedCustomToolTargetName(
|
|
71
|
+
value: unknown,
|
|
72
|
+
names: ReadonlySet<string>,
|
|
73
|
+
declaredNames?: ReadonlySet<string>,
|
|
74
|
+
): string | undefined {
|
|
75
|
+
const wireName = routedCustomToolWireName(value);
|
|
76
|
+
if (wireName === undefined) return undefined;
|
|
77
|
+
if (names.has(wireName)) return wireName;
|
|
78
|
+
if (!isPlainObject(value) || typeof value.namespace === "string") return undefined;
|
|
79
|
+
const normalized = normalizeDeclaredToolName(wireName, declaredNames);
|
|
80
|
+
return normalized !== wireName && names.has(normalized) ? normalized : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
/**
|
|
69
84
|
* Names of custom declarations after namespace lowering. The selection flag separates converted
|
|
70
85
|
* names from native passthrough names while keeping same-named function and custom children distinct.
|
|
@@ -250,29 +265,37 @@ export function restoreRoutedCustomCalls(
|
|
|
250
265
|
value: unknown,
|
|
251
266
|
names: ReadonlySet<string>,
|
|
252
267
|
repairNames: ReadonlySet<string> = new Set(),
|
|
268
|
+
declaredNames?: ReadonlySet<string>,
|
|
253
269
|
): { value: unknown; changed: boolean } {
|
|
254
270
|
if (!isPlainObject(value)) return { value, changed: false };
|
|
255
271
|
|
|
256
272
|
const restoreItem = (item: unknown): { value: unknown; changed: boolean } => {
|
|
257
273
|
if (!isPlainObject(item)) return { value: item, changed: false };
|
|
258
274
|
const wireName = routedCustomToolWireName(item);
|
|
275
|
+
const targetName = routedCustomToolTargetName(item, names, declaredNames);
|
|
259
276
|
if (
|
|
260
|
-
item.type === "function_call"
|
|
277
|
+
(item.type === "function_call" || item.type === "custom_tool_call")
|
|
261
278
|
&& typeof item.name === "string"
|
|
262
279
|
&& wireName !== undefined
|
|
263
|
-
&&
|
|
280
|
+
&& targetName !== undefined
|
|
264
281
|
) {
|
|
282
|
+
const sourceInput = item.type === "function_call" ? item.arguments : item.input;
|
|
283
|
+
const aliased = targetName !== wireName;
|
|
265
284
|
const restored: Record<string, unknown> = {
|
|
266
285
|
...item,
|
|
267
286
|
type: "custom_tool_call",
|
|
268
287
|
id: customToolItemId(item.id),
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
item.name
|
|
272
|
-
|
|
273
|
-
|
|
288
|
+
name: aliased ? targetName : item.name,
|
|
289
|
+
input: aliased && sourceInput !== ""
|
|
290
|
+
? compileCodeModeHelperInput(sourceInput, item.name)
|
|
291
|
+
: repairFreeformToolInput(
|
|
292
|
+
sourceInput,
|
|
293
|
+
targetName,
|
|
294
|
+
typeof item.namespace === "string" ? item.namespace : undefined,
|
|
295
|
+
),
|
|
274
296
|
};
|
|
275
297
|
delete restored.arguments;
|
|
298
|
+
if (aliased) delete restored.namespace;
|
|
276
299
|
return { value: restored, changed: true };
|
|
277
300
|
}
|
|
278
301
|
if (
|
|
@@ -323,7 +346,7 @@ export function restoreRoutedCustomCalls(
|
|
|
323
346
|
&& value.type.startsWith("response.")
|
|
324
347
|
&& isPlainObject(value.response)
|
|
325
348
|
) {
|
|
326
|
-
const response = restoreRoutedCustomCalls(value.response, names, repairNames);
|
|
349
|
+
const response = restoreRoutedCustomCalls(value.response, names, repairNames, declaredNames);
|
|
327
350
|
if (response.changed) {
|
|
328
351
|
restored.response = response.value;
|
|
329
352
|
changed = true;
|
|
@@ -337,6 +360,7 @@ export function restoreRoutedCustomCallsInJson(
|
|
|
337
360
|
text: string,
|
|
338
361
|
names: ReadonlySet<string>,
|
|
339
362
|
repairNames: ReadonlySet<string> = new Set(),
|
|
363
|
+
declaredNames?: ReadonlySet<string>,
|
|
340
364
|
): string {
|
|
341
365
|
if (names.size === 0 && repairNames.size === 0) return text;
|
|
342
366
|
let payload: unknown;
|
|
@@ -345,7 +369,7 @@ export function restoreRoutedCustomCallsInJson(
|
|
|
345
369
|
} catch {
|
|
346
370
|
return text;
|
|
347
371
|
}
|
|
348
|
-
const restored = restoreRoutedCustomCalls(payload, names, repairNames);
|
|
372
|
+
const restored = restoreRoutedCustomCalls(payload, names, repairNames, declaredNames);
|
|
349
373
|
return restored.changed ? JSON.stringify(restored.value) : text;
|
|
350
374
|
}
|
|
351
375
|
|
package/src/responses/parser.ts
CHANGED
|
@@ -45,6 +45,7 @@ type InputBlock =
|
|
|
45
45
|
| { type: "input_text"; text: string }
|
|
46
46
|
| { type: "text"; text: string }
|
|
47
47
|
| { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
|
|
48
|
+
| { type: "input_video"; video_url?: string }
|
|
48
49
|
| { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
|
|
49
50
|
|
|
50
51
|
/** A usable reference string, or undefined. Empty strings and non-strings are not references. */
|
|
@@ -80,6 +81,9 @@ function inputContentParts(blocks: unknown): string | OcxContentPart[] {
|
|
|
80
81
|
}
|
|
81
82
|
// No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
|
|
82
83
|
// the request never carried, which is worse than dropping malformed input.
|
|
84
|
+
} else if (block.type === "input_video") {
|
|
85
|
+
const videoUrl = nonEmptyString(block.video_url);
|
|
86
|
+
if (videoUrl) parts.push({ type: "video", videoUrl });
|
|
83
87
|
} else if (block.type === "input_file") {
|
|
84
88
|
const b = block as { file_id?: string; filename?: string; file_data?: string };
|
|
85
89
|
const fileId = nonEmptyString(b.file_id);
|
package/src/responses/schema.ts
CHANGED
|
@@ -11,6 +11,10 @@ const inputImageBlockSchema = z.object({
|
|
|
11
11
|
}).refine(v => typeof v.image_url === "string" || typeof v.file_id === "string", {
|
|
12
12
|
message: "input_image requires at least one of image_url or file_id",
|
|
13
13
|
});
|
|
14
|
+
const inputVideoBlockSchema = z.object({
|
|
15
|
+
type: z.literal("input_video"),
|
|
16
|
+
video_url: z.string().min(1),
|
|
17
|
+
});
|
|
14
18
|
const inputFileBlockSchema = z.object({
|
|
15
19
|
type: z.literal("input_file"),
|
|
16
20
|
file_id: z.string().optional(),
|
|
@@ -24,7 +28,7 @@ const reasoningTextSchema = z.object({ type: z.literal("reasoning_text"), text:
|
|
|
24
28
|
// codex-rs FunctionCallOutputContentItem (protocol/src/models.rs): input_text | input_image | encrypted_content.
|
|
25
29
|
const encryptedContentBlockSchema = z.object({ type: z.literal("encrypted_content"), encrypted_content: z.string() });
|
|
26
30
|
|
|
27
|
-
const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputFileBlockSchema]);
|
|
31
|
+
const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputVideoBlockSchema, inputFileBlockSchema]);
|
|
28
32
|
const outputContentBlockSchema = z.union([outputTextSchema, plainTextSchema, outputRefusalSchema]);
|
|
29
33
|
// Tool outputs on the wire mix codex-rs FunctionCallOutputContentItem with legacy output blocks.
|
|
30
34
|
const toolOutputContentBlockSchema = z.union([
|
|
@@ -308,6 +308,23 @@ export function lookupReplayThoughtSignature(
|
|
|
308
308
|
return entry.sig;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/** Drop a remembered signature for a specific callId and scope (e.g. when upstream rejects it). */
|
|
312
|
+
export function forgetThoughtSignatureForReplay(
|
|
313
|
+
callId: string,
|
|
314
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
315
|
+
): boolean {
|
|
316
|
+
const key = keyFor(callId, scope);
|
|
317
|
+
if (key === undefined) return false;
|
|
318
|
+
load();
|
|
319
|
+
const entry = entries.get(key);
|
|
320
|
+
if (!entry) return false;
|
|
321
|
+
entries.delete(key);
|
|
322
|
+
totalBytes -= entry.sig.length;
|
|
323
|
+
prune(Date.now());
|
|
324
|
+
void persist();
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
311
328
|
/** Test seams: clear in-memory state and the loaded flag without touching the file. */
|
|
312
329
|
export function resetThoughtSignatureReplayForTests(): void {
|
|
313
330
|
entries = new Map();
|
package/src/router.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
OPENAI_CODEX_PROVIDER_ID,
|
|
34
34
|
} from "./providers/openai-tiers";
|
|
35
35
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
36
|
+
import { resolveModelAlias } from "./providers/default-aliases";
|
|
36
37
|
import { getStaleCached } from "./codex/model-cache";
|
|
37
38
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
38
39
|
import {
|
|
@@ -117,6 +118,7 @@ export function knownModelIdsForProvider(
|
|
|
117
118
|
registry?.modelReasoningEffortMap,
|
|
118
119
|
registry?.modelMaxOutputTokens,
|
|
119
120
|
registry?.modelSupportsServiceTier,
|
|
121
|
+
registry?.modelSupportsVerbosity,
|
|
120
122
|
]) {
|
|
121
123
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
122
124
|
}
|
|
@@ -318,6 +320,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
318
320
|
: undefined,
|
|
319
321
|
provider.modelSupportsServiceTier,
|
|
320
322
|
);
|
|
323
|
+
const modelSupportsVerbosity = mergeRecordFill(
|
|
324
|
+
registryEntry.modelSupportsVerbosity,
|
|
325
|
+
provider.modelSupportsVerbosity,
|
|
326
|
+
);
|
|
321
327
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
322
328
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
323
329
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -435,6 +441,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
435
441
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
436
442
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
437
443
|
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
444
|
+
...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
|
|
438
445
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
439
446
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
440
447
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -649,7 +656,14 @@ function routeModelInternal(
|
|
|
649
656
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
650
657
|
// no such provider exists.
|
|
651
658
|
if (slash > 0) {
|
|
652
|
-
const
|
|
659
|
+
const requestedProvider = modelId.slice(0, slash);
|
|
660
|
+
const provName = hasOwnProvider(config.providers, requestedProvider)
|
|
661
|
+
? requestedProvider
|
|
662
|
+
: Object.entries(config.providers).find(([, provider]) =>
|
|
663
|
+
typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
|
|
664
|
+
if (!provName) {
|
|
665
|
+
// A genuine slash-containing native model id still falls through unchanged.
|
|
666
|
+
} else {
|
|
653
667
|
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
654
668
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
655
669
|
}
|
|
@@ -665,14 +679,20 @@ function routeModelInternal(
|
|
|
665
679
|
}
|
|
666
680
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
667
681
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
682
|
+
const requestedModel = modelId.slice(slash + 1);
|
|
683
|
+
const decoded = decodeRoutedModelIdOrThrow(requestedModel, known);
|
|
684
|
+
const nativeModel = known.includes(decoded)
|
|
685
|
+
? decoded
|
|
686
|
+
: resolveModelAlias(config, prov, known, requestedModel) ?? decoded;
|
|
668
687
|
return routeResult(
|
|
669
688
|
provName,
|
|
670
689
|
prov,
|
|
671
|
-
|
|
690
|
+
nativeModel,
|
|
672
691
|
"explicit-provider",
|
|
673
692
|
"explicit-provider-namespace",
|
|
674
693
|
);
|
|
675
694
|
}
|
|
695
|
+
}
|
|
676
696
|
}
|
|
677
697
|
|
|
678
698
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
@@ -702,6 +722,24 @@ function routeModelInternal(
|
|
|
702
722
|
}
|
|
703
723
|
}
|
|
704
724
|
|
|
725
|
+
const aliasMatches: Array<{ provider: string; model: string; qualified: string }> = [];
|
|
726
|
+
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
727
|
+
const known = knownModelIdsForProvider(provName, prov, config);
|
|
728
|
+
const native = resolveModelAlias(config, prov, known, modelId);
|
|
729
|
+
if (native) aliasMatches.push({
|
|
730
|
+
provider: provName,
|
|
731
|
+
model: native,
|
|
732
|
+
qualified: `${prov.alias || provName}/${modelId}`,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (aliasMatches.length > 1) {
|
|
736
|
+
throw new Error(`model alias '${modelId}' is ambiguous: ${aliasMatches.map(match => match.qualified).sort().join(", ")}`);
|
|
737
|
+
}
|
|
738
|
+
if (aliasMatches[0]) {
|
|
739
|
+
const match = aliasMatches[0];
|
|
740
|
+
return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
|
|
741
|
+
}
|
|
742
|
+
|
|
705
743
|
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
706
744
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
707
745
|
}
|
package/src/routing/analytics.ts
CHANGED
package/src/routing/quota.ts
CHANGED
|
@@ -41,6 +41,12 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
|
|
|
41
41
|
const percents = [
|
|
42
42
|
...(monthly ? [] : [quota.weeklyPercent]),
|
|
43
43
|
quota.monthlyPercent,
|
|
44
|
+
// The burst window is upstream-enforced independently of the governing window, so an
|
|
45
|
+
// account at 97% here has 3% headroom whatever its weekly figure says. This was invisible
|
|
46
|
+
// while the header parser misfiled 5h readings into weeklyPercent - routing saw the burst
|
|
47
|
+
// by accident. Once that is fixed, omitting it here reports 88% headroom for an account
|
|
48
|
+
// that is one request away from a 429. computeCodexUsageScore already folds it in.
|
|
49
|
+
quota.shortPercent,
|
|
44
50
|
].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
45
51
|
const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
|
|
46
52
|
// Credits-only snapshots prove neither usage nor exhaustion. Unknown must not
|
|
@@ -49,6 +55,10 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
|
|
|
49
55
|
const resets = [
|
|
50
56
|
...(monthly ? [] : [quota.weeklyResetAt]),
|
|
51
57
|
quota.monthlyResetAt,
|
|
58
|
+
// Pair the reset with the window that can actually gate the next request: a burst-limited
|
|
59
|
+
// account recovers in hours, and reporting a distant weekly reset would defer a retry that
|
|
60
|
+
// is already safe.
|
|
61
|
+
quota.shortResetAt,
|
|
52
62
|
].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
|
53
63
|
.filter(value => value > Date.now());
|
|
54
64
|
return {
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
azureCredentialConfigError,
|
|
14
14
|
booleanRecordConfigError,
|
|
15
15
|
modelAdapterRecordConfigError,
|
|
16
|
+
maxWsFrameBytesConfigError,
|
|
16
17
|
nonBlankStringArrayConfigError,
|
|
17
18
|
positiveIntegerConfigError,
|
|
18
19
|
positiveIntegerRecordConfigError,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
providerHeadersConfigError,
|
|
21
22
|
reasoningSummaryDeliveryRecordConfigError,
|
|
22
23
|
upstreamHttpVersionConfigError,
|
|
24
|
+
wsUpstreamConfigError,
|
|
23
25
|
isAzureIdentityProvider,
|
|
24
26
|
} from "../config/provider-validation";
|
|
25
27
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
@@ -32,6 +34,7 @@ import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compac
|
|
|
32
34
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
33
35
|
import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
|
|
34
36
|
import { antigravityOAuthDestinationConfigError, getProviderTlsProfileStatus, providerTlsProfileConfigError } from "../lib/provider-tls-profile";
|
|
37
|
+
import { resolveAiStudioCredentials } from "../oauth/aistudio-credentials";
|
|
35
38
|
|
|
36
39
|
let _corsOrigin = "http://localhost:10100";
|
|
37
40
|
export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
|
|
@@ -571,6 +574,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
571
574
|
delete canonicalCandidate.modelContextWindows;
|
|
572
575
|
// User-owned soft compaction policy; it does not alter the canonical transport seed.
|
|
573
576
|
delete canonicalCandidate.modelAutoCompactTokenLimits;
|
|
577
|
+
// Transport controls are user-owned overlays, not part of the immutable seed.
|
|
578
|
+
delete canonicalCandidate.wsUpstream;
|
|
579
|
+
delete canonicalCandidate.maxWsFrameBytes;
|
|
574
580
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
575
581
|
if (!canonical) {
|
|
576
582
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -609,6 +615,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
609
615
|
if (upstreamHttpVersionError) {
|
|
610
616
|
return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
|
|
611
617
|
}
|
|
618
|
+
const wsUpstreamError = wsUpstreamConfigError(raw.wsUpstream);
|
|
619
|
+
if (wsUpstreamError) return `provider ${name} ${wsUpstreamError}`;
|
|
620
|
+
const maxWsFrameBytesError = maxWsFrameBytesConfigError(raw.maxWsFrameBytes);
|
|
621
|
+
if (maxWsFrameBytesError) return `provider ${name} ${maxWsFrameBytesError}`;
|
|
612
622
|
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
613
623
|
if (modelCostsError) {
|
|
614
624
|
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
@@ -718,6 +728,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
718
728
|
}
|
|
719
729
|
for (const key of [
|
|
720
730
|
"defaultModel",
|
|
731
|
+
"alias",
|
|
732
|
+
"modelAliases",
|
|
733
|
+
"defaultAliases",
|
|
721
734
|
"disabled",
|
|
722
735
|
"allowPrivateNetwork",
|
|
723
736
|
"authMode",
|
|
@@ -732,6 +745,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
732
745
|
"contextWindow",
|
|
733
746
|
"modelContextWindows",
|
|
734
747
|
"modelAutoCompactTokenLimits",
|
|
748
|
+
"wsUpstream",
|
|
749
|
+
"maxWsFrameBytes",
|
|
735
750
|
"defaultMaxOutputTokens",
|
|
736
751
|
"modelMaxOutputTokens",
|
|
737
752
|
"openRouterRouting",
|
|
@@ -766,12 +781,20 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
766
781
|
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
767
782
|
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
768
783
|
if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
|
|
784
|
+
if (effectiveGoogleMode(name, provider) === "ai-studio-web" || name === "google-aistudio") {
|
|
785
|
+
const credentials = resolveAiStudioCredentials(provider);
|
|
786
|
+
dto.hasAiStudioSession = credentials.kind === "ready";
|
|
787
|
+
dto.aiStudioAuthState = process.platform !== "darwin"
|
|
788
|
+
? "unsupported"
|
|
789
|
+
: credentials.kind === "ready" ? "checking" : "needs_reauth";
|
|
790
|
+
}
|
|
769
791
|
providers[name] = dto;
|
|
770
792
|
}
|
|
771
793
|
return {
|
|
772
794
|
port: config.port,
|
|
773
795
|
hostname: config.hostname ?? "127.0.0.1",
|
|
774
796
|
defaultProvider: config.defaultProvider,
|
|
797
|
+
defaultModelAliases: config.defaultModelAliases,
|
|
775
798
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
776
799
|
websockets: config.websockets,
|
|
777
800
|
// The GUI's browser-open toggle reads and writes this; absent means the
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
responsesJsonToChatCompletion,
|
|
20
20
|
responsesSseToChatCompletionsSse,
|
|
21
21
|
} from "../chat/outbound";
|
|
22
|
-
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
22
|
+
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
23
23
|
import { redactSecretString } from "../lib/redact";
|
|
24
24
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
25
25
|
import { estimateTokens } from "../lib/token-estimate";
|
|
@@ -121,6 +121,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
121
121
|
logCtx.model = route.modelId;
|
|
122
122
|
logCtx.providerAdapter = route.provider.adapter;
|
|
123
123
|
logCtx.requestedModel = requestedModel;
|
|
124
|
+
if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
|
|
124
125
|
logCtx.provider = route.providerName;
|
|
125
126
|
logCtx.routeDecision = route.routeDecision;
|
|
126
127
|
settledRoute = route;
|
|
@@ -282,26 +283,26 @@ async function handleChatCompletionsWithBudget(
|
|
|
282
283
|
const parsed = JSON.parse(text) as {
|
|
283
284
|
error?: { message?: string; type?: string; code?: string | null } | string;
|
|
284
285
|
message?: string;
|
|
286
|
+
type?: string;
|
|
287
|
+
code?: string | null;
|
|
285
288
|
};
|
|
286
289
|
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
|
|
287
290
|
const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
|
|
288
291
|
const rawFallback = text
|
|
289
292
|
? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
|
|
290
293
|
: message;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
const upstreamMessage = nested?.message || flat;
|
|
295
|
+
message = upstreamMessage
|
|
296
|
+
? redactSecretString(upstreamMessage).slice(0, 500)
|
|
297
|
+
: rawFallback;
|
|
298
|
+
const structuredType = nested?.type ?? parsed.type;
|
|
299
|
+
const structuredCode = nested?.code ?? parsed.code;
|
|
300
|
+
if (typeof structuredType === "string") upstreamType = structuredType;
|
|
301
|
+
if (structuredCode === null || typeof structuredCode === "string") upstreamCode = structuredCode;
|
|
296
302
|
} catch {
|
|
297
303
|
if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
|
|
298
304
|
}
|
|
299
305
|
} catch { /* keep fallback */ }
|
|
300
|
-
const retryAfter = resolveClientRetryAfter({
|
|
301
|
-
status: upstream.status,
|
|
302
|
-
message,
|
|
303
|
-
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
304
|
-
});
|
|
305
306
|
const classified = classifyError(
|
|
306
307
|
upstream.status,
|
|
307
308
|
upstreamType
|
|
@@ -311,9 +312,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
311
312
|
: "invalid_request_error"),
|
|
312
313
|
message,
|
|
313
314
|
);
|
|
314
|
-
if (isCyberPolicyCode(upstreamCode)) {
|
|
315
|
+
if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
315
316
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
316
|
-
classified.type =
|
|
317
|
+
classified.type = cyberPolicyErrorType(upstreamType);
|
|
317
318
|
} else if (upstreamCode === "model_not_found") {
|
|
318
319
|
// Structured model_not_found must win over classifyError's generic remaps.
|
|
319
320
|
classified.code = "model_not_found";
|
|
@@ -322,6 +323,13 @@ async function handleChatCompletionsWithBudget(
|
|
|
322
323
|
classified.code = upstreamCode;
|
|
323
324
|
}
|
|
324
325
|
const status = isCyberPolicyCode(classified.code) ? 400 : upstream.status;
|
|
326
|
+
const retryAfter = isCyberPolicyCode(classified.code)
|
|
327
|
+
? undefined
|
|
328
|
+
: resolveClientRetryAfter({
|
|
329
|
+
status: upstream.status,
|
|
330
|
+
message,
|
|
331
|
+
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
332
|
+
});
|
|
325
333
|
const rewritten = new Response(JSON.stringify({
|
|
326
334
|
error: {
|
|
327
335
|
message: classified.message,
|
|
@@ -388,14 +396,14 @@ async function handleChatCompletionsWithBudget(
|
|
|
388
396
|
const status = (json as Rec)?.status;
|
|
389
397
|
if (status === "failed") {
|
|
390
398
|
const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
|
|
391
|
-
const message = error?.message ?? "upstream request failed";
|
|
399
|
+
const message = redactSecretString(error?.message ?? "upstream request failed");
|
|
392
400
|
const classified = classifyError(502, error?.type ?? "server_error", message);
|
|
393
401
|
if (error?.code === "translation_buffer_limit") {
|
|
394
402
|
classified.code = "translation_buffer_limit";
|
|
395
403
|
classified.type = "upstream_error";
|
|
396
|
-
} else if (isCyberPolicyCode(error?.code)) {
|
|
404
|
+
} else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
397
405
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
398
|
-
classified.type =
|
|
406
|
+
classified.type = cyberPolicyErrorType(error?.type);
|
|
399
407
|
} else if (error?.code === "model_not_found") {
|
|
400
408
|
// Same deliberate preserve as the non-OK path: structured code beats generic classify.
|
|
401
409
|
classified.code = "model_not_found";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chatCompletionsErrorBody } from "../chat/outbound";
|
|
2
|
-
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
2
|
+
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
3
3
|
import { redactSecretString } from "../lib/redact";
|
|
4
4
|
import {
|
|
5
5
|
isTranslatorBudgetExceededError,
|
|
@@ -226,9 +226,9 @@ export function nativeChatSse(
|
|
|
226
226
|
if (error) {
|
|
227
227
|
const status = error.status ?? 502;
|
|
228
228
|
const classified = classifyError(status, error.type ?? "upstream_error", error.message);
|
|
229
|
-
if (isCyberPolicyCode(error.code)) {
|
|
229
|
+
if (isCyberPolicyCode(error.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
230
230
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
231
|
-
classified.type =
|
|
231
|
+
classified.type = cyberPolicyErrorType(error.type);
|
|
232
232
|
} else if (error.code !== undefined && error.code !== null) {
|
|
233
233
|
classified.code = error.code;
|
|
234
234
|
}
|
|
@@ -6,7 +6,13 @@ import {
|
|
|
6
6
|
collectChatCompletion,
|
|
7
7
|
isChatCompletionsStreamError,
|
|
8
8
|
} from "../chat/outbound";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
classifyError,
|
|
11
|
+
cyberPolicyErrorType,
|
|
12
|
+
CYBER_POLICY_ERROR_CODE,
|
|
13
|
+
isCyberPolicyCode,
|
|
14
|
+
isCyberPolicyMessage,
|
|
15
|
+
} from "../lib/errors";
|
|
10
16
|
import type { AdmissionLease } from "../lib/admission";
|
|
11
17
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
12
18
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -281,13 +287,24 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
281
287
|
const detail = activeAdapter.formatErrorBody?.(response.status, response.headers, bodyText) ?? "";
|
|
282
288
|
let upstreamType: string | undefined;
|
|
283
289
|
let upstreamCode: string | null | undefined;
|
|
290
|
+
let upstreamMessage: string | undefined;
|
|
284
291
|
try {
|
|
285
292
|
const parsedError = JSON.parse(bodyText) as Rec;
|
|
286
293
|
const nested = isRec(parsedError.error) ? parsedError.error : undefined;
|
|
287
|
-
|
|
288
|
-
if (
|
|
294
|
+
const details = nested ?? parsedError;
|
|
295
|
+
if (typeof details.type === "string") upstreamType = details.type;
|
|
296
|
+
if (details.code === null || typeof details.code === "string") upstreamCode = details.code;
|
|
297
|
+
const rawMessage = typeof details.message === "string"
|
|
298
|
+
? details.message
|
|
299
|
+
: typeof parsedError.error === "string" ? parsedError.error : undefined;
|
|
300
|
+
if (rawMessage?.trim()) {
|
|
301
|
+
upstreamMessage = redactSecretString(rawMessage.trim());
|
|
302
|
+
}
|
|
289
303
|
} catch { /* keep generic classification */ }
|
|
290
|
-
const message =
|
|
304
|
+
const message = upstreamMessage
|
|
305
|
+
&& (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
|
|
306
|
+
? upstreamMessage
|
|
307
|
+
: detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
|
|
291
308
|
const classified = classifyError(
|
|
292
309
|
response.status,
|
|
293
310
|
upstreamType ?? (response.status === 401 ? "authentication_error"
|
|
@@ -295,9 +312,9 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
295
312
|
: response.status >= 500 ? "server_error" : "invalid_request_error"),
|
|
296
313
|
message,
|
|
297
314
|
);
|
|
298
|
-
if (isCyberPolicyCode(upstreamCode)) {
|
|
315
|
+
if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
299
316
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
300
|
-
classified.type =
|
|
317
|
+
classified.type = cyberPolicyErrorType(upstreamType);
|
|
301
318
|
} else if (upstreamCode === "model_not_found") {
|
|
302
319
|
classified.code = "model_not_found";
|
|
303
320
|
classified.type = "invalid_request_error";
|
|
@@ -305,11 +322,13 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
305
322
|
classified.code = upstreamCode;
|
|
306
323
|
}
|
|
307
324
|
const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
|
|
308
|
-
const retryAfter =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
325
|
+
const retryAfter = isCyberPolicyCode(classified.code)
|
|
326
|
+
? undefined
|
|
327
|
+
: resolveClientRetryAfter({
|
|
328
|
+
status: response.status,
|
|
329
|
+
message: classified.message,
|
|
330
|
+
upstreamRetryAfter: response.headers.get("retry-after"),
|
|
331
|
+
});
|
|
313
332
|
finishLog(status, classified.message);
|
|
314
333
|
return new Response(JSON.stringify(chatCompletionsErrorBody(status, classified.message, classified.type, classified.code)), {
|
|
315
334
|
status,
|
|
@@ -553,7 +553,7 @@ export async function fetchWithHeaderDeadline(
|
|
|
553
553
|
): Promise<HeaderDeadlineFetchResult> {
|
|
554
554
|
const deadline = makeDeadline(timeoutMs, parent);
|
|
555
555
|
try {
|
|
556
|
-
const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
|
|
556
|
+
const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
|
|
557
557
|
return { kind: "response", upstream };
|
|
558
558
|
} catch (error) {
|
|
559
559
|
if (deadline.didExpire()) return { kind: "timeout" };
|
|
@@ -116,6 +116,22 @@ export function stripEmptyLadderEffort(
|
|
|
116
116
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Strip reasoning effort from parsed options and _rawBody when the routed model
|
|
121
|
+
* has an empty supported ladder (effortless models like Composer 2.5).
|
|
122
|
+
*/
|
|
123
|
+
export function sanitizeEffortForModel(
|
|
124
|
+
parsed: OcxParsedRequest,
|
|
125
|
+
ladder: readonly string[] | undefined,
|
|
126
|
+
): void {
|
|
127
|
+
if (ladder === undefined || ladder.length > 0) return;
|
|
128
|
+
parsed.options.reasoning = undefined;
|
|
129
|
+
const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
|
|
130
|
+
if (!raw || typeof raw !== "object" || !("reasoning" in raw)) return;
|
|
131
|
+
raw.reasoning = stripEmptyLadderEffort(raw.reasoning, ladder);
|
|
132
|
+
if (raw.reasoning === undefined) delete raw.reasoning;
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
export function supportedLadderFor(route: { provider: OcxProviderConfig; modelId: string }): string[] | undefined {
|
|
120
136
|
const { provider, modelId } = route;
|
|
121
137
|
if (modelInList(provider.noReasoningModels, modelId)) return [];
|