@bitkyc08/opencodex 2.7.34 → 2.7.35

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.
@@ -1,98 +1,173 @@
1
1
  type Schema = Record<string, unknown>;
2
2
 
3
- // Gemini / Antigravity (CCA) accept only an OpenAPI-3.0 subset for function `parameters`. Codex
4
- // emits full JSON-Schema (draft 2020-12) tool definitions, so passing them through verbatim makes
5
- // CCA reject the whole request with "Request contains an invalid argument" / "Unknown name ...".
6
- // Every keyword below was confirmed live against the Antigravity backend to trigger a 400.
7
- // `encrypted` is Codex's Responses-only marker (openai/codex 5f4d06ef, PR #26210) stamped on v2
8
- // collaboration tool schemas (spawn_agent/send_message/followup_task `message`); CCA rejects it
9
- // with 400 "Unknown name \"encrypted\"" (issue #85). It is an annotation for the ChatGPT backend
10
- // only, so dropping it never changes tool behavior.
11
- const DROPPED_SCHEMA_KEYS = new Set([
12
- "$schema", "$id", "$comment", "$ref", "$defs", "definitions",
13
- "examples", "patternProperties", "if", "then", "else",
14
- "uniqueItems", "additionalItems", "unevaluatedProperties", "unevaluatedItems",
15
- "dependentRequired", "dependentSchemas", "propertyNames", "contains",
16
- "encrypted",
17
- ]);
18
-
19
- const MAX_DEREF_DEPTH = 64;
3
+ // Google documents this function-schema subset: type, nullable, required, format, description,
4
+ // properties, items, enum, anyOf, $ref, and $defs. We inline local refs and normalize anyOf, so
5
+ // only the eight scalar/container keywords below are ever emitted. Building from an allowlist
6
+ // prevents new MCP/JSON-Schema annotations from turning into provider-wide 400 responses.
7
+ const ALLOWED_TYPES = new Set(["string", "integer", "number", "boolean", "array", "object"]);
8
+ const MAX_SCHEMA_DEPTH = 24; // Google's documented nesting limit is 32; leave headroom for CCA.
9
+ const MAX_DEREF_DEPTH = 16;
10
+
11
+ function isRecord(value: unknown): value is Schema {
12
+ return !!value && typeof value === "object" && !Array.isArray(value);
13
+ }
20
14
 
21
15
  function resolveRef(ref: string, defs: Map<string, unknown>): unknown {
22
- // Only local pointers into the schema's own $defs/definitions are supported (e.g.
23
- // "#/$defs/Foo"). Anything else cannot be inlined, so it collapses to an unconstrained object.
16
+ // Only local pointers into the schema's own $defs/definitions are safe to inline.
24
17
  const match = /^#\/(?:\$defs|definitions)\/(.+)$/.exec(ref);
25
18
  if (!match) return undefined;
26
- return defs.get(decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")));
19
+ try {
20
+ return defs.get(decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")));
21
+ } catch {
22
+ return undefined;
23
+ }
27
24
  }
28
25
 
29
26
  function collectDefs(root: unknown, defs: Map<string, unknown>): void {
30
- if (!root || typeof root !== "object") return;
27
+ if (!isRecord(root)) return;
31
28
  for (const bag of ["$defs", "definitions"] as const) {
32
- const group = (root as Schema)[bag];
33
- if (group && typeof group === "object" && !Array.isArray(group)) {
34
- for (const [name, value] of Object.entries(group as Schema)) {
35
- if (!defs.has(name)) defs.set(name, value);
36
- }
29
+ const group = root[bag];
30
+ if (!isRecord(group)) continue;
31
+ for (const [name, value] of Object.entries(group)) {
32
+ if (!defs.has(name)) defs.set(name, value);
37
33
  }
38
34
  }
39
35
  }
40
36
 
41
- function normalizeType(value: unknown, out: Schema): void {
42
- // JSON-Schema allows `type` to be an array (e.g. ["string","null"]); OpenAPI 3.0 does not.
43
- // Collapse to the first non-null type and mark the field nullable when "null" was present.
44
- if (!Array.isArray(value)) {
45
- out.type = value;
46
- return;
37
+ function normalizeType(value: unknown, out: Schema, preserveNullType: boolean): void {
38
+ const candidates = Array.isArray(value) ? value : [value];
39
+ let sawNull = false;
40
+
41
+ for (const candidate of candidates) {
42
+ if (typeof candidate !== "string") continue;
43
+ const type = candidate.toLowerCase();
44
+ if (type === "null") {
45
+ sawNull = true;
46
+ } else if (out.type === undefined && ALLOWED_TYPES.has(type)) {
47
+ out.type = type;
48
+ }
49
+ }
50
+
51
+ if (!sawNull) return;
52
+ if (out.type !== undefined) out.nullable = true;
53
+ else if (preserveNullType) out.type = "null";
54
+ else out.nullable = true;
55
+ }
56
+
57
+ function sanitizeEnum(value: unknown): string[] | undefined {
58
+ if (!Array.isArray(value)) return undefined;
59
+ const values = [...new Set(value.filter((item): item is string => typeof item === "string"))];
60
+ return values.length > 0 ? values : undefined;
61
+ }
62
+
63
+ function normalizeAnyOf(
64
+ value: unknown,
65
+ defs: Map<string, unknown>,
66
+ depth: number,
67
+ refDepth: number,
68
+ ): Schema {
69
+ if (!Array.isArray(value) || value.length === 0) return {};
70
+ const schemas = value.map(item => sanitizeSchema(item, defs, depth + 1, refDepth, true));
71
+
72
+ const nonNullSchemas = schemas.filter(schema => schema.type !== "null");
73
+ const nullSchemas = schemas.filter(schema => schema.type === "null");
74
+ if (
75
+ nonNullSchemas.length === 1
76
+ && nullSchemas.length > 0
77
+ && nullSchemas.every(schema => Object.keys(schema).every(key => key === "type"))
78
+ ) {
79
+ return { ...nonNullSchemas[0], nullable: true };
80
+ }
81
+
82
+ const type = schemas[0]?.type;
83
+ const sameType = schemas.length > 0 && schemas.every(schema => schema.type === type);
84
+ const enumOnly = schemas.every(schema => {
85
+ const allowedKeys = type === undefined ? new Set(["enum"]) : new Set(["type", "enum"]);
86
+ return Array.isArray(schema.enum) && Object.keys(schema).every(key => allowedKeys.has(key));
87
+ });
88
+ if (sameType && enumOnly && type !== "null") {
89
+ const values = sanitizeEnum(schemas.flatMap(schema => schema.enum as unknown[]));
90
+ if (values) return { ...(typeof type === "string" ? { type } : {}), enum: values };
91
+ }
92
+
93
+ // CCA's Claude bridge turns typed anyOf branches into an invalid input_schema. Widen only this
94
+ // node when a union cannot be collapsed losslessly; parent annotations and structure survive.
95
+ return {};
96
+ }
97
+
98
+ function sanitizeProperties(
99
+ value: unknown,
100
+ defs: Map<string, unknown>,
101
+ depth: number,
102
+ refDepth: number,
103
+ ): Record<string, Schema> | undefined {
104
+ if (!isRecord(value)) return undefined;
105
+ const properties: Record<string, Schema> = Object.create(null) as Record<string, Schema>;
106
+ for (const [name, schema] of Object.entries(value)) {
107
+ // Property names form a name bag and must never be interpreted as schema keywords.
108
+ properties[name] = sanitizeSchema(schema, defs, depth + 1, refDepth, false);
47
109
  }
48
- const nonNull = value.filter(t => t !== "null");
49
- if (value.includes("null")) out.nullable = true;
50
- if (nonNull.length > 0) out.type = nonNull[0];
110
+ return properties;
51
111
  }
52
112
 
53
- function sanitize(node: unknown, defs: Map<string, unknown>, depth: number): unknown {
54
- if (Array.isArray(node)) return node.map(item => sanitize(item, defs, depth));
55
- if (!node || typeof node !== "object") return node;
56
- const input = node as Schema;
113
+ function sanitizeSchema(
114
+ node: unknown,
115
+ defs: Map<string, unknown>,
116
+ depth: number,
117
+ refDepth: number,
118
+ preserveNullType: boolean,
119
+ ): Schema {
120
+ if (depth >= MAX_SCHEMA_DEPTH || !isRecord(node)) return {};
57
121
 
58
- if (typeof input.$ref === "string" && depth < MAX_DEREF_DEPTH) {
59
- const target = resolveRef(input.$ref, defs);
60
- if (target && typeof target === "object") {
61
- const merged: Schema = { ...(target as Schema) };
62
- for (const [key, value] of Object.entries(input)) {
122
+ if (typeof node.$ref === "string" && refDepth < MAX_DEREF_DEPTH) {
123
+ const target = resolveRef(node.$ref, defs);
124
+ if (isRecord(target)) {
125
+ const merged: Schema = { ...target };
126
+ for (const [key, value] of Object.entries(node)) {
63
127
  if (key !== "$ref") merged[key] = value;
64
128
  }
65
- return sanitize(merged, defs, depth + 1);
129
+ return sanitizeSchema(merged, defs, depth, refDepth + 1, preserveNullType);
66
130
  }
67
131
  }
68
132
 
69
133
  const out: Schema = {};
70
- for (const [key, value] of Object.entries(input)) {
71
- if (DROPPED_SCHEMA_KEYS.has(key)) continue;
72
- if (key === "type") { normalizeType(value, out); continue; }
73
- if (key === "const") { out.enum = [value]; continue; }
74
- if (key === "exclusiveMinimum" && typeof value === "number") { out.minimum = value; continue; }
75
- if (key === "exclusiveMaximum" && typeof value === "number") { out.maximum = value; continue; }
76
- if (key === "required" && Array.isArray(value)) {
77
- out.required = [...new Set(value.filter((item): item is string => typeof item === "string"))];
78
- continue;
79
- }
80
- if (key === "additionalProperties") {
81
- // A boolean additionalProperties is accepted, but a nested schema is only meaningful with
82
- // its own sanitize pass.
83
- out.additionalProperties = typeof value === "boolean" ? value : sanitize(value, defs, depth);
84
- continue;
85
- }
86
- out[key] = sanitize(value, defs, depth);
134
+ normalizeType(node.type, out, preserveNullType);
135
+
136
+ if (typeof node.nullable === "boolean") out.nullable = node.nullable;
137
+ if (typeof node.description === "string") out.description = node.description;
138
+ if (typeof node.format === "string") out.format = node.format;
139
+
140
+ const enumValues = sanitizeEnum(node.enum ?? (typeof node.const === "string" ? [node.const] : undefined));
141
+ if (enumValues) out.enum = enumValues;
142
+
143
+ const properties = sanitizeProperties(node.properties, defs, depth, refDepth);
144
+ if (properties) out.properties = properties;
145
+
146
+ if (isRecord(node.items)) {
147
+ out.items = sanitizeSchema(node.items, defs, depth + 1, refDepth, false);
148
+ }
149
+
150
+ if (Array.isArray(node.required)) {
151
+ out.required = [...new Set(node.required.filter((item): item is string => typeof item === "string"))];
87
152
  }
153
+
154
+ if (node.anyOf !== undefined) Object.assign(out, normalizeAnyOf(node.anyOf, defs, depth, refDepth));
88
155
  return out;
89
156
  }
90
157
 
91
158
  export function sanitizeGeminiToolParameters(parameters: unknown): Record<string, unknown> {
92
- const defs = new Map<string, unknown>();
93
- collectDefs(parameters, defs);
94
- const result = sanitize(parameters, defs, 0);
95
- return result && typeof result === "object" && !Array.isArray(result)
96
- ? result as Record<string, unknown>
97
- : { type: "object" };
159
+ try {
160
+ const defs = new Map<string, unknown>();
161
+ collectDefs(parameters, defs);
162
+ const root = sanitizeSchema(parameters, defs, 0, 0, false);
163
+
164
+ // Function arguments are always an object. Claude additionally rejects root composition and a
165
+ // missing root type even when those forms are valid general-purpose JSON Schema.
166
+ root.type = "object";
167
+ if (!isRecord(root.properties)) root.properties = {};
168
+ return root;
169
+ } catch {
170
+ // Last-resort containment: no third-party schema may break every tool in the request.
171
+ return { type: "object", properties: {} };
172
+ }
98
173
  }
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { sanitizeGeminiToolParameters } from "./google-tool-schema";
3
+
4
+ type JsonObject = Record<string, unknown>;
5
+
6
+ const GOOGLE_TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
7
+ const GOOGLE_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
8
+
9
+ function isObject(value: unknown): value is JsonObject {
10
+ return !!value && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ function toolNameCodec(names: readonly string[]): {
14
+ toWire: (name: string) => string;
15
+ fromWire: (name: string) => string;
16
+ } {
17
+ const toWire = new Map<string, string>();
18
+ const fromWire = new Map<string, string>();
19
+ const used = new Set<string>();
20
+
21
+ for (const name of names) {
22
+ if (toWire.has(name)) continue;
23
+ if (GOOGLE_TOOL_NAME.test(name) && !used.has(name)) {
24
+ toWire.set(name, name);
25
+ fromWire.set(name, name);
26
+ used.add(name);
27
+ continue;
28
+ }
29
+
30
+ let cleaned = name.replace(/[^A-Za-z0-9_-]/g, "_");
31
+ if (!/^[A-Za-z_]/.test(cleaned)) cleaned = `_${cleaned}`;
32
+ const prefix = (cleaned || "tool").slice(0, 55);
33
+ for (let salt = 0; ; salt++) {
34
+ const hashInput = salt === 0 ? name : `${name}#${salt}`;
35
+ const suffix = createHash("sha256").update(hashInput).digest("hex").slice(0, 8);
36
+ const candidate = `${prefix}_${suffix}`;
37
+ if (used.has(candidate)) continue;
38
+ toWire.set(name, candidate);
39
+ fromWire.set(candidate, name);
40
+ used.add(candidate);
41
+ break;
42
+ }
43
+ }
44
+
45
+ return {
46
+ toWire: name => toWire.get(name) ?? name,
47
+ fromWire: name => fromWire.get(name) ?? name,
48
+ };
49
+ }
50
+
51
+ function collectToolNames(body: JsonObject): string[] {
52
+ const names: string[] = [];
53
+ if (Array.isArray(body.tools)) {
54
+ for (const rawTool of body.tools) {
55
+ if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) continue;
56
+ for (const rawDeclaration of rawTool.functionDeclarations) {
57
+ if (isObject(rawDeclaration) && typeof rawDeclaration.name === "string") names.push(rawDeclaration.name);
58
+ }
59
+ }
60
+ }
61
+ if (Array.isArray(body.contents)) {
62
+ for (const rawContent of body.contents) {
63
+ if (!isObject(rawContent) || !Array.isArray(rawContent.parts)) continue;
64
+ for (const rawPart of rawContent.parts) {
65
+ if (!isObject(rawPart)) continue;
66
+ for (const key of ["functionCall", "functionResponse"]) {
67
+ const call = rawPart[key];
68
+ if (isObject(call) && typeof call.name === "string") names.push(call.name);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ return names;
74
+ }
75
+
76
+ function compileContents(value: unknown, toWireName: (name: string) => string): unknown[] | undefined {
77
+ if (!Array.isArray(value)) return undefined;
78
+ return value.map(rawContent => {
79
+ if (!isObject(rawContent)) return {};
80
+ const content = { ...rawContent };
81
+ if (!Array.isArray(rawContent.parts)) return content;
82
+ content.parts = rawContent.parts.map(rawPart => {
83
+ if (!isObject(rawPart)) return {};
84
+ const part = { ...rawPart };
85
+ for (const key of ["functionCall", "functionResponse"]) {
86
+ const call = rawPart[key];
87
+ if (isObject(call) && typeof call.name === "string") {
88
+ part[key] = { ...call, name: toWireName(call.name) };
89
+ }
90
+ }
91
+ return part;
92
+ });
93
+ return content;
94
+ });
95
+ }
96
+
97
+ function compileTools(value: unknown, toWireName: (name: string) => string): unknown[] | undefined {
98
+ if (!Array.isArray(value)) return undefined;
99
+ const tools = value.flatMap(rawTool => {
100
+ if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return [];
101
+ const functionDeclarations = rawTool.functionDeclarations.flatMap(rawDeclaration => {
102
+ if (!isObject(rawDeclaration) || typeof rawDeclaration.name !== "string") return [];
103
+ return [{
104
+ name: toWireName(rawDeclaration.name),
105
+ ...(typeof rawDeclaration.description === "string" ? { description: rawDeclaration.description } : {}),
106
+ parameters: sanitizeGeminiToolParameters(rawDeclaration.parameters),
107
+ }];
108
+ });
109
+ return functionDeclarations.length > 0 ? [{ functionDeclarations }] : [];
110
+ });
111
+ return tools.length > 0 ? tools : undefined;
112
+ }
113
+
114
+ function finiteNumber(value: unknown): number | undefined {
115
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
116
+ }
117
+
118
+ function compileGenerationConfig(value: unknown): JsonObject | undefined {
119
+ if (!isObject(value)) return undefined;
120
+ const out: JsonObject = {};
121
+ const maxOutputTokens = finiteNumber(value.maxOutputTokens);
122
+ if (maxOutputTokens !== undefined && maxOutputTokens > 0) out.maxOutputTokens = Math.floor(maxOutputTokens);
123
+ const temperature = finiteNumber(value.temperature);
124
+ if (temperature !== undefined && temperature >= 0) out.temperature = Math.min(2, temperature);
125
+ const topP = finiteNumber(value.topP);
126
+ if (topP !== undefined && topP >= 0) out.topP = Math.min(1, topP);
127
+ if (Array.isArray(value.stopSequences)) {
128
+ const stopSequences = [...new Set(value.stopSequences.filter(
129
+ (item): item is string => typeof item === "string" && item.length > 0,
130
+ ))].slice(0, 5);
131
+ if (stopSequences.length > 0) out.stopSequences = stopSequences;
132
+ }
133
+ if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") {
134
+ const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
135
+ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
136
+ ? raw
137
+ : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
138
+ if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
139
+ }
140
+ return Object.keys(out).length > 0 ? out : undefined;
141
+ }
142
+
143
+ function compileToolConfig(value: unknown, toWireName: (name: string) => string): JsonObject | undefined {
144
+ if (!isObject(value) || !isObject(value.functionCallingConfig)) return undefined;
145
+ const raw = value.functionCallingConfig;
146
+ const out: JsonObject = {};
147
+ if (typeof raw.mode === "string" && ["AUTO", "ANY", "NONE", "VALIDATED"].includes(raw.mode.toUpperCase())) {
148
+ out.mode = raw.mode.toUpperCase();
149
+ }
150
+ if (Array.isArray(raw.allowedFunctionNames)) {
151
+ const names = raw.allowedFunctionNames
152
+ .filter((name): name is string => typeof name === "string")
153
+ .map(toWireName);
154
+ if (names.length > 0) out.allowedFunctionNames = names;
155
+ }
156
+ return Object.keys(out).length > 0 ? { functionCallingConfig: out } : undefined;
157
+ }
158
+
159
+ /**
160
+ * Final trust boundary for every Google-family request. The adapter may build a convenient
161
+ * Gemini-shaped object; only this compiler is allowed to decide what reaches the wire.
162
+ */
163
+ export function compileGoogleWireBody(input: unknown): {
164
+ body: JsonObject;
165
+ restoreToolName: (name: string) => string;
166
+ } {
167
+ const source = isObject(input) ? input : {};
168
+ const names = toolNameCodec(collectToolNames(source));
169
+ const body: JsonObject = {};
170
+ const contents = compileContents(source.contents, names.toWire);
171
+ if (contents) body.contents = contents;
172
+ if (isObject(source.systemInstruction)) body.systemInstruction = source.systemInstruction;
173
+ const tools = compileTools(source.tools, names.toWire);
174
+ if (tools) body.tools = tools;
175
+ const generationConfig = compileGenerationConfig(source.generationConfig);
176
+ if (generationConfig) body.generationConfig = generationConfig;
177
+ const toolConfig = compileToolConfig(source.toolConfig, names.toWire);
178
+ if (toolConfig) body.toolConfig = toolConfig;
179
+ if (typeof source.sessionId === "string" && source.sessionId.length > 0) body.sessionId = source.sessionId;
180
+ return { body, restoreToolName: names.fromWire };
181
+ }
182
+
183
+ function functionDeclarations(root: JsonObject): JsonObject[] {
184
+ if (!Array.isArray(root.tools)) return [];
185
+ return root.tools.flatMap(rawTool => {
186
+ if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return [];
187
+ return rawTool.functionDeclarations.filter(isObject);
188
+ });
189
+ }
190
+
191
+ /** Build a changed request for one known-safe replay of an INVALID_ARGUMENT response. */
192
+ export function repairGoogleInvalidRequestBody(body: string, errorPayload: string): string | undefined {
193
+ const schemaError = /(?:input[_ ]schema|json schema|function[_ ]declarations?|x-mcp-header)/i.test(errorPayload);
194
+ const thinkingError = /thinking[_ ]?(?:config|level)/i.test(errorPayload);
195
+ if (!schemaError && !thinkingError) return undefined;
196
+ let parsed: unknown;
197
+ try {
198
+ parsed = JSON.parse(body) as unknown;
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ if (!isObject(parsed)) return undefined;
203
+ const root = isObject(parsed.request) ? parsed.request : parsed;
204
+ let changed = false;
205
+
206
+ if (thinkingError && isObject(root.generationConfig) && "thinkingConfig" in root.generationConfig) {
207
+ delete root.generationConfig.thinkingConfig;
208
+ if (Object.keys(root.generationConfig).length === 0) delete root.generationConfig;
209
+ changed = true;
210
+ }
211
+
212
+ if (schemaError) {
213
+ const declarations = functionDeclarations(root);
214
+ if (declarations.length > 0) {
215
+ const indexed = /tools(?:\.|\[)(\d+)(?:\])?\.custom\.input_schema/i.exec(errorPayload)?.[1]
216
+ ?? /function[_]?declarations(?:\.|\[)(\d+)/i.exec(errorPayload)?.[1];
217
+ const index = indexed === undefined ? -1 : Number.parseInt(indexed, 10);
218
+ const rejected = declarations[index];
219
+ const targets = rejected ? [rejected] : declarations;
220
+ for (const declaration of targets) {
221
+ declaration.parameters = { type: "object", properties: {} };
222
+ delete declaration.parametersJsonSchema;
223
+ }
224
+ changed = true;
225
+ }
226
+ }
227
+ return changed ? JSON.stringify(parsed) : undefined;
228
+ }
@@ -18,7 +18,7 @@ import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
18
18
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
19
19
  import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
20
20
  import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
21
- import { sanitizeGeminiToolParameters } from "./google-tool-schema";
21
+ import { compileGoogleWireBody } from "./google-wire-compiler";
22
22
  import { neutralizeIdentity } from "./identity";
23
23
  import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
24
24
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
@@ -179,7 +179,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
179
179
  functionDeclarations: tools.map(t => ({
180
180
  name: namespacedToolName(t.namespace, t.name),
181
181
  description: t.description,
182
- parameters: sanitizeGeminiToolParameters(t.parameters),
182
+ parameters: t.parameters,
183
183
  })),
184
184
  }];
185
185
  }
@@ -199,6 +199,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
199
199
  // can stash the CCA model/session for parseStream's reasoning-replay observation.
200
200
  let antigravityModel: string | undefined;
201
201
  let antigravitySession: string | undefined;
202
+ let restoreGoogleToolName = (name: string): string => name;
202
203
  return {
203
204
  name: "google",
204
205
 
@@ -262,23 +263,27 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
262
263
  }
263
264
  // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
264
265
  // sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
265
- if (Array.isArray((body as { contents?: unknown[] }).contents)) {
266
- const contents = (body as { contents: unknown[] }).contents;
267
- if (antigravityUsesReplayCache(wireModelId)) {
268
- applyAntigravityReplay(wireModelId, sessionId, contents);
269
- } else {
270
- sanitizeAntigravityClaudeSignatures(contents);
271
- }
272
- }
273
266
  // The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase,
274
267
  // nested) — matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case
275
268
  // spelling is a non-first-party key, so we send the single canonical location.
276
- const request: Record<string, unknown> = { ...body, sessionId };
269
+ const draftRequest: Record<string, unknown> = { ...body, sessionId };
277
270
  // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it).
278
271
  if (/claude/i.test(wireModelId)) {
279
- const existing = (request.toolConfig ?? {}) as Record<string, unknown>;
272
+ const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>;
280
273
  const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>;
281
- request.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
274
+ draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } };
275
+ }
276
+ const compiled = compileGoogleWireBody(draftRequest);
277
+ const request = compiled.body;
278
+ restoreGoogleToolName = compiled.restoreToolName;
279
+ // Compile names before replay: signatures are keyed by the exact provider-visible name.
280
+ if (Array.isArray((request as { contents?: unknown[] }).contents)) {
281
+ const contents = (request as { contents: unknown[] }).contents;
282
+ if (antigravityUsesReplayCache(wireModelId)) {
283
+ applyAntigravityReplay(wireModelId, sessionId, contents);
284
+ } else {
285
+ sanitizeAntigravityClaudeSignatures(contents);
286
+ }
282
287
  }
283
288
  const envelope = {
284
289
  model: wireModelId,
@@ -297,12 +302,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
297
302
  }
298
303
 
299
304
  if (provider.googleMode === "vertex") {
305
+ const compiled = compileGoogleWireBody(body);
306
+ restoreGoogleToolName = compiled.restoreToolName;
300
307
  // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
301
308
  const apiKey = resolveVertexApiKey(provider.apiKey);
302
309
  if (apiKey) {
303
310
  const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
304
311
  headers["x-goog-api-key"] = apiKey;
305
- return { url, method: "POST", headers, body: JSON.stringify(body) };
312
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
306
313
  }
307
314
  const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
308
315
  if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
@@ -312,7 +319,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
312
319
  const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
313
320
  const token = await getVertexAccessToken();
314
321
  headers["Authorization"] = `Bearer ${token}`;
315
- return { url, method: "POST", headers, body: JSON.stringify(body) };
322
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
316
323
  }
317
324
 
318
325
  // ai-studio (default): Generative Language API + x-goog-api-key.
@@ -321,7 +328,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
321
328
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
322
329
  headers["x-goog-api-key"] = apiKey;
323
330
 
324
- return { url, method: "POST", headers, body: JSON.stringify(body) };
331
+ const compiled = compileGoogleWireBody(body);
332
+ restoreGoogleToolName = compiled.restoreToolName;
333
+ return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
325
334
  },
326
335
 
327
336
  async *parseStream(response: Response): AsyncGenerator<AdapterEvent> {
@@ -403,7 +412,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
403
412
  if (part.functionCall) {
404
413
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
405
414
  toolCallsStarted++;
406
- yield { type: "tool_call_start", id, name: part.functionCall.name };
415
+ yield { type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) };
407
416
  yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
408
417
  yield { type: "tool_call_end" };
409
418
  }
@@ -456,7 +465,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
456
465
  if (part.functionCall) {
457
466
  const id = `call_${crypto.randomUUID().slice(0, 8)}`;
458
467
  toolCallsStarted++;
459
- events.push({ type: "tool_call_start", id, name: part.functionCall.name });
468
+ events.push({ type: "tool_call_start", id, name: restoreGoogleToolName(part.functionCall.name) });
460
469
  events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
461
470
  events.push({ type: "tool_call_end" });
462
471
  }
@@ -846,6 +846,14 @@ async function* parseKiroAttempt(
846
846
  ...(contextWindow ? { configuredContextWindow: contextWindow } : {}),
847
847
  });
848
848
  }
849
+ debugProviderDiagnostic("kiro", "attempt_complete", {
850
+ mode,
851
+ sawText,
852
+ sawReasoning,
853
+ sawRealTool,
854
+ completionCalls,
855
+ assistantChars: assistantText.length,
856
+ });
849
857
 
850
858
  if (mode === "text_fallback") {
851
859
  if (completionAnswer !== undefined) {
@@ -909,7 +917,23 @@ async function* parseKiroAttempt(
909
917
  terminal: { type: "done", usage: finalUsage, endTurn: false, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
910
918
  };
911
919
  }
912
- if (mode === "required" && (sawText || sawReasoning)) {
920
+ // A clean Smithy EOF after ordinary assistant text is a complete answer even when the
921
+ // private completion tool was advertised. Replaying that answer through a second Kiro
922
+ // request adds latency and can hang an otherwise-finished Codex turn if the retry stalls.
923
+ // Reasoning-only output still needs the bounded fallback because it has no user-facing text.
924
+ if (mode === "required" && sawText) {
925
+ return {
926
+ assistantText,
927
+ sawReasoning,
928
+ terminal: {
929
+ type: "done",
930
+ usage: finalUsage,
931
+ endTurn: true,
932
+ ...(finalProviderState ? { providerState: finalProviderState } : {}),
933
+ },
934
+ };
935
+ }
936
+ if (mode === "required" && sawReasoning) {
913
937
  return { assistantText, sawReasoning, needsFallback: true, usage: finalUsage, providerState: finalProviderState };
914
938
  }
915
939
  if (!sawText && !sawReasoning) {