@tt-a1i/openpi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Code Assist (Antigravity) streamSimple implementation.
|
|
3
|
+
*
|
|
4
|
+
* Wire shape (reference: oh-my-pi packages/ai/src/providers/google-gemini-cli.ts,
|
|
5
|
+
* shared google-gemini-cli/google-antigravity implementation):
|
|
6
|
+
*
|
|
7
|
+
* POST {endpoint}/v1internal:streamGenerateContent?alt=sse
|
|
8
|
+
* { project, requestId, model, userAgent: "antigravity", requestType: "agent",
|
|
9
|
+
* request: { contents, systemInstruction, tools, toolConfig,
|
|
10
|
+
* generationConfig, labels, sessionId } }
|
|
11
|
+
*
|
|
12
|
+
* SSE frames carry `{ response: GenerateContentResponse }` envelopes. Endpoint
|
|
13
|
+
* failover: daily-cloudcode-pa → daily-cloudcode-pa.sandbox. Message/tool
|
|
14
|
+
* conversion is kept local because Pi extensions cannot resolve pi-ai's
|
|
15
|
+
* internal google-shared module at installed runtime.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
+
import type {
|
|
20
|
+
Api,
|
|
21
|
+
AssistantMessage,
|
|
22
|
+
AssistantMessageEventStream,
|
|
23
|
+
Context,
|
|
24
|
+
Model,
|
|
25
|
+
SimpleStreamOptions,
|
|
26
|
+
ToolCall,
|
|
27
|
+
} from "@earendil-works/pi-ai/compat";
|
|
28
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/compat";
|
|
29
|
+
import { emptyUsage } from "../usage.ts";
|
|
30
|
+
import { decodeApiKey } from "./credentials.ts";
|
|
31
|
+
import {
|
|
32
|
+
convertMessages,
|
|
33
|
+
convertTools,
|
|
34
|
+
isThinkingPart,
|
|
35
|
+
mapStopReasonString,
|
|
36
|
+
retainThoughtSignature,
|
|
37
|
+
} from "./google-conversion.ts";
|
|
38
|
+
import { ensureAntigravityVersion, getAntigravityUserAgent } from "./oauth.ts";
|
|
39
|
+
import { routeAntigravityModel } from "./routing.ts";
|
|
40
|
+
|
|
41
|
+
const ENDPOINTS = [
|
|
42
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
43
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14";
|
|
47
|
+
|
|
48
|
+
const FLASH_FIRST_EVENT_TIMEOUT_MS = 60_000;
|
|
49
|
+
const DEFAULT_FIRST_EVENT_TIMEOUT_MS = 300_000;
|
|
50
|
+
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
51
|
+
|
|
52
|
+
type AntigravityStreamOptions = Omit<SimpleStreamOptions, "toolChoice"> & {
|
|
53
|
+
/** Keep Antigravity's richer modes compatible across Pi 0.84.1 and 0.84.3+. */
|
|
54
|
+
toolChoice?:
|
|
55
|
+
| "auto"
|
|
56
|
+
| "none"
|
|
57
|
+
| "any"
|
|
58
|
+
| { mode: "ANY"; allowedFunctionNames: [string, ...string[]] };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const FORCED_TOOL_DIRECTIVE =
|
|
62
|
+
"TOOL-ONLY TURN. This turn accepts a tool call and nothing else; " +
|
|
63
|
+
"a text reply here is discarded unread and you will be re-prompted. " +
|
|
64
|
+
"Emit the tool call now.";
|
|
65
|
+
|
|
66
|
+
interface AntigravitySessionState {
|
|
67
|
+
agentId: string;
|
|
68
|
+
trajectoryId: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
stepIndex: number;
|
|
71
|
+
lastExecutionId?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sessionStates = new Map<string, AntigravitySessionState>();
|
|
75
|
+
const MAX_SESSION_STATES = 64;
|
|
76
|
+
|
|
77
|
+
// Cloud Code Assist 400s on these JSON Schema keywords (reference: omp
|
|
78
|
+
// packages/ai/src/utils/schema/fields.ts — union of UNSUPPORTED_SCHEMA_FIELDS
|
|
79
|
+
// and LIFTABLE_TO_DESCRIPTION_FIELDS). Constraints that remain useful to the
|
|
80
|
+
// model are serialized into the sibling description before being removed.
|
|
81
|
+
const CCA_UNSUPPORTED_SCHEMA_FIELDS: Record<string, true> = {
|
|
82
|
+
$schema: true,
|
|
83
|
+
$ref: true,
|
|
84
|
+
$defs: true,
|
|
85
|
+
$dynamicRef: true,
|
|
86
|
+
$dynamicAnchor: true,
|
|
87
|
+
$comment: true,
|
|
88
|
+
examples: true,
|
|
89
|
+
prefixItems: true,
|
|
90
|
+
unevaluatedProperties: true,
|
|
91
|
+
unevaluatedItems: true,
|
|
92
|
+
patternProperties: true,
|
|
93
|
+
additionalProperties: true,
|
|
94
|
+
propertyNames: true,
|
|
95
|
+
minItems: true,
|
|
96
|
+
maxItems: true,
|
|
97
|
+
minLength: true,
|
|
98
|
+
maxLength: true,
|
|
99
|
+
minProperties: true,
|
|
100
|
+
maxProperties: true,
|
|
101
|
+
minimum: true,
|
|
102
|
+
maximum: true,
|
|
103
|
+
exclusiveMinimum: true,
|
|
104
|
+
exclusiveMaximum: true,
|
|
105
|
+
multipleOf: true,
|
|
106
|
+
uniqueItems: true,
|
|
107
|
+
pattern: true,
|
|
108
|
+
format: true,
|
|
109
|
+
default: true,
|
|
110
|
+
deprecated: true,
|
|
111
|
+
readOnly: true,
|
|
112
|
+
writeOnly: true,
|
|
113
|
+
dependencies: true,
|
|
114
|
+
dependentSchemas: true,
|
|
115
|
+
dependentRequired: true,
|
|
116
|
+
"x-mcp-header": true,
|
|
117
|
+
};
|
|
118
|
+
// Stripped keywords whose constraint stays model-visible by spilling into the
|
|
119
|
+
// node's description (omp LIFTABLE_TO_DESCRIPTION_FIELDS, "spill" format).
|
|
120
|
+
const CCA_LIFTABLE_TO_DESCRIPTION: Record<string, true> = {
|
|
121
|
+
pattern: true,
|
|
122
|
+
format: true,
|
|
123
|
+
minLength: true,
|
|
124
|
+
maxLength: true,
|
|
125
|
+
minimum: true,
|
|
126
|
+
maximum: true,
|
|
127
|
+
exclusiveMinimum: true,
|
|
128
|
+
exclusiveMaximum: true,
|
|
129
|
+
multipleOf: true,
|
|
130
|
+
minItems: true,
|
|
131
|
+
maxItems: true,
|
|
132
|
+
uniqueItems: true,
|
|
133
|
+
minProperties: true,
|
|
134
|
+
maxProperties: true,
|
|
135
|
+
default: true,
|
|
136
|
+
examples: true,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function sanitizeSchemaForCcaValue(
|
|
140
|
+
value: unknown,
|
|
141
|
+
insidePropertiesMap: boolean,
|
|
142
|
+
): unknown {
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return value.map((entry) => sanitizeSchemaForCcaValue(entry, false));
|
|
145
|
+
}
|
|
146
|
+
if (value === null || typeof value !== "object") return value;
|
|
147
|
+
const out: Record<string, unknown> = {};
|
|
148
|
+
const spill: Array<[string, unknown]> = [];
|
|
149
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
150
|
+
// Keys below `properties` are user-defined parameter names, not JSON
|
|
151
|
+
// Schema keywords. A tool parameter named `pattern`, for example, must be
|
|
152
|
+
// preserved while its schema value is sanitized normally.
|
|
153
|
+
if (insidePropertiesMap) {
|
|
154
|
+
out[key] = sanitizeSchemaForCcaValue(entry, false);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (Object.hasOwn(CCA_UNSUPPORTED_SCHEMA_FIELDS, key)) {
|
|
158
|
+
if (
|
|
159
|
+
entry !== undefined &&
|
|
160
|
+
Object.hasOwn(CCA_LIFTABLE_TO_DESCRIPTION, key)
|
|
161
|
+
) {
|
|
162
|
+
spill.push([key, entry]);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
out[key] = sanitizeSchemaForCcaValue(entry, key === "properties");
|
|
167
|
+
}
|
|
168
|
+
if (spill.length > 0) {
|
|
169
|
+
const formatted = `{${spill.map(([key, entry]) => `${key}: ${JSON.stringify(entry)}`).join(", ")}}`;
|
|
170
|
+
const existing = typeof out.description === "string" ? out.description : "";
|
|
171
|
+
out.description = existing ? `${existing}\n\n${formatted}` : formatted;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Recursively drop schema keywords Cloud Code Assist rejects. */
|
|
177
|
+
export function sanitizeSchemaForCca(value: unknown): unknown {
|
|
178
|
+
return sanitizeSchemaForCcaValue(value, false);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Request construction
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
function isClaudeRoute(modelId: string): boolean {
|
|
186
|
+
return modelId.toLowerCase().includes("claude");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeSystemPrompts(
|
|
190
|
+
systemPrompt: Context["systemPrompt"],
|
|
191
|
+
): string[] {
|
|
192
|
+
if (!systemPrompt) return [];
|
|
193
|
+
return Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Deterministic conversation id: hash of the first user text, like the client. */
|
|
197
|
+
function deriveSessionId(context: Context): string {
|
|
198
|
+
for (const message of context.messages) {
|
|
199
|
+
if (message.role !== "user") continue;
|
|
200
|
+
const content = message.content;
|
|
201
|
+
const text =
|
|
202
|
+
typeof content === "string"
|
|
203
|
+
? content
|
|
204
|
+
: content.find((part) => part.type === "text")?.text;
|
|
205
|
+
if (text && text.trim().length > 0) {
|
|
206
|
+
const digest = createHash("sha256").update(text).digest();
|
|
207
|
+
let value = 0n;
|
|
208
|
+
for (let i = 0; i < 8; i++) {
|
|
209
|
+
value = (value << 8n) | BigInt(digest[i]);
|
|
210
|
+
}
|
|
211
|
+
// The real client formats its bounded int63 identifier as a negative
|
|
212
|
+
// decimal string rather than using a UUID on the wire.
|
|
213
|
+
return `-${String(value & 0x7fffffffffffffffn)}`;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const random = BigInt(`0x${randomUUID().replaceAll("-", "").slice(0, 16)}`);
|
|
218
|
+
return `-${String(random & 0x7fffffffffffffffn)}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getSessionState(
|
|
222
|
+
options: AntigravityStreamOptions | undefined,
|
|
223
|
+
context: Context,
|
|
224
|
+
): AntigravitySessionState | undefined {
|
|
225
|
+
const key = options?.sessionId;
|
|
226
|
+
if (!key) return undefined;
|
|
227
|
+
const existing = sessionStates.get(key);
|
|
228
|
+
if (existing) return existing;
|
|
229
|
+
if (sessionStates.size >= MAX_SESSION_STATES) {
|
|
230
|
+
const oldest = sessionStates.keys().next().value;
|
|
231
|
+
if (oldest) sessionStates.delete(oldest);
|
|
232
|
+
}
|
|
233
|
+
const created: AntigravitySessionState = {
|
|
234
|
+
agentId: randomUUID(),
|
|
235
|
+
trajectoryId: randomUUID(),
|
|
236
|
+
sessionId: deriveSessionId(context),
|
|
237
|
+
stepIndex: 1,
|
|
238
|
+
};
|
|
239
|
+
sessionStates.set(key, created);
|
|
240
|
+
return created;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildToolConfig(
|
|
244
|
+
model: Model<Api>,
|
|
245
|
+
hasTools: boolean,
|
|
246
|
+
toolChoice: AntigravityStreamOptions["toolChoice"],
|
|
247
|
+
): Record<string, unknown> | undefined {
|
|
248
|
+
if (!hasTools) {
|
|
249
|
+
return isClaudeRoute(model.id) && toolChoice !== "none"
|
|
250
|
+
? { functionCallingConfig: { mode: "VALIDATED" } }
|
|
251
|
+
: undefined;
|
|
252
|
+
}
|
|
253
|
+
if (toolChoice === "none") {
|
|
254
|
+
return { functionCallingConfig: { mode: "NONE" } };
|
|
255
|
+
}
|
|
256
|
+
if (toolChoice === "any") {
|
|
257
|
+
return { functionCallingConfig: { mode: "ANY" } };
|
|
258
|
+
}
|
|
259
|
+
if (typeof toolChoice === "object") {
|
|
260
|
+
return {
|
|
261
|
+
functionCallingConfig: {
|
|
262
|
+
mode: "ANY",
|
|
263
|
+
allowedFunctionNames: [...toolChoice.allowedFunctionNames],
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// Antigravity's default tool mode is VALIDATED (verified upstream for both
|
|
268
|
+
// Gemini and Claude routes).
|
|
269
|
+
return { functionCallingConfig: { mode: "VALIDATED" } };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Convert pi tools to CCA functionDeclarations with sanitized schemas. */
|
|
273
|
+
function buildTools(
|
|
274
|
+
context: Context,
|
|
275
|
+
toolChoice: AntigravityStreamOptions["toolChoice"],
|
|
276
|
+
): Record<string, unknown>[] | undefined {
|
|
277
|
+
if (toolChoice === "none") return undefined;
|
|
278
|
+
const tools = context.tools;
|
|
279
|
+
if (!tools || tools.length === 0) return undefined;
|
|
280
|
+
const converted = convertTools([...tools], true) as
|
|
281
|
+
| { functionDeclarations: Record<string, unknown>[] }[]
|
|
282
|
+
| undefined;
|
|
283
|
+
if (!converted) return undefined;
|
|
284
|
+
return converted.map((group) => ({
|
|
285
|
+
...group,
|
|
286
|
+
functionDeclarations: group.functionDeclarations.map((declaration) => ({
|
|
287
|
+
...declaration,
|
|
288
|
+
parameters: sanitizeSchemaForCca(declaration.parameters),
|
|
289
|
+
})),
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function buildRequestBody(
|
|
294
|
+
model: Model<Api>,
|
|
295
|
+
context: Context,
|
|
296
|
+
options: AntigravityStreamOptions | undefined,
|
|
297
|
+
projectId: string,
|
|
298
|
+
state?: AntigravitySessionState,
|
|
299
|
+
): Record<string, unknown> {
|
|
300
|
+
const contents = convertMessages(model, context);
|
|
301
|
+
|
|
302
|
+
const request: Record<string, unknown> = { contents };
|
|
303
|
+
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
304
|
+
if (systemPrompts.length > 0) {
|
|
305
|
+
request.systemInstruction = {
|
|
306
|
+
role: "user",
|
|
307
|
+
parts: systemPrompts.map((text) => ({ text })),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const tools = buildTools(context, options?.toolChoice);
|
|
312
|
+
if (tools) request.tools = tools;
|
|
313
|
+
const toolConfig = buildToolConfig(
|
|
314
|
+
model,
|
|
315
|
+
Boolean(tools),
|
|
316
|
+
options?.toolChoice,
|
|
317
|
+
);
|
|
318
|
+
if (toolConfig) request.toolConfig = toolConfig;
|
|
319
|
+
if (
|
|
320
|
+
tools &&
|
|
321
|
+
!isClaudeRoute(model.id) &&
|
|
322
|
+
(options?.toolChoice === "any" || typeof options?.toolChoice === "object")
|
|
323
|
+
) {
|
|
324
|
+
contents.push({ role: "user", parts: [{ text: FORCED_TOOL_DIRECTIVE }] });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const route = routeAntigravityModel(
|
|
328
|
+
model.id,
|
|
329
|
+
model.reasoning ? options?.reasoning : undefined,
|
|
330
|
+
options?.thinkingBudgets,
|
|
331
|
+
model as Model<Api> & {
|
|
332
|
+
requestModelId?: string;
|
|
333
|
+
antigravityEffortRouting?: Partial<
|
|
334
|
+
Record<NonNullable<SimpleStreamOptions["reasoning"]>, string>
|
|
335
|
+
>;
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
const requestedMaxTokens = options?.maxTokens ?? model.maxTokens;
|
|
339
|
+
const generationConfig: Record<string, unknown> = {
|
|
340
|
+
maxOutputTokens: isClaudeRoute(route.wireModelId)
|
|
341
|
+
? Math.min(requestedMaxTokens, 64_000)
|
|
342
|
+
: requestedMaxTokens,
|
|
343
|
+
};
|
|
344
|
+
if (options?.temperature !== undefined) {
|
|
345
|
+
generationConfig.temperature = options.temperature;
|
|
346
|
+
}
|
|
347
|
+
const thinkingConfig = model.reasoning ? route.thinkingConfig : undefined;
|
|
348
|
+
if (thinkingConfig) generationConfig.thinkingConfig = thinkingConfig;
|
|
349
|
+
request.generationConfig = generationConfig;
|
|
350
|
+
|
|
351
|
+
const agentId = state?.agentId ?? randomUUID();
|
|
352
|
+
const trajectoryId = state?.trajectoryId ?? randomUUID();
|
|
353
|
+
const sessionId = state?.sessionId ?? deriveSessionId(context);
|
|
354
|
+
const stepIndex = state ? ++state.stepIndex : 2;
|
|
355
|
+
request.sessionId = sessionId;
|
|
356
|
+
request.labels = {
|
|
357
|
+
...(state?.lastExecutionId
|
|
358
|
+
? { last_execution_id: state.lastExecutionId }
|
|
359
|
+
: {}),
|
|
360
|
+
last_step_index: String(stepIndex - 1),
|
|
361
|
+
trajectory_id: trajectoryId,
|
|
362
|
+
used_claude: String(isClaudeRoute(model.id)),
|
|
363
|
+
used_claude_conservative: String(isClaudeRoute(model.id)),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
project: projectId,
|
|
368
|
+
requestId: `agent/${agentId}/${Date.now()}/${trajectoryId}/${stepIndex}`,
|
|
369
|
+
model: route.wireModelId,
|
|
370
|
+
userAgent: "antigravity",
|
|
371
|
+
requestType: "agent",
|
|
372
|
+
request,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// SSE decoding
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
interface CcaPart {
|
|
381
|
+
text?: string;
|
|
382
|
+
thought?: boolean;
|
|
383
|
+
thoughtSignature?: string;
|
|
384
|
+
functionCall?: {
|
|
385
|
+
id?: string;
|
|
386
|
+
name?: string;
|
|
387
|
+
args?: Record<string, unknown>;
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
interface CcaChunk {
|
|
392
|
+
response?: {
|
|
393
|
+
responseId?: string;
|
|
394
|
+
candidates?: {
|
|
395
|
+
content?: { parts?: CcaPart[] };
|
|
396
|
+
finishReason?: string;
|
|
397
|
+
}[];
|
|
398
|
+
promptFeedback?: {
|
|
399
|
+
blockReason?: string;
|
|
400
|
+
blockReasonMessage?: string;
|
|
401
|
+
};
|
|
402
|
+
usageMetadata?: {
|
|
403
|
+
promptTokenCount?: number;
|
|
404
|
+
cachedContentTokenCount?: number;
|
|
405
|
+
candidatesTokenCount?: number;
|
|
406
|
+
thoughtsTokenCount?: number;
|
|
407
|
+
totalTokenCount?: number;
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
error?: { code?: number; message?: string; status?: string };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function readErrorResponseBody(
|
|
414
|
+
body: ReadableStream<Uint8Array> | null,
|
|
415
|
+
signal: AbortSignal | undefined,
|
|
416
|
+
deadline: number,
|
|
417
|
+
): Promise<string> {
|
|
418
|
+
if (!body) return "";
|
|
419
|
+
const reader = body.getReader();
|
|
420
|
+
const decoder = new TextDecoder();
|
|
421
|
+
const chunks: string[] = [];
|
|
422
|
+
let bytesRead = 0;
|
|
423
|
+
let reachedEnd = false;
|
|
424
|
+
let truncated = false;
|
|
425
|
+
const timeoutError = () =>
|
|
426
|
+
new Error("Timed out reading Cloud Code Assist error response body");
|
|
427
|
+
try {
|
|
428
|
+
while (bytesRead < MAX_ERROR_BODY_BYTES) {
|
|
429
|
+
if (signal?.aborted) throw new Error("Request aborted");
|
|
430
|
+
const remaining = deadline - Date.now();
|
|
431
|
+
if (remaining <= 0) throw timeoutError();
|
|
432
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
433
|
+
let onAbort: (() => void) | undefined;
|
|
434
|
+
const gates: Promise<ReadableStreamReadResult<Uint8Array>>[] = [
|
|
435
|
+
reader.read(),
|
|
436
|
+
new Promise<never>((_, reject) => {
|
|
437
|
+
timer = setTimeout(
|
|
438
|
+
() => reject(timeoutError()),
|
|
439
|
+
Math.max(1, remaining),
|
|
440
|
+
);
|
|
441
|
+
}),
|
|
442
|
+
];
|
|
443
|
+
if (signal) {
|
|
444
|
+
gates.push(
|
|
445
|
+
new Promise<never>((_, reject) => {
|
|
446
|
+
onAbort = () => reject(new Error("Request aborted"));
|
|
447
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const result = await Promise.race(gates).finally(() => {
|
|
452
|
+
if (timer) clearTimeout(timer);
|
|
453
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
454
|
+
});
|
|
455
|
+
if (Date.now() >= deadline) throw timeoutError();
|
|
456
|
+
if (result.done) {
|
|
457
|
+
reachedEnd = true;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
const remainingBytes = MAX_ERROR_BODY_BYTES - bytesRead;
|
|
461
|
+
const value = result.value.subarray(0, remainingBytes);
|
|
462
|
+
bytesRead += value.byteLength;
|
|
463
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
464
|
+
if (value.byteLength < result.value.byteLength) {
|
|
465
|
+
truncated = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
if (bytesRead === MAX_ERROR_BODY_BYTES) truncated = true;
|
|
469
|
+
}
|
|
470
|
+
chunks.push(decoder.decode());
|
|
471
|
+
return chunks.join("") + (truncated ? "… [truncated]" : "");
|
|
472
|
+
} finally {
|
|
473
|
+
if (!reachedEnd) await reader.cancel().catch(() => {});
|
|
474
|
+
reader.releaseLock();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function* readSseChunks(
|
|
479
|
+
body: ReadableStream<Uint8Array>,
|
|
480
|
+
signal: AbortSignal | undefined,
|
|
481
|
+
firstEventDeadline: number,
|
|
482
|
+
requestDeadline: number | undefined,
|
|
483
|
+
): AsyncGenerator<CcaChunk> {
|
|
484
|
+
const reader = body.getReader();
|
|
485
|
+
const decoder = new TextDecoder();
|
|
486
|
+
let buffer = "";
|
|
487
|
+
let dataLines: string[] = [];
|
|
488
|
+
const flush = (): CcaChunk | undefined => {
|
|
489
|
+
if (dataLines.length === 0) return undefined;
|
|
490
|
+
const payload = dataLines.join("\n");
|
|
491
|
+
dataLines = [];
|
|
492
|
+
if (payload === "[DONE]") return undefined;
|
|
493
|
+
return JSON.parse(payload) as CcaChunk;
|
|
494
|
+
};
|
|
495
|
+
let sawEvent = false;
|
|
496
|
+
let reachedEnd = false;
|
|
497
|
+
const timeoutError = () =>
|
|
498
|
+
new Error(
|
|
499
|
+
sawEvent
|
|
500
|
+
? "Timed out waiting for the next SSE event"
|
|
501
|
+
: "Timed out waiting for the first SSE event",
|
|
502
|
+
);
|
|
503
|
+
try {
|
|
504
|
+
while (true) {
|
|
505
|
+
if (signal?.aborted) throw new Error("Request aborted");
|
|
506
|
+
const deadline = sawEvent ? requestDeadline : firstEventDeadline;
|
|
507
|
+
const remaining =
|
|
508
|
+
deadline === undefined ? undefined : deadline - Date.now();
|
|
509
|
+
if (remaining !== undefined && remaining <= 0) throw timeoutError();
|
|
510
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
511
|
+
let onAbort: (() => void) | undefined;
|
|
512
|
+
const read = reader.read();
|
|
513
|
+
const gates: Promise<ReadableStreamReadResult<Uint8Array>>[] = [read];
|
|
514
|
+
if (remaining !== undefined) {
|
|
515
|
+
gates.push(
|
|
516
|
+
new Promise<never>((_, reject) => {
|
|
517
|
+
timer = setTimeout(
|
|
518
|
+
() => reject(timeoutError()),
|
|
519
|
+
Math.max(1, remaining),
|
|
520
|
+
);
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (signal) {
|
|
525
|
+
gates.push(
|
|
526
|
+
new Promise<never>((_, reject) => {
|
|
527
|
+
onAbort = () => reject(new Error("Request aborted"));
|
|
528
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const result = await Promise.race(gates).finally(() => {
|
|
533
|
+
if (timer) clearTimeout(timer);
|
|
534
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
535
|
+
});
|
|
536
|
+
if (deadline !== undefined && Date.now() >= deadline)
|
|
537
|
+
throw timeoutError();
|
|
538
|
+
const { done, value } = result;
|
|
539
|
+
if (done) {
|
|
540
|
+
reachedEnd = true;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
buffer += decoder.decode(value, { stream: true });
|
|
544
|
+
let newline = buffer.indexOf("\n");
|
|
545
|
+
while (newline !== -1) {
|
|
546
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
547
|
+
buffer = buffer.slice(newline + 1);
|
|
548
|
+
newline = buffer.indexOf("\n");
|
|
549
|
+
if (line === "") {
|
|
550
|
+
const event = flush();
|
|
551
|
+
if (event) {
|
|
552
|
+
sawEvent = true;
|
|
553
|
+
yield event;
|
|
554
|
+
}
|
|
555
|
+
} else if (line.startsWith("data:")) {
|
|
556
|
+
dataLines.push(line.slice(5).trimStart());
|
|
557
|
+
}
|
|
558
|
+
// event:/id:/retry: lines carry no payload for this API.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
buffer += decoder.decode();
|
|
562
|
+
const tail = buffer.trim();
|
|
563
|
+
if (tail.startsWith("data:")) dataLines.push(tail.slice(5).trimStart());
|
|
564
|
+
const event = flush();
|
|
565
|
+
if (event) yield event;
|
|
566
|
+
} finally {
|
|
567
|
+
if (!reachedEnd) await reader.cancel().catch(() => {});
|
|
568
|
+
reader.releaseLock();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function* prependChunks(
|
|
573
|
+
initial: readonly CcaChunk[],
|
|
574
|
+
rest: AsyncGenerator<CcaChunk>,
|
|
575
|
+
): AsyncGenerator<CcaChunk> {
|
|
576
|
+
yield* initial;
|
|
577
|
+
yield* rest;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function transientStatus(status: number | undefined): boolean {
|
|
581
|
+
return (
|
|
582
|
+
status === 408 || status === 429 || (status !== undefined && status >= 500)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
class EndpointAttemptError extends Error {
|
|
587
|
+
readonly retryable: boolean;
|
|
588
|
+
|
|
589
|
+
constructor(message: string, retryable: boolean) {
|
|
590
|
+
super(message);
|
|
591
|
+
this.name = "EndpointAttemptError";
|
|
592
|
+
this.retryable = retryable;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function preflightChunks(
|
|
597
|
+
chunks: AsyncGenerator<CcaChunk>,
|
|
598
|
+
): Promise<CcaChunk[]> {
|
|
599
|
+
const initial: CcaChunk[] = [];
|
|
600
|
+
while (true) {
|
|
601
|
+
const next = await chunks.next();
|
|
602
|
+
if (next.done) {
|
|
603
|
+
throw new EndpointAttemptError(
|
|
604
|
+
"Cloud Code Assist stream ended before returning content",
|
|
605
|
+
true,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const chunk = next.value;
|
|
609
|
+
initial.push(chunk);
|
|
610
|
+
if (chunk.error) {
|
|
611
|
+
const code = chunk.error.code;
|
|
612
|
+
const detail =
|
|
613
|
+
chunk.error.message || chunk.error.status || "unknown error";
|
|
614
|
+
throw new EndpointAttemptError(
|
|
615
|
+
`Cloud Code Assist stream error: ${detail}`,
|
|
616
|
+
transientStatus(code),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
const data = chunk.response;
|
|
620
|
+
if (!data) continue;
|
|
621
|
+
if (!data.candidates?.length && data.promptFeedback?.blockReason) {
|
|
622
|
+
const detail = data.promptFeedback.blockReasonMessage;
|
|
623
|
+
throw new EndpointAttemptError(
|
|
624
|
+
`Request blocked by Google (${data.promptFeedback.blockReason})` +
|
|
625
|
+
(detail ? `: ${detail}` : ""),
|
|
626
|
+
false,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const candidate = data.candidates?.[0];
|
|
630
|
+
if (
|
|
631
|
+
(candidate?.content?.parts ?? []).some(
|
|
632
|
+
(part) =>
|
|
633
|
+
Boolean(part.functionCall) ||
|
|
634
|
+
(Boolean(part.text?.trim()) && part.thought !== true),
|
|
635
|
+
)
|
|
636
|
+
) {
|
|
637
|
+
return initial;
|
|
638
|
+
}
|
|
639
|
+
if (candidate?.finishReason) {
|
|
640
|
+
throw new EndpointAttemptError(
|
|
641
|
+
"Cloud Code Assist returned an empty response",
|
|
642
|
+
true,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function mergeRequestHeaders(
|
|
649
|
+
defaults: Record<string, string>,
|
|
650
|
+
overrides: AntigravityStreamOptions["headers"],
|
|
651
|
+
): Record<string, string> {
|
|
652
|
+
const headers = new Headers(defaults);
|
|
653
|
+
for (const [name, value] of Object.entries(overrides ?? {})) {
|
|
654
|
+
if (value === null) headers.delete(name);
|
|
655
|
+
else headers.set(name, value);
|
|
656
|
+
}
|
|
657
|
+
return Object.fromEntries(headers.entries());
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// Stream entry point
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
|
|
664
|
+
let toolCallCounter = 0;
|
|
665
|
+
|
|
666
|
+
export function streamAntigravity(
|
|
667
|
+
model: Model<Api>,
|
|
668
|
+
context: Context,
|
|
669
|
+
options?: SimpleStreamOptions,
|
|
670
|
+
): AssistantMessageEventStream {
|
|
671
|
+
const requestOptions = options as AntigravityStreamOptions | undefined;
|
|
672
|
+
const stream = createAssistantMessageEventStream();
|
|
673
|
+
|
|
674
|
+
(async () => {
|
|
675
|
+
const output: AssistantMessage = {
|
|
676
|
+
role: "assistant",
|
|
677
|
+
content: [],
|
|
678
|
+
api: model.api,
|
|
679
|
+
provider: model.provider,
|
|
680
|
+
model: model.id,
|
|
681
|
+
usage: emptyUsage(),
|
|
682
|
+
stopReason: "pending",
|
|
683
|
+
timestamp: Date.now(),
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const fail = (error: unknown) => {
|
|
687
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
688
|
+
output.errorMessage =
|
|
689
|
+
error instanceof Error ? error.message : String(error);
|
|
690
|
+
stream.push({
|
|
691
|
+
type: "error",
|
|
692
|
+
reason: output.stopReason,
|
|
693
|
+
error: output,
|
|
694
|
+
});
|
|
695
|
+
stream.end();
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const { token, projectId } = decodeApiKey(requestOptions?.apiKey ?? "");
|
|
700
|
+
if (!token) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"No Antigravity access token — run /login google-antigravity",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (!projectId) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
"No Cloud Code Assist project id in credentials — " +
|
|
708
|
+
"re-run /login google-antigravity",
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const fetcher = requestOptions?.fetch ?? fetch;
|
|
713
|
+
await ensureAntigravityVersion(requestOptions?.signal, fetcher);
|
|
714
|
+
const providerState = getSessionState(requestOptions, context);
|
|
715
|
+
let payload: unknown = buildRequestBody(
|
|
716
|
+
model,
|
|
717
|
+
context,
|
|
718
|
+
requestOptions,
|
|
719
|
+
projectId,
|
|
720
|
+
providerState,
|
|
721
|
+
);
|
|
722
|
+
const replacement = await requestOptions?.onPayload?.(payload, model);
|
|
723
|
+
if (replacement !== undefined) payload = replacement;
|
|
724
|
+
const body = JSON.stringify(payload);
|
|
725
|
+
const headers = mergeRequestHeaders(
|
|
726
|
+
{
|
|
727
|
+
Authorization: `Bearer ${token}`,
|
|
728
|
+
"Content-Type": "application/json",
|
|
729
|
+
Accept: "text/event-stream",
|
|
730
|
+
"User-Agent": getAntigravityUserAgent(),
|
|
731
|
+
...(isClaudeRoute(model.id) && model.reasoning
|
|
732
|
+
? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER }
|
|
733
|
+
: {}),
|
|
734
|
+
},
|
|
735
|
+
requestOptions?.headers,
|
|
736
|
+
);
|
|
737
|
+
const defaultFirstEventTimeout = model.id.includes("flash")
|
|
738
|
+
? FLASH_FIRST_EVENT_TIMEOUT_MS
|
|
739
|
+
: DEFAULT_FIRST_EVENT_TIMEOUT_MS;
|
|
740
|
+
const requestTimeout =
|
|
741
|
+
requestOptions?.timeoutMs && requestOptions.timeoutMs > 0
|
|
742
|
+
? requestOptions.timeoutMs
|
|
743
|
+
: undefined;
|
|
744
|
+
const firstEventTimeout =
|
|
745
|
+
requestTimeout !== undefined
|
|
746
|
+
? Math.min(requestTimeout, defaultFirstEventTimeout)
|
|
747
|
+
: defaultFirstEventTimeout;
|
|
748
|
+
|
|
749
|
+
let chunks: AsyncGenerator<CcaChunk> | undefined;
|
|
750
|
+
let lastError: Error | undefined;
|
|
751
|
+
for (const endpoint of ENDPOINTS) {
|
|
752
|
+
const isLast = endpoint === ENDPOINTS[ENDPOINTS.length - 1];
|
|
753
|
+
const attemptStartedAt = Date.now();
|
|
754
|
+
const firstEventDeadline = attemptStartedAt + firstEventTimeout;
|
|
755
|
+
const requestDeadline =
|
|
756
|
+
requestTimeout === undefined
|
|
757
|
+
? undefined
|
|
758
|
+
: attemptStartedAt + requestTimeout;
|
|
759
|
+
const attemptAbort = new AbortController();
|
|
760
|
+
const attemptSignal = requestOptions?.signal
|
|
761
|
+
? AbortSignal.any([requestOptions.signal, attemptAbort.signal])
|
|
762
|
+
: attemptAbort.signal;
|
|
763
|
+
let attempt: Response;
|
|
764
|
+
let headersTimer: ReturnType<typeof setTimeout> | undefined;
|
|
765
|
+
let onAbort: (() => void) | undefined;
|
|
766
|
+
try {
|
|
767
|
+
const pending = fetcher(
|
|
768
|
+
`${endpoint}/v1internal:streamGenerateContent?alt=sse`,
|
|
769
|
+
{ method: "POST", headers, body, signal: attemptSignal },
|
|
770
|
+
);
|
|
771
|
+
const gates: Promise<Response>[] = [pending];
|
|
772
|
+
gates.push(
|
|
773
|
+
new Promise<never>((_, reject) => {
|
|
774
|
+
headersTimer = setTimeout(
|
|
775
|
+
() => {
|
|
776
|
+
const error = new Error(
|
|
777
|
+
"Timed out waiting for Cloud Code Assist response headers",
|
|
778
|
+
);
|
|
779
|
+
reject(error);
|
|
780
|
+
attemptAbort.abort(error);
|
|
781
|
+
},
|
|
782
|
+
Math.max(1, firstEventDeadline - Date.now()),
|
|
783
|
+
);
|
|
784
|
+
}),
|
|
785
|
+
);
|
|
786
|
+
if (requestOptions?.signal) {
|
|
787
|
+
gates.push(
|
|
788
|
+
new Promise<never>((_, reject) => {
|
|
789
|
+
onAbort = () => {
|
|
790
|
+
const error = new Error("Request aborted");
|
|
791
|
+
reject(error);
|
|
792
|
+
attemptAbort.abort(error);
|
|
793
|
+
};
|
|
794
|
+
requestOptions.signal?.addEventListener("abort", onAbort, {
|
|
795
|
+
once: true,
|
|
796
|
+
});
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
attempt = await Promise.race(gates);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
// Network/transport failure: fail over before any bytes stream.
|
|
803
|
+
if (requestOptions?.signal?.aborted) throw error;
|
|
804
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
805
|
+
if (isLast) throw lastError;
|
|
806
|
+
continue;
|
|
807
|
+
} finally {
|
|
808
|
+
if (headersTimer) clearTimeout(headersTimer);
|
|
809
|
+
if (onAbort) {
|
|
810
|
+
requestOptions?.signal?.removeEventListener("abort", onAbort);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
await requestOptions?.onResponse?.(
|
|
814
|
+
{
|
|
815
|
+
status: attempt.status,
|
|
816
|
+
headers: Object.fromEntries(attempt.headers.entries()),
|
|
817
|
+
},
|
|
818
|
+
model,
|
|
819
|
+
);
|
|
820
|
+
if (!attempt.ok) {
|
|
821
|
+
let errorText: string;
|
|
822
|
+
try {
|
|
823
|
+
errorText = await readErrorResponseBody(
|
|
824
|
+
attempt.body,
|
|
825
|
+
attemptSignal,
|
|
826
|
+
requestDeadline ?? firstEventDeadline,
|
|
827
|
+
);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
attemptAbort.abort(error);
|
|
830
|
+
if (requestOptions?.signal?.aborted) throw error;
|
|
831
|
+
errorText = error instanceof Error ? error.message : String(error);
|
|
832
|
+
}
|
|
833
|
+
lastError = new Error(
|
|
834
|
+
`Cloud Code Assist API error (${attempt.status}): ${errorText}`,
|
|
835
|
+
);
|
|
836
|
+
if (!transientStatus(attempt.status) || isLast) throw lastError;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (!attempt.body) {
|
|
840
|
+
lastError = new Error(
|
|
841
|
+
"Cloud Code Assist returned an empty response body",
|
|
842
|
+
);
|
|
843
|
+
if (isLast) throw lastError;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const candidateChunks = readSseChunks(
|
|
847
|
+
attempt.body,
|
|
848
|
+
attemptSignal,
|
|
849
|
+
firstEventDeadline,
|
|
850
|
+
requestDeadline,
|
|
851
|
+
);
|
|
852
|
+
try {
|
|
853
|
+
const initial = await preflightChunks(candidateChunks);
|
|
854
|
+
chunks = prependChunks(initial, candidateChunks);
|
|
855
|
+
break;
|
|
856
|
+
} catch (error) {
|
|
857
|
+
await candidateChunks.return(undefined).catch(() => {});
|
|
858
|
+
if (requestOptions?.signal?.aborted) throw error;
|
|
859
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
860
|
+
if (
|
|
861
|
+
isLast ||
|
|
862
|
+
(error instanceof EndpointAttemptError && !error.retryable)
|
|
863
|
+
) {
|
|
864
|
+
throw lastError;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (!chunks) throw lastError ?? new Error("No endpoint reachable");
|
|
869
|
+
|
|
870
|
+
stream.push({ type: "start", partial: output });
|
|
871
|
+
|
|
872
|
+
let sawFinishReason = false;
|
|
873
|
+
let sawMeaningfulContent = false;
|
|
874
|
+
const contentIndex = () => output.content.length - 1;
|
|
875
|
+
const closeOpenBlock = () => {
|
|
876
|
+
const block = output.content[contentIndex()];
|
|
877
|
+
if (!block) return;
|
|
878
|
+
if (block.type === "text") {
|
|
879
|
+
stream.push({
|
|
880
|
+
type: "text_end",
|
|
881
|
+
contentIndex: contentIndex(),
|
|
882
|
+
content: block.text,
|
|
883
|
+
partial: output,
|
|
884
|
+
});
|
|
885
|
+
} else if (block.type === "thinking") {
|
|
886
|
+
stream.push({
|
|
887
|
+
type: "thinking_end",
|
|
888
|
+
contentIndex: contentIndex(),
|
|
889
|
+
content: block.thinking,
|
|
890
|
+
partial: output,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
let lastResponseId: string | undefined;
|
|
896
|
+
for await (const chunk of chunks) {
|
|
897
|
+
if (chunk.error) {
|
|
898
|
+
const detail =
|
|
899
|
+
chunk.error.message || chunk.error.status || "unknown error";
|
|
900
|
+
throw new Error(`Cloud Code Assist stream error: ${detail}`);
|
|
901
|
+
}
|
|
902
|
+
const data = chunk.response;
|
|
903
|
+
if (!data) continue;
|
|
904
|
+
if (data.responseId) lastResponseId = data.responseId;
|
|
905
|
+
if (!data.candidates?.length && data.promptFeedback?.blockReason) {
|
|
906
|
+
const detail = data.promptFeedback.blockReasonMessage;
|
|
907
|
+
throw new Error(
|
|
908
|
+
`Request blocked by Google (${data.promptFeedback.blockReason})` +
|
|
909
|
+
(detail ? `: ${detail}` : ""),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const candidate = data.candidates?.[0];
|
|
914
|
+
for (const part of candidate?.content?.parts ?? []) {
|
|
915
|
+
if (part.text !== undefined && part.text !== "") {
|
|
916
|
+
if (isThinkingPart(part)) {
|
|
917
|
+
const open = output.content[contentIndex()];
|
|
918
|
+
if (open?.type !== "thinking") {
|
|
919
|
+
closeOpenBlock();
|
|
920
|
+
output.content.push({
|
|
921
|
+
type: "thinking",
|
|
922
|
+
thinking: "",
|
|
923
|
+
});
|
|
924
|
+
stream.push({
|
|
925
|
+
type: "thinking_start",
|
|
926
|
+
contentIndex: contentIndex(),
|
|
927
|
+
partial: output,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const block = output.content[contentIndex()];
|
|
931
|
+
if (block.type === "thinking") {
|
|
932
|
+
block.thinking += part.text;
|
|
933
|
+
block.thinkingSignature = retainThoughtSignature(
|
|
934
|
+
block.thinkingSignature,
|
|
935
|
+
part.thoughtSignature,
|
|
936
|
+
);
|
|
937
|
+
stream.push({
|
|
938
|
+
type: "thinking_delta",
|
|
939
|
+
contentIndex: contentIndex(),
|
|
940
|
+
delta: part.text,
|
|
941
|
+
partial: output,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
if (part.text.trim().length > 0) sawMeaningfulContent = true;
|
|
946
|
+
const open = output.content[contentIndex()];
|
|
947
|
+
if (open?.type !== "text") {
|
|
948
|
+
closeOpenBlock();
|
|
949
|
+
output.content.push({ type: "text", text: "" });
|
|
950
|
+
stream.push({
|
|
951
|
+
type: "text_start",
|
|
952
|
+
contentIndex: contentIndex(),
|
|
953
|
+
partial: output,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const block = output.content[contentIndex()];
|
|
957
|
+
if (block.type === "text") {
|
|
958
|
+
block.text += part.text;
|
|
959
|
+
block.textSignature = retainThoughtSignature(
|
|
960
|
+
block.textSignature,
|
|
961
|
+
part.thoughtSignature,
|
|
962
|
+
);
|
|
963
|
+
stream.push({
|
|
964
|
+
type: "text_delta",
|
|
965
|
+
contentIndex: contentIndex(),
|
|
966
|
+
delta: part.text,
|
|
967
|
+
partial: output,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} else if (
|
|
972
|
+
part.text === "" &&
|
|
973
|
+
part.thoughtSignature &&
|
|
974
|
+
!part.functionCall
|
|
975
|
+
) {
|
|
976
|
+
const open = output.content[contentIndex()];
|
|
977
|
+
if (open?.type === "thinking") {
|
|
978
|
+
open.thinkingSignature = retainThoughtSignature(
|
|
979
|
+
open.thinkingSignature,
|
|
980
|
+
part.thoughtSignature,
|
|
981
|
+
);
|
|
982
|
+
} else if (open?.type === "text") {
|
|
983
|
+
open.textSignature = retainThoughtSignature(
|
|
984
|
+
open.textSignature,
|
|
985
|
+
part.thoughtSignature,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (part.functionCall) {
|
|
991
|
+
sawMeaningfulContent = true;
|
|
992
|
+
closeOpenBlock();
|
|
993
|
+
const call = part.functionCall;
|
|
994
|
+
const providedId = call.id;
|
|
995
|
+
const duplicated =
|
|
996
|
+
providedId !== undefined &&
|
|
997
|
+
output.content.some(
|
|
998
|
+
(b) => b.type === "toolCall" && b.id === providedId,
|
|
999
|
+
);
|
|
1000
|
+
const toolCall: ToolCall = {
|
|
1001
|
+
type: "toolCall",
|
|
1002
|
+
id:
|
|
1003
|
+
providedId && !duplicated
|
|
1004
|
+
? providedId
|
|
1005
|
+
: `call_${call.name ?? "tool"}_${++toolCallCounter}`,
|
|
1006
|
+
name: call.name ?? "",
|
|
1007
|
+
arguments: call.args ?? {},
|
|
1008
|
+
...(part.thoughtSignature
|
|
1009
|
+
? { thoughtSignature: part.thoughtSignature }
|
|
1010
|
+
: {}),
|
|
1011
|
+
};
|
|
1012
|
+
output.content.push(toolCall);
|
|
1013
|
+
const index = contentIndex();
|
|
1014
|
+
stream.push({
|
|
1015
|
+
type: "toolcall_start",
|
|
1016
|
+
contentIndex: index,
|
|
1017
|
+
partial: output,
|
|
1018
|
+
});
|
|
1019
|
+
stream.push({
|
|
1020
|
+
type: "toolcall_delta",
|
|
1021
|
+
contentIndex: index,
|
|
1022
|
+
delta: JSON.stringify(toolCall.arguments),
|
|
1023
|
+
partial: output,
|
|
1024
|
+
});
|
|
1025
|
+
stream.push({
|
|
1026
|
+
type: "toolcall_end",
|
|
1027
|
+
contentIndex: index,
|
|
1028
|
+
toolCall,
|
|
1029
|
+
partial: output,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (candidate?.finishReason) {
|
|
1035
|
+
sawFinishReason = true;
|
|
1036
|
+
const mapped = mapStopReasonString(candidate.finishReason);
|
|
1037
|
+
const hasToolCalls = output.content.some(
|
|
1038
|
+
(b) => b.type === "toolCall",
|
|
1039
|
+
);
|
|
1040
|
+
if ((mapped === "stop" || mapped === "length") && hasToolCalls) {
|
|
1041
|
+
output.stopReason = "toolUse";
|
|
1042
|
+
} else {
|
|
1043
|
+
output.stopReason = mapped;
|
|
1044
|
+
if (mapped === "error") {
|
|
1045
|
+
output.errorMessage = `Generation failed with finish reason: ${candidate.finishReason}`;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (data.usageMetadata) {
|
|
1051
|
+
const usage = data.usageMetadata;
|
|
1052
|
+
const promptTokens = usage.promptTokenCount ?? 0;
|
|
1053
|
+
const cacheReadTokens = usage.cachedContentTokenCount ?? 0;
|
|
1054
|
+
const thinkingTokens = usage.thoughtsTokenCount ?? 0;
|
|
1055
|
+
output.usage = {
|
|
1056
|
+
input: Math.max(0, promptTokens - cacheReadTokens),
|
|
1057
|
+
output: (usage.candidatesTokenCount ?? 0) + thinkingTokens,
|
|
1058
|
+
cacheRead: cacheReadTokens,
|
|
1059
|
+
cacheWrite: 0,
|
|
1060
|
+
totalTokens: usage.totalTokenCount ?? 0,
|
|
1061
|
+
cost: {
|
|
1062
|
+
input: 0,
|
|
1063
|
+
output: 0,
|
|
1064
|
+
cacheRead: 0,
|
|
1065
|
+
cacheWrite: 0,
|
|
1066
|
+
total: 0,
|
|
1067
|
+
},
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
closeOpenBlock();
|
|
1073
|
+
|
|
1074
|
+
if (requestOptions?.signal?.aborted) {
|
|
1075
|
+
output.stopReason = "aborted";
|
|
1076
|
+
} else if (output.stopReason === "pending") {
|
|
1077
|
+
if (!sawFinishReason) {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
"Cloud Code Assist stream ended without a finish reason " +
|
|
1080
|
+
"(connection dropped or response truncated)",
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
output.stopReason = "stop";
|
|
1084
|
+
}
|
|
1085
|
+
if (!sawMeaningfulContent && output.stopReason === "stop") {
|
|
1086
|
+
throw new Error("Cloud Code Assist API returned an empty response");
|
|
1087
|
+
}
|
|
1088
|
+
if (
|
|
1089
|
+
providerState &&
|
|
1090
|
+
output.stopReason !== "error" &&
|
|
1091
|
+
output.stopReason !== "aborted"
|
|
1092
|
+
) {
|
|
1093
|
+
providerState.lastExecutionId = lastResponseId;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (output.stopReason === "error" || output.stopReason === "aborted") {
|
|
1097
|
+
stream.push({
|
|
1098
|
+
type: "error",
|
|
1099
|
+
reason: output.stopReason,
|
|
1100
|
+
error: output,
|
|
1101
|
+
});
|
|
1102
|
+
} else {
|
|
1103
|
+
stream.push({
|
|
1104
|
+
type: "done",
|
|
1105
|
+
reason: output.stopReason,
|
|
1106
|
+
message: output,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
stream.end();
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
fail(error);
|
|
1112
|
+
}
|
|
1113
|
+
})();
|
|
1114
|
+
|
|
1115
|
+
return stream;
|
|
1116
|
+
}
|