@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
|
|
2
|
+
import { namespacedToolName } from "../types/tools";
|
|
3
|
+
import { rewriteRoutedNamespaceToolsForUpstream } from "./namespace-tool-compat";
|
|
4
|
+
import { collectResponsesToolGroups } from "./tool-groups";
|
|
5
|
+
|
|
6
|
+
export interface FunctionCallRepairSchema {
|
|
7
|
+
name: string;
|
|
8
|
+
namespace?: string;
|
|
9
|
+
parameters?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Keys are canonical original identities, never a bare-name fallback for a namespace. */
|
|
13
|
+
export type FunctionCallRepairSchemas = ReadonlyMap<string, FunctionCallRepairSchema>;
|
|
14
|
+
|
|
15
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** JSON object member order is immaterial; array elements retain their exact order. */
|
|
20
|
+
function sameSchemaValue(left: unknown, right: unknown): boolean {
|
|
21
|
+
if (left === right) return true;
|
|
22
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
23
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length
|
|
24
|
+
&& left.every((value, index) => sameSchemaValue(value, right[index]));
|
|
25
|
+
}
|
|
26
|
+
if (!isObject(left) || !isObject(right)) return false;
|
|
27
|
+
const keys = Object.keys(left);
|
|
28
|
+
return keys.length === Object.keys(right).length
|
|
29
|
+
&& keys.every(key => Object.hasOwn(right, key) && sameSchemaValue(left[key], right[key]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function namespaceOf(value: unknown): string | undefined {
|
|
33
|
+
return typeof value === "string" && value !== "functions" ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function selectorAllows(
|
|
37
|
+
selector: unknown,
|
|
38
|
+
lowered: unknown,
|
|
39
|
+
wireName: string,
|
|
40
|
+
identity: FunctionCallRepairSchema,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!isObject(selector) || selector.type !== "function" || typeof selector.name !== "string") return false;
|
|
43
|
+
if ("namespace" in selector) {
|
|
44
|
+
if (typeof selector.namespace !== "string" || selector.namespace.length === 0) return false;
|
|
45
|
+
return namespaceOf(selector.namespace) === identity.namespace && selector.name === identity.name;
|
|
46
|
+
}
|
|
47
|
+
return isObject(lowered) && lowered.type === "function" && lowered.name === wireName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Caller supplies currentTurnWireToolCatalogBody BEFORE provider schema lowering. */
|
|
51
|
+
export function collectFunctionCallRepairSchemas(body: unknown): Map<string, FunctionCallRepairSchema> {
|
|
52
|
+
const schemas = new Map<string, FunctionCallRepairSchema>();
|
|
53
|
+
if (!isObject(body)) return schemas;
|
|
54
|
+
const groups = collectResponsesToolGroups(body);
|
|
55
|
+
if (Array.isArray(body.input)) {
|
|
56
|
+
for (const entry of body.input) {
|
|
57
|
+
if (isObject(entry) && entry.type === "tool_search_output" && Array.isArray(entry.tools)) groups.push(entry.tools);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Reuse namespace selector resolution, retaining schemas from the original objects below.
|
|
61
|
+
// This local catalog view includes loaded definitions without revisiting replay history or
|
|
62
|
+
// teaching the shared tool-group collector a new transport-wide interpretation.
|
|
63
|
+
const lowered = rewriteRoutedNamespaceToolsForUpstream({ ...body, tools: groups.flat(), input: [] }).body;
|
|
64
|
+
const choice = body.tool_choice;
|
|
65
|
+
const loweredChoice = isObject(lowered) ? lowered.tool_choice : undefined;
|
|
66
|
+
const occupied = new Map<string, { kind: unknown; identity: FunctionCallRepairSchema } | null>();
|
|
67
|
+
const register = (tool: unknown, namespace?: string): void => {
|
|
68
|
+
if (!isObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
|
|
69
|
+
const identity: FunctionCallRepairSchema = {
|
|
70
|
+
name: tool.name,
|
|
71
|
+
...(namespace ? { namespace } : {}),
|
|
72
|
+
...(isObject(tool.parameters) ? { parameters: tool.parameters } : {}),
|
|
73
|
+
};
|
|
74
|
+
const key = namespacedToolName(namespace, tool.name);
|
|
75
|
+
if (occupied.has(key)) {
|
|
76
|
+
const previous = occupied.get(key);
|
|
77
|
+
// Conflicting duplicate declarations cannot choose a schema by insertion order.
|
|
78
|
+
if (!previous || previous.kind !== tool.type
|
|
79
|
+
|| previous.identity.namespace !== namespace
|
|
80
|
+
|| previous.identity.name !== tool.name
|
|
81
|
+
|| !sameSchemaValue(previous.identity.parameters, identity.parameters)) {
|
|
82
|
+
occupied.set(key, null);
|
|
83
|
+
}
|
|
84
|
+
} else occupied.set(key, { kind: tool.type, identity });
|
|
85
|
+
};
|
|
86
|
+
for (const group of groups) {
|
|
87
|
+
for (const tool of group) {
|
|
88
|
+
if (!isObject(tool)) continue;
|
|
89
|
+
if (tool.type === "namespace") {
|
|
90
|
+
if (typeof tool.name !== "string" || !tool.name || !Array.isArray(tool.tools)) continue;
|
|
91
|
+
for (const child of tool.tools) register(child, namespaceOf(tool.name));
|
|
92
|
+
} else if (tool.type === "function" && isObject(tool.function)) {
|
|
93
|
+
register({ ...tool.function, type: "function" });
|
|
94
|
+
} else register(tool);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [key, entry] of occupied) {
|
|
98
|
+
if (!entry || entry.kind !== "function") continue;
|
|
99
|
+
let allowed = choice === undefined || choice === "auto" || choice === "required";
|
|
100
|
+
if (isObject(choice)) {
|
|
101
|
+
if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
|
|
102
|
+
const selectors = isObject(loweredChoice) && Array.isArray(loweredChoice.tools) ? loweredChoice.tools : [];
|
|
103
|
+
allowed = choice.tools.some((selector, index) => selectorAllows(selector, selectors[index], key, entry.identity));
|
|
104
|
+
} else allowed = selectorAllows(choice, loweredChoice, key, entry.identity);
|
|
105
|
+
}
|
|
106
|
+
if (allowed) schemas.set(key, entry.identity);
|
|
107
|
+
}
|
|
108
|
+
return schemas;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function repairItem(item: unknown, schemas: FunctionCallRepairSchemas, completed: boolean): unknown {
|
|
112
|
+
if (!isObject(item) || item.type !== "function_call" || typeof item.name !== "string"
|
|
113
|
+
|| typeof item.arguments !== "string") return item;
|
|
114
|
+
if (item.status !== "completed" && !(item.status === undefined && completed)) return item;
|
|
115
|
+
if ("namespace" in item && (typeof item.namespace !== "string" || !item.namespace)) return item;
|
|
116
|
+
const namespace = namespaceOf(item.namespace);
|
|
117
|
+
const schema = schemas.get(namespacedToolName(namespace, item.name));
|
|
118
|
+
if (!schema) return item;
|
|
119
|
+
// An explicit namespace is an identity coordinate, not another spelling to guess at.
|
|
120
|
+
if ("namespace" in item && (schema.namespace !== namespace || schema.name !== item.name)) return item;
|
|
121
|
+
const raw = item.arguments;
|
|
122
|
+
if (raw !== "") {
|
|
123
|
+
try {
|
|
124
|
+
let unsafe = false;
|
|
125
|
+
JSON.parse(raw, (_key, value: unknown) => {
|
|
126
|
+
if (typeof value === "number" && (!Number.isFinite(value)
|
|
127
|
+
|| (Number.isInteger(value) && !Number.isSafeInteger(value)))) unsafe = true;
|
|
128
|
+
return value;
|
|
129
|
+
});
|
|
130
|
+
// Re-stringifying another repaired field must not round an unsafe sibling number.
|
|
131
|
+
if (unsafe) return item;
|
|
132
|
+
} catch { return item; }
|
|
133
|
+
}
|
|
134
|
+
const argumentsText = coerceIntegerToolArguments(raw || "{}", schema.parameters, schema.namespace ? undefined : schema.name);
|
|
135
|
+
return argumentsText === raw ? item : { ...item, arguments: argumentsText };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Only executable completion slots are visited; metadata and custom input are opaque. */
|
|
139
|
+
export function repairFunctionCalls(
|
|
140
|
+
value: unknown,
|
|
141
|
+
schemas: FunctionCallRepairSchemas,
|
|
142
|
+
): { value: unknown; changed: boolean } {
|
|
143
|
+
if (schemas.size === 0 || !isObject(value)) return { value, changed: false };
|
|
144
|
+
if (typeof value.status === "string" && ["failed", "incomplete", "cancelled", "in_progress", "queued"].includes(value.status)) return { value, changed: false };
|
|
145
|
+
let next: unknown = value;
|
|
146
|
+
if (value.type === "function_call") next = repairItem(value, schemas, false);
|
|
147
|
+
else if (value.type === "response.output_item.done") {
|
|
148
|
+
const item = repairItem(value.item, schemas, true);
|
|
149
|
+
if (item !== value.item) next = { ...value, item };
|
|
150
|
+
} else if (value.type === "response.completed" && isObject(value.response)) {
|
|
151
|
+
const response = value.response;
|
|
152
|
+
if ((response.status === undefined || response.status === "completed") && Array.isArray(response.output)) {
|
|
153
|
+
const original = response.output;
|
|
154
|
+
const output = original.map(item => repairItem(item, schemas, true));
|
|
155
|
+
if (output.some((item, index) => item !== original[index])) next = { ...value, response: { ...response, output } };
|
|
156
|
+
}
|
|
157
|
+
} else if (typeof value.type !== "string" || !value.type.startsWith("response.")) {
|
|
158
|
+
if (Array.isArray(value.output)) {
|
|
159
|
+
const original = value.output;
|
|
160
|
+
const output = original.map(item => repairItem(item, schemas, value.status === "completed"));
|
|
161
|
+
if (output.some((item, index) => item !== original[index])) next = { ...value, output };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { value: next, changed: next !== value };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function repairFunctionCallsInJson(text: string, schemas: FunctionCallRepairSchemas): string {
|
|
168
|
+
if (schemas.size === 0) return text;
|
|
169
|
+
let payload: unknown;
|
|
170
|
+
try { payload = JSON.parse(text); } catch { return text; }
|
|
171
|
+
const repaired = repairFunctionCalls(payload, schemas);
|
|
172
|
+
return repaired.changed ? JSON.stringify(repaired.value) : text;
|
|
173
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { namespacedToolName } from "../types";
|
|
1
|
+
import { dottedToolName, namespacedToolName } from "../types";
|
|
2
2
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
3
|
+
import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous } from "./tool-name-aliases";
|
|
3
4
|
|
|
4
5
|
export interface RoutedNamespaceToolIdentity {
|
|
5
6
|
namespace: string;
|
|
@@ -340,7 +341,10 @@ function rewriteInputItem(item: unknown, plan: NamespaceRewritePlan, emitted: Se
|
|
|
340
341
|
* `<namespace>__<name>` wire identity as the chat adapters. The returned request-local aliases
|
|
341
342
|
* are the only names response restoration is allowed to expand.
|
|
342
343
|
*/
|
|
343
|
-
export function rewriteRoutedNamespaceToolsForUpstream(
|
|
344
|
+
export function rewriteRoutedNamespaceToolsForUpstream(
|
|
345
|
+
body: unknown,
|
|
346
|
+
convertedCustomToolNames?: ReadonlySet<string>,
|
|
347
|
+
): {
|
|
344
348
|
body: unknown;
|
|
345
349
|
aliases: Map<string, RoutedNamespaceToolIdentity>;
|
|
346
350
|
} {
|
|
@@ -365,6 +369,21 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
|
|
|
365
369
|
}
|
|
366
370
|
|
|
367
371
|
const toolChoice = rewriteToolChoice(body.tool_choice, plan);
|
|
372
|
+
const aliases = authorizedAliases(plan.aliases, toolChoice);
|
|
373
|
+
const ambiguousDotted = collectAmbiguousDottedAliases(groups);
|
|
374
|
+
// Authorize canonical identities first, then add only unambiguous spellings.
|
|
375
|
+
// Selection cannot hide a collision elsewhere in the original declaration set.
|
|
376
|
+
for (const identity of [...aliases.values()]) {
|
|
377
|
+
const dotted = dottedToolName(identity.namespace, identity.name);
|
|
378
|
+
if (dottedAliasIsUnambiguous(identity.namespace, identity.name)
|
|
379
|
+
&& !ambiguousDotted.has(dotted) && !plan.bareWireNames.has(dotted)
|
|
380
|
+
&& !aliases.has(dotted)) aliases.set(dotted, identity);
|
|
381
|
+
}
|
|
382
|
+
// The adapter lowers custom tools before namespaces. Preserve their declared
|
|
383
|
+
// kind only in already-authorized response aliases; wire selectors remain lowered.
|
|
384
|
+
for (const identity of aliases.values()) {
|
|
385
|
+
if (convertedCustomToolNames?.has(namespacedToolName(identity.namespace, identity.name))) identity.kind = "custom";
|
|
386
|
+
}
|
|
368
387
|
return {
|
|
369
388
|
body: {
|
|
370
389
|
...body,
|
|
@@ -372,7 +391,7 @@ export function rewriteRoutedNamespaceToolsForUpstream(body: unknown): {
|
|
|
372
391
|
...(input !== body.input ? { input } : {}),
|
|
373
392
|
...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
|
|
374
393
|
},
|
|
375
|
-
aliases
|
|
394
|
+
aliases,
|
|
376
395
|
};
|
|
377
396
|
}
|
|
378
397
|
|
|
@@ -404,7 +423,11 @@ export function restoreRoutedNamespaceCalls(
|
|
|
404
423
|
&& typeof value.name === "string"
|
|
405
424
|
) {
|
|
406
425
|
const identity = aliases.get(value.name);
|
|
407
|
-
if (identity
|
|
426
|
+
if (identity
|
|
427
|
+
// Custom declarations may be lowered to function calls upstream, but an
|
|
428
|
+
// ordinary function declaration never authorizes a custom call payload.
|
|
429
|
+
&& (value.type !== "custom_tool_call" || identity.kind === "custom")
|
|
430
|
+
&& (!Object.hasOwn(value, "namespace") || value.namespace === identity.namespace)) {
|
|
408
431
|
restored.name = identity.name;
|
|
409
432
|
restored.namespace = identity.namespace;
|
|
410
433
|
changed = true;
|
package/src/responses/parser.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { toolSearchDescription, toolSearchParameters } from "./tool-search-compa
|
|
|
25
25
|
import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, toolOutputContainsEncryptedContent } from "./parser-content";
|
|
26
26
|
import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools";
|
|
27
27
|
import { parseTextFormat } from "./parser-text-format";
|
|
28
|
+
import { externalTaskInputContent } from "./task-input";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Wrap a remembered proxy-side signature as provider metadata for a replayed tool call.
|
|
@@ -146,6 +147,7 @@ export function parseRequest(
|
|
|
146
147
|
const item = data.input[inputIndex];
|
|
147
148
|
const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
|
|
148
149
|
const itemRole = (item as { role?: string }).role;
|
|
150
|
+
const externalTaskInput = effectiveType === "function_call_output" ? externalTaskInputContent(item) : undefined;
|
|
149
151
|
// Raw protocol items do not map one-to-one onto context messages. Capture the boundary while
|
|
150
152
|
// both representations are available so later metadata can stay before conversation in both.
|
|
151
153
|
if (
|
|
@@ -154,6 +156,7 @@ export function parseRequest(
|
|
|
154
156
|
&& continuationConversationMessageIndex === undefined
|
|
155
157
|
&& (
|
|
156
158
|
effectiveType === "agent_message"
|
|
159
|
+
|| externalTaskInput !== undefined
|
|
157
160
|
|| (effectiveType === "message" && (itemRole === "user" || itemRole === "assistant"))
|
|
158
161
|
)
|
|
159
162
|
) {
|
|
@@ -429,6 +432,11 @@ export function parseRequest(
|
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
if (effectiveType === "function_call_output") {
|
|
435
|
+
if (externalTaskInput !== undefined) {
|
|
436
|
+
pendingReasoning.length = 0;
|
|
437
|
+
messages.push({ role: "user", content: externalTaskInput, timestamp: now });
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
432
440
|
const output = item as { call_id: string; output?: string | unknown[] };
|
|
433
441
|
attachPendingReasoningToCallOwner(messages, output.call_id, pendingReasoning);
|
|
434
442
|
pendingReasoning.length = 0;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OcxContentPart } from "../types";
|
|
2
|
+
import { inputContentParts, isObj } from "./parser-content";
|
|
3
|
+
|
|
4
|
+
type TaskInputBlock =
|
|
5
|
+
| { type: "input_text" | "output_text" | "text"; text: string }
|
|
6
|
+
| { type: "input_image"; image_url: string; detail?: string };
|
|
7
|
+
|
|
8
|
+
const imageDetails = new Set(["auto", "low", "high", "original"]);
|
|
9
|
+
|
|
10
|
+
function nonBlank(value: unknown): value is string {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function supportedBlock(value: unknown): value is TaskInputBlock {
|
|
15
|
+
if (!isObj(value)) return false;
|
|
16
|
+
if (value.type === "input_text" || value.type === "output_text" || value.type === "text") {
|
|
17
|
+
return typeof value.text === "string";
|
|
18
|
+
}
|
|
19
|
+
if (value.type !== "input_image" || !nonBlank(value.image_url)) return false;
|
|
20
|
+
return value.detail === undefined || (typeof value.detail === "string" && imageDetails.has(value.detail));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Recognize Codex external task input without repairing ordinary orphaned tool results. */
|
|
24
|
+
export function externalTaskInputContent(item: unknown): string | OcxContentPart[] | undefined {
|
|
25
|
+
if (!isObj(item) || item.type !== "function_call_output" || "call_id" in item) return undefined;
|
|
26
|
+
if (!nonBlank(item.id) || !nonBlank(item.name) || !nonBlank(item.namespace)) return undefined;
|
|
27
|
+
const output = item.output;
|
|
28
|
+
if (typeof output === "string") return nonBlank(output) ? output : undefined;
|
|
29
|
+
if (!Array.isArray(output) || output.length === 0 || !output.every(supportedBlock)) return undefined;
|
|
30
|
+
if (!output.some(block => block.type === "input_image" || nonBlank(block.text))) return undefined;
|
|
31
|
+
// Validate the entire array first: the general converter intentionally drops unknown
|
|
32
|
+
// blocks, while a partial external task would silently lose the caller's input.
|
|
33
|
+
return inputContentParts(output.map(block =>
|
|
34
|
+
block.type === "output_text" ? { ...block, type: "input_text" } : block,
|
|
35
|
+
));
|
|
36
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { dottedToolName, namespacedToolName } from "../types";
|
|
2
|
+
|
|
3
|
+
const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
|
|
4
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A dotted spelling is a safe alias only when it cannot ALSO be read as some other identity's
|
|
10
|
+
* canonical `ns__name`.
|
|
11
|
+
*
|
|
12
|
+
* `{namespace: "x__y", name: "z"}` produces the dotted spelling "x__y.z", which is exactly the
|
|
13
|
+
* canonical wire name of `{namespace: "x", name: "y.z"}`. If only the latter is declared, an
|
|
14
|
+
* echoed call for the former would still find "x__y.z" in the declared set and be authorized as
|
|
15
|
+
* a tool the caller never granted. Requiring both halves to be free of the `__` separator keeps
|
|
16
|
+
* a dotted alias from ever impersonating a canonical name.
|
|
17
|
+
*/
|
|
18
|
+
export function dottedAliasIsUnambiguous(namespace: string, name: string): boolean {
|
|
19
|
+
return !namespace.includes("__") && !name.includes("__");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function wireToolInnerName(tool: unknown): string | undefined {
|
|
23
|
+
if (!isPlainObject(tool)) return undefined;
|
|
24
|
+
const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
|
|
25
|
+
? tool.function
|
|
26
|
+
: undefined;
|
|
27
|
+
return typeof tool.name === "string" && tool.name.length > 0
|
|
28
|
+
? tool.name
|
|
29
|
+
: typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
|
|
30
|
+
? nestedFunction.name
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Dotted aliases that more than one declared identity would claim, plus dotted aliases that
|
|
36
|
+
* collide with a canonical or bare declared name.
|
|
37
|
+
*
|
|
38
|
+
* Resolved over the WHOLE catalog before any name is registered, so which identity "wins" can
|
|
39
|
+
* never depend on declaration order -- an order the caller controls.
|
|
40
|
+
*/
|
|
41
|
+
export function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): Set<string> {
|
|
42
|
+
const owners = new Map<string, string | null>();
|
|
43
|
+
const claim = (alias: string, identity: string): void => {
|
|
44
|
+
const owner = owners.get(alias);
|
|
45
|
+
if (owner === undefined) owners.set(alias, identity);
|
|
46
|
+
else if (owner !== identity) owners.set(alias, null);
|
|
47
|
+
};
|
|
48
|
+
for (const specs of specGroups) {
|
|
49
|
+
if (!Array.isArray(specs)) continue;
|
|
50
|
+
for (const spec of specs) {
|
|
51
|
+
if (!isPlainObject(spec)) continue;
|
|
52
|
+
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
53
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
54
|
+
if (!namespace) continue;
|
|
55
|
+
for (const inner of spec.tools) {
|
|
56
|
+
const name = wireToolInnerName(inner);
|
|
57
|
+
if (!name) continue;
|
|
58
|
+
if (namespace === BUILTIN_FUNCTIONS_NAMESPACE) {
|
|
59
|
+
claim(name, JSON.stringify([undefined, name]));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const identity = JSON.stringify([namespace, name]);
|
|
63
|
+
claim(dottedToolName(namespace, name), identity);
|
|
64
|
+
// A canonical or bare name already owned by a different identity poisons the dotted
|
|
65
|
+
// alias that would shadow it.
|
|
66
|
+
claim(namespacedToolName(namespace, name), identity);
|
|
67
|
+
claim(name, identity);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const name = wireToolInnerName(spec);
|
|
72
|
+
if (name) claim(name, JSON.stringify([undefined, name]));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const ambiguous = new Set<string>();
|
|
76
|
+
for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
|
|
77
|
+
return ambiguous;
|
|
78
|
+
}
|
|
79
|
+
|
package/src/router.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import type { NormalizedComboConfig } from "./combos/types";
|
|
11
11
|
import { hasOwnProvider } from "./config/provider-name";
|
|
12
12
|
import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
|
|
13
|
+
import { captureProviderApiKeySelection } from "./providers/api-key-selection";
|
|
13
14
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
14
15
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
15
16
|
import {
|
|
@@ -297,6 +298,7 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
|
|
|
297
298
|
}
|
|
298
299
|
|
|
299
300
|
export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
301
|
+
provider = { ...provider, _apiKeyAttempt: provider._apiKeyAttempt ?? captureProviderApiKeySelection(provider) };
|
|
300
302
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
301
303
|
if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
|
|
302
304
|
assertProviderDestinationAllowed(providerName, provider);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* how that affects eligibility.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { modelInList, type OcxConfig } from "../types";
|
|
13
|
+
import { modelInList, type OcxConfig, type OcxProviderConfig } from "../types";
|
|
14
14
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
15
15
|
import { serviceTierSupportForModel } from "../providers/service-tier";
|
|
16
16
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
@@ -149,14 +149,20 @@ function localRemoteEvidence(baseUrl: string | undefined): Pick<RouteCapabilityE
|
|
|
149
149
|
* Assemble canonical capability evidence for one `provider/model` candidate.
|
|
150
150
|
* Sources (in priority order): provider config maps, provider registry hints,
|
|
151
151
|
* cached Codex catalog row, native-model metadata.
|
|
152
|
+
* Policy assembly supplies the resolved provider so every transport capability
|
|
153
|
+
* describes the destination dispatch will use. Its maps already include applicable
|
|
154
|
+
* registry defaults; name-only registry fallbacks must not override that authority.
|
|
152
155
|
*/
|
|
153
156
|
export function candidateCapabilityEvidence(
|
|
154
157
|
config: OcxConfig,
|
|
155
158
|
providerName: string,
|
|
156
159
|
modelId: string,
|
|
160
|
+
resolvedProvider?: OcxProviderConfig,
|
|
157
161
|
): RouteCapabilityEvidence {
|
|
158
|
-
const provider = config.providers[providerName];
|
|
159
|
-
const registryEntry =
|
|
162
|
+
const provider = resolvedProvider ?? config.providers[providerName];
|
|
163
|
+
const registryEntry = resolvedProvider === undefined
|
|
164
|
+
? PROVIDER_REGISTRY.find(entry => entry.id === providerName)
|
|
165
|
+
: undefined;
|
|
160
166
|
const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId);
|
|
161
167
|
const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/");
|
|
162
168
|
|
|
@@ -224,6 +230,7 @@ export function candidateCapabilityEvidence(
|
|
|
224
230
|
? []
|
|
225
231
|
: modelRecordValue(provider?.modelReasoningEfforts, modelId)
|
|
226
232
|
?? modelRecordValue(registryEntry?.modelReasoningEfforts, modelId)
|
|
233
|
+
?? provider?.reasoningEfforts
|
|
227
234
|
?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
|
|
228
235
|
|
|
229
236
|
const tierSupport = provider
|
|
@@ -52,11 +52,26 @@ export function assemblePolicyCandidateEvidence(
|
|
|
52
52
|
return profile.candidates.map(candidate => {
|
|
53
53
|
const key = `${candidate.provider}/${candidate.model}`;
|
|
54
54
|
const compatibility = compatibilityByCandidate?.get(key);
|
|
55
|
+
const provider = config.providers[candidate.provider];
|
|
56
|
+
let routed: OcxProviderConfig | undefined;
|
|
57
|
+
let routeResolutionFailed = !provider || provider.disabled === true;
|
|
58
|
+
if (provider && provider.disabled !== true) {
|
|
59
|
+
try {
|
|
60
|
+
routed = options.routedProviderConfig(candidate.provider, provider);
|
|
61
|
+
} catch {
|
|
62
|
+
// This is known unavailability, not unknown capability evidence. Keep
|
|
63
|
+
// the failure separate so permissive unknown policies cannot select it.
|
|
64
|
+
routeResolutionFailed = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
55
67
|
|
|
56
68
|
return {
|
|
57
69
|
provider: candidate.provider,
|
|
58
70
|
model: candidate.model,
|
|
59
|
-
|
|
71
|
+
...(routeResolutionFailed ? { routeResolutionFailed: true } : {}),
|
|
72
|
+
capability: routed
|
|
73
|
+
? candidateCapabilityEvidence(config, candidate.provider, candidate.model, routed)
|
|
74
|
+
: undefined,
|
|
60
75
|
health: policyCandidateHealthEvidence(config, candidate, now),
|
|
61
76
|
quota: quotaEvidenceForCandidate({
|
|
62
77
|
provider: candidate.provider,
|
package/src/routing/evaluator.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface PolicyCandidateEvidence {
|
|
|
54
54
|
accountRef?: string;
|
|
55
55
|
/** Codex pool account id (provider "openai"); used to derive account-scoped quota evidence. */
|
|
56
56
|
codexAccountId?: string;
|
|
57
|
+
/** A failed effective-transport resolution excludes the candidate under every unknown policy. */
|
|
58
|
+
routeResolutionFailed?: boolean;
|
|
57
59
|
capability?: RouteCapabilityEvidence;
|
|
58
60
|
health?: RouteHealthEvidence;
|
|
59
61
|
quota?: RouteQuotaEvidence;
|
|
@@ -278,6 +280,8 @@ export function evaluatePolicyProfile(
|
|
|
278
280
|
...requestRequirementFor(requestEvidence, evidence.capability),
|
|
279
281
|
];
|
|
280
282
|
const exclusions: RouteExclusionReason[] = [];
|
|
283
|
+
const routeUnavailable = evidence.routeResolutionFailed === true;
|
|
284
|
+
if (routeUnavailable) exclusions.push({ code: "route-unavailable" });
|
|
281
285
|
const bad = unsatisfiedOrUnknown(requirements);
|
|
282
286
|
for (const requirement of bad) {
|
|
283
287
|
if (requirement.outcome === "unsatisfied") {
|
|
@@ -310,7 +314,7 @@ export function evaluatePolicyProfile(
|
|
|
310
314
|
if (unknownCostBlocked) {
|
|
311
315
|
exclusions.push({ code: "cost-limit-unknown", detail: "maxEstimatedCostUsd" });
|
|
312
316
|
}
|
|
313
|
-
let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked;
|
|
317
|
+
let eligible = !routeUnavailable && !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked;
|
|
314
318
|
|
|
315
319
|
// Trace/dry-run copy only: report the profile cap that was applied and the
|
|
316
320
|
// operator-visible outcome. Do not feed this copy into costScore() — that
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -530,6 +530,7 @@ export function requireResponsesApiAuth(req: Request, config: RequestPolicyView)
|
|
|
530
530
|
const FORBIDDEN_PROVIDER_RUNTIME_FIELDS = [
|
|
531
531
|
"virtualModels", "codexAuthContext", "selectedForwardHeaders",
|
|
532
532
|
"sidecarOutcomeRecorder", "_codexAccountOverride", "_codexAccountRequired",
|
|
533
|
+
"_apiKeyAttempt",
|
|
533
534
|
] as const;
|
|
534
535
|
|
|
535
536
|
function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
|
|
@@ -795,6 +796,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
795
796
|
apiKey: "redacted",
|
|
796
797
|
apiKeyTransport: "editor",
|
|
797
798
|
apiKeyPool: "redacted",
|
|
799
|
+
apiKeySelectionRevision: "runtime",
|
|
800
|
+
_apiKeyAttempt: "runtime",
|
|
798
801
|
defaultModel: "editor",
|
|
799
802
|
models: "editor",
|
|
800
803
|
liveModels: "editor",
|
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
transientRetryPolicyFor,
|
|
38
38
|
} from "../providers/key-failover";
|
|
39
39
|
import { fastPolicyForModel } from "../providers/service-tier";
|
|
40
|
+
import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
|
|
41
|
+
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
40
42
|
import type { RouteResult } from "../router";
|
|
41
43
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
42
44
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./responses/fetch-helpers";
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
beginRequestAttempt,
|
|
47
49
|
noteAttemptSend,
|
|
48
50
|
recordFirstOutput,
|
|
51
|
+
recordAttemptCredentialSource,
|
|
49
52
|
sealRequestAttemptIdentity,
|
|
50
53
|
type RequestLogContext,
|
|
51
54
|
} from "./request-log";
|
|
@@ -183,14 +186,17 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
183
186
|
translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
|
|
184
187
|
retainedRequestBytes = bytes;
|
|
185
188
|
};
|
|
186
|
-
const buildActiveRequest = () =>
|
|
187
|
-
activeProvider,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
const buildActiveRequest = () => {
|
|
190
|
+
recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name);
|
|
191
|
+
return buildOpenAIChatPassthroughRequest(
|
|
192
|
+
activeProvider,
|
|
193
|
+
options.chatBody,
|
|
194
|
+
route.modelId,
|
|
195
|
+
requestedStream,
|
|
196
|
+
fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
|
|
197
|
+
config.fastMode,
|
|
198
|
+
);
|
|
199
|
+
};
|
|
194
200
|
try {
|
|
195
201
|
activeRequest = buildActiveRequest();
|
|
196
202
|
retainRequest(activeRequest);
|
|
@@ -224,7 +230,6 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
224
230
|
const fetchWithPolicy = requestTransientPolicy ? fetchWithTransientRetry : fetchWithResetRetry;
|
|
225
231
|
return await fetchWithPolicy(
|
|
226
232
|
(transportRecovery?: UpstreamSendRecovery) => {
|
|
227
|
-
noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
228
233
|
return fetchWithHeaderTimeout(
|
|
229
234
|
request.url,
|
|
230
235
|
applyUpstreamRecoveryInit({
|
|
@@ -238,6 +243,32 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
238
243
|
providerFetch(activeProvider, undefined, {
|
|
239
244
|
providerName: route.providerName,
|
|
240
245
|
modelId: route.modelId,
|
|
246
|
+
dispatchOverride: async (_input, init, execute) => {
|
|
247
|
+
if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
|
|
248
|
+
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
249
|
+
if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, options.chatBody)) {
|
|
250
|
+
throw new Error("Provider key selection is no longer available for native Chat");
|
|
251
|
+
}
|
|
252
|
+
activeProvider = current;
|
|
253
|
+
activeAdapter = createOpenAIChatAdapter(current);
|
|
254
|
+
activeRequest.releaseBodyObservation?.();
|
|
255
|
+
releaseRetainedRequest();
|
|
256
|
+
activeRequest = buildActiveRequest();
|
|
257
|
+
try { retainRequest(activeRequest); }
|
|
258
|
+
catch (error) { activeRequest.releaseBodyObservation?.(); throw error; }
|
|
259
|
+
}
|
|
260
|
+
// The retry closure may still hold a pre-pacing request. Replace its entire
|
|
261
|
+
// wire shape, not just Authorization, and retain transport recovery flags.
|
|
262
|
+
request = activeRequest;
|
|
263
|
+
const headers = new Headers(request.headers);
|
|
264
|
+
const encoding = new Headers(init.headers).get("accept-encoding");
|
|
265
|
+
if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
|
|
266
|
+
if (init.signal?.aborted) throw init.signal.reason;
|
|
267
|
+
noteAttemptSend(attempt, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
|
|
268
|
+
return ((activeProvider as OcxProviderTransport).fetch ?? execute)(request.url, applyUpstreamRecoveryInit({
|
|
269
|
+
...init, method: request.method, headers, body: request.body,
|
|
270
|
+
}, transportRecovery));
|
|
271
|
+
},
|
|
241
272
|
}),
|
|
242
273
|
);
|
|
243
274
|
},
|
|
@@ -282,6 +313,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
282
313
|
retryAfter: response.headers.get("retry-after"),
|
|
283
314
|
now: Date.now(),
|
|
284
315
|
attemptedKey: activeProvider.apiKey,
|
|
316
|
+
attemptedSelection: activeProvider._apiKeyAttempt,
|
|
285
317
|
promptCacheKey: typeof options.chatBody.prompt_cache_key === "string" ? options.chatBody.prompt_cache_key : undefined,
|
|
286
318
|
});
|
|
287
319
|
if (!rotated) break;
|
|
@@ -302,6 +334,9 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
302
334
|
cleanupAbort();
|
|
303
335
|
upstream.abort();
|
|
304
336
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
|
|
337
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
338
|
+
return fail(413, "request translation buffer exceeded the safe limit", "request_too_large", "translation_buffer_limit");
|
|
339
|
+
}
|
|
305
340
|
return fail(502, error instanceof Error ? error.message : String(error), "server_error");
|
|
306
341
|
}
|
|
307
342
|
releaseRetainedRequest();
|