@oh-my-pi/pi-ai 17.3.1 → 17.3.3
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 +18 -0
- package/dist/types/error/flags.d.ts +1 -0
- package/dist/types/error/provider.d.ts +2 -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/package.json +5 -5
- package/src/error/flags.ts +11 -1
- package/src/error/provider.ts +6 -5
- package/src/error/rate-limit.ts +71 -1
- package/src/providers/google-gemini-cli.ts +31 -30
- package/src/providers/google-shared.ts +3 -0
- package/src/registry/oauth/google-antigravity.ts +99 -44
- package/src/usage/gemini.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.3] - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Distinguished Gemini thought-only `STOP` responses from empty transports, avoiding repeated identical reasoning requests and duplicate Antigravity endpoint streams while surfacing the missing final output for session-level recovery.
|
|
10
|
+
|
|
11
|
+
## [17.3.2] - 2026-08-13
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- 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"`).
|
|
21
|
+
- 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)).
|
|
22
|
+
|
|
5
23
|
## [17.3.0] - 2026-08-13
|
|
6
24
|
|
|
7
25
|
### Breaking Changes
|
|
@@ -8,6 +8,7 @@ export declare const Flag: {
|
|
|
8
8
|
readonly StaleResponsesItem: 1048576;
|
|
9
9
|
readonly MalformedFunctionCall: 2097152;
|
|
10
10
|
readonly ProviderFinishError: 4194304;
|
|
11
|
+
readonly EmptyResponse: 8192;
|
|
11
12
|
readonly ContentBlocked: 32768;
|
|
12
13
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
13
14
|
readonly AccountPolicy: 16384;
|
|
@@ -7,6 +7,8 @@ export type ProviderResponseErrorKind =
|
|
|
7
7
|
| "output"
|
|
8
8
|
/** Response body was empty/missing when content was required. */
|
|
9
9
|
| "empty-body"
|
|
10
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
11
|
+
| "empty-output"
|
|
10
12
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
11
13
|
| "envelope"
|
|
12
14
|
/** Content was blocked by a provider safety filter. */
|
|
@@ -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/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.3",
|
|
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.3",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.3.3",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.3.3",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.3.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
package/src/error/flags.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const Flag = {
|
|
|
24
24
|
StaleResponsesItem: 0x0010_0000,
|
|
25
25
|
MalformedFunctionCall: 0x0020_0000,
|
|
26
26
|
ProviderFinishError: 0x0040_0000,
|
|
27
|
+
EmptyResponse: 0x0000_2000,
|
|
27
28
|
ContentBlocked: 0x0000_8000,
|
|
28
29
|
/** Account-scoped provider policy denial that may succeed with another credential. */
|
|
29
30
|
AccountPolicy: 0x0000_4000,
|
|
@@ -50,6 +51,7 @@ const KIND_MASK =
|
|
|
50
51
|
Flag.StaleResponsesItem |
|
|
51
52
|
Flag.MalformedFunctionCall |
|
|
52
53
|
Flag.ProviderFinishError |
|
|
54
|
+
Flag.EmptyResponse |
|
|
53
55
|
Flag.ContentBlocked |
|
|
54
56
|
Flag.AccountPolicy |
|
|
55
57
|
Flag.ContextOverflow |
|
|
@@ -62,7 +64,12 @@ const KIND_MASK =
|
|
|
62
64
|
Flag.OAuthExpiry;
|
|
63
65
|
|
|
64
66
|
const RETRIABLE_KINDS =
|
|
65
|
-
Flag.Transient |
|
|
67
|
+
Flag.Transient |
|
|
68
|
+
Flag.UsageLimit |
|
|
69
|
+
Flag.ThinkingLoop |
|
|
70
|
+
Flag.StaleResponsesItem |
|
|
71
|
+
Flag.ProviderFinishError |
|
|
72
|
+
Flag.EmptyResponse;
|
|
66
73
|
|
|
67
74
|
const OVERFLOW_PATTERNS = [
|
|
68
75
|
/prompt is too long/i, // Anthropic
|
|
@@ -104,6 +111,7 @@ const AUTH_FAILURE_PATTERN =
|
|
|
104
111
|
/\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
|
|
105
112
|
const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
|
|
106
113
|
const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reason|finish_reason:\s*error)\b/i;
|
|
114
|
+
const EMPTY_RESPONSE_PATTERN = /\bthought-only response without final output\b/i;
|
|
107
115
|
const CONTENT_FILTER_PATTERN = /\b(?:incomplete:\s*)?content_filter\b/i;
|
|
108
116
|
const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
109
117
|
const STALE_RESPONSE_ITEM_PATTERNS = [/\bItem with id ['"][^'"]+['"] not found\.?/i, /previous[ _]?response/i] as const;
|
|
@@ -197,6 +205,7 @@ const ERROR_KIND_LABELS: readonly [Flag, string][] = [
|
|
|
197
205
|
[Flag.StaleResponsesItem, "stale-responses-item"],
|
|
198
206
|
[Flag.MalformedFunctionCall, "malformed-function-call"],
|
|
199
207
|
[Flag.ProviderFinishError, "provider-finish-error"],
|
|
208
|
+
[Flag.EmptyResponse, "empty-response"],
|
|
200
209
|
[Flag.ContentBlocked, "content-blocked"],
|
|
201
210
|
[Flag.AccountPolicy, "account-policy"],
|
|
202
211
|
[Flag.ContextOverflow, "context-overflow"],
|
|
@@ -340,6 +349,7 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
|
|
|
340
349
|
if (matchesOverflowText(errorMessage)) kinds |= Flag.ContextOverflow;
|
|
341
350
|
if (isMalformedFunctionCallText(errorMessage)) kinds |= Flag.MalformedFunctionCall;
|
|
342
351
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
352
|
+
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
343
353
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
344
354
|
if (ACCOUNT_POLICY_PATTERN.test(errorMessage)) kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
345
355
|
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
package/src/error/provider.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ProviderResponseErrorKind =
|
|
|
9
9
|
| "output"
|
|
10
10
|
/** Response body was empty/missing when content was required. */
|
|
11
11
|
| "empty-body"
|
|
12
|
+
/** Response completed without actionable output (for example, thoughts only). */
|
|
13
|
+
| "empty-output"
|
|
12
14
|
/** Malformed wire envelope (unexpected message ordering / shape). */
|
|
13
15
|
| "envelope"
|
|
14
16
|
/** Content was blocked by a provider safety filter. */
|
|
@@ -38,11 +40,10 @@ export class ProviderResponseError extends Error {
|
|
|
38
40
|
this.kind = options.kind ?? "output";
|
|
39
41
|
// A safety filter block is terminal and intentionally non-retryable.
|
|
40
42
|
if (this.kind === "content-blocked") attach(this, create(Flag.ContentBlocked));
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// output was already emitted.
|
|
43
|
+
// A logically empty completed output needs a session-level reminder that
|
|
44
|
+
// asks for the missing final answer. Empty bodies and incomplete streams
|
|
45
|
+
// stay on the generic transient retry/model-fallback path.
|
|
46
|
+
else if (this.kind === "empty-output") attach(this, create(Flag.Transient, Flag.EmptyResponse));
|
|
46
47
|
else if (this.kind === "incomplete-stream" || this.kind === "empty-body") attach(this, create(Flag.Transient));
|
|
47
48
|
}
|
|
48
49
|
}
|
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,8 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
635
622
|
const isFlashLeakModel = model.id.includes("flash");
|
|
636
623
|
|
|
637
624
|
let started = false;
|
|
625
|
+
// Once any stream event starts, the endpoint is committed downstream.
|
|
626
|
+
// Failover remains safe only while `started` is false.
|
|
638
627
|
let sawFinishReason = false;
|
|
639
628
|
let lastResponseId: string | undefined;
|
|
640
629
|
const ensureStarted = () => {
|
|
@@ -938,6 +927,11 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
938
927
|
};
|
|
939
928
|
|
|
940
929
|
let receivedContent = false;
|
|
930
|
+
const hasThinkingOutput = () =>
|
|
931
|
+
output.content.some(
|
|
932
|
+
block =>
|
|
933
|
+
block.type === "thinking" && (block.thinking.trim().length > 0 || Boolean(block.thinkingSignature)),
|
|
934
|
+
);
|
|
941
935
|
|
|
942
936
|
for (let i = 0; i < endpoints.length; i++) {
|
|
943
937
|
const endpoint = endpoints[i];
|
|
@@ -1027,17 +1021,26 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1027
1021
|
}
|
|
1028
1022
|
|
|
1029
1023
|
const streamed = await streamResponse(currentResponse);
|
|
1030
|
-
//
|
|
1031
|
-
// endpoint
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1024
|
+
// Eventless silence may fail over to the alternate Antigravity
|
|
1025
|
+
// endpoint. Once thinking has streamed, the endpoint is already
|
|
1026
|
+
// committed downstream; Advisor mode may accept that silence,
|
|
1027
|
+
// while normal sessions surface it to final-output recovery.
|
|
1028
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1034
1029
|
const acceptedSilence =
|
|
1035
|
-
options?.acceptEmptyResponse === true &&
|
|
1030
|
+
options?.acceptEmptyResponse === true &&
|
|
1031
|
+
!streamed.strippedPlanningLeak &&
|
|
1032
|
+
(isLastEndpoint || thoughtOnly);
|
|
1036
1033
|
if (output.stopReason !== "stop" || streamed.meaningful || acceptedSilence) {
|
|
1037
1034
|
receivedContent = streamed.meaningful || acceptedSilence;
|
|
1038
1035
|
break;
|
|
1039
1036
|
}
|
|
1040
1037
|
|
|
1038
|
+
// A thought-only STOP is a complete provider response, not a
|
|
1039
|
+
// transiently empty transport. Replaying the identical request
|
|
1040
|
+
// burns another full reasoning pass; let session recovery add
|
|
1041
|
+
// an explicit final-output reminder instead.
|
|
1042
|
+
if (thoughtOnly) break;
|
|
1043
|
+
|
|
1041
1044
|
if (emptyAttempt < MAX_EMPTY_STREAM_RETRIES) {
|
|
1042
1045
|
resetOutput();
|
|
1043
1046
|
}
|
|
@@ -1051,10 +1054,16 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
1051
1054
|
}
|
|
1052
1055
|
|
|
1053
1056
|
if (!receivedContent) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1057
|
+
const thoughtOnly = hasThinkingOutput();
|
|
1058
|
+
throw new AIError.ProviderResponseError(
|
|
1059
|
+
thoughtOnly
|
|
1060
|
+
? "Cloud Code Assist API returned a thought-only response without final output"
|
|
1061
|
+
: "Cloud Code Assist API returned an empty response",
|
|
1062
|
+
{
|
|
1063
|
+
provider: model.provider,
|
|
1064
|
+
kind: thoughtOnly ? "empty-output" : "empty-body",
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1058
1067
|
}
|
|
1059
1068
|
|
|
1060
1069
|
if (options?.signal?.aborted) {
|
|
@@ -1320,14 +1329,6 @@ export function buildRequest(
|
|
|
1320
1329
|
};
|
|
1321
1330
|
}
|
|
1322
1331
|
|
|
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
1332
|
if (context.tools && context.tools.length > 0) {
|
|
1332
1333
|
const convertedTools = convertTools(context.tools, model);
|
|
1333
1334
|
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/usage/gemini.ts
CHANGED