@bitkyc08/opencodex 2.6.7 → 2.6.8
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-CgCKvnPc.js +9 -0
- package/gui/dist/assets/{index-CSI5RHdZ.css → index-DIBiVVC0.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/azure.ts +2 -2
- package/src/adapters/base.ts +6 -1
- package/src/adapters/google-antigravity-replay.ts +136 -0
- package/src/adapters/google-antigravity-wire.ts +94 -0
- package/src/adapters/google-errors.ts +95 -0
- package/src/adapters/google-http.ts +109 -0
- package/src/adapters/google-tool-schema.ts +89 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google.ts +163 -20
- package/src/adapters/kiro-tools.ts +55 -2
- package/src/cli-status.ts +4 -0
- package/src/cli.ts +7 -0
- package/src/codex-history-provider.ts +195 -22
- package/src/codex-plugins-doctor.ts +242 -0
- package/src/config.ts +1 -0
- package/src/lib/gcp-adc.ts +292 -0
- package/src/oauth/google-antigravity.ts +225 -0
- package/src/oauth/index.ts +19 -1
- package/src/oauth/store.ts +1 -0
- package/src/oauth/types.ts +2 -0
- package/src/provider-context-cap.ts +30 -1
- package/src/providers/antigravity-models.ts +30 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/registry.ts +7 -0
- package/src/redact.ts +26 -0
- package/src/router.ts +6 -0
- package/src/server.ts +45 -9
- package/src/types.ts +12 -0
- package/src/usage-summary.ts +30 -3
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Ct8koBOe.js +0 -9
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type Schema = Record<string, unknown>;
|
|
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
|
+
const DROPPED_SCHEMA_KEYS = new Set([
|
|
8
|
+
"$schema", "$id", "$comment", "$ref", "$defs", "definitions",
|
|
9
|
+
"examples", "patternProperties", "if", "then", "else",
|
|
10
|
+
"uniqueItems", "additionalItems", "unevaluatedProperties", "unevaluatedItems",
|
|
11
|
+
"dependentRequired", "dependentSchemas", "propertyNames", "contains",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const MAX_DEREF_DEPTH = 64;
|
|
15
|
+
|
|
16
|
+
function resolveRef(ref: string, defs: Map<string, unknown>): unknown {
|
|
17
|
+
// Only local pointers into the schema's own $defs/definitions are supported (e.g.
|
|
18
|
+
// "#/$defs/Foo"). Anything else cannot be inlined, so it collapses to an unconstrained object.
|
|
19
|
+
const match = /^#\/(?:\$defs|definitions)\/(.+)$/.exec(ref);
|
|
20
|
+
if (!match) return undefined;
|
|
21
|
+
return defs.get(decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~")));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectDefs(root: unknown, defs: Map<string, unknown>): void {
|
|
25
|
+
if (!root || typeof root !== "object") return;
|
|
26
|
+
for (const bag of ["$defs", "definitions"] as const) {
|
|
27
|
+
const group = (root as Schema)[bag];
|
|
28
|
+
if (group && typeof group === "object" && !Array.isArray(group)) {
|
|
29
|
+
for (const [name, value] of Object.entries(group as Schema)) {
|
|
30
|
+
if (!defs.has(name)) defs.set(name, value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeType(value: unknown, out: Schema): void {
|
|
37
|
+
// JSON-Schema allows `type` to be an array (e.g. ["string","null"]); OpenAPI 3.0 does not.
|
|
38
|
+
// Collapse to the first non-null type and mark the field nullable when "null" was present.
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
out.type = value;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const nonNull = value.filter(t => t !== "null");
|
|
44
|
+
if (value.includes("null")) out.nullable = true;
|
|
45
|
+
if (nonNull.length > 0) out.type = nonNull[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitize(node: unknown, defs: Map<string, unknown>, depth: number): unknown {
|
|
49
|
+
if (Array.isArray(node)) return node.map(item => sanitize(item, defs, depth));
|
|
50
|
+
if (!node || typeof node !== "object") return node;
|
|
51
|
+
const input = node as Schema;
|
|
52
|
+
|
|
53
|
+
if (typeof input.$ref === "string" && depth < MAX_DEREF_DEPTH) {
|
|
54
|
+
const target = resolveRef(input.$ref, defs);
|
|
55
|
+
if (target && typeof target === "object") {
|
|
56
|
+
const merged: Schema = { ...(target as Schema) };
|
|
57
|
+
for (const [key, value] of Object.entries(input)) {
|
|
58
|
+
if (key !== "$ref") merged[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return sanitize(merged, defs, depth + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const out: Schema = {};
|
|
65
|
+
for (const [key, value] of Object.entries(input)) {
|
|
66
|
+
if (DROPPED_SCHEMA_KEYS.has(key)) continue;
|
|
67
|
+
if (key === "type") { normalizeType(value, out); continue; }
|
|
68
|
+
if (key === "const") { out.enum = [value]; continue; }
|
|
69
|
+
if (key === "exclusiveMinimum" && typeof value === "number") { out.minimum = value; continue; }
|
|
70
|
+
if (key === "exclusiveMaximum" && typeof value === "number") { out.maximum = value; continue; }
|
|
71
|
+
if (key === "additionalProperties") {
|
|
72
|
+
// A boolean additionalProperties is accepted, but a nested schema is only meaningful with
|
|
73
|
+
// its own sanitize pass.
|
|
74
|
+
out.additionalProperties = typeof value === "boolean" ? value : sanitize(value, defs, depth);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out[key] = sanitize(value, defs, depth);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sanitizeGeminiToolParameters(parameters: unknown): Record<string, unknown> {
|
|
83
|
+
const defs = new Map<string, unknown>();
|
|
84
|
+
collectDefs(parameters, defs);
|
|
85
|
+
const result = sanitize(parameters, defs, 0);
|
|
86
|
+
return result && typeof result === "object" && !Array.isArray(result)
|
|
87
|
+
? result as Record<string, unknown>
|
|
88
|
+
: { type: "object" };
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { redactSecretString } from "../redact";
|
|
2
|
+
|
|
3
|
+
/** Gemini/Vertex finishReason values that mean the turn was cut off, not cleanly stopped. */
|
|
4
|
+
const TRUNCATION_REASONS = new Set(["MAX_TOKENS", "MALFORMED_FUNCTION_CALL"]);
|
|
5
|
+
|
|
6
|
+
export function isVertexTruncationReason(finishReason: string | undefined): boolean {
|
|
7
|
+
return finishReason !== undefined && TRUNCATION_REASONS.has(finishReason);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function vertexTruncationErrorMessage(reason?: string): string {
|
|
11
|
+
const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
|
|
12
|
+
return `Vertex AI response truncated upstream before the turn completed${suffix}`;
|
|
13
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderAdapter } from "./base";
|
|
1
|
+
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
2
2
|
import { debugDroppedFrame } from "../debug";
|
|
3
3
|
import type {
|
|
4
4
|
AdapterEvent,
|
|
@@ -12,6 +12,31 @@ import type {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { isAllowedToolChoice, namespacedToolName, toolAllowedByChoice } from "../types";
|
|
14
14
|
import { contentPartsToText, parseDataUrl } from "./image";
|
|
15
|
+
import { getVertexAccessToken } from "../lib/gcp-adc";
|
|
16
|
+
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
|
|
17
|
+
import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google-truncation";
|
|
18
|
+
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
19
|
+
import { sanitizeGeminiToolParameters } from "./google-tool-schema";
|
|
20
|
+
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
21
|
+
|
|
22
|
+
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
23
|
+
// tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
|
|
24
|
+
// while still driving tools to completion. The FINAL answer is explicitly exempt so task output is
|
|
25
|
+
// not truncated. Appended to systemInstruction for the `google` adapter only, so non-Google
|
|
26
|
+
// providers are unaffected.
|
|
27
|
+
const GOOGLE_BREVITY_INSTRUCTION = [
|
|
28
|
+
"Output style for this session:",
|
|
29
|
+
"- While you are still working (between tool calls), keep any text you emit to a single short line; do not narrate at length.",
|
|
30
|
+
"- Do detailed reasoning internally, not as visible intermediate output.",
|
|
31
|
+
"- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
|
|
32
|
+
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
|
|
35
|
+
/** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
|
|
36
|
+
function resolveVertexApiKey(optKey?: string): string | undefined {
|
|
37
|
+
const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
|
|
38
|
+
return realKey || process.env.GOOGLE_CLOUD_API_KEY;
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
/**
|
|
17
42
|
* Inline image parts (Gemini `inline_data`) extracted from tool-result content. Only base64 data URLs
|
|
@@ -30,9 +55,8 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
|
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
: undefined;
|
|
58
|
+
const systemText = [...(parsed.context.systemPrompt ?? []), GOOGLE_BREVITY_INSTRUCTION].join("\n\n");
|
|
59
|
+
const systemInstruction = { parts: [{ text: systemText }] };
|
|
36
60
|
|
|
37
61
|
const contents: unknown[] = [];
|
|
38
62
|
|
|
@@ -63,7 +87,14 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?:
|
|
|
63
87
|
if (p.type === "text") parts.push({ text: (p as OcxTextContent).text });
|
|
64
88
|
else if (p.type === "toolCall") {
|
|
65
89
|
const tc = p as OcxToolCall;
|
|
66
|
-
|
|
90
|
+
// Preserve the thought signature on the function-call part so Antigravity/Gemini-3
|
|
91
|
+
// reasoning continuity survives history-driven (stateless) turns, not just same-process
|
|
92
|
+
// streaming covered by the replay cache. Only forward a REAL upstream signature — the
|
|
93
|
+
// Responses parser also stashes synthetic item ids (`fc_...`) on this field, and sending
|
|
94
|
+
// those as a thoughtSignature breaks continuity (the replay cache supplies the real one).
|
|
95
|
+
const part: Record<string, unknown> = { functionCall: { name: namespacedToolName(tc.namespace, tc.name), args: tc.arguments } };
|
|
96
|
+
if (isLikelyRealThoughtSignature(tc.thoughtSignature)) part.thoughtSignature = tc.thoughtSignature;
|
|
97
|
+
parts.push(part);
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
100
|
contents.push({ role: "model", parts });
|
|
@@ -100,7 +131,7 @@ function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
|
100
131
|
functionDeclarations: tools.map(t => ({
|
|
101
132
|
name: namespacedToolName(t.namespace, t.name),
|
|
102
133
|
description: t.description,
|
|
103
|
-
parameters: t.parameters,
|
|
134
|
+
parameters: sanitizeGeminiToolParameters(t.parameters),
|
|
104
135
|
})),
|
|
105
136
|
}];
|
|
106
137
|
}
|
|
@@ -116,10 +147,23 @@ function usageFromGemini(usage: Record<string, number> | undefined): OcxUsage |
|
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
150
|
+
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
151
|
+
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
152
|
+
let antigravityModel: string | undefined;
|
|
153
|
+
let antigravitySession: string | undefined;
|
|
119
154
|
return {
|
|
120
155
|
name: "google",
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
// Vertex + Antigravity get Kiro-style retry/timeout + classified, redacted errors. AI-Studio
|
|
158
|
+
// Gemini keeps the default server fetch path (fetchResponse stays undefined so server.ts falls back).
|
|
159
|
+
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
160
|
+
? {
|
|
161
|
+
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
162
|
+
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
|
|
163
|
+
}
|
|
164
|
+
: {}),
|
|
165
|
+
|
|
166
|
+
async buildRequest(parsed: OcxParsedRequest) {
|
|
123
167
|
const { systemInstruction, contents } = messagesToGeminiFormat(parsed);
|
|
124
168
|
const tools = toolsToGeminiFormat(parsed);
|
|
125
169
|
|
|
@@ -136,12 +180,67 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
136
180
|
|
|
137
181
|
const method = parsed.stream ? "streamGenerateContent" : "generateContent";
|
|
138
182
|
const streamParam = parsed.stream ? "?alt=sse" : "";
|
|
139
|
-
const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
|
|
140
|
-
|
|
141
183
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
142
|
-
if (provider.apiKey) headers["x-goog-api-key"] = provider.apiKey;
|
|
143
184
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
144
185
|
|
|
186
|
+
if (provider.googleMode === "cloud-code-assist") {
|
|
187
|
+
// Google Antigravity (Cloud Code Assist): wrap the flat Gemini body in the CCA envelope.
|
|
188
|
+
const base = provider.baseUrl || "https://daily-cloudcode-pa.googleapis.com";
|
|
189
|
+
const url = `${base}/v1internal:${method}${streamParam}`;
|
|
190
|
+
const project = provider.project;
|
|
191
|
+
if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
|
|
192
|
+
const sessionId = antigravitySessionId(parsed);
|
|
193
|
+
antigravityModel = parsed.modelId;
|
|
194
|
+
antigravitySession = sessionId;
|
|
195
|
+
// Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
|
|
196
|
+
// sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures.
|
|
197
|
+
if (Array.isArray((body as { contents?: unknown[] }).contents)) {
|
|
198
|
+
const contents = (body as { contents: unknown[] }).contents;
|
|
199
|
+
if (antigravityUsesReplayCache(parsed.modelId)) {
|
|
200
|
+
applyAntigravityReplay(parsed.modelId, sessionId, contents);
|
|
201
|
+
} else {
|
|
202
|
+
sanitizeAntigravityClaudeSignatures(contents);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// The CCA client serializes `session_id` (snake_case); send both spellings so the
|
|
206
|
+
// deterministic session id is honored regardless of which the backend accepts.
|
|
207
|
+
const request: Record<string, unknown> = { ...body, sessionId, session_id: sessionId };
|
|
208
|
+
const envelope = {
|
|
209
|
+
model: parsed.modelId,
|
|
210
|
+
userAgent: ANTIGRAVITY_REQUEST_UA,
|
|
211
|
+
requestType: "agent",
|
|
212
|
+
project,
|
|
213
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
214
|
+
request,
|
|
215
|
+
};
|
|
216
|
+
headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
|
|
217
|
+
if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
218
|
+
return { url, method: "POST", headers, body: JSON.stringify(envelope) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (provider.googleMode === "vertex") {
|
|
222
|
+
// Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
|
|
223
|
+
const apiKey = resolveVertexApiKey(provider.apiKey);
|
|
224
|
+
if (apiKey) {
|
|
225
|
+
const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
226
|
+
headers["x-goog-api-key"] = apiKey;
|
|
227
|
+
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
228
|
+
}
|
|
229
|
+
const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
|
|
230
|
+
if (!project) throw new Error("Vertex AI requires a project id (provider.project or GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT).");
|
|
231
|
+
const location = provider.location || process.env.GOOGLE_CLOUD_LOCATION;
|
|
232
|
+
if (!location) throw new Error("Vertex AI requires a location (provider.location or GOOGLE_CLOUD_LOCATION).");
|
|
233
|
+
const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
|
|
234
|
+
const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
|
|
235
|
+
const token = await getVertexAccessToken();
|
|
236
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
237
|
+
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ai-studio (default): Generative Language API + x-goog-api-key.
|
|
241
|
+
const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
|
|
242
|
+
if (provider.apiKey) headers["x-goog-api-key"] = provider.apiKey;
|
|
243
|
+
|
|
145
244
|
return { url, method: "POST", headers, body: JSON.stringify(body) };
|
|
146
245
|
},
|
|
147
246
|
|
|
@@ -155,6 +254,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
155
254
|
const decoder = new TextDecoder();
|
|
156
255
|
let buffer = "";
|
|
157
256
|
let pendingUsage: OcxUsage | undefined;
|
|
257
|
+
let toolCallsStarted = 0;
|
|
258
|
+
let lastFinishReason: string | undefined;
|
|
158
259
|
|
|
159
260
|
try {
|
|
160
261
|
while (true) {
|
|
@@ -176,14 +277,38 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
176
277
|
// Inline provider error inside a 200 stream → terminal error (see openai-chat.ts).
|
|
177
278
|
if (chunk.error) {
|
|
178
279
|
const err = chunk.error as { message?: string } | undefined;
|
|
280
|
+
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
281
|
+
// Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
|
|
282
|
+
if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession
|
|
283
|
+
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
284
|
+
clearAntigravityReplay(antigravityModel, antigravitySession);
|
|
285
|
+
}
|
|
179
286
|
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
180
287
|
return;
|
|
181
288
|
}
|
|
182
289
|
|
|
183
|
-
|
|
290
|
+
// Antigravity (CCA) nests the standard Gemini payload under `response`.
|
|
291
|
+
const root = (provider.googleMode === "cloud-code-assist"
|
|
292
|
+
? (chunk.response as Record<string, unknown> | undefined) ?? chunk
|
|
293
|
+
: chunk);
|
|
294
|
+
// usageMetadata is a top-level field independent of candidates; read it BEFORE the
|
|
295
|
+
// candidates guard so a usage-only final chunk is not dropped.
|
|
296
|
+
const usageMeta = root.usageMetadata as Record<string, number> | undefined;
|
|
297
|
+
if (usageMeta) {
|
|
298
|
+
// Accumulate usage; emit a single terminal `done` post-loop so usage is never
|
|
299
|
+
// dropped on EOF and the stream never yields two `done` events.
|
|
300
|
+
pendingUsage = usageFromGemini(usageMeta);
|
|
301
|
+
}
|
|
302
|
+
const candidates = root.candidates as { content?: { parts?: unknown[] }; finishReason?: string }[] | undefined;
|
|
184
303
|
if (!candidates?.length) continue;
|
|
185
304
|
|
|
305
|
+
lastFinishReason = candidates[0].finishReason ?? lastFinishReason;
|
|
306
|
+
|
|
186
307
|
const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
|
|
308
|
+
// Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
|
|
309
|
+
if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravitySession) {
|
|
310
|
+
observeAntigravityReplay(antigravityModel, antigravitySession, parts as unknown[]);
|
|
311
|
+
}
|
|
187
312
|
if (parts) {
|
|
188
313
|
for (const part of parts) {
|
|
189
314
|
if (part.text) {
|
|
@@ -191,21 +316,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
191
316
|
}
|
|
192
317
|
if (part.functionCall) {
|
|
193
318
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
319
|
+
toolCallsStarted++;
|
|
194
320
|
yield { type: "tool_call_start", id, name: part.functionCall.name };
|
|
195
321
|
yield { type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) };
|
|
196
322
|
yield { type: "tool_call_end" };
|
|
197
323
|
}
|
|
198
324
|
}
|
|
199
325
|
}
|
|
200
|
-
|
|
201
|
-
const usageMeta = chunk.usageMetadata as Record<string, number> | undefined;
|
|
202
|
-
if (usageMeta) {
|
|
203
|
-
// Accumulate usage; emit a single terminal `done` post-loop so usage is never
|
|
204
|
-
// dropped on EOF and the stream never yields two `done` events.
|
|
205
|
-
pendingUsage = usageFromGemini(usageMeta);
|
|
206
|
-
}
|
|
207
326
|
}
|
|
208
327
|
}
|
|
328
|
+
// Fail-closed: a turn cut off mid tool call (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces
|
|
329
|
+
// an error instead of a silently-incomplete done. Mirrors kiro-truncation.
|
|
330
|
+
if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
|
|
331
|
+
&& toolCallsStarted > 0 && isVertexTruncationReason(lastFinishReason)) {
|
|
332
|
+
yield { type: "error", message: vertexTruncationErrorMessage(lastFinishReason) };
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
209
335
|
yield { type: "done", usage: pendingUsage };
|
|
210
336
|
} finally {
|
|
211
337
|
reader.releaseLock();
|
|
@@ -213,15 +339,25 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
213
339
|
},
|
|
214
340
|
|
|
215
341
|
async parseResponse(response: Response): Promise<AdapterEvent[]> {
|
|
216
|
-
const
|
|
342
|
+
const raw = await response.json() as Record<string, unknown>;
|
|
343
|
+
// Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
|
|
344
|
+
const json = (provider.googleMode === "cloud-code-assist"
|
|
345
|
+
? (raw.response as Record<string, unknown> | undefined) ?? raw
|
|
346
|
+
: raw);
|
|
217
347
|
const events: AdapterEvent[] = [];
|
|
218
348
|
|
|
219
|
-
const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] } }[] | undefined;
|
|
349
|
+
const candidates = json.candidates as { content?: { parts?: { text?: string; functionCall?: { name: string; args: unknown } }[] }; finishReason?: string }[] | undefined;
|
|
350
|
+
let toolCallsStarted = 0;
|
|
220
351
|
if (candidates?.[0]?.content?.parts) {
|
|
352
|
+
// Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
|
|
353
|
+
if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravitySession) {
|
|
354
|
+
observeAntigravityReplay(antigravityModel, antigravitySession, candidates[0].content.parts as unknown[]);
|
|
355
|
+
}
|
|
221
356
|
for (const part of candidates[0].content.parts) {
|
|
222
357
|
if (part.text) events.push({ type: "text_delta", text: part.text });
|
|
223
358
|
if (part.functionCall) {
|
|
224
359
|
const id = `call_${crypto.randomUUID().slice(0, 8)}`;
|
|
360
|
+
toolCallsStarted++;
|
|
225
361
|
events.push({ type: "tool_call_start", id, name: part.functionCall.name });
|
|
226
362
|
events.push({ type: "tool_call_delta", arguments: JSON.stringify(part.functionCall.args ?? {}) });
|
|
227
363
|
events.push({ type: "tool_call_end" });
|
|
@@ -229,6 +365,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
229
365
|
}
|
|
230
366
|
}
|
|
231
367
|
|
|
368
|
+
// Fail-closed truncation, same as the stream path: a non-stream turn cut off mid tool call
|
|
369
|
+
// (MAX_TOKENS / MALFORMED_FUNCTION_CALL) surfaces an error instead of a silent done.
|
|
370
|
+
if ((provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist")
|
|
371
|
+
&& toolCallsStarted > 0 && isVertexTruncationReason(candidates?.[0]?.finishReason)) {
|
|
372
|
+
return [{ type: "error", message: vertexTruncationErrorMessage(candidates?.[0]?.finishReason) }];
|
|
373
|
+
}
|
|
374
|
+
|
|
232
375
|
const usage = json.usageMetadata as Record<string, number> | undefined;
|
|
233
376
|
events.push({
|
|
234
377
|
type: "done",
|
|
@@ -3,14 +3,67 @@ import { namespacedToolName } from "../types";
|
|
|
3
3
|
|
|
4
4
|
const MAX_KIRO_TOOL_DESCRIPTION = 1024;
|
|
5
5
|
|
|
6
|
+
// JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator
|
|
7
|
+
// rejects ("ValidationException: Invalid tool use format."). Codex's built-in tools omit these,
|
|
8
|
+
// but the `memories__*` tools (add_ad_hoc_note/read/search/list) emit pattern/length/range
|
|
9
|
+
// constraints via schemars, which trip the validator. Strip them everywhere in the schema tree;
|
|
10
|
+
// the constraints are advisory for the model, so dropping them does not change tool behavior.
|
|
11
|
+
const KIRO_REJECTED_SCHEMA_KEYS = new Set([
|
|
12
|
+
"additionalProperties",
|
|
13
|
+
"pattern",
|
|
14
|
+
"format",
|
|
15
|
+
"minLength",
|
|
16
|
+
"maxLength",
|
|
17
|
+
"minimum",
|
|
18
|
+
"maximum",
|
|
19
|
+
"exclusiveMinimum",
|
|
20
|
+
"exclusiveMaximum",
|
|
21
|
+
"multipleOf",
|
|
22
|
+
"minItems",
|
|
23
|
+
"maxItems",
|
|
24
|
+
"uniqueItems",
|
|
25
|
+
"minProperties",
|
|
26
|
+
"maxProperties",
|
|
27
|
+
"contentEncoding",
|
|
28
|
+
"contentMediaType",
|
|
29
|
+
"$schema",
|
|
30
|
+
// Validation-only composition/applicator keywords that Bedrock/Kiro do not support. Unlike
|
|
31
|
+
// `properties`/`$defs`, these are not plain property->schema maps the model needs, so they are
|
|
32
|
+
// dropped outright rather than recursed into.
|
|
33
|
+
"patternProperties",
|
|
34
|
+
"propertyNames",
|
|
35
|
+
"dependentSchemas",
|
|
36
|
+
"dependentRequired",
|
|
37
|
+
"if",
|
|
38
|
+
"then",
|
|
39
|
+
"else",
|
|
40
|
+
"contains",
|
|
41
|
+
"unevaluatedProperties",
|
|
42
|
+
"unevaluatedItems",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Keys whose values are maps of *property/definition name -> schema* (not schema keywords). Their
|
|
46
|
+
// child keys must never be treated as schema keywords, or a legitimate property named e.g.
|
|
47
|
+
// "format"/"pattern" would be deleted. We recurse into the value schemas but keep every name intact.
|
|
48
|
+
const SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
|
|
49
|
+
|
|
50
|
+
function sanitizeSchemaMap(value: unknown): unknown {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeKiroSchema(value);
|
|
52
|
+
const out: Record<string, unknown> = {};
|
|
53
|
+
for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
|
|
54
|
+
out[name] = sanitizeKiroSchema(child);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
6
59
|
function sanitizeKiroSchema(value: unknown): unknown {
|
|
7
60
|
if (Array.isArray(value)) return value.map(sanitizeKiroSchema);
|
|
8
61
|
if (!value || typeof value !== "object") return value;
|
|
9
62
|
const out: Record<string, unknown> = {};
|
|
10
63
|
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
11
|
-
if (key
|
|
64
|
+
if (KIRO_REJECTED_SCHEMA_KEYS.has(key)) continue;
|
|
12
65
|
if (key === "required" && Array.isArray(child) && child.length === 0) continue;
|
|
13
|
-
out[key] = sanitizeKiroSchema(child);
|
|
66
|
+
out[key] = SCHEMA_MAP_KEYS.has(key) ? sanitizeSchemaMap(child) : sanitizeKiroSchema(child);
|
|
14
67
|
}
|
|
15
68
|
return out;
|
|
16
69
|
}
|
package/src/cli-status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { durableBunRuntime } from "./bun-runtime";
|
|
2
2
|
import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "./config";
|
|
3
|
+
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "./codex-plugins-doctor";
|
|
3
4
|
import type { OcxConfig } from "./types";
|
|
4
5
|
import { serviceStatusSummary } from "./service";
|
|
5
6
|
|
|
@@ -44,6 +45,7 @@ export type CliStatusJson = {
|
|
|
44
45
|
};
|
|
45
46
|
service: { summary: string };
|
|
46
47
|
codexShim: { summary: string };
|
|
48
|
+
codexPlugins: CodexPluginsDiagnostic;
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
export type CliStatusView = {
|
|
@@ -114,6 +116,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
114
116
|
const serviceSummary = serviceStatusSummary();
|
|
115
117
|
const { codexShimStatus } = await import("./codex-shim");
|
|
116
118
|
const codexShimSummary = codexShimStatus();
|
|
119
|
+
const codexPlugins = diagnoseCodexBundledPlugins();
|
|
117
120
|
const proxyLabel = pid && health.ok
|
|
118
121
|
? `running (PID ${pid})`
|
|
119
122
|
: pid
|
|
@@ -159,6 +162,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
159
162
|
},
|
|
160
163
|
service: { summary: serviceSummary },
|
|
161
164
|
codexShim: { summary: codexShimSummary },
|
|
165
|
+
codexPlugins,
|
|
162
166
|
},
|
|
163
167
|
};
|
|
164
168
|
}
|
package/src/cli.ts
CHANGED
|
@@ -348,6 +348,13 @@ async function handleStatus() {
|
|
|
348
348
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
349
349
|
console.log(` Service: ${status.json.service.summary}`);
|
|
350
350
|
console.log(` ${status.json.codexShim.summary}`);
|
|
351
|
+
if (status.json.codexPlugins.applicable) {
|
|
352
|
+
const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
|
|
353
|
+
console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
|
|
354
|
+
if (status.json.codexPlugins.suggestedRepair) {
|
|
355
|
+
console.log(` Suggested: ${status.json.codexPlugins.suggestedRepair}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
function handleRecoverHistory() {
|