@bitkyc08/opencodex 2.11.1 → 2.12.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 +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-Cw1_mxO8.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BNVYzdn0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -21,6 +21,10 @@ const COMMAND_CODE_MODEL_ALIASES: Readonly<Record<string, string>> = {
|
|
|
21
21
|
"glm-5.2": "zai-org/GLM-5.2",
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
function canonicalCommandCodeModelId(modelId: string): string {
|
|
25
|
+
return Object.hasOwn(COMMAND_CODE_MODEL_ALIASES, modelId) ? COMMAND_CODE_MODEL_ALIASES[modelId]! : modelId;
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
/** Flatten tool-result content for the text-only wire output, keeping an `[image]` marker per image part in content order. */
|
|
25
29
|
function toolResultText(content: string | OcxContentPart[]): string {
|
|
26
30
|
if (typeof content === "string") return content;
|
|
@@ -44,20 +48,79 @@ function wireImagePart(imageUrl: string): Record<string, unknown> {
|
|
|
44
48
|
return { type: "image", image: imageUrl, ...(mediaType ? { mediaType } : {}) };
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The /alpha/generate wire pairs every assistant `tool-call` with a following `tool` message
|
|
53
|
+
* whose `tool-result.toolCallId` matches it. Codex history is not guaranteed to carry that
|
|
54
|
+
* pairing (interrupted turns, compacted threads, and multi-step tool rounds all can leave an
|
|
55
|
+
* assistant call with no recorded result), and the upstream rejects an unpaired call with
|
|
56
|
+
* `Tool result is missing for tool call <id>`, which currently surfaces as a generic 502
|
|
57
|
+
* (#1383). This builder keeps the pairing invariant:
|
|
58
|
+
*
|
|
59
|
+
* - a `toolResult` that matches a declared assistant call emits the native `tool-result`;
|
|
60
|
+
* - a `toolResult` with no matching declared call degrades to a text carrier so the model
|
|
61
|
+
* still sees the outcome without a 400-prone standalone `tool` message;
|
|
62
|
+
* - every declared assistant call that never received a result gets an explicit error
|
|
63
|
+
* `tool-result`, so the upstream never sees an unpaired call.
|
|
64
|
+
*/
|
|
47
65
|
function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
48
66
|
const out: Array<Record<string, unknown>> = [];
|
|
67
|
+
const pendingCalls: Array<{ id: string; name: string }> = [];
|
|
68
|
+
// Image parts returned by a tool cannot live inside the text-only `tool-result` wire
|
|
69
|
+
// output. They ride a follow-up user message, but that user message must not break the
|
|
70
|
+
// adjacency of the assistant turn's tool results, so carriers are buffered and flushed
|
|
71
|
+
// only after every declared call has its native or synthesized result.
|
|
72
|
+
const pendingImageCarriers: Array<Record<string, unknown>> = [];
|
|
73
|
+
// The /alpha/generate wire requires every assistant tool-call to be closed by a matching
|
|
74
|
+
// `tool-result` immediately after the declaring assistant message. Close any declared call
|
|
75
|
+
// that never received a result before a non-toolResult message moves the turn forward, so a
|
|
76
|
+
// synthesized `tool` message never lands after a user message or a new assistant turn.
|
|
77
|
+
const closePendingCalls = (): void => {
|
|
78
|
+
for (const call of pendingCalls) {
|
|
79
|
+
out.push({ role: "tool", content: [{
|
|
80
|
+
type: "tool-result",
|
|
81
|
+
toolCallId: call.id,
|
|
82
|
+
toolName: call.name,
|
|
83
|
+
output: { type: "error-text", value: "[ocx] no tool result was recorded for this tool call; execution status unknown." },
|
|
84
|
+
}] });
|
|
85
|
+
}
|
|
86
|
+
pendingCalls.length = 0;
|
|
87
|
+
if (pendingImageCarriers.length > 0) {
|
|
88
|
+
out.push(...pendingImageCarriers);
|
|
89
|
+
pendingImageCarriers.length = 0;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
49
92
|
for (const message of messages) {
|
|
50
93
|
if (message.role === "assistant") {
|
|
94
|
+
closePendingCalls();
|
|
51
95
|
const content: Array<Record<string, unknown>> = [];
|
|
52
96
|
for (const part of message.content) {
|
|
53
97
|
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
54
98
|
else if (part.type === "thinking") content.push({ type: "reasoning", text: part.thinking });
|
|
55
|
-
else
|
|
99
|
+
else {
|
|
100
|
+
const wireName = namespacedToolName(part.namespace, part.name);
|
|
101
|
+
content.push({ type: "tool-call", toolCallId: part.id, toolName: wireName, input: part.arguments });
|
|
102
|
+
pendingCalls.push({ id: part.id, name: wireName });
|
|
103
|
+
}
|
|
56
104
|
}
|
|
57
105
|
out.push({ role: "assistant", content });
|
|
58
106
|
continue;
|
|
59
107
|
}
|
|
60
108
|
if (message.role === "toolResult") {
|
|
109
|
+
const callIndex = pendingCalls.findIndex(call => call.id === message.toolCallId);
|
|
110
|
+
const paired = callIndex >= 0;
|
|
111
|
+
if (paired) pendingCalls.splice(callIndex, 1);
|
|
112
|
+
if (!paired) {
|
|
113
|
+
// Pending calls from an earlier assistant turn must still be closed before any user
|
|
114
|
+
// message lands, or their synthesized results would follow the orphan carrier.
|
|
115
|
+
closePendingCalls();
|
|
116
|
+
// The upstream rejects a standalone tool message whose call was never declared by an
|
|
117
|
+
// assistant turn. Preserve the outcome as text so the model can still act on it.
|
|
118
|
+
const label = message.toolName ? `${message.toolName} (${message.toolCallId})` : message.toolCallId;
|
|
119
|
+
const text = toolResultText(message.content);
|
|
120
|
+
// The orphan result cannot ride a `tool` message; carry it in a user message instead.
|
|
121
|
+
out.push({ role: "user", content: [{ type: "text", text: `[tool result without adjacent tool call: ${label}]\n${text}` }] });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
61
124
|
out.push({ role: "tool", content: [{
|
|
62
125
|
type: "tool-result",
|
|
63
126
|
toolCallId: message.toolCallId,
|
|
@@ -69,10 +132,12 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
|
69
132
|
// message using the same image encoding as the user branch so the bytes reach the model.
|
|
70
133
|
const images = typeof message.content === "string" ? [] : message.content.filter(part => part.type === "image");
|
|
71
134
|
if (images.length > 0) {
|
|
72
|
-
|
|
135
|
+
pendingImageCarriers.push({ role: "user", content: images.map(part => wireImagePart((part as { imageUrl: string }).imageUrl)) });
|
|
73
136
|
}
|
|
74
137
|
continue;
|
|
75
138
|
}
|
|
139
|
+
// User/developer message: no pending tool results may follow it on the wire.
|
|
140
|
+
closePendingCalls();
|
|
76
141
|
const content: Array<Record<string, unknown>> = [];
|
|
77
142
|
if (typeof message.content === "string") content.push({ type: "text", text: message.content });
|
|
78
143
|
else for (const part of message.content) {
|
|
@@ -81,6 +146,7 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
|
81
146
|
}
|
|
82
147
|
out.push({ role: "user", content });
|
|
83
148
|
}
|
|
149
|
+
closePendingCalls();
|
|
84
150
|
return out;
|
|
85
151
|
}
|
|
86
152
|
|
|
@@ -237,6 +303,18 @@ function eventError(value: unknown): string {
|
|
|
237
303
|
return "Command Code stream error";
|
|
238
304
|
}
|
|
239
305
|
|
|
306
|
+
/**
|
|
307
|
+
* True when the upstream rejected a tool-result continuation because an assistant tool call
|
|
308
|
+
* had no matching result (`Tool result is missing for tool call <id>`). The proxy now keeps
|
|
309
|
+
* that pairing invariant before sending, so this error is a distinct provider-side
|
|
310
|
+
* validation failure rather than a generic stream fault; classify it as such for the
|
|
311
|
+
* dashboard/logs instead of a plain upstream 502.
|
|
312
|
+
*/
|
|
313
|
+
function isMissingToolResultError(value: unknown): boolean {
|
|
314
|
+
const text = eventError(value).toLowerCase();
|
|
315
|
+
return text.includes("tool result is missing") || text.includes("tool_result is missing");
|
|
316
|
+
}
|
|
317
|
+
|
|
240
318
|
async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenerator<Record<string, unknown>> {
|
|
241
319
|
if (!response.body) throw new Error("Command Code response body missing");
|
|
242
320
|
const reader = response.body.getReader();
|
|
@@ -318,7 +396,7 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
|
|
|
318
396
|
// Compatibility ids (deepseek-v4-flash / glm-5.2) must resolve to their canonical
|
|
319
397
|
// Command Code id before the effort lookup, or legacy requests silently lose the
|
|
320
398
|
// reasoning effort because the official table is keyed by the canonical ids.
|
|
321
|
-
const canonicalId =
|
|
399
|
+
const canonicalId = canonicalCommandCodeModelId(modelId);
|
|
322
400
|
const supported = commandCodeReasoningEfforts(canonicalId) ?? configuredReasoningEfforts(provider, canonicalId);
|
|
323
401
|
if (!supported) return undefined;
|
|
324
402
|
// Command Code's official profiles describe xhigh and ultra as the CLI labels that map to
|
|
@@ -347,7 +425,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
347
425
|
config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
|
|
348
426
|
permissionMode: "standard", mode: "agent",
|
|
349
427
|
params: {
|
|
350
|
-
model:
|
|
428
|
+
model: canonicalCommandCodeModelId(parsed.modelId),
|
|
351
429
|
messages: wireMessages(parsed.context.messages),
|
|
352
430
|
tools: wireTools(tools),
|
|
353
431
|
system,
|
|
@@ -437,7 +515,18 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
437
515
|
yield { type: "done", usage: usage(usageValue), stopReason };
|
|
438
516
|
break;
|
|
439
517
|
}
|
|
440
|
-
case "error":
|
|
518
|
+
case "error": {
|
|
519
|
+
const message = eventError(event.error);
|
|
520
|
+
if (isMissingToolResultError(message)) {
|
|
521
|
+
// Provider-side tool-result validation: the request carried an assistant tool
|
|
522
|
+
// call the upstream refused to accept. This is not a network/stream stall; the
|
|
523
|
+
// proxy normally prevents it by pairing every call, so flag it distinctly.
|
|
524
|
+
yield { type: "error", message, status: 502, errorType: "upstream_error", code: "missing_tool_result" };
|
|
525
|
+
} else {
|
|
526
|
+
yield { type: "error", message, status: 502 };
|
|
527
|
+
}
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
441
530
|
}
|
|
442
531
|
}
|
|
443
532
|
// A stream that ends without a finish event still needs a terminal done so the
|
|
@@ -1086,12 +1086,25 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
return;
|
|
1088
1088
|
}
|
|
1089
|
+
// A completion may carry only callId. Capture its ownership before mapping removes the open
|
|
1090
|
+
// call, because the embedded-tool classifier cannot identify that valid compact frame.
|
|
1091
|
+
const update = message.message.case === "interactionUpdate" ? message.message.value.message : undefined;
|
|
1092
|
+
const completesOpenClientTool = update?.case === "toolCallCompleted"
|
|
1093
|
+
&& state.openToolCalls.has(update.value.callId);
|
|
1089
1094
|
const mapped = mapCursorProtobufServerMessage(message, state);
|
|
1090
1095
|
if (mapped.length > 0) {
|
|
1091
1096
|
// A client tool call announced/committed via interactionUpdate (toolCallStarted/partialToolCall/
|
|
1092
1097
|
// toolCallCompleted) changes the call set, so revoke any finalize armed by an earlier drain.
|
|
1093
|
-
|
|
1098
|
+
// A completion can also commit and drain a late call without a following mcpArgs frame; in
|
|
1099
|
+
// that case re-arm finalization here so the Responses bridge does not wait forever.
|
|
1100
|
+
const clientToolFrame = completesOpenClientTool || isClientToolFrame(message);
|
|
1101
|
+
if (clientToolFrame) this.noteClientToolActivity();
|
|
1094
1102
|
for (const event of mapped) push(event);
|
|
1103
|
+
if (
|
|
1104
|
+
clientToolFrame
|
|
1105
|
+
&& state.openToolCalls.size === 0
|
|
1106
|
+
&& mapped.some(event => event.type === "tool_call_end")
|
|
1107
|
+
) this.scheduleClientToolFinalize(state, push);
|
|
1095
1108
|
return;
|
|
1096
1109
|
}
|
|
1097
1110
|
// The frame produced no outward Responses event (e.g. toolCallStarted / partialToolCall args
|
|
@@ -7,6 +7,27 @@ type Schema = Record<string, unknown>;
|
|
|
7
7
|
const ALLOWED_TYPES = new Set(["string", "integer", "number", "boolean", "array", "object"]);
|
|
8
8
|
const MAX_SCHEMA_DEPTH = 24; // Google's documented nesting limit is 32; leave headroom for CCA.
|
|
9
9
|
const MAX_DEREF_DEPTH = 16;
|
|
10
|
+
const MAX_SCHEMA_NODES = 1_024;
|
|
11
|
+
const BUDGET_EXHAUSTED = Symbol("schema-budget-exhausted");
|
|
12
|
+
const MERGED_SCHEMA_KEYS = [
|
|
13
|
+
"type",
|
|
14
|
+
"nullable",
|
|
15
|
+
"description",
|
|
16
|
+
"format",
|
|
17
|
+
"enum",
|
|
18
|
+
"const",
|
|
19
|
+
"properties",
|
|
20
|
+
"items",
|
|
21
|
+
"required",
|
|
22
|
+
"anyOf",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
type SanitizeResult = Schema | typeof BUDGET_EXHAUSTED;
|
|
26
|
+
|
|
27
|
+
interface SanitizeState {
|
|
28
|
+
activeRefs: Set<string>;
|
|
29
|
+
remainingNodes: number;
|
|
30
|
+
}
|
|
10
31
|
|
|
11
32
|
function isRecord(value: unknown): value is Schema {
|
|
12
33
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -34,6 +55,16 @@ function collectDefs(root: unknown, defs: Map<string, unknown>): void {
|
|
|
34
55
|
}
|
|
35
56
|
}
|
|
36
57
|
|
|
58
|
+
function mergeRefTarget(target: Schema, overlay: Schema): Schema {
|
|
59
|
+
const merged: Schema = {};
|
|
60
|
+
if (Object.hasOwn(target, "$ref")) merged.$ref = target.$ref;
|
|
61
|
+
for (const key of MERGED_SCHEMA_KEYS) {
|
|
62
|
+
if (Object.hasOwn(overlay, key)) merged[key] = overlay[key];
|
|
63
|
+
else if (Object.hasOwn(target, key)) merged[key] = target[key];
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
function normalizeType(value: unknown, out: Schema, preserveNullType: boolean): void {
|
|
38
69
|
const candidates = Array.isArray(value) ? value : [value];
|
|
39
70
|
let sawNull = false;
|
|
@@ -65,9 +96,16 @@ function normalizeAnyOf(
|
|
|
65
96
|
defs: Map<string, unknown>,
|
|
66
97
|
depth: number,
|
|
67
98
|
refDepth: number,
|
|
99
|
+
state: SanitizeState,
|
|
68
100
|
): Schema {
|
|
69
101
|
if (!Array.isArray(value) || value.length === 0) return {};
|
|
70
|
-
const schemas =
|
|
102
|
+
const schemas: Schema[] = [];
|
|
103
|
+
for (let index = 0; index < value.length; index++) {
|
|
104
|
+
if (state.remainingNodes <= 0) return {};
|
|
105
|
+
const schema = sanitizeSchema(value[index], defs, depth + 1, refDepth, true, state);
|
|
106
|
+
if (schema === BUDGET_EXHAUSTED) return {};
|
|
107
|
+
schemas.push(schema);
|
|
108
|
+
}
|
|
71
109
|
|
|
72
110
|
const nonNullSchemas = schemas.filter(schema => schema.type !== "null");
|
|
73
111
|
const nullSchemas = schemas.filter(schema => schema.type === "null");
|
|
@@ -100,12 +138,17 @@ function sanitizeProperties(
|
|
|
100
138
|
defs: Map<string, unknown>,
|
|
101
139
|
depth: number,
|
|
102
140
|
refDepth: number,
|
|
141
|
+
state: SanitizeState,
|
|
103
142
|
): Record<string, Schema> | undefined {
|
|
104
143
|
if (!isRecord(value)) return undefined;
|
|
105
144
|
const properties: Record<string, Schema> = Object.create(null) as Record<string, Schema>;
|
|
106
|
-
for (const
|
|
145
|
+
for (const name in value) {
|
|
146
|
+
if (!Object.hasOwn(value, name)) continue;
|
|
147
|
+
if (state.remainingNodes <= 0) break;
|
|
107
148
|
// Property names form a name bag and must never be interpreted as schema keywords.
|
|
108
|
-
|
|
149
|
+
const schema = sanitizeSchema(value[name], defs, depth + 1, refDepth, false, state);
|
|
150
|
+
if (schema === BUDGET_EXHAUSTED) break;
|
|
151
|
+
properties[name] = schema;
|
|
109
152
|
}
|
|
110
153
|
return properties;
|
|
111
154
|
}
|
|
@@ -116,17 +159,25 @@ function sanitizeSchema(
|
|
|
116
159
|
depth: number,
|
|
117
160
|
refDepth: number,
|
|
118
161
|
preserveNullType: boolean,
|
|
119
|
-
|
|
162
|
+
state: SanitizeState,
|
|
163
|
+
): SanitizeResult {
|
|
164
|
+
if (state.remainingNodes <= 0) return BUDGET_EXHAUSTED;
|
|
165
|
+
state.remainingNodes -= 1;
|
|
120
166
|
if (depth >= MAX_SCHEMA_DEPTH || !isRecord(node)) return {};
|
|
121
167
|
|
|
122
168
|
if (typeof node.$ref === "string" && refDepth < MAX_DEREF_DEPTH) {
|
|
123
169
|
const target = resolveRef(node.$ref, defs);
|
|
124
170
|
if (isRecord(target)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
if (state.activeRefs.has(node.$ref)) return {};
|
|
172
|
+
state.activeRefs.add(node.$ref);
|
|
173
|
+
// Select only inputs the sanitizer can consume. Spreading an untrusted definition here would
|
|
174
|
+
// enumerate and allocate every unsupported annotation before the node budget can stop work.
|
|
175
|
+
const merged = mergeRefTarget(target, node);
|
|
176
|
+
try {
|
|
177
|
+
return sanitizeSchema(merged, defs, depth, refDepth + 1, preserveNullType, state);
|
|
178
|
+
} finally {
|
|
179
|
+
state.activeRefs.delete(node.$ref);
|
|
128
180
|
}
|
|
129
|
-
return sanitizeSchema(merged, defs, depth, refDepth + 1, preserveNullType);
|
|
130
181
|
}
|
|
131
182
|
}
|
|
132
183
|
|
|
@@ -140,18 +191,27 @@ function sanitizeSchema(
|
|
|
140
191
|
const enumValues = sanitizeEnum(node.enum ?? (typeof node.const === "string" ? [node.const] : undefined));
|
|
141
192
|
if (enumValues) out.enum = enumValues;
|
|
142
193
|
|
|
143
|
-
const properties = sanitizeProperties(node.properties, defs, depth, refDepth);
|
|
194
|
+
const properties = sanitizeProperties(node.properties, defs, depth, refDepth, state);
|
|
144
195
|
if (properties) out.properties = properties;
|
|
145
196
|
|
|
146
|
-
if (
|
|
147
|
-
|
|
197
|
+
if (properties && Array.isArray(node.required)) {
|
|
198
|
+
const required = [...new Set(node.required.filter((item): item is string => (
|
|
199
|
+
typeof item === "string" && Object.hasOwn(properties, item)
|
|
200
|
+
)))];
|
|
201
|
+
if (required.length > 0) out.required = required;
|
|
148
202
|
}
|
|
149
203
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
204
|
+
if (state.remainingNodes <= 0) return out;
|
|
205
|
+
|
|
206
|
+
if (isRecord(node.items)) {
|
|
207
|
+
const items = sanitizeSchema(node.items, defs, depth + 1, refDepth, false, state);
|
|
208
|
+
if (items !== BUDGET_EXHAUSTED) out.items = items;
|
|
152
209
|
}
|
|
153
210
|
|
|
154
|
-
if (
|
|
211
|
+
if (state.remainingNodes <= 0) return out;
|
|
212
|
+
if (node.anyOf !== undefined) {
|
|
213
|
+
Object.assign(out, normalizeAnyOf(node.anyOf, defs, depth, refDepth, state));
|
|
214
|
+
}
|
|
155
215
|
return out;
|
|
156
216
|
}
|
|
157
217
|
|
|
@@ -159,7 +219,12 @@ export function sanitizeGeminiToolParameters(parameters: unknown): Record<string
|
|
|
159
219
|
try {
|
|
160
220
|
const defs = new Map<string, unknown>();
|
|
161
221
|
collectDefs(parameters, defs);
|
|
162
|
-
const
|
|
222
|
+
const state: SanitizeState = {
|
|
223
|
+
activeRefs: new Set(),
|
|
224
|
+
remainingNodes: MAX_SCHEMA_NODES,
|
|
225
|
+
};
|
|
226
|
+
const sanitized = sanitizeSchema(parameters, defs, 0, 0, false, state);
|
|
227
|
+
const root = sanitized === BUDGET_EXHAUSTED ? {} : sanitized;
|
|
163
228
|
|
|
164
229
|
// Function arguments are always an object. Claude additionally rejects root composition and a
|
|
165
230
|
// missing root type even when those forms are valid general-purpose JSON Schema.
|
package/src/adapters/google.ts
CHANGED
|
@@ -54,8 +54,8 @@ function resolveVertexApiKey(optKey?: string): string | undefined {
|
|
|
54
54
|
/** Prefer Codex's stable opaque thread key; retain the existing deterministic fallback for clients
|
|
55
55
|
* that omit it. The replay store hashes this value and never retains the raw session identifier. */
|
|
56
56
|
function vertexReplaySessionId(parsed: OcxParsedRequest): string {
|
|
57
|
-
const
|
|
58
|
-
return
|
|
57
|
+
const threadId = parsed._clientThreadId?.trim();
|
|
58
|
+
return threadId || antigravitySessionId(parsed);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -577,15 +577,32 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
577
577
|
pendingUsage = usageFromGemini(usageMeta);
|
|
578
578
|
sawTerminalSignal = true;
|
|
579
579
|
}
|
|
580
|
-
const
|
|
581
|
-
if (
|
|
580
|
+
const rawCandidates = root.candidates;
|
|
581
|
+
if (rawCandidates === undefined) return "continue";
|
|
582
|
+
if (!Array.isArray(rawCandidates)) {
|
|
583
|
+
yield { type: "error", message: "google response contained invalid candidates" };
|
|
584
|
+
return "terminate";
|
|
585
|
+
}
|
|
586
|
+
if (rawCandidates.length === 0) return "continue";
|
|
587
|
+
const rawCandidate = rawCandidates[0];
|
|
588
|
+
if (rawCandidate === null || typeof rawCandidate !== "object" || Array.isArray(rawCandidate)) {
|
|
589
|
+
// Unlike a root `data: null` keepalive, this is a claimed response candidate. Treat it
|
|
590
|
+
// as terminal protocol corruption so the turn cannot complete after silently losing
|
|
591
|
+
// a candidate or tool call (#1325).
|
|
592
|
+
yield { type: "error", message: "google response contained invalid candidates" };
|
|
593
|
+
return "terminate";
|
|
594
|
+
}
|
|
595
|
+
const candidate = rawCandidate as {
|
|
596
|
+
content?: { parts?: unknown[] };
|
|
597
|
+
finishReason?: string;
|
|
598
|
+
};
|
|
582
599
|
|
|
583
|
-
if (typeof
|
|
584
|
-
lastFinishReason =
|
|
600
|
+
if (typeof candidate.finishReason === "string" && candidate.finishReason) {
|
|
601
|
+
lastFinishReason = candidate.finishReason;
|
|
585
602
|
sawTerminalSignal = true;
|
|
586
603
|
}
|
|
587
604
|
|
|
588
|
-
const parts =
|
|
605
|
+
const parts = candidate.content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
|
|
589
606
|
// Record Gemini thought signatures for the next stateless tool-result turn. Vertex and
|
|
590
607
|
// Antigravity use separate model namespaces so opaque provider state cannot cross routes.
|
|
591
608
|
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|