ampcode-connector 0.1.20 → 0.1.21
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/package.json +1 -1
- package/src/constants.ts +2 -1
- package/src/providers/forward.ts +28 -1
- package/src/providers/google.ts +125 -12
- package/src/server/server.ts +4 -1
- package/src/utils/code-assist.ts +12 -3
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Single source of truth — no magic strings scattered across files. */
|
|
2
2
|
|
|
3
3
|
export const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
4
|
-
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.
|
|
4
|
+
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
5
|
+
export const ANTIGRAVITY_DAILY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
5
6
|
export const AUTOPUSH_ENDPOINT = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
|
|
6
7
|
export const DEFAULT_ANTIGRAVITY_PROJECT = "rising-fact-p41fc";
|
|
7
8
|
|
package/src/providers/forward.ts
CHANGED
|
@@ -53,7 +53,7 @@ export async function forward(opts: ForwardOptions): Promise<Response> {
|
|
|
53
53
|
await Bun.sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
return transportErrorResponse(opts.providerName, err);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Retry on server errors (429 handled at routing layer)
|
|
@@ -117,3 +117,30 @@ export async function forward(opts: ForwardOptions): Promise<Response> {
|
|
|
117
117
|
export function denied(providerName: string): Response {
|
|
118
118
|
return apiError(401, `No ${providerName} OAuth token available. Run login first.`);
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
function transportErrorResponse(providerName: string, err: unknown): Response {
|
|
122
|
+
const message = transportErrorMessage(providerName, err);
|
|
123
|
+
logger.error(`${providerName} transport error after retries exhausted`, { error: String(err) });
|
|
124
|
+
return apiError(502, message, "connection_error");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function transportErrorMessage(providerName: string, err: unknown): string {
|
|
128
|
+
const base = `${providerName} connection error after retries were exhausted.`;
|
|
129
|
+
const details = String(err);
|
|
130
|
+
|
|
131
|
+
if (providerName !== "Anthropic") {
|
|
132
|
+
return `${base} ${details}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const looksLikeReset =
|
|
136
|
+
details.includes("ECONNRESET") ||
|
|
137
|
+
details.includes("socket connection was closed unexpectedly") ||
|
|
138
|
+
details.includes("tls") ||
|
|
139
|
+
details.includes("network");
|
|
140
|
+
|
|
141
|
+
if (!looksLikeReset) {
|
|
142
|
+
return `${base} ${details}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `${base} ${details} This is often a local network issue rather than an OAuth bug: check Wi-Fi MTU (1492 is a common fix), hotspot stability, and iPhone dual-SIM Cellular Data Switching if you are tethering.`;
|
|
146
|
+
}
|
package/src/providers/google.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { google as config } from "../auth/configs.ts";
|
|
5
5
|
import * as oauth from "../auth/oauth.ts";
|
|
6
6
|
import * as store from "../auth/store.ts";
|
|
7
|
-
import { ANTIGRAVITY_DAILY_ENDPOINT,
|
|
7
|
+
import { ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_DAILY_SANDBOX_ENDPOINT, CODE_ASSIST_ENDPOINT } from "../constants.ts";
|
|
8
8
|
import { buildUrl, maybeWrap, withUnwrap } from "../utils/code-assist.ts";
|
|
9
9
|
import { logger } from "../utils/logger.ts";
|
|
10
10
|
import * as path from "../utils/path.ts";
|
|
@@ -26,7 +26,7 @@ interface GoogleStrategy {
|
|
|
26
26
|
wrapOpts: {
|
|
27
27
|
userAgent: "antigravity" | "pi-coding-agent";
|
|
28
28
|
requestIdPrefix: "agent" | "pi";
|
|
29
|
-
requestType?: "agent";
|
|
29
|
+
requestType?: "agent" | "image_gen";
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -44,15 +44,23 @@ const geminiStrategy: GoogleStrategy = {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/** Antigravity uses different model names than what Amp CLI sends. */
|
|
48
|
+
const antigravityModelMap: Record<string, string> = {
|
|
49
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
50
|
+
"gemini-3-pro-preview": "gemini-3-pro-high",
|
|
51
|
+
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
|
|
52
|
+
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
|
|
53
|
+
};
|
|
54
|
+
|
|
47
55
|
const antigravityStrategy: GoogleStrategy = {
|
|
48
56
|
name: "antigravity",
|
|
49
57
|
headers: {
|
|
50
|
-
"User-Agent": "antigravity/1.
|
|
58
|
+
"User-Agent": "antigravity/1.104.0 darwin/arm64",
|
|
51
59
|
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
52
60
|
"Client-Metadata": GOOGLE_CLIENT_METADATA,
|
|
53
61
|
},
|
|
54
|
-
endpoints: [ANTIGRAVITY_DAILY_ENDPOINT,
|
|
55
|
-
modelMapper: (model: string) =>
|
|
62
|
+
endpoints: [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_DAILY_SANDBOX_ENDPOINT, CODE_ASSIST_ENDPOINT],
|
|
63
|
+
modelMapper: (model: string) => antigravityModelMap[model] ?? model,
|
|
56
64
|
wrapOpts: {
|
|
57
65
|
userAgent: "antigravity",
|
|
58
66
|
requestIdPrefix: "agent",
|
|
@@ -62,6 +70,9 @@ const antigravityStrategy: GoogleStrategy = {
|
|
|
62
70
|
|
|
63
71
|
const strategies: readonly GoogleStrategy[] = [geminiStrategy, antigravityStrategy];
|
|
64
72
|
|
|
73
|
+
/** Models that only work on the antigravity strategy. */
|
|
74
|
+
const ANTIGRAVITY_ONLY_MODELS = new Set(["gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"]);
|
|
75
|
+
|
|
65
76
|
const COOLDOWN_MS = 60_000;
|
|
66
77
|
|
|
67
78
|
interface StrategyPreference {
|
|
@@ -78,8 +89,16 @@ function cooldownKey(account: number, strategy: GoogleStrategy): string {
|
|
|
78
89
|
return `${account}:${strategy.name}`;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
|
-
function getOrderedStrategies(account: number): GoogleStrategy[] {
|
|
92
|
+
function getOrderedStrategies(account: number, model?: string): GoogleStrategy[] {
|
|
82
93
|
const now = Date.now();
|
|
94
|
+
|
|
95
|
+
// Some models only work on antigravity — skip other strategies entirely
|
|
96
|
+
if (model && ANTIGRAVITY_ONLY_MODELS.has(model)) {
|
|
97
|
+
const cd = cooldowns.get(cooldownKey(account, antigravityStrategy));
|
|
98
|
+
if (cd && cd > now) return [];
|
|
99
|
+
return [antigravityStrategy];
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
const pref = preferredStrategy.get(account);
|
|
84
103
|
const ordered =
|
|
85
104
|
pref && pref.until > now ? [pref.strategy, ...strategies.filter((s) => s !== pref.strategy)] : [...strategies];
|
|
@@ -103,6 +122,78 @@ function markFailure(account: number, strategy: GoogleStrategy): void {
|
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
/** Buffer an SSE response and merge all chunks into a single JSON response.
|
|
126
|
+
* Used when we force streamGenerateContent but the client expects non-streaming JSON.
|
|
127
|
+
* Accumulates all candidate parts across chunks (image inlineData may be in earlier chunks). */
|
|
128
|
+
async function bufferSSEToJSON(response: Response): Promise<Response | null> {
|
|
129
|
+
const text = await response.text();
|
|
130
|
+
const chunks: Record<string, unknown>[] = [];
|
|
131
|
+
|
|
132
|
+
for (const line of text.split("\n")) {
|
|
133
|
+
if (!line.startsWith("data: ")) continue;
|
|
134
|
+
const data = line.slice(6).trim();
|
|
135
|
+
if (!data || data === "[DONE]") continue;
|
|
136
|
+
try {
|
|
137
|
+
chunks.push(JSON.parse(data) as Record<string, unknown>);
|
|
138
|
+
} catch {
|
|
139
|
+
// skip non-JSON
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (chunks.length === 0) return null;
|
|
144
|
+
if (chunks.length === 1) {
|
|
145
|
+
return new Response(JSON.stringify(chunks[0]), {
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Merge: accumulate parts from all chunks into the first candidate
|
|
152
|
+
const merged = chunks[0]! as Record<string, unknown>;
|
|
153
|
+
const allParts: unknown[] = [];
|
|
154
|
+
|
|
155
|
+
for (const chunk of chunks) {
|
|
156
|
+
const candidates = chunk.candidates as { content?: { parts?: unknown[] } }[] | undefined;
|
|
157
|
+
if (!candidates) continue;
|
|
158
|
+
for (const candidate of candidates) {
|
|
159
|
+
if (candidate.content?.parts) {
|
|
160
|
+
allParts.push(...candidate.content.parts);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Use last chunk's metadata (finishReason, usageMetadata)
|
|
166
|
+
const last = chunks[chunks.length - 1]! as Record<string, unknown>;
|
|
167
|
+
const lastCandidates = last.candidates as Record<string, unknown>[] | undefined;
|
|
168
|
+
const mergedCandidates = merged.candidates as Record<string, unknown>[] | undefined;
|
|
169
|
+
|
|
170
|
+
if (mergedCandidates?.[0] && allParts.length > 0) {
|
|
171
|
+
const content = (mergedCandidates[0] as Record<string, unknown>).content as Record<string, unknown> | undefined;
|
|
172
|
+
if (content) content.parts = allParts;
|
|
173
|
+
if (lastCandidates?.[0]) {
|
|
174
|
+
(mergedCandidates[0] as Record<string, unknown>).finishReason = (
|
|
175
|
+
lastCandidates[0] as Record<string, unknown>
|
|
176
|
+
).finishReason;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (last.usageMetadata) merged.usageMetadata = last.usageMetadata;
|
|
180
|
+
|
|
181
|
+
return new Response(JSON.stringify(merged), {
|
|
182
|
+
status: 200,
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Generate a fallback project ID when none is stored (matches CLIProxyAPI behavior). */
|
|
188
|
+
function generateProjectId(): string {
|
|
189
|
+
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
|
|
190
|
+
const nouns = ["fuze", "wave", "spark", "flow", "core"];
|
|
191
|
+
const adj = adjectives[Math.floor(Math.random() * adjectives.length)]!;
|
|
192
|
+
const noun = nouns[Math.floor(Math.random() * nouns.length)]!;
|
|
193
|
+
const rand = crypto.randomUUID().slice(0, 5).toLowerCase();
|
|
194
|
+
return `${adj}-${noun}-${rand}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
106
197
|
export const provider: Provider = {
|
|
107
198
|
name: "Google",
|
|
108
199
|
routeDecision: "LOCAL_GOOGLE",
|
|
@@ -117,7 +208,7 @@ export const provider: Provider = {
|
|
|
117
208
|
if (!accessToken) return denied("Google");
|
|
118
209
|
|
|
119
210
|
const creds = store.get("google", account);
|
|
120
|
-
const projectId = creds?.projectId
|
|
211
|
+
const projectId = creds?.projectId || generateProjectId();
|
|
121
212
|
const email = creds?.email;
|
|
122
213
|
|
|
123
214
|
const modelAction = path.googleModel(sub);
|
|
@@ -127,7 +218,7 @@ export const provider: Provider = {
|
|
|
127
218
|
}
|
|
128
219
|
|
|
129
220
|
const unwrapThenRewrite = withUnwrap(rewrite);
|
|
130
|
-
const orderedStrategies = getOrderedStrategies(account);
|
|
221
|
+
const orderedStrategies = getOrderedStrategies(account, modelAction.model);
|
|
131
222
|
|
|
132
223
|
if (orderedStrategies.length === 0) {
|
|
133
224
|
const now = Date.now();
|
|
@@ -150,7 +241,9 @@ export const provider: Provider = {
|
|
|
150
241
|
|
|
151
242
|
for (const strategy of orderedStrategies) {
|
|
152
243
|
const model = strategy.modelMapper ? strategy.modelMapper(modelAction.model) : modelAction.model;
|
|
153
|
-
const
|
|
244
|
+
const isImageModel = model.includes("image");
|
|
245
|
+
const wrapOpts = isImageModel ? { ...strategy.wrapOpts, requestType: "image_gen" as const } : strategy.wrapOpts;
|
|
246
|
+
const requestBody = maybeWrap(body.parsed, body.forwardBody, projectId, model, wrapOpts);
|
|
154
247
|
|
|
155
248
|
const headers: Record<string, string> = {
|
|
156
249
|
...strategy.headers,
|
|
@@ -159,25 +252,45 @@ export const provider: Provider = {
|
|
|
159
252
|
Accept: body.stream ? "text/event-stream" : "application/json",
|
|
160
253
|
};
|
|
161
254
|
|
|
162
|
-
logger.info(`Google strategy=${strategy.name} account=${account}`);
|
|
255
|
+
logger.info(`Google strategy=${strategy.name} account=${account} model=${model}`);
|
|
256
|
+
|
|
257
|
+
// Antigravity forces streaming endpoint for gemini-3-pro* and image models (matches CLIProxyAPI behavior)
|
|
258
|
+
const forceStream = strategy.name === "antigravity" && (model.startsWith("gemini-3-pro") || isImageModel);
|
|
259
|
+
const action = forceStream ? "streamGenerateContent" : modelAction.action;
|
|
163
260
|
|
|
164
261
|
for (const endpoint of strategy.endpoints) {
|
|
165
|
-
const url = buildUrl(endpoint,
|
|
262
|
+
const url = buildUrl(endpoint, action);
|
|
166
263
|
try {
|
|
264
|
+
const forceStreamNonStreaming = forceStream && !body.stream;
|
|
167
265
|
const response = await forward({
|
|
168
266
|
url,
|
|
169
267
|
body: requestBody,
|
|
170
|
-
streaming: body.stream,
|
|
268
|
+
streaming: forceStreamNonStreaming ? true : body.stream,
|
|
171
269
|
headers,
|
|
172
270
|
providerName: `Google/${strategy.name}`,
|
|
173
271
|
rewrite: unwrapThenRewrite,
|
|
174
272
|
email,
|
|
175
273
|
});
|
|
176
274
|
|
|
275
|
+
// When we forced streaming but client expects JSON, buffer SSE and return last chunk
|
|
276
|
+
if (forceStreamNonStreaming && response.ok && response.body) {
|
|
277
|
+
const merged = await bufferSSEToJSON(response);
|
|
278
|
+
if (merged) {
|
|
279
|
+
markSuccess(account, strategy);
|
|
280
|
+
return merged;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
177
284
|
if (response.status === 401 || response.status === 403) {
|
|
178
285
|
return response;
|
|
179
286
|
}
|
|
180
287
|
|
|
288
|
+
if (response.status === 404) {
|
|
289
|
+
lastResponse = response;
|
|
290
|
+
logger.debug(`Google strategy=${strategy.name} model not found (404), trying next endpoint`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
181
294
|
if (response.status === 429) {
|
|
182
295
|
saw429 = true;
|
|
183
296
|
lastResponse = response;
|
package/src/server/server.ts
CHANGED
|
@@ -102,7 +102,10 @@ async function handleProvider(
|
|
|
102
102
|
const rewrite = ampModel ? rewriter.rewrite(ampModel) : undefined;
|
|
103
103
|
const handlerResponse = await route.handler.forward(sub, body, req.headers, rewrite, route.account);
|
|
104
104
|
|
|
105
|
-
if (
|
|
105
|
+
if (
|
|
106
|
+
(handlerResponse.status === 429 || handlerResponse.status === 403 || handlerResponse.status === 404) &&
|
|
107
|
+
route.pool
|
|
108
|
+
) {
|
|
106
109
|
const ctx = { providerName, ampModel, config, sub, body, headers: req.headers, rewrite, threadId };
|
|
107
110
|
// 429: try short wait to preserve prompt cache first
|
|
108
111
|
const cached =
|
package/src/utils/code-assist.ts
CHANGED
|
@@ -6,18 +6,23 @@ interface WrapOptions {
|
|
|
6
6
|
body: Record<string, unknown>;
|
|
7
7
|
userAgent: "antigravity" | "pi-coding-agent";
|
|
8
8
|
requestIdPrefix: "agent" | "pi";
|
|
9
|
-
requestType?: "agent";
|
|
9
|
+
requestType?: "agent" | "image_gen";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/** Wrap a raw request body in the Cloud Code Assist envelope. */
|
|
13
13
|
function wrapRequest(opts: WrapOptions): string {
|
|
14
|
+
const isImageGen = opts.requestType === "image_gen";
|
|
15
|
+
const requestId = isImageGen
|
|
16
|
+
? `image_gen/${Date.now()}/${crypto.randomUUID()}/12`
|
|
17
|
+
: `${opts.requestIdPrefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
18
|
+
|
|
14
19
|
return JSON.stringify({
|
|
15
20
|
project: opts.projectId,
|
|
16
21
|
model: opts.model,
|
|
17
22
|
request: opts.body,
|
|
18
23
|
...(opts.requestType && { requestType: opts.requestType }),
|
|
19
24
|
userAgent: opts.userAgent,
|
|
20
|
-
requestId
|
|
25
|
+
requestId,
|
|
21
26
|
});
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -118,7 +123,11 @@ export function maybeWrap(
|
|
|
118
123
|
raw: string,
|
|
119
124
|
projectId: string,
|
|
120
125
|
model: string,
|
|
121
|
-
opts: {
|
|
126
|
+
opts: {
|
|
127
|
+
userAgent: "antigravity" | "pi-coding-agent";
|
|
128
|
+
requestIdPrefix: "agent" | "pi";
|
|
129
|
+
requestType?: "agent" | "image_gen";
|
|
130
|
+
},
|
|
122
131
|
): string {
|
|
123
132
|
if (!parsed) return raw;
|
|
124
133
|
if (parsed.project) return raw;
|