@oh-my-pi/pi-ai 17.3.0 → 17.3.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 +12 -0
- package/dist/types/error/rate-limit.d.ts +1 -1
- package/dist/types/providers/google-gemini-cli.d.ts +0 -1
- package/dist/types/registry/oauth/google-antigravity.d.ts +6 -2
- package/dist/types/stream.d.ts +1 -0
- package/package.json +5 -5
- package/src/error/rate-limit.ts +71 -1
- package/src/providers/google-gemini-cli.ts +9 -22
- package/src/providers/google-shared.ts +3 -0
- package/src/registry/oauth/google-antigravity.ts +99 -44
- package/src/stream.ts +5 -0
- package/src/usage/gemini.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.2] - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Dropped unsigned thinking blocks from Antigravity Claude requests instead of sending them without a signature, preventing HTTP 400 responses when resuming sessions or switching models.
|
|
10
|
+
- Classified Antigravity HTTP 429 responses from structured `google.rpc.ErrorInfo` reasons (`QUOTA_EXHAUSTED`, `RATE_LIMIT_EXCEEDED`, and `INSUFFICIENT_G1_CREDITS_BALANCE`), using retry delays of five minutes or longer to distinguish rotatable quota windows from transient throttling instead of relying only on message regexes.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- Removed the Antigravity identity-prompt injection (`ANTIGRAVITY_SYSTEM_INSTRUCTION` and `shouldInjectAntigravitySystemInstruction`): Cloud Code Assist accepts arbitrary system instructions on gemini-3.x and Claude routes (verified live), and the injected stub never matched the real client's system prompt anyway. User system prompts are now sent unmodified (still tagged `role: "user"`).
|
|
15
|
+
- Fixed Antigravity `auto` mode not failing over to the sandbox endpoint when the daily endpoint returned a thinking-only `STOP`, which caused Advisor turns to be falsely recorded as empty-response failures ([#8480](https://github.com/can1357/oh-my-pi/issues/8480)).
|
|
16
|
+
|
|
5
17
|
## [17.3.0] - 2026-08-13
|
|
6
18
|
|
|
7
19
|
### Breaking Changes
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Rate limit reason classification and backoff calculation utilities.
|
|
3
3
|
* Ported from opencode-antigravity-auth plugin for consistency.
|
|
4
4
|
*/
|
|
5
|
-
export type RateLimitReason = "QUOTA_EXHAUSTED" | "RATE_LIMIT_EXCEEDED" | "CONCURRENT_LIMIT" | "MODEL_CAPACITY_EXHAUSTED" | "SERVER_ERROR" | "UNKNOWN";
|
|
5
|
+
export type RateLimitReason = "QUOTA_EXHAUSTED" | "INSUFFICIENT_G1_CREDITS_BALANCE" | "RATE_LIMIT_EXCEEDED" | "CONCURRENT_LIMIT" | "MODEL_CAPACITY_EXHAUSTED" | "SERVER_ERROR" | "UNKNOWN";
|
|
6
6
|
/**
|
|
7
7
|
* Classify a rate-limit error message into a reason category.
|
|
8
8
|
* Priority order: explicit details in a resource-exhausted error > QUOTA
|
|
@@ -69,7 +69,6 @@ export interface AntigravityProviderSessionState extends ProviderSessionState {
|
|
|
69
69
|
lastExecutionId?: string;
|
|
70
70
|
}
|
|
71
71
|
export declare function getAntigravityProviderSessionState(providerSessionState: Map<string, ProviderSessionState> | undefined): AntigravityProviderSessionState | undefined;
|
|
72
|
-
export { ANTIGRAVITY_SYSTEM_INSTRUCTION, getAntigravityUserAgent, getGeminiCliHeaders, getGeminiCliUserAgent, } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
73
72
|
interface ParsedGeminiCliCredentials {
|
|
74
73
|
accessToken: string;
|
|
75
74
|
projectId: string;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { OAuthController, OAuthCredentials } from "./types.js";
|
|
2
2
|
export declare const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA: Readonly<{
|
|
3
3
|
ideType: "ANTIGRAVITY";
|
|
4
|
-
platform: "PLATFORM_UNSPECIFIED";
|
|
5
|
-
pluginType: "GEMINI";
|
|
6
4
|
}>;
|
|
5
|
+
export interface AntigravityOnboardMetadata {
|
|
6
|
+
ide_type: string;
|
|
7
|
+
ide_version: string;
|
|
8
|
+
ide_name: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getAntigravityOnboardMetadata(): AntigravityOnboardMetadata;
|
|
7
11
|
export declare function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials>;
|
|
8
12
|
/**
|
|
9
13
|
* Refresh Antigravity token
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare const __providerInFlightForTesting: {
|
|
|
16
16
|
} | undefined): void;
|
|
17
17
|
setHeartbeatWriter(writer: ((writeProviderInFlightInfo: () => Promise<void>) => Promise<void>) | undefined): void;
|
|
18
18
|
setLeaseRemover(remover: ((leasePath: string) => Promise<void>) | undefined): void;
|
|
19
|
+
setWaitObserver(observer: ((provider: string) => void) | undefined): void;
|
|
19
20
|
providerDir(provider: string): string;
|
|
20
21
|
lockDir(provider: string): string;
|
|
21
22
|
captureStaleLockRelease(provider: string): Promise<(() => Promise<void>) | null>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/omptype": "17.3.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
43
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
41
|
+
"@oh-my-pi/omptype": "17.3.2",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.3.2",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.3.2",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.3.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { extractRetryHint } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Rate limit reason classification and backoff calculation utilities.
|
|
3
5
|
* Ported from opencode-antigravity-auth plugin for consistency.
|
|
@@ -5,6 +7,7 @@
|
|
|
5
7
|
|
|
6
8
|
export type RateLimitReason =
|
|
7
9
|
| "QUOTA_EXHAUSTED"
|
|
10
|
+
| "INSUFFICIENT_G1_CREDITS_BALANCE"
|
|
8
11
|
| "RATE_LIMIT_EXCEEDED"
|
|
9
12
|
| "CONCURRENT_LIMIT"
|
|
10
13
|
| "MODEL_CAPACITY_EXHAUSTED"
|
|
@@ -67,6 +70,66 @@ const CN_TRANSIENT_CAP_PATTERN =
|
|
|
67
70
|
// of rotating through the opaque-429 fallback.
|
|
68
71
|
const CN_THROTTLE_PATTERN = /速率(?:限制|过快)|频率(?:过高|过快)|过于频繁|稍后[重再]试/;
|
|
69
72
|
|
|
73
|
+
const GOOGLE_RPC_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo";
|
|
74
|
+
const LONG_RATE_LIMIT_DELAY_MS = 5 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
78
|
+
? (value as Record<string, unknown>)
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseJsonBody(errorMessage: string): Record<string, unknown> | undefined {
|
|
83
|
+
const start = errorMessage.indexOf("{");
|
|
84
|
+
const end = errorMessage.lastIndexOf("}");
|
|
85
|
+
if (start < 0 || end < start) return undefined;
|
|
86
|
+
try {
|
|
87
|
+
const parsed: unknown = JSON.parse(errorMessage.slice(start, end + 1));
|
|
88
|
+
return asRecord(parsed);
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Classify structured Google RESOURCE_EXHAUSTED bodies before consulting text.
|
|
96
|
+
* Cloud Code Assist prefixes the JSON with its HTTP error label, so accept an
|
|
97
|
+
* embedded top-level object as well as a raw JSON body.
|
|
98
|
+
*/
|
|
99
|
+
function parseGoogleRpcRateLimitReason(errorMessage: string): RateLimitReason | undefined {
|
|
100
|
+
const body = parseJsonBody(errorMessage);
|
|
101
|
+
const error = asRecord(body?.error);
|
|
102
|
+
if (typeof error?.status !== "string" || error.status.trim().toUpperCase() !== "RESOURCE_EXHAUSTED") {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(error.details)) return undefined;
|
|
106
|
+
|
|
107
|
+
for (const value of error.details) {
|
|
108
|
+
const detail = asRecord(value);
|
|
109
|
+
if (detail?.["@type"] !== GOOGLE_RPC_ERROR_INFO_TYPE || typeof detail.reason !== "string") continue;
|
|
110
|
+
const reason = detail.reason.trim().toUpperCase();
|
|
111
|
+
switch (reason) {
|
|
112
|
+
case "QUOTA_EXHAUSTED":
|
|
113
|
+
return "QUOTA_EXHAUSTED";
|
|
114
|
+
case "INSUFFICIENT_G1_CREDITS_BALANCE":
|
|
115
|
+
// Keep Google's specific credit-balance reason available to logs
|
|
116
|
+
// and callers while treating it as credential-rotatable below.
|
|
117
|
+
return "INSUFFICIENT_G1_CREDITS_BALANCE";
|
|
118
|
+
case "RATE_LIMIT_EXCEEDED": {
|
|
119
|
+
const retryDelayMs = extractRetryHint(undefined, errorMessage);
|
|
120
|
+
return retryDelayMs !== undefined && retryDelayMs >= LONG_RATE_LIMIT_DELAY_MS
|
|
121
|
+
? "QUOTA_EXHAUSTED"
|
|
122
|
+
: "RATE_LIMIT_EXCEEDED";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isQuotaExhaustedReason(reason: RateLimitReason): boolean {
|
|
130
|
+
return reason === "QUOTA_EXHAUSTED" || reason === "INSUFFICIENT_G1_CREDITS_BALANCE";
|
|
131
|
+
}
|
|
132
|
+
|
|
70
133
|
/**
|
|
71
134
|
* Classify a rate-limit error message into a reason category.
|
|
72
135
|
* Priority order: explicit details in a resource-exhausted error > QUOTA
|
|
@@ -77,6 +140,8 @@ const CN_THROTTLE_PATTERN = /速率(?:限制|过快)|频率(?:过高|过快)|过
|
|
|
77
140
|
* Explicit details such as "quota exceeded" retain their normal classification.
|
|
78
141
|
*/
|
|
79
142
|
export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
143
|
+
const structuredReason = parseGoogleRpcRateLimitReason(errorMessage);
|
|
144
|
+
if (structuredReason !== undefined) return structuredReason;
|
|
80
145
|
const lowerWithStatus = errorMessage.toLowerCase();
|
|
81
146
|
const lower = lowerWithStatus.replace(RESOURCE_EXHAUSTED_PATTERN, "");
|
|
82
147
|
const hasResourceExhaustedStatus = lower !== lowerWithStatus;
|
|
@@ -162,6 +227,7 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
162
227
|
*/
|
|
163
228
|
export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
164
229
|
switch (reason) {
|
|
230
|
+
case "INSUFFICIENT_G1_CREDITS_BALANCE":
|
|
165
231
|
case "QUOTA_EXHAUSTED":
|
|
166
232
|
return QUOTA_EXHAUSTED_BACKOFF_MS;
|
|
167
233
|
case "RATE_LIMIT_EXCEEDED":
|
|
@@ -215,6 +281,8 @@ export function isUsageLimitStatus(status: number | undefined): boolean {
|
|
|
215
281
|
* credentials.
|
|
216
282
|
*/
|
|
217
283
|
export function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean {
|
|
284
|
+
const structuredReason = message ? parseGoogleRpcRateLimitReason(message) : undefined;
|
|
285
|
+
if (structuredReason !== undefined) return isQuotaExhaustedReason(structuredReason);
|
|
218
286
|
// Concurrency caps are shed-and-backoff, not credential-rotatable — but only
|
|
219
287
|
// for quota-worded 429 / other statuses. HTTP 402 is categorically an
|
|
220
288
|
// account-billing cap, so a 402 whose body happens to mention concurrency is
|
|
@@ -235,7 +303,7 @@ export function isUsageLimitOutcome(status: number | undefined, message: string
|
|
|
235
303
|
const reason = parseRateLimitReason(message);
|
|
236
304
|
// For the categorical 402 billing cap a concurrency-worded body is still an
|
|
237
305
|
// exhausted cap (rotate); for 429 / other only QUOTA_EXHAUSTED rotates.
|
|
238
|
-
return reason
|
|
306
|
+
return isQuotaExhaustedReason(reason) || (isBillingCapStatus && reason === "CONCURRENT_LIMIT");
|
|
239
307
|
}
|
|
240
308
|
|
|
241
309
|
/**
|
|
@@ -272,6 +340,8 @@ export function isOpaqueStatusBody(message: string): boolean {
|
|
|
272
340
|
* {@link isUsageLimitOutcome} uses it for the account-rotation decision.
|
|
273
341
|
*/
|
|
274
342
|
export function matchesUsageLimitText(errorMessage: string): boolean {
|
|
343
|
+
const structuredReason = parseGoogleRpcRateLimitReason(errorMessage);
|
|
344
|
+
if (structuredReason !== undefined) return isQuotaExhaustedReason(structuredReason);
|
|
275
345
|
return (
|
|
276
346
|
USAGE_LIMIT_PATTERN.test(errorMessage) ||
|
|
277
347
|
(CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) ||
|
|
@@ -8,7 +8,6 @@ import { scheduler } from "node:timers/promises";
|
|
|
8
8
|
import { type } from "@oh-my-pi/omptype";
|
|
9
9
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
10
10
|
import {
|
|
11
|
-
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
12
11
|
getAntigravityModelWireProfile,
|
|
13
12
|
getAntigravityUserAgent,
|
|
14
13
|
getGeminiCliHeaders,
|
|
@@ -316,13 +315,6 @@ const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
|
316
315
|
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
317
316
|
const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
|
|
318
317
|
|
|
319
|
-
export {
|
|
320
|
-
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
321
|
-
getAntigravityUserAgent,
|
|
322
|
-
getGeminiCliHeaders,
|
|
323
|
-
getGeminiCliUserAgent,
|
|
324
|
-
} from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
325
|
-
|
|
326
318
|
// Retry configuration
|
|
327
319
|
const MAX_RETRIES = 3;
|
|
328
320
|
const BASE_DELAY_MS = 1000;
|
|
@@ -342,11 +334,6 @@ function needsClaudeThinkingBetaHeader(model: Model<"google-gemini-cli">): boole
|
|
|
342
334
|
return model.provider === "google-antigravity" && model.id.startsWith("claude-") && model.reasoning;
|
|
343
335
|
}
|
|
344
336
|
|
|
345
|
-
function shouldInjectAntigravitySystemInstruction(modelId: string): boolean {
|
|
346
|
-
const normalized = modelId.toLowerCase();
|
|
347
|
-
return normalized.includes("claude") || normalized.includes("gemini-3");
|
|
348
|
-
}
|
|
349
|
-
|
|
350
337
|
const optionalCredentialString = type("unknown").pipe(raw => {
|
|
351
338
|
const out = type("string")(raw);
|
|
352
339
|
return out instanceof type.errors ? undefined : out;
|
|
@@ -635,6 +622,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
635
622
|
const isFlashLeakModel = model.id.includes("flash");
|
|
636
623
|
|
|
637
624
|
let started = false;
|
|
625
|
+
// Tracks whether *visible* content (text delta or tool call) has been
|
|
626
|
+
// pushed downstream. `started` alone is a poor failover guard because a
|
|
627
|
+
// hidden thought part also flips it (via `ensureStarted`); a thinking-only
|
|
628
|
+
// STOP must still fail over to the alternate Antigravity endpoint (#8480).
|
|
629
|
+
let emittedVisibleContent = false;
|
|
638
630
|
let sawFinishReason = false;
|
|
639
631
|
let lastResponseId: string | undefined;
|
|
640
632
|
const ensureStarted = () => {
|
|
@@ -713,6 +705,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
713
705
|
|
|
714
706
|
const emitVisibleText = (delta: string, thoughtSignature?: string): void => {
|
|
715
707
|
if (!delta) return;
|
|
708
|
+
emittedVisibleContent = true;
|
|
716
709
|
const block = startTextBlock();
|
|
717
710
|
block.text += delta;
|
|
718
711
|
block.textSignature = retainThoughtSignature(block.textSignature, thoughtSignature);
|
|
@@ -871,6 +864,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
871
864
|
};
|
|
872
865
|
|
|
873
866
|
output.content.push(toolCall);
|
|
867
|
+
emittedVisibleContent = true;
|
|
874
868
|
ensureStarted();
|
|
875
869
|
pushToolCallEvents(toolCall, blockIndex(), output, stream);
|
|
876
870
|
}
|
|
@@ -944,6 +938,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
944
938
|
const isLastEndpoint = i === endpoints.length - 1;
|
|
945
939
|
try {
|
|
946
940
|
started = false;
|
|
941
|
+
emittedVisibleContent = false;
|
|
947
942
|
resetOutput();
|
|
948
943
|
|
|
949
944
|
// Per attempt: arm a pre-response (TTFT) timer, cleared the instant
|
|
@@ -1086,7 +1081,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1086
1081
|
const status = extractHttpStatusFromError(error);
|
|
1087
1082
|
if (
|
|
1088
1083
|
!isLastEndpoint &&
|
|
1089
|
-
!
|
|
1084
|
+
!emittedVisibleContent &&
|
|
1090
1085
|
(AIError.isTransientStatus(status) ||
|
|
1091
1086
|
(status === undefined &&
|
|
1092
1087
|
!(error instanceof AIError.ProviderResponseError && error.kind === "output") &&
|
|
@@ -1320,14 +1315,6 @@ export function buildRequest(
|
|
|
1320
1315
|
};
|
|
1321
1316
|
}
|
|
1322
1317
|
|
|
1323
|
-
if (isAntigravity && shouldInjectAntigravitySystemInstruction(model.id)) {
|
|
1324
|
-
const existingParts = request.systemInstruction?.parts ?? [];
|
|
1325
|
-
request.systemInstruction = {
|
|
1326
|
-
role: "user",
|
|
1327
|
-
parts: [{ text: ANTIGRAVITY_SYSTEM_INSTRUCTION }, ...existingParts],
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
1318
|
if (context.tools && context.tools.length > 0) {
|
|
1332
1319
|
const convertedTools = convertTools(context.tools, model);
|
|
1333
1320
|
request.tools = isAntigravity ? normalizeAntigravityTools(convertedTools) : convertedTools;
|
|
@@ -233,6 +233,8 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
233
233
|
const parts: Part[] = [];
|
|
234
234
|
// Check if message is from same provider and model - only then keep thinking blocks
|
|
235
235
|
const isSameProviderAndModel = msg.provider === model.provider && msg.model === model.id;
|
|
236
|
+
const dropsUnsignedThinking =
|
|
237
|
+
model.provider === "google-antigravity" && model.id.toLowerCase().includes("claude");
|
|
236
238
|
|
|
237
239
|
for (const block of msg.content) {
|
|
238
240
|
if (block.type === "text") {
|
|
@@ -247,6 +249,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
247
249
|
// Skip empty thinking blocks
|
|
248
250
|
if (!block.thinking || block.thinking.trim() === "") continue;
|
|
249
251
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature);
|
|
252
|
+
if (dropsUnsignedThinking && !thoughtSignature) continue;
|
|
250
253
|
if (thoughtSignature) {
|
|
251
254
|
parts.push({
|
|
252
255
|
thought: true,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
|
3
3
|
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
|
4
4
|
*/
|
|
5
|
-
import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
5
|
+
import { getAntigravityUserAgent, getAntigravityVersion } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
6
6
|
import * as AIError from "../../error";
|
|
7
7
|
import { oauthFetch, runGoogleOAuthLogin, throwIfLoginCancelled } from "./google-oauth-shared";
|
|
8
8
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
@@ -26,10 +26,12 @@ const SCOPES = [
|
|
|
26
26
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
27
27
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
28
28
|
const CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
29
|
-
const
|
|
29
|
+
const DAILY_CLOUD_CODE_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
30
|
+
const NODE_API_CLIENT_USER_AGENT = "google-api-nodejs-client/10.3.0";
|
|
31
|
+
const GOOG_API_CLIENT_HEADER = "gl-node/22.21.1";
|
|
32
|
+
const TIER_FREE = "free-tier";
|
|
30
33
|
const PROJECT_ONBOARD_MAX_ATTEMPTS = 5;
|
|
31
34
|
const PROJECT_ONBOARD_INTERVAL_MS = 2000;
|
|
32
|
-
|
|
33
35
|
interface LoadCodeAssistPayload {
|
|
34
36
|
cloudaicompanionProject?: string | { id?: string };
|
|
35
37
|
currentTier?: { id?: string };
|
|
@@ -45,35 +47,65 @@ interface LongRunningOperationResponse {
|
|
|
45
47
|
|
|
46
48
|
export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
|
|
47
49
|
ideType: "ANTIGRAVITY",
|
|
48
|
-
platform: "PLATFORM_UNSPECIFIED",
|
|
49
|
-
pluginType: "GEMINI",
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
export interface AntigravityOnboardMetadata {
|
|
53
|
+
ide_type: string;
|
|
54
|
+
ide_version: string;
|
|
55
|
+
ide_name: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getAntigravityOnboardMetadata(): AntigravityOnboardMetadata {
|
|
59
|
+
return {
|
|
60
|
+
ide_type: "ANTIGRAVITY",
|
|
61
|
+
ide_version: getAntigravityVersion(),
|
|
62
|
+
ide_name: "antigravity",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readProjectId(value: unknown): string | undefined {
|
|
53
67
|
if (typeof value === "string" && value.length > 0) {
|
|
54
|
-
return value;
|
|
68
|
+
return value.trim();
|
|
69
|
+
}
|
|
70
|
+
if (value && typeof value === "object" && "id" in value && typeof (value as { id?: unknown }).id === "string") {
|
|
71
|
+
const id = (value as { id: string }).id.trim();
|
|
72
|
+
if (id.length > 0) return id;
|
|
55
73
|
}
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractProjectId(payload: unknown): string | undefined {
|
|
78
|
+
if (!payload || typeof payload !== "object") return undefined;
|
|
79
|
+
const record = payload as Record<string, unknown>;
|
|
80
|
+
for (const key of ["cloudaicompanionProject", "projectId", "project"]) {
|
|
81
|
+
const id = readProjectId(record[key]);
|
|
82
|
+
if (id) return id;
|
|
58
83
|
}
|
|
59
84
|
return undefined;
|
|
60
85
|
}
|
|
61
86
|
|
|
62
|
-
function getDefaultTierId(
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
function getDefaultTierId(
|
|
88
|
+
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>,
|
|
89
|
+
currentTier?: { id?: string },
|
|
90
|
+
): string {
|
|
91
|
+
if (allowedTiers && allowedTiers.length > 0) {
|
|
92
|
+
const defaultTier = allowedTiers.find(
|
|
93
|
+
tier => tier.isDefault && typeof tier.id === "string" && tier.id.trim().length > 0,
|
|
94
|
+
);
|
|
95
|
+
if (defaultTier?.id) {
|
|
96
|
+
return defaultTier.id.trim();
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return defaultTier.id;
|
|
99
|
+
if (currentTier && typeof currentTier.id === "string" && currentTier.id.trim().length > 0) {
|
|
100
|
+
return currentTier.id.trim();
|
|
69
101
|
}
|
|
70
|
-
return
|
|
102
|
+
return TIER_FREE;
|
|
71
103
|
}
|
|
72
104
|
|
|
73
105
|
async function onboardProjectWithRetries(
|
|
74
106
|
endpoint: string,
|
|
75
107
|
headers: Record<string, string>,
|
|
76
|
-
onboardBody: {
|
|
108
|
+
onboardBody: { tier_id: string; metadata: AntigravityOnboardMetadata },
|
|
77
109
|
signal: AbortSignal | undefined,
|
|
78
110
|
onProgress?: (message: string) => void,
|
|
79
111
|
): Promise<string> {
|
|
@@ -104,7 +136,7 @@ async function onboardProjectWithRetries(
|
|
|
104
136
|
continue;
|
|
105
137
|
}
|
|
106
138
|
|
|
107
|
-
const projectId =
|
|
139
|
+
const projectId = extractProjectId(operation.response);
|
|
108
140
|
if (projectId) {
|
|
109
141
|
return projectId;
|
|
110
142
|
}
|
|
@@ -128,42 +160,65 @@ async function discoverProject(
|
|
|
128
160
|
};
|
|
129
161
|
|
|
130
162
|
onProgress?.("Checking for existing project...");
|
|
131
|
-
const endpoint = CLOUD_CODE_ENDPOINT;
|
|
132
163
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers,
|
|
139
|
-
body: JSON.stringify({
|
|
140
|
-
metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
|
|
141
|
-
}),
|
|
142
|
-
},
|
|
143
|
-
{ provider: "google-antigravity", signal },
|
|
144
|
-
);
|
|
164
|
+
let lastErrorText: string | undefined;
|
|
165
|
+
let lastStatus: number | undefined;
|
|
166
|
+
let fallbackTierId = TIER_FREE;
|
|
167
|
+
let loadedSuccessfully = false;
|
|
145
168
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
{
|
|
169
|
+
for (const endpoint of [DAILY_CLOUD_CODE_ENDPOINT, CLOUD_CODE_ENDPOINT]) {
|
|
170
|
+
throwIfLoginCancelled(signal);
|
|
171
|
+
const loadResponse = await oauthFetch(
|
|
172
|
+
`${endpoint}/v1internal:loadCodeAssist`,
|
|
173
|
+
{
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
{ provider: "google-antigravity", signal },
|
|
151
181
|
);
|
|
182
|
+
|
|
183
|
+
if (!loadResponse.ok) {
|
|
184
|
+
lastStatus = loadResponse.status;
|
|
185
|
+
lastErrorText = await loadResponse.text();
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
loadedSuccessfully = true;
|
|
190
|
+
const loadPayload = (await loadResponse.json()) as LoadCodeAssistPayload;
|
|
191
|
+
const existingProject = extractProjectId(loadPayload);
|
|
192
|
+
if (existingProject) {
|
|
193
|
+
return existingProject;
|
|
194
|
+
}
|
|
195
|
+
fallbackTierId = getDefaultTierId(loadPayload.allowedTiers, loadPayload.currentTier);
|
|
152
196
|
}
|
|
153
197
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
198
|
+
if (!loadedSuccessfully && lastStatus !== undefined) {
|
|
199
|
+
throw new AIError.OAuthError(`loadCodeAssist failed: ${lastStatus}: ${lastErrorText || "unknown error"}`, {
|
|
200
|
+
kind: "discovery",
|
|
201
|
+
status: lastStatus,
|
|
202
|
+
});
|
|
158
203
|
}
|
|
159
204
|
|
|
160
|
-
const tierId = getDefaultTierId(loadPayload.allowedTiers);
|
|
161
205
|
onProgress?.("Provisioning project...");
|
|
162
206
|
const onboardBody = {
|
|
163
|
-
|
|
164
|
-
metadata:
|
|
207
|
+
tier_id: fallbackTierId,
|
|
208
|
+
metadata: getAntigravityOnboardMetadata(),
|
|
165
209
|
};
|
|
166
|
-
const
|
|
210
|
+
const onboardHeaders: Record<string, string> = {
|
|
211
|
+
...headers,
|
|
212
|
+
"User-Agent": `${headers["User-Agent"]} ${NODE_API_CLIENT_USER_AGENT}`,
|
|
213
|
+
"X-Goog-Api-Client": GOOG_API_CLIENT_HEADER,
|
|
214
|
+
};
|
|
215
|
+
const provisionedProject = await onboardProjectWithRetries(
|
|
216
|
+
DAILY_CLOUD_CODE_ENDPOINT,
|
|
217
|
+
onboardHeaders,
|
|
218
|
+
onboardBody,
|
|
219
|
+
signal,
|
|
220
|
+
onProgress,
|
|
221
|
+
);
|
|
167
222
|
return provisionedProject;
|
|
168
223
|
} catch (error) {
|
|
169
224
|
if (error instanceof AIError.LoginCancelledError || error instanceof AIError.OAuthError) {
|
package/src/stream.ts
CHANGED
|
@@ -189,6 +189,7 @@ let providerInFlightHeartbeatWriterOverride:
|
|
|
189
189
|
| ((writeProviderInFlightInfo: () => Promise<void>) => Promise<void>)
|
|
190
190
|
| undefined;
|
|
191
191
|
let providerInFlightLeaseRemoverOverride: ((leasePath: string) => Promise<void>) | undefined;
|
|
192
|
+
let providerInFlightWaitObserverOverride: ((provider: string) => void) | undefined;
|
|
192
193
|
|
|
193
194
|
export function configureProviderMaxInFlightRequests(limits: Record<string, number> | undefined): void {
|
|
194
195
|
configuredProviderMaxInFlightRequests = limits ?? {};
|
|
@@ -489,6 +490,7 @@ function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal):
|
|
|
489
490
|
if (signal?.aborted)
|
|
490
491
|
return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
|
|
491
492
|
const signalPath = providerInFlightSignalPath(provider);
|
|
493
|
+
providerInFlightWaitObserverOverride?.(provider);
|
|
492
494
|
const waitStarted = Date.now();
|
|
493
495
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
494
496
|
let settled = false;
|
|
@@ -615,6 +617,9 @@ export const __providerInFlightForTesting = {
|
|
|
615
617
|
setLeaseRemover(remover: ((leasePath: string) => Promise<void>) | undefined): void {
|
|
616
618
|
providerInFlightLeaseRemoverOverride = remover;
|
|
617
619
|
},
|
|
620
|
+
setWaitObserver(observer: ((provider: string) => void) | undefined): void {
|
|
621
|
+
providerInFlightWaitObserverOverride = observer;
|
|
622
|
+
},
|
|
618
623
|
providerDir(provider: string): string {
|
|
619
624
|
return providerInFlightDir(provider);
|
|
620
625
|
},
|
package/src/usage/gemini.ts
CHANGED