@yansigit/opencodex 2.33.0 → 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 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- 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 +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- 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 +219 -36
- 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 +335 -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 +29 -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 +6 -1
- 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 -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 +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 +279 -159
- package/src/generated/model-metadata.ts +3 -0
- 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 +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- 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 +29 -1
- 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 +157 -46
- package/src/providers/registry.ts +184 -71
- 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 +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -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 +180 -14
- 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 +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- 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/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- 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/policy-fallback.ts +1 -1
- 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 +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- 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 +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -101,3 +101,55 @@ export function slugEquivalenceKey(slug: string): string {
|
|
|
101
101
|
export function slugsEquivalent(a: string, b: string): boolean {
|
|
102
102
|
return a === b || slugEquivalenceKey(a) === slugEquivalenceKey(b);
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve one config selection against a provider's known native ids (#2491).
|
|
107
|
+
*
|
|
108
|
+
* `slugEquivalenceKey` is deliberately lossy — the Codex one-slash rule forces `a/b` and
|
|
109
|
+
* `a-b` onto the same encoded form — so a selection written in either spelling matches BOTH
|
|
110
|
+
* when a provider publishes both. Filtering and persisted sync share that key, which keeps
|
|
111
|
+
* them consistent with each other but silently over-grants.
|
|
112
|
+
*
|
|
113
|
+
* This resolver keeps the tolerant behaviour (a selection still matches through either
|
|
114
|
+
* spelling, and an id absent from an incomplete live roster still resolves) while reporting
|
|
115
|
+
* whether the match was EXACT or merely equivalent. A caller that can afford to be strict —
|
|
116
|
+
* one holding a complete known-id set — can then prefer the exact row instead of granting the
|
|
117
|
+
* whole collision class.
|
|
118
|
+
*
|
|
119
|
+
* Returning the ambiguity rather than resolving it is deliberate: the roster is an incomplete
|
|
120
|
+
* dictionary, so silently narrowing to the exact spelling would hide a published id whenever
|
|
121
|
+
* discovery omitted it. The caller owns that tradeoff because only the caller knows whether
|
|
122
|
+
* its id set is complete.
|
|
123
|
+
*/
|
|
124
|
+
export interface SlugSelectionMatch {
|
|
125
|
+
/** Native ids this selection admits. */
|
|
126
|
+
readonly matched: readonly string[];
|
|
127
|
+
/** The id whose raw form the selection names exactly, when one exists. */
|
|
128
|
+
readonly exact: string | undefined;
|
|
129
|
+
/** True when more than one known id shares the selection's encoded form. */
|
|
130
|
+
readonly ambiguous: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolveSlugSelection(
|
|
134
|
+
provider: string,
|
|
135
|
+
selection: string,
|
|
136
|
+
knownIds: Iterable<string>,
|
|
137
|
+
): SlugSelectionMatch {
|
|
138
|
+
// A slash in the selection is ambiguous on its own: `p/a-b` is provider-qualified, while
|
|
139
|
+
// `a/b` is a bare NATIVE id that happens to contain a slash. Treating every slash-bearing
|
|
140
|
+
// selection as provider-qualified made `a/b` resolve against provider "a", so the same
|
|
141
|
+
// collision reported ambiguous through the dash spelling and unambiguous through the slash
|
|
142
|
+
// spelling — the exact asymmetry this resolver exists to remove.
|
|
143
|
+
const qualified = selection.startsWith(`${provider}/`)
|
|
144
|
+
? selection
|
|
145
|
+
: routedSlug(provider, selection);
|
|
146
|
+
const selectionKey = slugEquivalenceKey(qualified);
|
|
147
|
+
const matched: string[] = [];
|
|
148
|
+
let exact: string | undefined;
|
|
149
|
+
for (const id of knownIds) {
|
|
150
|
+
if (slugEquivalenceKey(routedSlug(provider, id)) !== selectionKey) continue;
|
|
151
|
+
matched.push(id);
|
|
152
|
+
if (id === selection || `${provider}/${id}` === selection) exact = id;
|
|
153
|
+
}
|
|
154
|
+
return { matched, exact, ambiguous: matched.length > 1 };
|
|
155
|
+
}
|
|
@@ -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);
|
|
@@ -398,6 +404,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
398
404
|
authMode: canonicalAuthMode,
|
|
399
405
|
apiKey: resolvedApiKey,
|
|
400
406
|
...(staticModelCatalog ? { liveModels: false } : {}),
|
|
407
|
+
...(provider.requestPacing === undefined && registryEntry.requestPacing
|
|
408
|
+
? { requestPacing: structuredClone(registryEntry.requestPacing) }
|
|
409
|
+
: {}),
|
|
401
410
|
...(headers ? { headers } : {}),
|
|
402
411
|
// Backfill the Google wire mode + Vertex project/location from the registry when the user
|
|
403
412
|
// config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
|
|
@@ -432,6 +441,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
432
441
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
433
442
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
434
443
|
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
444
|
+
...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
|
|
435
445
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
436
446
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
437
447
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -646,7 +656,14 @@ function routeModelInternal(
|
|
|
646
656
|
// slash-containing model ids (e.g. "anthropic/claude-...") fall through when
|
|
647
657
|
// no such provider exists.
|
|
648
658
|
if (slash > 0) {
|
|
649
|
-
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 {
|
|
650
667
|
if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
|
|
651
668
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
652
669
|
}
|
|
@@ -662,14 +679,20 @@ function routeModelInternal(
|
|
|
662
679
|
}
|
|
663
680
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
664
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;
|
|
665
687
|
return routeResult(
|
|
666
688
|
provName,
|
|
667
689
|
prov,
|
|
668
|
-
|
|
690
|
+
nativeModel,
|
|
669
691
|
"explicit-provider",
|
|
670
692
|
"explicit-provider-namespace",
|
|
671
693
|
);
|
|
672
694
|
}
|
|
695
|
+
}
|
|
673
696
|
}
|
|
674
697
|
|
|
675
698
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
@@ -699,6 +722,24 @@ function routeModelInternal(
|
|
|
699
722
|
}
|
|
700
723
|
}
|
|
701
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
|
+
|
|
702
743
|
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
703
744
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
704
745
|
}
|
|
@@ -63,6 +63,14 @@ export function clearCooldownState(poolKey?: string): void {
|
|
|
63
63
|
registryByPool.delete(poolKey);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export function clearPoolAccountCooldown(poolKey: string, accountId: string): boolean {
|
|
67
|
+
const registry = registryByPool.get(poolKey);
|
|
68
|
+
if (!registry) return false;
|
|
69
|
+
const existed = registry.get(accountId) !== null;
|
|
70
|
+
registry.clear(accountId);
|
|
71
|
+
return existed;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
export function parseRetryAfterMs(
|
|
67
75
|
value: string | null | undefined,
|
|
68
76
|
now = Date.now(),
|
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,9 +728,13 @@ 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",
|
|
737
|
+
"googleMode",
|
|
724
738
|
"apiKeyTransport",
|
|
725
739
|
"keyOptional",
|
|
726
740
|
"freeTier",
|
|
@@ -731,6 +745,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
731
745
|
"contextWindow",
|
|
732
746
|
"modelContextWindows",
|
|
733
747
|
"modelAutoCompactTokenLimits",
|
|
748
|
+
"wsUpstream",
|
|
749
|
+
"maxWsFrameBytes",
|
|
734
750
|
"defaultMaxOutputTokens",
|
|
735
751
|
"modelMaxOutputTokens",
|
|
736
752
|
"openRouterRouting",
|
|
@@ -765,12 +781,20 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
765
781
|
if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
|
|
766
782
|
const codexAccountMode = providerCodexAccountMode(name, provider);
|
|
767
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
|
+
}
|
|
768
791
|
providers[name] = dto;
|
|
769
792
|
}
|
|
770
793
|
return {
|
|
771
794
|
port: config.port,
|
|
772
795
|
hostname: config.hostname ?? "127.0.0.1",
|
|
773
796
|
defaultProvider: config.defaultProvider,
|
|
797
|
+
defaultModelAliases: config.defaultModelAliases,
|
|
774
798
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
775
799
|
websockets: config.websockets,
|
|
776
800
|
// The GUI's browser-open toggle reads and writes this; absent means the
|
|
@@ -9,6 +9,7 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
|
9
9
|
import {
|
|
10
10
|
assertChatCompletionsRoutingBody,
|
|
11
11
|
ChatCompletionsRequestError,
|
|
12
|
+
copyChatResponsesSessionHeaders,
|
|
12
13
|
chatCompletionsToResponsesBody,
|
|
13
14
|
} from "../chat/inbound";
|
|
14
15
|
import {
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
responsesJsonToChatCompletion,
|
|
19
20
|
responsesSseToChatCompletionsSse,
|
|
20
21
|
} from "../chat/outbound";
|
|
21
|
-
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
22
|
+
import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
22
23
|
import { redactSecretString } from "../lib/redact";
|
|
23
24
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
24
25
|
import { estimateTokens } from "../lib/token-estimate";
|
|
@@ -120,6 +121,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
120
121
|
logCtx.model = route.modelId;
|
|
121
122
|
logCtx.providerAdapter = route.provider.adapter;
|
|
122
123
|
logCtx.requestedModel = requestedModel;
|
|
124
|
+
if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
|
|
123
125
|
logCtx.provider = route.providerName;
|
|
124
126
|
logCtx.routeDecision = route.routeDecision;
|
|
125
127
|
settledRoute = route;
|
|
@@ -201,6 +203,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
201
203
|
const value = req.headers.get(name);
|
|
202
204
|
if (value) headers.set(name, value);
|
|
203
205
|
}
|
|
206
|
+
copyChatResponsesSessionHeaders(req.headers, headers);
|
|
204
207
|
// Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
|
|
205
208
|
if (!directRoute) {
|
|
206
209
|
// This enrichment is optional for routed/non-main providers. If native main
|
|
@@ -280,26 +283,26 @@ async function handleChatCompletionsWithBudget(
|
|
|
280
283
|
const parsed = JSON.parse(text) as {
|
|
281
284
|
error?: { message?: string; type?: string; code?: string | null } | string;
|
|
282
285
|
message?: string;
|
|
286
|
+
type?: string;
|
|
287
|
+
code?: string | null;
|
|
283
288
|
};
|
|
284
289
|
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
|
|
285
290
|
const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
|
|
286
291
|
const rawFallback = text
|
|
287
292
|
? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
|
|
288
293
|
: message;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
294
302
|
} catch {
|
|
295
303
|
if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
|
|
296
304
|
}
|
|
297
305
|
} catch { /* keep fallback */ }
|
|
298
|
-
const retryAfter = resolveClientRetryAfter({
|
|
299
|
-
status: upstream.status,
|
|
300
|
-
message,
|
|
301
|
-
upstreamRetryAfter: upstream.headers.get("retry-after"),
|
|
302
|
-
});
|
|
303
306
|
const classified = classifyError(
|
|
304
307
|
upstream.status,
|
|
305
308
|
upstreamType
|
|
@@ -309,9 +312,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
309
312
|
: "invalid_request_error"),
|
|
310
313
|
message,
|
|
311
314
|
);
|
|
312
|
-
if (isCyberPolicyCode(upstreamCode)) {
|
|
315
|
+
if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
313
316
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
314
|
-
classified.type =
|
|
317
|
+
classified.type = cyberPolicyErrorType(upstreamType);
|
|
315
318
|
} else if (upstreamCode === "model_not_found") {
|
|
316
319
|
// Structured model_not_found must win over classifyError's generic remaps.
|
|
317
320
|
classified.code = "model_not_found";
|
|
@@ -320,6 +323,13 @@ async function handleChatCompletionsWithBudget(
|
|
|
320
323
|
classified.code = upstreamCode;
|
|
321
324
|
}
|
|
322
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
|
+
});
|
|
323
333
|
const rewritten = new Response(JSON.stringify({
|
|
324
334
|
error: {
|
|
325
335
|
message: classified.message,
|
|
@@ -386,14 +396,14 @@ async function handleChatCompletionsWithBudget(
|
|
|
386
396
|
const status = (json as Rec)?.status;
|
|
387
397
|
if (status === "failed") {
|
|
388
398
|
const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
|
|
389
|
-
const message = error?.message ?? "upstream request failed";
|
|
399
|
+
const message = redactSecretString(error?.message ?? "upstream request failed");
|
|
390
400
|
const classified = classifyError(502, error?.type ?? "server_error", message);
|
|
391
401
|
if (error?.code === "translation_buffer_limit") {
|
|
392
402
|
classified.code = "translation_buffer_limit";
|
|
393
403
|
classified.type = "upstream_error";
|
|
394
|
-
} else if (isCyberPolicyCode(error?.code)) {
|
|
404
|
+
} else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
395
405
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
396
|
-
classified.type =
|
|
406
|
+
classified.type = cyberPolicyErrorType(error?.type);
|
|
397
407
|
} else if (error?.code === "model_not_found") {
|
|
398
408
|
// Same deliberate preserve as the non-OK path: structured code beats generic classify.
|
|
399
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
|
}
|