@f5-sales-demo/pi-ai 19.51.2
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/CHANGELOG.md +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini CLI / Antigravity provider.
|
|
3
|
+
* Shared implementation for both google-gemini-cli and google-antigravity providers.
|
|
4
|
+
* Uses the Cloud Code Assist API endpoint to access Gemini and Claude models.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
|
+
import { abortableSleep, readSseJson } from "@f5xc-salesdemos/pi-utils";
|
|
8
|
+
import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "@google/genai";
|
|
9
|
+
import { calculateCost } from "../models";
|
|
10
|
+
import type {
|
|
11
|
+
Api,
|
|
12
|
+
AssistantMessage,
|
|
13
|
+
Context,
|
|
14
|
+
Model,
|
|
15
|
+
StreamFunction,
|
|
16
|
+
StreamOptions,
|
|
17
|
+
TextContent,
|
|
18
|
+
ThinkingContent,
|
|
19
|
+
ToolCall,
|
|
20
|
+
} from "../types";
|
|
21
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
22
|
+
import { appendRawHttpRequestDumpFor400, type RawHttpRequestDump, withHttpStatus } from "../utils/http-inspector";
|
|
23
|
+
import { refreshAntigravityToken } from "../utils/oauth/google-antigravity";
|
|
24
|
+
import { refreshGoogleCloudToken } from "../utils/oauth/google-gemini-cli";
|
|
25
|
+
import { extractHttpStatusFromError } from "../utils/retry";
|
|
26
|
+
import { sanitizeSchemaForCCA } from "../utils/schema";
|
|
27
|
+
import {
|
|
28
|
+
convertMessages,
|
|
29
|
+
convertTools,
|
|
30
|
+
isThinkingPart,
|
|
31
|
+
mapStopReasonString,
|
|
32
|
+
mapToolChoice,
|
|
33
|
+
retainThoughtSignature,
|
|
34
|
+
} from "./google-shared";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Thinking level for Gemini 3 models.
|
|
38
|
+
* Mirrors Google's ThinkingLevel enum values.
|
|
39
|
+
*/
|
|
40
|
+
export type GoogleThinkingLevel = "THINKING_LEVEL_UNSPECIFIED" | "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
|
|
41
|
+
|
|
42
|
+
export interface GoogleGeminiCliOptions extends StreamOptions {
|
|
43
|
+
toolChoice?: "auto" | "none" | "any";
|
|
44
|
+
/**
|
|
45
|
+
* Thinking/reasoning configuration.
|
|
46
|
+
* - Gemini 2.x models: use `budgetTokens` to set the thinking budget
|
|
47
|
+
* - Gemini 3 models (gemini-3-pro-*, gemini-3-flash-*): use `level` instead
|
|
48
|
+
*
|
|
49
|
+
* When using `streamSimple`, this is handled automatically based on the model.
|
|
50
|
+
*/
|
|
51
|
+
thinking?: {
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
/** Thinking budget in tokens. Use for Gemini 2.x models. */
|
|
54
|
+
budgetTokens?: number;
|
|
55
|
+
/** Thinking level. Use for Gemini 3 models (LOW/HIGH for Pro, MINIMAL/LOW/MEDIUM/HIGH for Flash). */
|
|
56
|
+
level?: GoogleThinkingLevel;
|
|
57
|
+
};
|
|
58
|
+
projectId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
62
|
+
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
63
|
+
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
64
|
+
const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a User-Agent string that identifies as Gemini CLI to unlock higher rate limits.
|
|
68
|
+
* Uses the same format as the official Gemini CLI (v0.35+):
|
|
69
|
+
* GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
|
|
70
|
+
*/
|
|
71
|
+
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
72
|
+
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.35.3";
|
|
73
|
+
const platform = process.platform === "win32" ? "win32" : process.platform;
|
|
74
|
+
const arch = process.arch === "x64" ? "x64" : process.arch;
|
|
75
|
+
return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ANTIGRAVITY_USER_AGENT = (() => {
|
|
79
|
+
const DEFAULT_ANTIGRAVITY_VERSION = "1.104.0";
|
|
80
|
+
const version = process.env.PI_AI_ANTIGRAVITY_VERSION || DEFAULT_ANTIGRAVITY_VERSION;
|
|
81
|
+
// Map Node.js platform/arch to Antigravity's expected format.
|
|
82
|
+
// Verified against Antigravity source: _qn() and wqn() in main.js.
|
|
83
|
+
// process.platform: win32→windows, others pass through (darwin, linux)
|
|
84
|
+
// process.arch: x64→amd64, ia32→386, others pass through (arm64)
|
|
85
|
+
const os = process.platform === "win32" ? "windows" : process.platform;
|
|
86
|
+
const arch = process.arch === "x64" ? "amd64" : process.arch === "ia32" ? "386" : process.arch;
|
|
87
|
+
return `antigravity/${version} ${os}/${arch}`;
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
const GEMINI_CLI_HEADERS = (modelId?: string) =>
|
|
91
|
+
Object.freeze({
|
|
92
|
+
"User-Agent": getGeminiCliUserAgent(modelId),
|
|
93
|
+
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Antigravity auth headers (project discovery/onboarding).
|
|
97
|
+
// Verified from binary: kae.w() and kae.y() send only Content-Type + User-Agent.
|
|
98
|
+
// X-Goog-Api-Client and Client-Metadata are NOT sent by the real client — product
|
|
99
|
+
// identification (ideType, ideName, ideVersion) goes in the protobuf request body.
|
|
100
|
+
const ANTIGRAVITY_AUTH_HEADERS = Object.freeze({
|
|
101
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Antigravity executor headers (streaming/generation).
|
|
105
|
+
// Same header set as auth calls — only User-Agent per binary analysis.
|
|
106
|
+
const ANTIGRAVITY_STREAMING_HEADERS = Object.freeze({
|
|
107
|
+
"User-Agent": ANTIGRAVITY_USER_AGENT,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Headers for Gemini CLI (prod endpoint)
|
|
111
|
+
export function getGeminiCliHeaders(modelId?: string) {
|
|
112
|
+
return GEMINI_CLI_HEADERS(modelId);
|
|
113
|
+
}
|
|
114
|
+
export function getGeminiCliUserAgentValue(modelId?: string) {
|
|
115
|
+
return getGeminiCliUserAgent(modelId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Headers for Antigravity (sandbox endpoint)
|
|
119
|
+
export function getAntigravityAuthHeaders() {
|
|
120
|
+
return ANTIGRAVITY_AUTH_HEADERS;
|
|
121
|
+
}
|
|
122
|
+
export function getAntigravityHeaders() {
|
|
123
|
+
return ANTIGRAVITY_STREAMING_HEADERS;
|
|
124
|
+
}
|
|
125
|
+
export function getAntigravityUserAgent() {
|
|
126
|
+
return ANTIGRAVITY_USER_AGENT;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Antigravity system instruction (compact version from CLIProxyAPI).
|
|
130
|
+
export const ANTIGRAVITY_SYSTEM_INSTRUCTION =
|
|
131
|
+
"You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding." +
|
|
132
|
+
"You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question." +
|
|
133
|
+
"**Absolute paths only**" +
|
|
134
|
+
"**Proactiveness**";
|
|
135
|
+
|
|
136
|
+
// Counter for generating unique tool call IDs
|
|
137
|
+
let toolCallCounter = 0;
|
|
138
|
+
|
|
139
|
+
// Retry configuration
|
|
140
|
+
const MAX_RETRIES = 3;
|
|
141
|
+
const BASE_DELAY_MS = 1000;
|
|
142
|
+
const MAX_EMPTY_STREAM_RETRIES = 2;
|
|
143
|
+
const EMPTY_STREAM_BASE_DELAY_MS = 500;
|
|
144
|
+
const RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
145
|
+
const CLAUDE_THINKING_BETA_HEADER = "interleaved-thinking-2025-05-14";
|
|
146
|
+
const GOOGLE_GEMINI_REFRESH_SKEW_MS = 60_000;
|
|
147
|
+
const ANTIGRAVITY_REFRESH_SKEW_MS = 60_000;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract retry delay from Gemini error response (in milliseconds).
|
|
151
|
+
* Checks headers first (Retry-After, x-ratelimit-reset, x-ratelimit-reset-after),
|
|
152
|
+
* then parses body patterns like:
|
|
153
|
+
* - "Your quota will reset after 39s"
|
|
154
|
+
* - "Your quota will reset after 18h31m10s"
|
|
155
|
+
* - "Please retry in Xs" or "Please retry in Xms"
|
|
156
|
+
* - "retryDelay": "34.074824224s" (JSON field)
|
|
157
|
+
*/
|
|
158
|
+
export function extractRetryDelay(errorText: string, response?: Response | Headers): number | undefined {
|
|
159
|
+
const normalizeDelay = (ms: number): number | undefined => (ms > 0 ? Math.ceil(ms + 1000) : undefined);
|
|
160
|
+
|
|
161
|
+
const headers = response instanceof Headers ? response : response?.headers;
|
|
162
|
+
if (headers) {
|
|
163
|
+
const retryAfter = headers.get("retry-after");
|
|
164
|
+
if (retryAfter) {
|
|
165
|
+
const retryAfterSeconds = Number(retryAfter);
|
|
166
|
+
if (Number.isFinite(retryAfterSeconds)) {
|
|
167
|
+
const delay = normalizeDelay(retryAfterSeconds * 1000);
|
|
168
|
+
if (delay !== undefined) {
|
|
169
|
+
return delay;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const retryAfterDate = new Date(retryAfter);
|
|
173
|
+
const retryAfterMs = retryAfterDate.getTime();
|
|
174
|
+
if (!Number.isNaN(retryAfterMs)) {
|
|
175
|
+
const delay = normalizeDelay(retryAfterMs - Date.now());
|
|
176
|
+
if (delay !== undefined) {
|
|
177
|
+
return delay;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const rateLimitReset = headers.get("x-ratelimit-reset");
|
|
183
|
+
if (rateLimitReset) {
|
|
184
|
+
const resetSeconds = Number.parseInt(rateLimitReset, 10);
|
|
185
|
+
if (!Number.isNaN(resetSeconds)) {
|
|
186
|
+
const delay = normalizeDelay(resetSeconds * 1000 - Date.now());
|
|
187
|
+
if (delay !== undefined) {
|
|
188
|
+
return delay;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rateLimitResetAfter = headers.get("x-ratelimit-reset-after");
|
|
194
|
+
if (rateLimitResetAfter) {
|
|
195
|
+
const resetAfterSeconds = Number(rateLimitResetAfter);
|
|
196
|
+
if (Number.isFinite(resetAfterSeconds)) {
|
|
197
|
+
const delay = normalizeDelay(resetAfterSeconds * 1000);
|
|
198
|
+
if (delay !== undefined) {
|
|
199
|
+
return delay;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Pattern 1: "Your quota will reset after ..." (formats: "18h31m10s", "10m15s", "6s", "39s")
|
|
206
|
+
const durationMatch = errorText.match(/reset after (?:(\d+)h)?(?:(\d+)m)?(\d+(?:\.\d+)?)s/i);
|
|
207
|
+
if (durationMatch) {
|
|
208
|
+
const hours = durationMatch[1] ? parseInt(durationMatch[1], 10) : 0;
|
|
209
|
+
const minutes = durationMatch[2] ? parseInt(durationMatch[2], 10) : 0;
|
|
210
|
+
const seconds = parseFloat(durationMatch[3]);
|
|
211
|
+
if (!Number.isNaN(seconds)) {
|
|
212
|
+
const totalMs = ((hours * 60 + minutes) * 60 + seconds) * 1000;
|
|
213
|
+
const delay = normalizeDelay(totalMs);
|
|
214
|
+
if (delay !== undefined) {
|
|
215
|
+
return delay;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Pattern 2: "Please retry in X[ms|s]"
|
|
221
|
+
const retryInMatch = errorText.match(/Please retry in ([0-9.]+)(ms|s)/i);
|
|
222
|
+
if (retryInMatch?.[1]) {
|
|
223
|
+
const value = parseFloat(retryInMatch[1]);
|
|
224
|
+
if (!Number.isNaN(value) && value > 0) {
|
|
225
|
+
const ms = retryInMatch[2].toLowerCase() === "ms" ? value : value * 1000;
|
|
226
|
+
const delay = normalizeDelay(ms);
|
|
227
|
+
if (delay !== undefined) {
|
|
228
|
+
return delay;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Pattern 3: "retryDelay": "34.074824224s" (JSON field in error details)
|
|
234
|
+
const retryDelayMatch = errorText.match(/"retryDelay":\s*"([0-9.]+)(ms|s)"/i);
|
|
235
|
+
if (retryDelayMatch?.[1]) {
|
|
236
|
+
const value = parseFloat(retryDelayMatch[1]);
|
|
237
|
+
if (!Number.isNaN(value) && value > 0) {
|
|
238
|
+
const ms = retryDelayMatch[2].toLowerCase() === "ms" ? value : value * 1000;
|
|
239
|
+
const delay = normalizeDelay(ms);
|
|
240
|
+
if (delay !== undefined) {
|
|
241
|
+
return delay;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isClaudeModel(modelId: string): boolean {
|
|
250
|
+
return modelId.toLowerCase().includes("claude");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function needsClaudeThinkingBetaHeader(model: Model<"google-gemini-cli">): boolean {
|
|
254
|
+
return model.provider === "google-antigravity" && model.id.startsWith("claude-") && model.reasoning;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function shouldInjectAntigravitySystemInstruction(modelId: string): boolean {
|
|
258
|
+
const normalized = modelId.toLowerCase();
|
|
259
|
+
return normalized.includes("claude") || normalized.includes("gemini-3-pro-high");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if an error is retryable (rate limit, server error, network error, etc.)
|
|
264
|
+
*/
|
|
265
|
+
function isRetryableError(status: number, errorText: string): boolean {
|
|
266
|
+
if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return /resource.?exhausted|rate.?limit|overloaded|service.?unavailable|other.?side.?closed/i.test(errorText);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract a clean, user-friendly error message from Google API error response.
|
|
274
|
+
* Parses JSON error responses and returns just the message field.
|
|
275
|
+
*/
|
|
276
|
+
function extractErrorMessage(errorText: string): string {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(errorText) as { error?: { message?: string } };
|
|
279
|
+
if (parsed.error?.message) {
|
|
280
|
+
return parsed.error.message;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// Not JSON, return as-is
|
|
284
|
+
}
|
|
285
|
+
return errorText;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface GeminiCliApiKeyPayload {
|
|
289
|
+
token?: unknown;
|
|
290
|
+
projectId?: unknown;
|
|
291
|
+
project_id?: unknown;
|
|
292
|
+
refreshToken?: unknown;
|
|
293
|
+
expiresAt?: unknown;
|
|
294
|
+
refresh?: unknown;
|
|
295
|
+
expires?: unknown;
|
|
296
|
+
}
|
|
297
|
+
interface ParsedGeminiCliCredentials {
|
|
298
|
+
accessToken: string;
|
|
299
|
+
projectId: string;
|
|
300
|
+
refreshToken?: string;
|
|
301
|
+
expiresAt?: number;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeExpiryMs(value: unknown): number | undefined {
|
|
305
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
return value < 10_000_000_000 ? value * 1000 : value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function parseGeminiCliCredentials(apiKeyRaw: string): ParsedGeminiCliCredentials {
|
|
312
|
+
const invalidCredentialsMessage = "Invalid Google Cloud Code Assist credentials. Use /login to re-authenticate.";
|
|
313
|
+
const missingCredentialsMessage =
|
|
314
|
+
"Missing token or projectId in Google Cloud credentials. Use /login to re-authenticate.";
|
|
315
|
+
|
|
316
|
+
let parsed: GeminiCliApiKeyPayload;
|
|
317
|
+
try {
|
|
318
|
+
parsed = JSON.parse(apiKeyRaw) as GeminiCliApiKeyPayload;
|
|
319
|
+
} catch {
|
|
320
|
+
throw new Error(invalidCredentialsMessage);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const projectId =
|
|
324
|
+
typeof parsed.projectId === "string"
|
|
325
|
+
? parsed.projectId
|
|
326
|
+
: typeof parsed.project_id === "string"
|
|
327
|
+
? parsed.project_id
|
|
328
|
+
: undefined;
|
|
329
|
+
|
|
330
|
+
if (typeof parsed.token !== "string" || typeof projectId !== "string") {
|
|
331
|
+
throw new Error(missingCredentialsMessage);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const refreshToken =
|
|
335
|
+
typeof parsed.refreshToken === "string"
|
|
336
|
+
? parsed.refreshToken
|
|
337
|
+
: typeof parsed.refresh === "string"
|
|
338
|
+
? parsed.refresh
|
|
339
|
+
: undefined;
|
|
340
|
+
const expiresAt = normalizeExpiryMs(parsed.expiresAt ?? parsed.expires);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
accessToken: parsed.token,
|
|
344
|
+
projectId,
|
|
345
|
+
refreshToken,
|
|
346
|
+
expiresAt,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function shouldRefreshGeminiCliCredentials(
|
|
351
|
+
expiresAt: number | undefined,
|
|
352
|
+
isAntigravity: boolean,
|
|
353
|
+
nowMs = Date.now(),
|
|
354
|
+
): boolean {
|
|
355
|
+
if (expiresAt === undefined) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const skewMs = isAntigravity ? ANTIGRAVITY_REFRESH_SKEW_MS : GOOGLE_GEMINI_REFRESH_SKEW_MS;
|
|
360
|
+
return nowMs + skewMs >= expiresAt;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function refreshGeminiCliCredentialsIfNeeded(
|
|
364
|
+
credentials: ParsedGeminiCliCredentials,
|
|
365
|
+
isAntigravity: boolean,
|
|
366
|
+
): Promise<ParsedGeminiCliCredentials> {
|
|
367
|
+
if (!credentials.refreshToken || !shouldRefreshGeminiCliCredentials(credentials.expiresAt, isAntigravity)) {
|
|
368
|
+
return credentials;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const refreshed = isAntigravity
|
|
373
|
+
? await refreshAntigravityToken(credentials.refreshToken, credentials.projectId)
|
|
374
|
+
: await refreshGoogleCloudToken(credentials.refreshToken, credentials.projectId);
|
|
375
|
+
return {
|
|
376
|
+
accessToken: refreshed.access,
|
|
377
|
+
projectId: credentials.projectId,
|
|
378
|
+
refreshToken: refreshed.refresh,
|
|
379
|
+
expiresAt: refreshed.expires,
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
383
|
+
// Permanent auth failure (revoked/invalid token) — re-authentication required.
|
|
384
|
+
// Google returns 400 invalid_grant when a token is revoked or expired server-side.
|
|
385
|
+
if (/invalid_grant|invalid_token|token.*revoked|account.*disabled/i.test(reason)) {
|
|
386
|
+
throw new Error(`OAuth token has been revoked or invalidated. Use /login to re-authenticate. (${reason})`);
|
|
387
|
+
}
|
|
388
|
+
// Transient failure (network, 5xx) — fall back to existing token if not yet expired.
|
|
389
|
+
if (credentials.expiresAt !== undefined && Date.now() >= credentials.expiresAt) {
|
|
390
|
+
throw new Error(`OAuth token refresh failed before request: ${reason}`);
|
|
391
|
+
}
|
|
392
|
+
return credentials;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
interface CloudCodeAssistRequest {
|
|
396
|
+
project: string;
|
|
397
|
+
model: string;
|
|
398
|
+
request: {
|
|
399
|
+
contents: Content[];
|
|
400
|
+
sessionId?: string;
|
|
401
|
+
systemInstruction?: { role?: string; parts: { text: string }[] };
|
|
402
|
+
generationConfig?: {
|
|
403
|
+
maxOutputTokens?: number;
|
|
404
|
+
temperature?: number;
|
|
405
|
+
topP?: number;
|
|
406
|
+
topK?: number;
|
|
407
|
+
minP?: number;
|
|
408
|
+
presencePenalty?: number;
|
|
409
|
+
repetitionPenalty?: number;
|
|
410
|
+
thinkingConfig?: ThinkingConfig;
|
|
411
|
+
};
|
|
412
|
+
tools?: { functionDeclarations: Record<string, unknown>[] }[] | undefined;
|
|
413
|
+
toolConfig?: {
|
|
414
|
+
functionCallingConfig: {
|
|
415
|
+
mode: FunctionCallingConfigMode;
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
requestType?: string;
|
|
420
|
+
userAgent?: string;
|
|
421
|
+
requestId?: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
interface CloudCodeAssistResponseChunk {
|
|
425
|
+
response?: {
|
|
426
|
+
candidates?: Array<{
|
|
427
|
+
content?: {
|
|
428
|
+
role: string;
|
|
429
|
+
parts?: Array<{
|
|
430
|
+
text?: string;
|
|
431
|
+
thought?: boolean;
|
|
432
|
+
thoughtSignature?: string;
|
|
433
|
+
functionCall?: {
|
|
434
|
+
name: string;
|
|
435
|
+
args: Record<string, unknown>;
|
|
436
|
+
id?: string;
|
|
437
|
+
};
|
|
438
|
+
}>;
|
|
439
|
+
};
|
|
440
|
+
finishReason?: string;
|
|
441
|
+
}>;
|
|
442
|
+
usageMetadata?: {
|
|
443
|
+
promptTokenCount?: number;
|
|
444
|
+
candidatesTokenCount?: number;
|
|
445
|
+
thoughtsTokenCount?: number;
|
|
446
|
+
totalTokenCount?: number;
|
|
447
|
+
cachedContentTokenCount?: number;
|
|
448
|
+
};
|
|
449
|
+
modelVersion?: string;
|
|
450
|
+
responseId?: string;
|
|
451
|
+
};
|
|
452
|
+
traceId?: string;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
456
|
+
model: Model<"google-gemini-cli">,
|
|
457
|
+
context: Context,
|
|
458
|
+
options?: GoogleGeminiCliOptions,
|
|
459
|
+
): AssistantMessageEventStream => {
|
|
460
|
+
const stream = new AssistantMessageEventStream();
|
|
461
|
+
|
|
462
|
+
(async () => {
|
|
463
|
+
const startTime = Date.now();
|
|
464
|
+
let firstTokenTime: number | undefined;
|
|
465
|
+
|
|
466
|
+
const output: AssistantMessage = {
|
|
467
|
+
role: "assistant",
|
|
468
|
+
content: [],
|
|
469
|
+
api: "google-gemini-cli" as Api,
|
|
470
|
+
provider: model.provider,
|
|
471
|
+
model: model.id,
|
|
472
|
+
usage: {
|
|
473
|
+
input: 0,
|
|
474
|
+
output: 0,
|
|
475
|
+
cacheRead: 0,
|
|
476
|
+
cacheWrite: 0,
|
|
477
|
+
totalTokens: 0,
|
|
478
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
479
|
+
},
|
|
480
|
+
stopReason: "stop",
|
|
481
|
+
timestamp: Date.now(),
|
|
482
|
+
};
|
|
483
|
+
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const apiKeyRaw = options?.apiKey;
|
|
487
|
+
if (!apiKeyRaw) {
|
|
488
|
+
throw new Error("Google Cloud Code Assist requires OAuth authentication. Use /login to authenticate.");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const isAntigravity = model.provider === "google-antigravity";
|
|
492
|
+
const parsedCredentials = parseGeminiCliCredentials(apiKeyRaw);
|
|
493
|
+
const activeCredentials = await refreshGeminiCliCredentialsIfNeeded(parsedCredentials, isAntigravity);
|
|
494
|
+
const { accessToken, projectId } = activeCredentials;
|
|
495
|
+
|
|
496
|
+
const baseUrl = model.baseUrl?.trim();
|
|
497
|
+
const endpoints = baseUrl ? [baseUrl] : isAntigravity ? ANTIGRAVITY_ENDPOINT_FALLBACKS : [DEFAULT_ENDPOINT];
|
|
498
|
+
|
|
499
|
+
let requestBody = buildRequest(model, context, projectId, options, isAntigravity);
|
|
500
|
+
const replacementPayload = await options?.onPayload?.(requestBody, model);
|
|
501
|
+
if (replacementPayload !== undefined) {
|
|
502
|
+
requestBody = replacementPayload as typeof requestBody;
|
|
503
|
+
}
|
|
504
|
+
const headers = isAntigravity ? getAntigravityHeaders() : getGeminiCliHeaders(model.id);
|
|
505
|
+
|
|
506
|
+
const requestHeaders = {
|
|
507
|
+
Authorization: `Bearer ${accessToken}`,
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
Accept: "text/event-stream",
|
|
510
|
+
...headers,
|
|
511
|
+
...(needsClaudeThinkingBetaHeader(model) ? { "anthropic-beta": CLAUDE_THINKING_BETA_HEADER } : {}),
|
|
512
|
+
...(options?.headers ?? {}),
|
|
513
|
+
};
|
|
514
|
+
const requestBodyJson = JSON.stringify(requestBody);
|
|
515
|
+
rawRequestDump = {
|
|
516
|
+
provider: model.provider,
|
|
517
|
+
api: output.api,
|
|
518
|
+
model: model.id,
|
|
519
|
+
method: "POST",
|
|
520
|
+
body: requestBody,
|
|
521
|
+
headers: requestHeaders,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Fetch with retry logic for rate limits and transient errors
|
|
525
|
+
let response: Response | undefined;
|
|
526
|
+
let lastError: Error | undefined;
|
|
527
|
+
let requestUrl: string | undefined;
|
|
528
|
+
let rateLimitTimeSpent = 0;
|
|
529
|
+
|
|
530
|
+
for (let attempt = 0; ; attempt++) {
|
|
531
|
+
if (options?.signal?.aborted) {
|
|
532
|
+
throw new Error("Request was aborted");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const endpoint = endpoints[Math.min(attempt, endpoints.length - 1)];
|
|
537
|
+
requestUrl = `${endpoint}/v1internal:streamGenerateContent?alt=sse`;
|
|
538
|
+
response = await fetch(requestUrl, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: requestHeaders,
|
|
541
|
+
body: requestBodyJson,
|
|
542
|
+
signal: options?.signal,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
if (response.ok) {
|
|
546
|
+
break; // Success, exit retry loop
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const errorText = await response.text();
|
|
550
|
+
|
|
551
|
+
// Handle 429 rate limits with time budget
|
|
552
|
+
if (response.status === 429) {
|
|
553
|
+
if (/quota|exhausted/i.test(errorText)) {
|
|
554
|
+
throw withHttpStatus(
|
|
555
|
+
new Error(`Cloud Code Assist API error (429): ${extractErrorMessage(errorText)}`),
|
|
556
|
+
429,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const serverDelay = extractRetryDelay(errorText, response);
|
|
560
|
+
if (serverDelay && rateLimitTimeSpent + serverDelay <= RATE_LIMIT_BUDGET_MS) {
|
|
561
|
+
rateLimitTimeSpent += serverDelay;
|
|
562
|
+
await abortableSleep(serverDelay, options?.signal);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
// Fallback: use exponential backoff if no server delay, up to MAX_RETRIES
|
|
566
|
+
if (!serverDelay && attempt < MAX_RETRIES) {
|
|
567
|
+
await abortableSleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
} else if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
571
|
+
// Non-429 retryable errors use standard attempt cap
|
|
572
|
+
const serverDelay = extractRetryDelay(errorText, response);
|
|
573
|
+
const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt;
|
|
574
|
+
|
|
575
|
+
// Check if server delay exceeds max allowed (default: 60s) for non-429 errors
|
|
576
|
+
const maxDelayMs = options?.maxRetryDelayMs ?? 60000;
|
|
577
|
+
if (maxDelayMs > 0 && serverDelay && serverDelay > maxDelayMs) {
|
|
578
|
+
const delaySeconds = Math.ceil(serverDelay / 1000);
|
|
579
|
+
throw withHttpStatus(
|
|
580
|
+
new Error(
|
|
581
|
+
`Server requested ${delaySeconds}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${extractErrorMessage(errorText)}`,
|
|
582
|
+
),
|
|
583
|
+
response.status,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await abortableSleep(delayMs, options?.signal);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Not retryable or budget exceeded
|
|
592
|
+
throw withHttpStatus(
|
|
593
|
+
new Error(`Cloud Code Assist API error (${response.status}): ${extractErrorMessage(errorText)}`),
|
|
594
|
+
response.status,
|
|
595
|
+
);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
// Check for abort - fetch throws AbortError, our code throws "Request was aborted"
|
|
598
|
+
if (error instanceof Error) {
|
|
599
|
+
if (error.name === "AbortError" || error.message === "Request was aborted") {
|
|
600
|
+
throw new Error("Request was aborted");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// HTTP responses are handled inside the try block.
|
|
605
|
+
// If we intentionally throw with status metadata, don't convert it into a network retry.
|
|
606
|
+
if (extractHttpStatusFromError(error) !== undefined) {
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
// Extract detailed error message from fetch errors (Node includes cause)
|
|
610
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
611
|
+
if (lastError.message === "fetch failed" && lastError.cause instanceof Error) {
|
|
612
|
+
lastError = new Error(`Network error: ${lastError.cause.message}`);
|
|
613
|
+
}
|
|
614
|
+
// Network errors are retryable
|
|
615
|
+
if (attempt < MAX_RETRIES) {
|
|
616
|
+
const delayMs = BASE_DELAY_MS * 2 ** attempt;
|
|
617
|
+
await abortableSleep(delayMs, options?.signal);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
throw lastError;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!response?.ok) {
|
|
625
|
+
throw lastError ?? new Error("Failed to get response after retries");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let started = false;
|
|
629
|
+
const ensureStarted = () => {
|
|
630
|
+
if (!started) {
|
|
631
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
632
|
+
stream.push({ type: "start", partial: output });
|
|
633
|
+
started = true;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const resetOutput = () => {
|
|
638
|
+
output.content = [];
|
|
639
|
+
output.usage = {
|
|
640
|
+
input: 0,
|
|
641
|
+
output: 0,
|
|
642
|
+
cacheRead: 0,
|
|
643
|
+
cacheWrite: 0,
|
|
644
|
+
totalTokens: 0,
|
|
645
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
646
|
+
};
|
|
647
|
+
output.stopReason = "stop";
|
|
648
|
+
output.errorMessage = undefined;
|
|
649
|
+
output.timestamp = Date.now();
|
|
650
|
+
started = false;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const streamResponse = async (activeResponse: Response): Promise<boolean> => {
|
|
654
|
+
if (!activeResponse.body) {
|
|
655
|
+
throw new Error("No response body");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let hasContent = false;
|
|
659
|
+
let currentBlock: TextContent | ThinkingContent | null = null;
|
|
660
|
+
const blocks = output.content;
|
|
661
|
+
const blockIndex = () => blocks.length - 1;
|
|
662
|
+
|
|
663
|
+
for await (const chunk of readSseJson<CloudCodeAssistResponseChunk>(
|
|
664
|
+
activeResponse.body!,
|
|
665
|
+
options?.signal,
|
|
666
|
+
)) {
|
|
667
|
+
const responseData = chunk.response;
|
|
668
|
+
if (!responseData) continue;
|
|
669
|
+
|
|
670
|
+
const candidate = responseData.candidates?.[0];
|
|
671
|
+
if (candidate?.content?.parts) {
|
|
672
|
+
for (const part of candidate.content.parts) {
|
|
673
|
+
if (part.text !== undefined) {
|
|
674
|
+
hasContent = true;
|
|
675
|
+
const isThinking = isThinkingPart(part);
|
|
676
|
+
if (
|
|
677
|
+
!currentBlock ||
|
|
678
|
+
(isThinking && currentBlock.type !== "thinking") ||
|
|
679
|
+
(!isThinking && currentBlock.type !== "text")
|
|
680
|
+
) {
|
|
681
|
+
if (currentBlock) {
|
|
682
|
+
if (currentBlock.type === "text") {
|
|
683
|
+
stream.push({
|
|
684
|
+
type: "text_end",
|
|
685
|
+
contentIndex: blocks.length - 1,
|
|
686
|
+
content: currentBlock.text,
|
|
687
|
+
partial: output,
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
stream.push({
|
|
691
|
+
type: "thinking_end",
|
|
692
|
+
contentIndex: blockIndex(),
|
|
693
|
+
content: currentBlock.thinking,
|
|
694
|
+
partial: output,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (isThinking) {
|
|
699
|
+
currentBlock = { type: "thinking", thinking: "", thinkingSignature: undefined };
|
|
700
|
+
output.content.push(currentBlock);
|
|
701
|
+
ensureStarted();
|
|
702
|
+
stream.push({
|
|
703
|
+
type: "thinking_start",
|
|
704
|
+
contentIndex: blockIndex(),
|
|
705
|
+
partial: output,
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
currentBlock = { type: "text", text: "" };
|
|
709
|
+
output.content.push(currentBlock);
|
|
710
|
+
ensureStarted();
|
|
711
|
+
stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (currentBlock.type === "thinking") {
|
|
715
|
+
currentBlock.thinking += part.text;
|
|
716
|
+
currentBlock.thinkingSignature = retainThoughtSignature(
|
|
717
|
+
currentBlock.thinkingSignature,
|
|
718
|
+
part.thoughtSignature,
|
|
719
|
+
);
|
|
720
|
+
stream.push({
|
|
721
|
+
type: "thinking_delta",
|
|
722
|
+
contentIndex: blockIndex(),
|
|
723
|
+
delta: part.text,
|
|
724
|
+
partial: output,
|
|
725
|
+
});
|
|
726
|
+
} else {
|
|
727
|
+
currentBlock.text += part.text;
|
|
728
|
+
currentBlock.textSignature = retainThoughtSignature(
|
|
729
|
+
currentBlock.textSignature,
|
|
730
|
+
part.thoughtSignature,
|
|
731
|
+
);
|
|
732
|
+
stream.push({
|
|
733
|
+
type: "text_delta",
|
|
734
|
+
contentIndex: blockIndex(),
|
|
735
|
+
delta: part.text,
|
|
736
|
+
partial: output,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (part.functionCall) {
|
|
742
|
+
hasContent = true;
|
|
743
|
+
if (currentBlock) {
|
|
744
|
+
if (currentBlock.type === "text") {
|
|
745
|
+
stream.push({
|
|
746
|
+
type: "text_end",
|
|
747
|
+
contentIndex: blockIndex(),
|
|
748
|
+
content: currentBlock.text,
|
|
749
|
+
partial: output,
|
|
750
|
+
});
|
|
751
|
+
} else {
|
|
752
|
+
stream.push({
|
|
753
|
+
type: "thinking_end",
|
|
754
|
+
contentIndex: blockIndex(),
|
|
755
|
+
content: currentBlock.thinking,
|
|
756
|
+
partial: output,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
currentBlock = null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const providedId = part.functionCall.id;
|
|
763
|
+
const needsNewId =
|
|
764
|
+
!providedId || output.content.some(b => b.type === "toolCall" && b.id === providedId);
|
|
765
|
+
const toolCallId = needsNewId
|
|
766
|
+
? `${part.functionCall.name}_${Date.now()}_${++toolCallCounter}`
|
|
767
|
+
: providedId;
|
|
768
|
+
|
|
769
|
+
const toolCall: ToolCall = {
|
|
770
|
+
type: "toolCall",
|
|
771
|
+
id: toolCallId,
|
|
772
|
+
name: part.functionCall.name || "",
|
|
773
|
+
arguments: part.functionCall.args as Record<string, unknown>,
|
|
774
|
+
...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }),
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
output.content.push(toolCall);
|
|
778
|
+
ensureStarted();
|
|
779
|
+
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
|
|
780
|
+
stream.push({
|
|
781
|
+
type: "toolcall_delta",
|
|
782
|
+
contentIndex: blockIndex(),
|
|
783
|
+
delta: JSON.stringify(toolCall.arguments),
|
|
784
|
+
partial: output,
|
|
785
|
+
});
|
|
786
|
+
stream.push({
|
|
787
|
+
type: "toolcall_end",
|
|
788
|
+
contentIndex: blockIndex(),
|
|
789
|
+
toolCall,
|
|
790
|
+
partial: output,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (candidate?.finishReason) {
|
|
797
|
+
output.stopReason = mapStopReasonString(candidate.finishReason);
|
|
798
|
+
if (output.content.some(b => b.type === "toolCall")) {
|
|
799
|
+
output.stopReason = "toolUse";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (responseData.usageMetadata) {
|
|
804
|
+
// promptTokenCount includes cachedContentTokenCount, so subtract to get fresh input
|
|
805
|
+
const promptTokens = responseData.usageMetadata.promptTokenCount || 0;
|
|
806
|
+
const cacheReadTokens = responseData.usageMetadata.cachedContentTokenCount || 0;
|
|
807
|
+
output.usage = {
|
|
808
|
+
input: promptTokens - cacheReadTokens,
|
|
809
|
+
output:
|
|
810
|
+
(responseData.usageMetadata.candidatesTokenCount || 0) +
|
|
811
|
+
(responseData.usageMetadata.thoughtsTokenCount || 0),
|
|
812
|
+
cacheRead: cacheReadTokens,
|
|
813
|
+
cacheWrite: 0,
|
|
814
|
+
totalTokens: responseData.usageMetadata.totalTokenCount || 0,
|
|
815
|
+
cost: {
|
|
816
|
+
input: 0,
|
|
817
|
+
output: 0,
|
|
818
|
+
cacheRead: 0,
|
|
819
|
+
cacheWrite: 0,
|
|
820
|
+
total: 0,
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
calculateCost(model, output.usage);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (currentBlock) {
|
|
828
|
+
if (currentBlock.type === "text") {
|
|
829
|
+
stream.push({
|
|
830
|
+
type: "text_end",
|
|
831
|
+
contentIndex: blockIndex(),
|
|
832
|
+
content: currentBlock.text,
|
|
833
|
+
partial: output,
|
|
834
|
+
});
|
|
835
|
+
} else {
|
|
836
|
+
stream.push({
|
|
837
|
+
type: "thinking_end",
|
|
838
|
+
contentIndex: blockIndex(),
|
|
839
|
+
content: currentBlock.thinking,
|
|
840
|
+
partial: output,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return hasContent;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
let receivedContent = false;
|
|
849
|
+
let currentResponse = response;
|
|
850
|
+
|
|
851
|
+
for (let emptyAttempt = 0; emptyAttempt <= MAX_EMPTY_STREAM_RETRIES; emptyAttempt++) {
|
|
852
|
+
if (options?.signal?.aborted) {
|
|
853
|
+
throw new Error("Request was aborted");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (emptyAttempt > 0) {
|
|
857
|
+
const backoffMs = EMPTY_STREAM_BASE_DELAY_MS * 2 ** (emptyAttempt - 1);
|
|
858
|
+
try {
|
|
859
|
+
await abortableSleep(backoffMs, options?.signal);
|
|
860
|
+
} catch {
|
|
861
|
+
// Normalize AbortError to expected message for consistent error handling
|
|
862
|
+
throw new Error("Request was aborted");
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!requestUrl) {
|
|
866
|
+
throw new Error("Missing request URL");
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
currentResponse = await fetch(requestUrl, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: requestHeaders,
|
|
872
|
+
body: requestBodyJson,
|
|
873
|
+
signal: options?.signal,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
if (!currentResponse.ok) {
|
|
877
|
+
const retryErrorText = await currentResponse.text();
|
|
878
|
+
throw withHttpStatus(
|
|
879
|
+
new Error(`Cloud Code Assist API error (${currentResponse.status}): ${retryErrorText}`),
|
|
880
|
+
currentResponse.status,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const streamed = await streamResponse(currentResponse);
|
|
886
|
+
if (streamed) {
|
|
887
|
+
receivedContent = true;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) {
|
|
892
|
+
resetOutput();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (!receivedContent) {
|
|
897
|
+
throw new Error("Cloud Code Assist API returned an empty response");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (options?.signal?.aborted) {
|
|
901
|
+
throw new Error("Request was aborted");
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
905
|
+
throw new Error("An unknown error occurred");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
output.duration = Date.now() - startTime;
|
|
909
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
910
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
911
|
+
stream.end();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
for (const block of output.content) {
|
|
914
|
+
if ("index" in block) {
|
|
915
|
+
delete (block as { index?: number }).index;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
919
|
+
output.errorMessage = await appendRawHttpRequestDumpFor400(
|
|
920
|
+
error instanceof Error ? error.message : JSON.stringify(error),
|
|
921
|
+
error,
|
|
922
|
+
rawRequestDump,
|
|
923
|
+
);
|
|
924
|
+
output.duration = Date.now() - startTime;
|
|
925
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
926
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
927
|
+
stream.end();
|
|
928
|
+
}
|
|
929
|
+
})();
|
|
930
|
+
|
|
931
|
+
return stream;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const INT63_MASK = (1n << 63n) - 1n;
|
|
935
|
+
const ANTIGRAVITY_RANDOM_BOUND = 9_000_000_000_000_000_000n;
|
|
936
|
+
|
|
937
|
+
function formatSignedDecimalSessionId(value: bigint): string {
|
|
938
|
+
return `-${value.toString()}`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function deriveSignedDecimalFromHash(text: string): string {
|
|
942
|
+
const digest = createHash("sha256").update(text).digest();
|
|
943
|
+
let value = 0n;
|
|
944
|
+
for (let index = 0; index < 8; index += 1) {
|
|
945
|
+
value = (value << 8n) | BigInt(digest[index] ?? 0);
|
|
946
|
+
}
|
|
947
|
+
return formatSignedDecimalSessionId(value & INT63_MASK);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function randomBoundedInt63(maxExclusive: bigint): bigint {
|
|
951
|
+
while (true) {
|
|
952
|
+
const bytes = randomBytes(8);
|
|
953
|
+
let value = 0n;
|
|
954
|
+
for (const byte of bytes) {
|
|
955
|
+
value = (value << 8n) | BigInt(byte);
|
|
956
|
+
}
|
|
957
|
+
value &= INT63_MASK;
|
|
958
|
+
if (value < maxExclusive) {
|
|
959
|
+
return value;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function randomSignedDecimalSessionId(): string {
|
|
965
|
+
return formatSignedDecimalSessionId(randomBoundedInt63(ANTIGRAVITY_RANDOM_BOUND));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function getFirstUserTextForAntigravitySession(context: Context): string | undefined {
|
|
969
|
+
for (const message of context.messages) {
|
|
970
|
+
if (message.role !== "user") {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (typeof message.content === "string") {
|
|
975
|
+
return message.content;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (Array.isArray(message.content)) {
|
|
979
|
+
const firstTextPart = message.content.find((item): item is TextContent => item.type === "text");
|
|
980
|
+
return firstTextPart?.text;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function deriveAntigravitySessionId(context: Context): string {
|
|
990
|
+
const text = getFirstUserTextForAntigravitySession(context);
|
|
991
|
+
if (text && text.trim().length > 0) {
|
|
992
|
+
return deriveSignedDecimalFromHash(text);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return randomSignedDecimalSessionId();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function normalizeAntigravityTools(
|
|
999
|
+
tools: CloudCodeAssistRequest["request"]["tools"],
|
|
1000
|
+
): CloudCodeAssistRequest["request"]["tools"] {
|
|
1001
|
+
return tools?.map(tool => ({
|
|
1002
|
+
...tool,
|
|
1003
|
+
functionDeclarations: tool.functionDeclarations.map(declaration => {
|
|
1004
|
+
if ("parameters" in declaration) {
|
|
1005
|
+
return declaration;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const { parametersJsonSchema, ...rest } = declaration;
|
|
1009
|
+
return {
|
|
1010
|
+
...rest,
|
|
1011
|
+
parameters: sanitizeSchemaForCCA(parametersJsonSchema),
|
|
1012
|
+
};
|
|
1013
|
+
}),
|
|
1014
|
+
}));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export function buildRequest(
|
|
1018
|
+
model: Model<"google-gemini-cli">,
|
|
1019
|
+
context: Context,
|
|
1020
|
+
projectId: string,
|
|
1021
|
+
options: GoogleGeminiCliOptions = {},
|
|
1022
|
+
isAntigravity = false,
|
|
1023
|
+
): CloudCodeAssistRequest {
|
|
1024
|
+
const contents = convertMessages(model, context);
|
|
1025
|
+
|
|
1026
|
+
const generationConfig: CloudCodeAssistRequest["request"]["generationConfig"] = {};
|
|
1027
|
+
if (options.temperature !== undefined) {
|
|
1028
|
+
generationConfig.temperature = options.temperature;
|
|
1029
|
+
}
|
|
1030
|
+
if (options.maxTokens !== undefined) {
|
|
1031
|
+
generationConfig.maxOutputTokens = options.maxTokens;
|
|
1032
|
+
}
|
|
1033
|
+
if (options.topP !== undefined) {
|
|
1034
|
+
generationConfig.topP = options.topP;
|
|
1035
|
+
}
|
|
1036
|
+
if (options.topK !== undefined) {
|
|
1037
|
+
generationConfig.topK = options.topK;
|
|
1038
|
+
}
|
|
1039
|
+
if (options.minP !== undefined) {
|
|
1040
|
+
generationConfig.minP = options.minP;
|
|
1041
|
+
}
|
|
1042
|
+
if (options.presencePenalty !== undefined) {
|
|
1043
|
+
generationConfig.presencePenalty = options.presencePenalty;
|
|
1044
|
+
}
|
|
1045
|
+
if (options.repetitionPenalty !== undefined) {
|
|
1046
|
+
generationConfig.repetitionPenalty = options.repetitionPenalty;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Thinking config
|
|
1050
|
+
if (options.thinking?.enabled && model.reasoning) {
|
|
1051
|
+
generationConfig.thinkingConfig = {
|
|
1052
|
+
includeThoughts: true,
|
|
1053
|
+
};
|
|
1054
|
+
// Gemini 3 models use thinkingLevel, older models use thinkingBudget
|
|
1055
|
+
if (options.thinking.level !== undefined) {
|
|
1056
|
+
// Cast to any since our GoogleThinkingLevel mirrors Google's ThinkingLevel enum values
|
|
1057
|
+
generationConfig.thinkingConfig.thinkingLevel = options.thinking.level as any;
|
|
1058
|
+
} else if (options.thinking.budgetTokens !== undefined) {
|
|
1059
|
+
generationConfig.thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const request: CloudCodeAssistRequest["request"] = {
|
|
1064
|
+
contents,
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
if (isAntigravity) {
|
|
1068
|
+
request.sessionId = deriveAntigravitySessionId(context);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// System instruction must be object with parts, not plain string
|
|
1072
|
+
if (context.systemPrompt) {
|
|
1073
|
+
request.systemInstruction = {
|
|
1074
|
+
parts: [{ text: context.systemPrompt.toWellFormed() }],
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
1079
|
+
request.generationConfig = generationConfig;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (context.tools && context.tools.length > 0) {
|
|
1083
|
+
const convertedTools = convertTools(context.tools, model);
|
|
1084
|
+
request.tools = isAntigravity ? normalizeAntigravityTools(convertedTools) : convertedTools;
|
|
1085
|
+
if (options.toolChoice) {
|
|
1086
|
+
request.toolConfig = {
|
|
1087
|
+
functionCallingConfig: {
|
|
1088
|
+
mode: mapToolChoice(options.toolChoice),
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (isAntigravity && !isClaudeModel(model.id) && request.generationConfig?.maxOutputTokens !== undefined) {
|
|
1095
|
+
delete request.generationConfig.maxOutputTokens;
|
|
1096
|
+
if (Object.keys(request.generationConfig).length === 0) {
|
|
1097
|
+
delete request.generationConfig;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (isAntigravity && isClaudeModel(model.id)) {
|
|
1102
|
+
request.toolConfig = {
|
|
1103
|
+
functionCallingConfig: {
|
|
1104
|
+
mode: "VALIDATED" as FunctionCallingConfigMode,
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (isAntigravity && shouldInjectAntigravitySystemInstruction(model.id)) {
|
|
1110
|
+
const existingParts = request.systemInstruction?.parts ?? [];
|
|
1111
|
+
request.systemInstruction = {
|
|
1112
|
+
role: "user",
|
|
1113
|
+
parts: [
|
|
1114
|
+
{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
|
|
1115
|
+
{ text: `Please ignore following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` },
|
|
1116
|
+
...existingParts,
|
|
1117
|
+
],
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return {
|
|
1122
|
+
project: projectId,
|
|
1123
|
+
model: model.id,
|
|
1124
|
+
request,
|
|
1125
|
+
...(isAntigravity
|
|
1126
|
+
? {
|
|
1127
|
+
requestType: "agent",
|
|
1128
|
+
userAgent: "antigravity",
|
|
1129
|
+
requestId: `agent-${randomUUID()}`,
|
|
1130
|
+
}
|
|
1131
|
+
: {}),
|
|
1132
|
+
};
|
|
1133
|
+
}
|