@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
addGoogleToolSchemaLoss,
|
|
4
|
+
createGoogleToolSchemaLossReport,
|
|
5
|
+
GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT,
|
|
6
|
+
mergeGoogleToolSchemaLossReport,
|
|
7
|
+
sanitizeGeminiToolParametersWithReport,
|
|
8
|
+
type GoogleToolSchemaLossReport,
|
|
9
|
+
type GoogleToolSchemaPolicy,
|
|
10
|
+
type GoogleToolSchemaProfile,
|
|
11
|
+
} from "./google-tool-schema";
|
|
3
12
|
|
|
4
13
|
type JsonObject = Record<string, unknown>;
|
|
5
14
|
|
|
6
15
|
const GOOGLE_TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
7
16
|
const GOOGLE_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
|
|
8
17
|
|
|
18
|
+
export class GoogleToolSchemaPolicyError extends Error {
|
|
19
|
+
readonly phase = "initial" as const;
|
|
20
|
+
readonly endpointClass: GoogleToolSchemaProfile["endpointClass"];
|
|
21
|
+
readonly categories: GoogleToolSchemaLossReport["categories"];
|
|
22
|
+
/** True when refusal is due to exhausted bounded comparison rather than proven loss. */
|
|
23
|
+
readonly indeterminate: boolean;
|
|
24
|
+
|
|
25
|
+
constructor(report: GoogleToolSchemaLossReport) {
|
|
26
|
+
const categories = { ...report.categories };
|
|
27
|
+
const indeterminate = report.uncertainComparisons > 0;
|
|
28
|
+
super(`google tool schema policy rejected ${report.lossy ? "lossy" : "indeterminate"} initial compilation (${report.endpointClass}; categories=${JSON.stringify(categories)}${indeterminate ? `; uncertainComparisons=${report.uncertainComparisons}` : ""})`);
|
|
29
|
+
this.name = "GoogleToolSchemaPolicyError";
|
|
30
|
+
this.endpointClass = report.endpointClass;
|
|
31
|
+
this.categories = categories;
|
|
32
|
+
this.indeterminate = indeterminate;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
function isObject(value: unknown): value is JsonObject {
|
|
10
37
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
11
38
|
}
|
|
@@ -94,16 +121,23 @@ function compileContents(value: unknown, toWireName: (name: string) => string):
|
|
|
94
121
|
});
|
|
95
122
|
}
|
|
96
123
|
|
|
97
|
-
function compileTools(
|
|
124
|
+
function compileTools(
|
|
125
|
+
value: unknown,
|
|
126
|
+
toWireName: (name: string) => string,
|
|
127
|
+
profile: GoogleToolSchemaProfile,
|
|
128
|
+
lossReport: GoogleToolSchemaLossReport,
|
|
129
|
+
): unknown[] | undefined {
|
|
98
130
|
if (!Array.isArray(value)) return undefined;
|
|
99
131
|
const tools = value.flatMap(rawTool => {
|
|
100
132
|
if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return [];
|
|
101
133
|
const functionDeclarations = rawTool.functionDeclarations.flatMap(rawDeclaration => {
|
|
102
134
|
if (!isObject(rawDeclaration) || typeof rawDeclaration.name !== "string") return [];
|
|
135
|
+
const sanitized = sanitizeGeminiToolParametersWithReport(rawDeclaration.parameters, profile);
|
|
136
|
+
mergeGoogleToolSchemaLossReport(lossReport, sanitized.lossReport);
|
|
103
137
|
return [{
|
|
104
138
|
name: toWireName(rawDeclaration.name),
|
|
105
139
|
...(typeof rawDeclaration.description === "string" ? { description: rawDeclaration.description } : {}),
|
|
106
|
-
parameters:
|
|
140
|
+
parameters: sanitized.parameters,
|
|
107
141
|
}];
|
|
108
142
|
});
|
|
109
143
|
return functionDeclarations.length > 0 ? [{ functionDeclarations }] : [];
|
|
@@ -180,24 +214,35 @@ function compileToolConfig(value: unknown, toWireName: (name: string) => string)
|
|
|
180
214
|
* Final trust boundary for every Google-family request. The adapter may build a convenient
|
|
181
215
|
* Gemini-shaped object; only this compiler is allowed to decide what reaches the wire.
|
|
182
216
|
*/
|
|
183
|
-
export function compileGoogleWireBody(
|
|
217
|
+
export function compileGoogleWireBody(
|
|
218
|
+
input: unknown,
|
|
219
|
+
profile: GoogleToolSchemaProfile = { endpointClass: "ai-studio" },
|
|
220
|
+
policy: GoogleToolSchemaPolicy = "compatible",
|
|
221
|
+
): {
|
|
184
222
|
body: JsonObject;
|
|
185
223
|
restoreToolName: (name: string) => string;
|
|
224
|
+
toolSchemaLossReport: GoogleToolSchemaLossReport;
|
|
186
225
|
} {
|
|
187
226
|
const source = isObject(input) ? input : {};
|
|
188
227
|
const names = toolNameCodec(collectToolNames(source));
|
|
189
228
|
const body: JsonObject = {};
|
|
229
|
+
const toolSchemaLossReport = createGoogleToolSchemaLossReport(profile);
|
|
190
230
|
const contents = compileContents(source.contents, names.toWire);
|
|
191
231
|
if (contents) body.contents = contents;
|
|
192
232
|
if (isObject(source.systemInstruction)) body.systemInstruction = source.systemInstruction;
|
|
193
|
-
const tools = compileTools(source.tools, names.toWire);
|
|
233
|
+
const tools = compileTools(source.tools, names.toWire, profile, toolSchemaLossReport);
|
|
194
234
|
if (tools) body.tools = tools;
|
|
235
|
+
// Strict mode refuses proven loss and bounded-comparison indeterminacy alike: an exhausted
|
|
236
|
+
// comparison can hide a changed constraint, so unknown is not admitted as lossless.
|
|
237
|
+
if (policy === "reject-lossy" && (toolSchemaLossReport.lossy || toolSchemaLossReport.uncertainComparisons > 0)) {
|
|
238
|
+
throw new GoogleToolSchemaPolicyError(toolSchemaLossReport);
|
|
239
|
+
}
|
|
195
240
|
const generationConfig = compileGenerationConfig(source.generationConfig);
|
|
196
241
|
if (generationConfig) body.generationConfig = generationConfig;
|
|
197
242
|
const toolConfig = compileToolConfig(source.toolConfig, names.toWire);
|
|
198
243
|
if (toolConfig) body.toolConfig = toolConfig;
|
|
199
244
|
if (typeof source.sessionId === "string" && source.sessionId.length > 0) body.sessionId = source.sessionId;
|
|
200
|
-
return { body, restoreToolName: names.fromWire };
|
|
245
|
+
return { body, restoreToolName: names.fromWire, toolSchemaLossReport };
|
|
201
246
|
}
|
|
202
247
|
|
|
203
248
|
function functionDeclarations(root: JsonObject): JsonObject[] {
|
|
@@ -208,10 +253,35 @@ function functionDeclarations(root: JsonObject): JsonObject[] {
|
|
|
208
253
|
});
|
|
209
254
|
}
|
|
210
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Upstream INVALID_ARGUMENT text blaming a tool or function declaration schema.
|
|
258
|
+
*
|
|
259
|
+
* Shared with the diagnostic error classifier so the repair path and the description of why a
|
|
260
|
+
* request was rejected cannot drift into disagreeing about the same payload.
|
|
261
|
+
*/
|
|
262
|
+
export function isGoogleToolSchemaErrorText(errorPayload: string): boolean {
|
|
263
|
+
return /(?:input[_ ]schema|json schema|function[_ ]declarations?|x-mcp-header)/i.test(errorPayload);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Upstream INVALID_ARGUMENT text blaming the thinking configuration. */
|
|
267
|
+
export function isGoogleThinkingConfigErrorText(errorPayload: string): boolean {
|
|
268
|
+
return /thinking[_ ]?(?:config|level)/i.test(errorPayload);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface GoogleInvalidRequestRepair {
|
|
272
|
+
body: string;
|
|
273
|
+
toolSchemaLoss?: GoogleToolSchemaLossReport;
|
|
274
|
+
toolSchemaDeclarationCount?: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
/** Build a changed request for one known-safe replay of an INVALID_ARGUMENT response. */
|
|
212
|
-
export function
|
|
213
|
-
|
|
214
|
-
|
|
278
|
+
export function repairGoogleInvalidRequestBodyWithReport(
|
|
279
|
+
body: string,
|
|
280
|
+
errorPayload: string,
|
|
281
|
+
profile: GoogleToolSchemaProfile = { endpointClass: "ai-studio" },
|
|
282
|
+
): GoogleInvalidRequestRepair | undefined {
|
|
283
|
+
const schemaError = isGoogleToolSchemaErrorText(errorPayload);
|
|
284
|
+
const thinkingError = isGoogleThinkingConfigErrorText(errorPayload);
|
|
215
285
|
if (!schemaError && !thinkingError) return undefined;
|
|
216
286
|
let parsed: unknown;
|
|
217
287
|
try {
|
|
@@ -222,6 +292,8 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
|
|
|
222
292
|
if (!isObject(parsed)) return undefined;
|
|
223
293
|
const root = isObject(parsed.request) ? parsed.request : parsed;
|
|
224
294
|
let changed = false;
|
|
295
|
+
let toolSchemaLoss: GoogleToolSchemaLossReport | undefined;
|
|
296
|
+
let toolSchemaDeclarationCount: number | undefined;
|
|
225
297
|
|
|
226
298
|
if (thinkingError && isObject(root.generationConfig) && "thinkingConfig" in root.generationConfig) {
|
|
227
299
|
delete root.generationConfig.thinkingConfig;
|
|
@@ -237,6 +309,9 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
|
|
|
237
309
|
const index = indexed === undefined ? -1 : Number.parseInt(indexed, 10);
|
|
238
310
|
const rejected = declarations[index];
|
|
239
311
|
const targets = rejected ? [rejected] : declarations;
|
|
312
|
+
toolSchemaLoss = createGoogleToolSchemaLossReport(profile);
|
|
313
|
+
addGoogleToolSchemaLoss(toolSchemaLoss, "repair-opened-schema", targets.length);
|
|
314
|
+
toolSchemaDeclarationCount = Math.min(targets.length, GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT);
|
|
240
315
|
for (const declaration of targets) {
|
|
241
316
|
declaration.parameters = { type: "object", properties: {} };
|
|
242
317
|
delete declaration.parametersJsonSchema;
|
|
@@ -244,5 +319,13 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
|
|
|
244
319
|
changed = true;
|
|
245
320
|
}
|
|
246
321
|
}
|
|
247
|
-
return changed ?
|
|
322
|
+
return changed ? {
|
|
323
|
+
body: JSON.stringify(parsed),
|
|
324
|
+
...(toolSchemaLoss ? { toolSchemaLoss, toolSchemaDeclarationCount } : {}),
|
|
325
|
+
} : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Compatibility facade for callers that need only the changed body. */
|
|
329
|
+
export function repairGoogleInvalidRequestBody(body: string, errorPayload: string): string | undefined {
|
|
330
|
+
return repairGoogleInvalidRequestBodyWithReport(body, errorPayload)?.body;
|
|
248
331
|
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { MAX_DEBUG_LINE_BYTES } from "../lib/debug-log-buffer";
|
|
2
|
+
import { THOUGHT_SIGNATURE_BYPASS } from "./google-antigravity-replay";
|
|
3
|
+
import type { AntigravitySessionAnchor } from "./google-antigravity-wire";
|
|
4
|
+
import { isGoogleThinkingConfigErrorText, isGoogleToolSchemaErrorText } from "./google-wire-compiler";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Content-free structural projection of a compiled Google-family wire request (#5008).
|
|
8
|
+
*
|
|
9
|
+
* Why this exists. The report behind #5008 is an intermittent HTTP 400 on the FIRST upstream
|
|
10
|
+
* send of a long, tool-heavy Antigravity session: "Please ensure that function call turn comes
|
|
11
|
+
* immediately after a user turn or after a function response turn." The reporter's own static
|
|
12
|
+
* matrix (A-F on the issue) exercised every adjacency hazard the translator can produce and all
|
|
13
|
+
* of them returned 200, so the failing wire cannot be reconstructed from the repro shapes. What
|
|
14
|
+
* is missing is a description of the request that actually failed, and the only description a
|
|
15
|
+
* user can attach to a public bug report is one that carries no conversation in it.
|
|
16
|
+
*
|
|
17
|
+
* This module is that description. It reads the request object AFTER compileGoogleWireBody and
|
|
18
|
+
* after Antigravity replay/signature adjustment — the exact object the envelope carries — and
|
|
19
|
+
* returns counts, positions, kinds and booleans.
|
|
20
|
+
*
|
|
21
|
+
* What it never retains, by construction rather than by redaction: prompt or system text, tool
|
|
22
|
+
* arguments, tool results, tool or function names, original or wire tool-call ids, thought
|
|
23
|
+
* signature text or any hash of it, inline file bytes, project or account identifiers, the
|
|
24
|
+
* request id, the Cloud Code Assist session id, the Codex thread or session ids, and the first
|
|
25
|
+
* user message. Tool-call identity survives only as a request-internal ordinal assigned in
|
|
26
|
+
* first-appearance order, which is enough to show pairing structure and is meaningless outside
|
|
27
|
+
* the single request that produced it.
|
|
28
|
+
*
|
|
29
|
+
* Totals are computed over the whole request; per-turn detail is cut at
|
|
30
|
+
* GOOGLE_WIRE_SHAPE_TURN_CEILING and the summary is marked truncated. A 440-message session
|
|
31
|
+
* therefore still reports its true turn and call counts.
|
|
32
|
+
*
|
|
33
|
+
* This is a projection, not a validator. It does not decide whether a request is acceptable to
|
|
34
|
+
* the upstream, and nothing in the request path consults its output.
|
|
35
|
+
*
|
|
36
|
+
* What it costs. With provider debug off the projection never runs: the call site passes a
|
|
37
|
+
* builder to debugProviderDiagnosticLazy, which returns before invoking it. With provider debug
|
|
38
|
+
* ON it runs synchronously on the dispatch path, before the request is sent, and it walks every
|
|
39
|
+
* turn, every part and every call id, then serializes, redacts and writes one line. The cost is
|
|
40
|
+
* linear in history length, which is largest for exactly the long sessions this exists to
|
|
41
|
+
* describe. That is the price of observing the real outbound body rather than a reconstruction,
|
|
42
|
+
* and an operator who turns provider debug on for a 440-message session is paying it on every
|
|
43
|
+
* turn.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** Per-turn detail beyond this many turns is dropped; totals are unaffected. */
|
|
47
|
+
export const GOOGLE_WIRE_SHAPE_TURN_CEILING = 64;
|
|
48
|
+
|
|
49
|
+
/** Per-turn ordinal lists are cut here; the turn's own counts are unaffected. */
|
|
50
|
+
export const GOOGLE_WIRE_SHAPE_ORDINAL_CEILING = 16;
|
|
51
|
+
|
|
52
|
+
/** Longest error text inspected by classifyGoogleWireUpstreamError. */
|
|
53
|
+
const UPSTREAM_ERROR_SCAN_LIMIT = 2048;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Serialized ceiling for one summary, held at half the debug buffer's per-line cap so the event
|
|
57
|
+
* prefix and any redaction copy still fit under it.
|
|
58
|
+
*
|
|
59
|
+
* The item ceilings above are not sufficient on their own: 64 retained turns carrying 16 ordinals
|
|
60
|
+
* each serialize past the buffer's limit, and the buffer truncates at a byte boundary. That
|
|
61
|
+
* leaves a consumer with JSON it cannot parse whose retained prefix still says truncated is
|
|
62
|
+
* false — a cut that hides itself. Trimming here instead keeps the line parseable and keeps the
|
|
63
|
+
* flag honest.
|
|
64
|
+
*/
|
|
65
|
+
export const GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES = Math.floor(MAX_DEBUG_LINE_BYTES / 2);
|
|
66
|
+
|
|
67
|
+
export type GoogleWireTurnRole = "user" | "model" | "other";
|
|
68
|
+
|
|
69
|
+
export type GoogleWirePartKind =
|
|
70
|
+
| "text"
|
|
71
|
+
| "thought"
|
|
72
|
+
| "functionCall"
|
|
73
|
+
| "functionResponse"
|
|
74
|
+
| "inlineData"
|
|
75
|
+
| "other";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ordering classes transcribed from the upstream error text, not inferred from it.
|
|
79
|
+
*
|
|
80
|
+
* The message names one relationship — a function-call turn must follow a user turn or a
|
|
81
|
+
* function-response turn — so call-turn-opens-request and call-turn-after-model-turn are the two
|
|
82
|
+
* ways a well-formed request can contradict it, call-turn-after-unknown-turn covers a predecessor
|
|
83
|
+
* that is neither, and response-turn-without-call-turn is its converse.
|
|
84
|
+
* request-ends-with-model-turn is a different upstream 400 that messagesToGeminiFormat already
|
|
85
|
+
* guards with its "(continue)" nudge; it is projected so a defeated guard is visible rather than
|
|
86
|
+
* silent.
|
|
87
|
+
*
|
|
88
|
+
* A flag is not a verdict. The reporter's case A produces call-turn-after-model-turn and
|
|
89
|
+
* returned 200, which is itself worth being able to observe in a real failing request.
|
|
90
|
+
*/
|
|
91
|
+
export type GoogleWireOrderingViolation =
|
|
92
|
+
| "call-turn-opens-request"
|
|
93
|
+
| "call-turn-after-model-turn"
|
|
94
|
+
| "call-turn-after-unknown-turn"
|
|
95
|
+
| "response-turn-without-call-turn"
|
|
96
|
+
| "request-ends-with-model-turn";
|
|
97
|
+
|
|
98
|
+
/** Bounded classes for an upstream rejection. The error text itself is never retained. */
|
|
99
|
+
export type GoogleWireUpstreamErrorClass =
|
|
100
|
+
| "turn-adjacency"
|
|
101
|
+
| "thought-signature"
|
|
102
|
+
| "tool-schema"
|
|
103
|
+
| "thinking-config"
|
|
104
|
+
| "other";
|
|
105
|
+
|
|
106
|
+
export interface GoogleWireTurnShape {
|
|
107
|
+
/** Position in contents. */
|
|
108
|
+
index: number;
|
|
109
|
+
role: GoogleWireTurnRole;
|
|
110
|
+
/** Number of parts in this turn. */
|
|
111
|
+
parts: number;
|
|
112
|
+
/** Distinct part kinds, in first-appearance order. */
|
|
113
|
+
kinds: GoogleWirePartKind[];
|
|
114
|
+
/** Request-internal ordinals of the function calls in this turn. */
|
|
115
|
+
calls: number[];
|
|
116
|
+
/** Request-internal ordinals of the function responses in this turn. */
|
|
117
|
+
responses: number[];
|
|
118
|
+
/** Function calls carrying any thought signature. */
|
|
119
|
+
signedCalls: number;
|
|
120
|
+
/** Of those, the ones carrying only the validator-bypass sentinel. */
|
|
121
|
+
sentinelCalls: number;
|
|
122
|
+
violation?: GoogleWireOrderingViolation;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Facts the caller knows and the wire body does not carry. */
|
|
126
|
+
export interface GoogleWireShapeFacts {
|
|
127
|
+
/** Which anchor class produced the Antigravity session id. Never the id itself. */
|
|
128
|
+
sessionAnchor?: AntigravitySessionAnchor;
|
|
129
|
+
/**
|
|
130
|
+
* Function calls the translator signed from client history or the durable replay store,
|
|
131
|
+
* counted before the Antigravity session cache runs.
|
|
132
|
+
*/
|
|
133
|
+
historySignedCalls?: number;
|
|
134
|
+
/** A durable replay scope was attached to this request. Its completeness is NOT asserted. */
|
|
135
|
+
replayScopeBound?: boolean;
|
|
136
|
+
/** The real physical send this body belongs to, 1 for the first send. */
|
|
137
|
+
sendOrdinal?: number;
|
|
138
|
+
/** Bounded class of the upstream rejection, when this body was rejected. */
|
|
139
|
+
errorClass?: GoogleWireUpstreamErrorClass;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type GoogleWireShapeSummary = {
|
|
143
|
+
version: 1;
|
|
144
|
+
/** Per-turn detail was cut by a ceiling. Totals below remain exact. */
|
|
145
|
+
truncated: boolean;
|
|
146
|
+
turns: number;
|
|
147
|
+
roles: Record<GoogleWireTurnRole, number>;
|
|
148
|
+
functionCalls: number;
|
|
149
|
+
functionResponses: number;
|
|
150
|
+
/** Distinct tool-call ids seen anywhere in the request. */
|
|
151
|
+
distinctCallIds: number;
|
|
152
|
+
/** Function calls that carried no id at all. */
|
|
153
|
+
callsWithoutId: number;
|
|
154
|
+
/** Function responses that carried no id at all. */
|
|
155
|
+
responsesWithoutId: number;
|
|
156
|
+
/** Call ordinals no response answers. */
|
|
157
|
+
unansweredCalls: number;
|
|
158
|
+
/** Response ordinals no call introduced. */
|
|
159
|
+
unmatchedResponses: number;
|
|
160
|
+
toolDeclarations: number;
|
|
161
|
+
hasSystemInstruction: boolean;
|
|
162
|
+
hasSessionId: boolean;
|
|
163
|
+
sessionAnchor: AntigravitySessionAnchor | "unknown";
|
|
164
|
+
signature: {
|
|
165
|
+
/** At least one function call reaches the wire with a signature. */
|
|
166
|
+
present: boolean;
|
|
167
|
+
/** Every signed call carries only the validator-bypass sentinel. */
|
|
168
|
+
sentinelOnly: boolean;
|
|
169
|
+
signedCalls: number;
|
|
170
|
+
sentinelCalls: number;
|
|
171
|
+
/** Signed at translation time, from client history or the durable replay store. */
|
|
172
|
+
historySignedCalls: number;
|
|
173
|
+
/**
|
|
174
|
+
* The remainder the Antigravity session cache supplied, clamped at zero. The cache never
|
|
175
|
+
* overwrites an existing signature, so the three counts partition the signed calls.
|
|
176
|
+
*/
|
|
177
|
+
sessionCacheSignedCalls: number;
|
|
178
|
+
/**
|
|
179
|
+
* A durable replay scope was attached. Whether it was COMPLETE enough to key a lookup is
|
|
180
|
+
* decided inside lookupReplayThoughtSignature and is deliberately not claimed here.
|
|
181
|
+
*/
|
|
182
|
+
replayScopeBound: boolean;
|
|
183
|
+
};
|
|
184
|
+
orderingViolations: number;
|
|
185
|
+
firstOrderingViolation: { index: number; kind: GoogleWireOrderingViolation } | null;
|
|
186
|
+
/** Present only when the caller supplied send facts. */
|
|
187
|
+
send: { ordinal: number; errorClass: GoogleWireUpstreamErrorClass | null } | null;
|
|
188
|
+
turnShapes: GoogleWireTurnShape[];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
192
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function turnRole(content: Record<string, unknown>): GoogleWireTurnRole {
|
|
196
|
+
const role = content.role;
|
|
197
|
+
return role === "user" || role === "model" ? role : "other";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function partKind(part: Record<string, unknown>): GoogleWirePartKind {
|
|
201
|
+
if (part.thought === true) return "thought";
|
|
202
|
+
if (isRecord(part.functionCall)) return "functionCall";
|
|
203
|
+
if (isRecord(part.functionResponse)) return "functionResponse";
|
|
204
|
+
if (isRecord(part.inline_data) || isRecord(part.inlineData)) return "inlineData";
|
|
205
|
+
if (typeof part.text === "string") return "text";
|
|
206
|
+
return "other";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A function call or response id, read only to be replaced by an ordinal. The value never
|
|
211
|
+
* leaves this function.
|
|
212
|
+
*/
|
|
213
|
+
function callIdOf(call: Record<string, unknown>): string | undefined {
|
|
214
|
+
const id = call.id;
|
|
215
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function signatureOf(part: Record<string, unknown>): string | undefined {
|
|
219
|
+
const camel = part.thoughtSignature;
|
|
220
|
+
if (typeof camel === "string" && camel.length > 0) return camel;
|
|
221
|
+
const snake = part.thought_signature;
|
|
222
|
+
if (typeof snake === "string" && snake.length > 0) return snake;
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Request-internal renumbering: first appearance wins, and the mapping dies with the call. */
|
|
227
|
+
function ordinalAllocator(): (rawId: string) => number {
|
|
228
|
+
const ordinals = new Map<string, number>();
|
|
229
|
+
return rawId => {
|
|
230
|
+
const existing = ordinals.get(rawId);
|
|
231
|
+
if (existing !== undefined) return existing;
|
|
232
|
+
const next = ordinals.size + 1;
|
|
233
|
+
ordinals.set(rawId, next);
|
|
234
|
+
return next;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function countToolDeclarations(tools: unknown): number {
|
|
239
|
+
if (!Array.isArray(tools)) return 0;
|
|
240
|
+
let total = 0;
|
|
241
|
+
for (const tool of tools) {
|
|
242
|
+
if (!isRecord(tool) || !Array.isArray(tool.functionDeclarations)) continue;
|
|
243
|
+
total += tool.functionDeclarations.length;
|
|
244
|
+
}
|
|
245
|
+
return total;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface TurnFacts {
|
|
249
|
+
role: GoogleWireTurnRole;
|
|
250
|
+
hasFunctionCall: boolean;
|
|
251
|
+
hasFunctionResponse: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The first ordering class this turn contradicts, judged against the turn before it.
|
|
256
|
+
*
|
|
257
|
+
* "Function response turn" is read as a user turn carrying at least one functionResponse, which
|
|
258
|
+
* is the shape messagesToGeminiFormat emits for a tool-result batch. A plain user turn satisfies
|
|
259
|
+
* the rule on its own, so a call turn after either one is unflagged.
|
|
260
|
+
*/
|
|
261
|
+
function violationFor(turn: TurnFacts, previous: TurnFacts | undefined, isLast: boolean): GoogleWireOrderingViolation | undefined {
|
|
262
|
+
if (turn.role === "model" && turn.hasFunctionCall) {
|
|
263
|
+
if (!previous) return "call-turn-opens-request";
|
|
264
|
+
if (previous.role === "model") return "call-turn-after-model-turn";
|
|
265
|
+
if (previous.role !== "user") return "call-turn-after-unknown-turn";
|
|
266
|
+
}
|
|
267
|
+
if (turn.role === "user" && turn.hasFunctionResponse) {
|
|
268
|
+
if (!previous || previous.role !== "model" || !previous.hasFunctionCall) {
|
|
269
|
+
return "response-turn-without-call-turn";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (isLast && turn.role === "model") return "request-ends-with-model-turn";
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function serializedBytes(summary: GoogleWireShapeSummary): number {
|
|
277
|
+
return new TextEncoder().encode(JSON.stringify(summary)).length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Drop per-turn detail from the tail until the summary serializes under its byte budget.
|
|
282
|
+
*
|
|
283
|
+
* Trimming from the tail rather than the head is deliberate: the opening turns of a request are
|
|
284
|
+
* where a first-send ordering violation lives, and they are what a reader needs most.
|
|
285
|
+
*/
|
|
286
|
+
function fitToSerializedBudget(summary: GoogleWireShapeSummary): GoogleWireShapeSummary {
|
|
287
|
+
if (serializedBytes(summary) <= GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES) return summary;
|
|
288
|
+
let kept = summary.turnShapes.length;
|
|
289
|
+
let fitted: GoogleWireShapeSummary = { ...summary, truncated: true, turnShapes: summary.turnShapes };
|
|
290
|
+
while (kept > 0 && serializedBytes(fitted) > GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES) {
|
|
291
|
+
kept -= 1;
|
|
292
|
+
fitted = { ...summary, truncated: true, turnShapes: summary.turnShapes.slice(0, kept) };
|
|
293
|
+
}
|
|
294
|
+
return fitted;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Project the structure of a compiled Google wire request body.
|
|
299
|
+
*
|
|
300
|
+
* Pure and non-mutating: every field is read, nothing is written back, and the returned object
|
|
301
|
+
* shares no reference with the input.
|
|
302
|
+
*/
|
|
303
|
+
export function summarizeGoogleWireShape(
|
|
304
|
+
body: unknown,
|
|
305
|
+
facts: GoogleWireShapeFacts = {},
|
|
306
|
+
): GoogleWireShapeSummary {
|
|
307
|
+
const root = isRecord(body) ? body : {};
|
|
308
|
+
const contents: unknown[] = Array.isArray(root.contents) ? root.contents : [];
|
|
309
|
+
const ordinalFor = ordinalAllocator();
|
|
310
|
+
const historySignedCalls = Math.max(0, facts.historySignedCalls ?? 0);
|
|
311
|
+
|
|
312
|
+
const roles: Record<GoogleWireTurnRole, number> = { user: 0, model: 0, other: 0 };
|
|
313
|
+
const callOrdinals = new Set<number>();
|
|
314
|
+
const responseOrdinals = new Set<number>();
|
|
315
|
+
const turnShapes: GoogleWireTurnShape[] = [];
|
|
316
|
+
|
|
317
|
+
let functionCalls = 0;
|
|
318
|
+
let functionResponses = 0;
|
|
319
|
+
let callsWithoutId = 0;
|
|
320
|
+
let responsesWithoutId = 0;
|
|
321
|
+
let signedCalls = 0;
|
|
322
|
+
let sentinelCalls = 0;
|
|
323
|
+
let orderingViolations = 0;
|
|
324
|
+
let truncated = false;
|
|
325
|
+
let firstOrderingViolation: GoogleWireShapeSummary["firstOrderingViolation"] = null;
|
|
326
|
+
let previous: TurnFacts | undefined;
|
|
327
|
+
|
|
328
|
+
for (let index = 0; index < contents.length; index++) {
|
|
329
|
+
const rawContent: unknown = contents[index];
|
|
330
|
+
const content = isRecord(rawContent) ? rawContent : {};
|
|
331
|
+
const role = turnRole(content);
|
|
332
|
+
roles[role] += 1;
|
|
333
|
+
const parts: unknown[] = Array.isArray(content.parts) ? content.parts : [];
|
|
334
|
+
const kinds: GoogleWirePartKind[] = [];
|
|
335
|
+
const calls: number[] = [];
|
|
336
|
+
const responses: number[] = [];
|
|
337
|
+
let turnSignedCalls = 0;
|
|
338
|
+
let turnSentinelCalls = 0;
|
|
339
|
+
|
|
340
|
+
for (const rawPart of parts) {
|
|
341
|
+
const part = isRecord(rawPart) ? rawPart : {};
|
|
342
|
+
const kind = partKind(part);
|
|
343
|
+
if (!kinds.includes(kind)) kinds.push(kind);
|
|
344
|
+
if (kind === "functionCall") {
|
|
345
|
+
functionCalls += 1;
|
|
346
|
+
const id = callIdOf(part.functionCall as Record<string, unknown>);
|
|
347
|
+
if (id === undefined) {
|
|
348
|
+
callsWithoutId += 1;
|
|
349
|
+
} else {
|
|
350
|
+
const ordinal = ordinalFor(id);
|
|
351
|
+
callOrdinals.add(ordinal);
|
|
352
|
+
if (calls.length < GOOGLE_WIRE_SHAPE_ORDINAL_CEILING) calls.push(ordinal);
|
|
353
|
+
else truncated = true;
|
|
354
|
+
}
|
|
355
|
+
const signature = signatureOf(part);
|
|
356
|
+
if (signature !== undefined) {
|
|
357
|
+
turnSignedCalls += 1;
|
|
358
|
+
if (signature === THOUGHT_SIGNATURE_BYPASS) turnSentinelCalls += 1;
|
|
359
|
+
}
|
|
360
|
+
} else if (kind === "functionResponse") {
|
|
361
|
+
functionResponses += 1;
|
|
362
|
+
const id = callIdOf(part.functionResponse as Record<string, unknown>);
|
|
363
|
+
if (id === undefined) {
|
|
364
|
+
responsesWithoutId += 1;
|
|
365
|
+
} else {
|
|
366
|
+
const ordinal = ordinalFor(id);
|
|
367
|
+
responseOrdinals.add(ordinal);
|
|
368
|
+
if (responses.length < GOOGLE_WIRE_SHAPE_ORDINAL_CEILING) responses.push(ordinal);
|
|
369
|
+
else truncated = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
signedCalls += turnSignedCalls;
|
|
375
|
+
sentinelCalls += turnSentinelCalls;
|
|
376
|
+
const current: TurnFacts = {
|
|
377
|
+
role,
|
|
378
|
+
hasFunctionCall: kinds.includes("functionCall"),
|
|
379
|
+
hasFunctionResponse: kinds.includes("functionResponse"),
|
|
380
|
+
};
|
|
381
|
+
const violation = violationFor(current, previous, index === contents.length - 1);
|
|
382
|
+
if (violation) {
|
|
383
|
+
orderingViolations += 1;
|
|
384
|
+
if (!firstOrderingViolation) firstOrderingViolation = { index, kind: violation };
|
|
385
|
+
}
|
|
386
|
+
if (turnShapes.length < GOOGLE_WIRE_SHAPE_TURN_CEILING) {
|
|
387
|
+
turnShapes.push({
|
|
388
|
+
index,
|
|
389
|
+
role,
|
|
390
|
+
parts: parts.length,
|
|
391
|
+
kinds,
|
|
392
|
+
calls,
|
|
393
|
+
responses,
|
|
394
|
+
signedCalls: turnSignedCalls,
|
|
395
|
+
sentinelCalls: turnSentinelCalls,
|
|
396
|
+
...(violation ? { violation } : {}),
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
truncated = true;
|
|
400
|
+
}
|
|
401
|
+
previous = current;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let unansweredCalls = 0;
|
|
405
|
+
for (const ordinal of callOrdinals) if (!responseOrdinals.has(ordinal)) unansweredCalls += 1;
|
|
406
|
+
let unmatchedResponses = 0;
|
|
407
|
+
for (const ordinal of responseOrdinals) if (!callOrdinals.has(ordinal)) unmatchedResponses += 1;
|
|
408
|
+
|
|
409
|
+
return fitToSerializedBudget({
|
|
410
|
+
version: 1,
|
|
411
|
+
truncated,
|
|
412
|
+
turns: contents.length,
|
|
413
|
+
roles,
|
|
414
|
+
functionCalls,
|
|
415
|
+
functionResponses,
|
|
416
|
+
distinctCallIds: new Set([...callOrdinals, ...responseOrdinals]).size,
|
|
417
|
+
callsWithoutId,
|
|
418
|
+
responsesWithoutId,
|
|
419
|
+
unansweredCalls,
|
|
420
|
+
unmatchedResponses,
|
|
421
|
+
toolDeclarations: countToolDeclarations(root.tools),
|
|
422
|
+
hasSystemInstruction: isRecord(root.systemInstruction),
|
|
423
|
+
hasSessionId: typeof root.sessionId === "string" && root.sessionId.length > 0,
|
|
424
|
+
sessionAnchor: facts.sessionAnchor ?? "unknown",
|
|
425
|
+
signature: {
|
|
426
|
+
present: signedCalls > 0,
|
|
427
|
+
sentinelOnly: signedCalls > 0 && signedCalls === sentinelCalls,
|
|
428
|
+
signedCalls,
|
|
429
|
+
sentinelCalls,
|
|
430
|
+
historySignedCalls,
|
|
431
|
+
sessionCacheSignedCalls: Math.max(0, signedCalls - sentinelCalls - historySignedCalls),
|
|
432
|
+
replayScopeBound: facts.replayScopeBound === true,
|
|
433
|
+
},
|
|
434
|
+
orderingViolations,
|
|
435
|
+
firstOrderingViolation,
|
|
436
|
+
send: facts.sendOrdinal === undefined
|
|
437
|
+
? null
|
|
438
|
+
: { ordinal: facts.sendOrdinal, errorClass: facts.errorClass ?? null },
|
|
439
|
+
turnShapes,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Classify an upstream rejection into one of a fixed set of classes.
|
|
445
|
+
*
|
|
446
|
+
* Bounded in both directions: at most UPSTREAM_ERROR_SCAN_LIMIT characters are inspected, and
|
|
447
|
+
* only the class is returned, so no upstream text reaches a caller through this function.
|
|
448
|
+
*/
|
|
449
|
+
export function classifyGoogleWireUpstreamError(message: unknown): GoogleWireUpstreamErrorClass {
|
|
450
|
+
if (typeof message !== "string" || message.length === 0) return "other";
|
|
451
|
+
const scanned = message.slice(0, UPSTREAM_ERROR_SCAN_LIMIT);
|
|
452
|
+
if (/function\s+call\s+turn|function\s+response\s+turn|model\s+turn\s+are\s+not\s+supported|assistant\s+message\s+prefill/i.test(scanned)) {
|
|
453
|
+
return "turn-adjacency";
|
|
454
|
+
}
|
|
455
|
+
if (/thought[_\s-]?signature|TYPE_BYTES/i.test(scanned)) return "thought-signature";
|
|
456
|
+
// Shared with the wire compiler's repair path so a payload cannot be repaired as one class
|
|
457
|
+
// and described as another.
|
|
458
|
+
if (isGoogleToolSchemaErrorText(scanned)) return "tool-schema";
|
|
459
|
+
if (isGoogleThinkingConfigErrorText(scanned)) return "thinking-config";
|
|
460
|
+
return "other";
|
|
461
|
+
}
|