@bitkyc08/opencodex 2.33.0 → 2.34.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-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- 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 +162 -0
- 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-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- 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/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- 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 +32 -4
- package/src/cli/account-extended.ts +1 -1
- 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/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/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 +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- 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 -3
- 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 +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- 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/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- 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/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/types.ts +15 -0
- 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 +65 -7
- 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 +4 -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/index.ts +77 -11
- 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/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- 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 +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- 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-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 +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
|
@@ -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
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
OPENAI_CODEX_PROVIDER_ID,
|
|
33
33
|
} from "./providers/openai-tiers";
|
|
34
34
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
35
|
+
import { resolveModelAlias } from "./providers/default-aliases";
|
|
35
36
|
import { getStaleCached } from "./codex/model-cache";
|
|
36
37
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
37
38
|
import {
|
|
@@ -116,6 +117,7 @@ export function knownModelIdsForProvider(
|
|
|
116
117
|
registry?.modelReasoningEffortMap,
|
|
117
118
|
registry?.modelMaxOutputTokens,
|
|
118
119
|
registry?.modelSupportsServiceTier,
|
|
120
|
+
registry?.modelSupportsVerbosity,
|
|
119
121
|
]) {
|
|
120
122
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
121
123
|
}
|
|
@@ -312,6 +314,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
312
314
|
: undefined,
|
|
313
315
|
provider.modelSupportsServiceTier,
|
|
314
316
|
);
|
|
317
|
+
const modelSupportsVerbosity = mergeRecordFill(
|
|
318
|
+
registryEntry.modelSupportsVerbosity,
|
|
319
|
+
provider.modelSupportsVerbosity,
|
|
320
|
+
);
|
|
315
321
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
316
322
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
317
323
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -426,6 +432,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
426
432
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
427
433
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
428
434
|
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
435
|
+
...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
|
|
429
436
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
430
437
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
431
438
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -640,7 +647,14 @@ function routeModelInternal(
|
|
|
640
647
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
641
648
|
// no such provider exists.
|
|
642
649
|
if (slash > 0) {
|
|
643
|
-
const
|
|
650
|
+
const requestedProvider = modelId.slice(0, slash);
|
|
651
|
+
const provName = hasOwnProvider(config.providers, requestedProvider)
|
|
652
|
+
? requestedProvider
|
|
653
|
+
: Object.entries(config.providers).find(([, provider]) =>
|
|
654
|
+
typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
|
|
655
|
+
if (!provName) {
|
|
656
|
+
// A genuine slash-containing native model id still falls through unchanged.
|
|
657
|
+
} else {
|
|
644
658
|
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
645
659
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
646
660
|
}
|
|
@@ -656,14 +670,20 @@ function routeModelInternal(
|
|
|
656
670
|
}
|
|
657
671
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
658
672
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
673
|
+
const requestedModel = modelId.slice(slash + 1);
|
|
674
|
+
const decoded = decodeRoutedModelIdOrThrow(requestedModel, known);
|
|
675
|
+
const nativeModel = known.includes(decoded)
|
|
676
|
+
? decoded
|
|
677
|
+
: resolveModelAlias(config, prov, known, requestedModel) ?? decoded;
|
|
659
678
|
return routeResult(
|
|
660
679
|
provName,
|
|
661
680
|
prov,
|
|
662
|
-
|
|
681
|
+
nativeModel,
|
|
663
682
|
"explicit-provider",
|
|
664
683
|
"explicit-provider-namespace",
|
|
665
684
|
);
|
|
666
685
|
}
|
|
686
|
+
}
|
|
667
687
|
}
|
|
668
688
|
|
|
669
689
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
@@ -693,6 +713,24 @@ function routeModelInternal(
|
|
|
693
713
|
}
|
|
694
714
|
}
|
|
695
715
|
|
|
716
|
+
const aliasMatches: Array<{ provider: string; model: string; qualified: string }> = [];
|
|
717
|
+
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
718
|
+
const known = knownModelIdsForProvider(provName, prov, config);
|
|
719
|
+
const native = resolveModelAlias(config, prov, known, modelId);
|
|
720
|
+
if (native) aliasMatches.push({
|
|
721
|
+
provider: provName,
|
|
722
|
+
model: native,
|
|
723
|
+
qualified: `${prov.alias || provName}/${modelId}`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
if (aliasMatches.length > 1) {
|
|
727
|
+
throw new Error(`model alias '${modelId}' is ambiguous: ${aliasMatches.map(match => match.qualified).sort().join(", ")}`);
|
|
728
|
+
}
|
|
729
|
+
if (aliasMatches[0]) {
|
|
730
|
+
const match = aliasMatches[0];
|
|
731
|
+
return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
|
|
732
|
+
}
|
|
733
|
+
|
|
696
734
|
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
697
735
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
698
736
|
}
|
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
|
@@ -706,6 +706,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
706
706
|
}
|
|
707
707
|
for (const key of [
|
|
708
708
|
"defaultModel",
|
|
709
|
+
"alias",
|
|
710
|
+
"modelAliases",
|
|
711
|
+
"defaultAliases",
|
|
709
712
|
"disabled",
|
|
710
713
|
"allowPrivateNetwork",
|
|
711
714
|
"authMode",
|
|
@@ -757,6 +760,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
757
760
|
port: config.port,
|
|
758
761
|
hostname: config.hostname ?? "127.0.0.1",
|
|
759
762
|
defaultProvider: config.defaultProvider,
|
|
763
|
+
defaultModelAliases: config.defaultModelAliases,
|
|
760
764
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
761
765
|
websockets: config.websockets,
|
|
762
766
|
// The GUI's browser-open toggle reads and writes this; absent means the
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
responsesJsonToChatCompletion,
|
|
19
19
|
responsesSseToChatCompletionsSse,
|
|
20
20
|
} from "../chat/outbound";
|
|
21
|
-
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
21
|
+
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
22
22
|
import { redactSecretString } from "../lib/redact";
|
|
23
23
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
24
24
|
import { estimateTokens } from "../lib/token-estimate";
|
|
@@ -120,6 +120,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
120
120
|
logCtx.model = route.modelId;
|
|
121
121
|
logCtx.providerAdapter = route.provider.adapter;
|
|
122
122
|
logCtx.requestedModel = requestedModel;
|
|
123
|
+
if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
|
|
123
124
|
logCtx.provider = route.providerName;
|
|
124
125
|
logCtx.routeDecision = route.routeDecision;
|
|
125
126
|
settledRoute = route;
|
|
@@ -280,26 +281,26 @@ async function handleChatCompletionsWithBudget(
|
|
|
280
281
|
const parsed = JSON.parse(text) as {
|
|
281
282
|
error?: { message?: string; type?: string; code?: string | null } | string;
|
|
282
283
|
message?: string;
|
|
284
|
+
type?: string;
|
|
285
|
+
code?: string | null;
|
|
283
286
|
};
|
|
284
287
|
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
|
|
285
288
|
const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
|
|
286
289
|
const rawFallback = text
|
|
287
290
|
? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
|
|
288
291
|
: message;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
const upstreamMessage = nested?.message || flat;
|
|
293
|
+
message = upstreamMessage
|
|
294
|
+
? redactSecretString(upstreamMessage).slice(0, 500)
|
|
295
|
+
: rawFallback;
|
|
296
|
+
const structuredType = nested?.type ?? parsed.type;
|
|
297
|
+
const structuredCode = nested?.code ?? parsed.code;
|
|
298
|
+
if (typeof structuredType === "string") upstreamType = structuredType;
|
|
299
|
+
if (structuredCode === null || typeof structuredCode === "string") upstreamCode = structuredCode;
|
|
294
300
|
} catch {
|
|
295
301
|
if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
|
|
296
302
|
}
|
|
297
303
|
} catch { /* keep fallback */ }
|
|
298
|
-
const retryAfter = resolveClientRetryAfter({
|
|
299
|
-
status: upstream.status,
|
|
300
|
-
message,
|
|
301
|
-
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
302
|
-
});
|
|
303
304
|
const classified = classifyError(
|
|
304
305
|
upstream.status,
|
|
305
306
|
upstreamType
|
|
@@ -309,9 +310,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
309
310
|
: "invalid_request_error"),
|
|
310
311
|
message,
|
|
311
312
|
);
|
|
312
|
-
if (isCyberPolicyCode(upstreamCode)) {
|
|
313
|
+
if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
313
314
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
314
|
-
classified.type =
|
|
315
|
+
classified.type = cyberPolicyErrorType(upstreamType);
|
|
315
316
|
} else if (upstreamCode === "model_not_found") {
|
|
316
317
|
// Structured model_not_found must win over classifyError's generic remaps.
|
|
317
318
|
classified.code = "model_not_found";
|
|
@@ -320,6 +321,13 @@ async function handleChatCompletionsWithBudget(
|
|
|
320
321
|
classified.code = upstreamCode;
|
|
321
322
|
}
|
|
322
323
|
const status = isCyberPolicyCode(classified.code) ? 400 : upstream.status;
|
|
324
|
+
const retryAfter = isCyberPolicyCode(classified.code)
|
|
325
|
+
? undefined
|
|
326
|
+
: resolveClientRetryAfter({
|
|
327
|
+
status: upstream.status,
|
|
328
|
+
message,
|
|
329
|
+
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
330
|
+
});
|
|
323
331
|
const rewritten = new Response(JSON.stringify({
|
|
324
332
|
error: {
|
|
325
333
|
message: classified.message,
|
|
@@ -386,14 +394,14 @@ async function handleChatCompletionsWithBudget(
|
|
|
386
394
|
const status = (json as Rec)?.status;
|
|
387
395
|
if (status === "failed") {
|
|
388
396
|
const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
|
|
389
|
-
const message = error?.message ?? "upstream request failed";
|
|
397
|
+
const message = redactSecretString(error?.message ?? "upstream request failed");
|
|
390
398
|
const classified = classifyError(502, error?.type ?? "server_error", message);
|
|
391
399
|
if (error?.code === "translation_buffer_limit") {
|
|
392
400
|
classified.code = "translation_buffer_limit";
|
|
393
401
|
classified.type = "upstream_error";
|
|
394
|
-
} else if (isCyberPolicyCode(error?.code)) {
|
|
402
|
+
} else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
395
403
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
396
|
-
classified.type =
|
|
404
|
+
classified.type = cyberPolicyErrorType(error?.type);
|
|
397
405
|
} else if (error?.code === "model_not_found") {
|
|
398
406
|
// Same deliberate preserve as the non-OK path: structured code beats generic classify.
|
|
399
407
|
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" };
|