@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 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.1",
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.1",
42
- "@oh-my-pi/pi-catalog": "17.3.1",
43
- "@oh-my-pi/pi-utils": "17.3.1",
44
- "@oh-my-pi/pi-wire": "17.3.1"
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",
@@ -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 | Flag.UsageLimit | Flag.ThinkingLoop | Flag.StaleResponsesItem | Flag.ProviderFinishError;
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;
@@ -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
- // An incomplete stream (connection dropped / truncated before any terminal
42
- // event) or an empty body never produced any content the request didn't
43
- // complete, so it is safe to retry and eligible for model fallback. The
44
- // retry layer's replay-unsafe guard still blocks a retry when partial tool
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
  }
@@ -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 === "QUOTA_EXHAUSTED" || (isBillingCapStatus && reason === "CONCURRENT_LIMIT");
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
- // Only accept an empty STOP as valid silence once every fallback
1031
- // endpoint is exhausted: an earlier endpoint returning empty
1032
- // successful streams must still fail over (Antigravity auto mode)
1033
- // rather than be recorded as a real silent review.
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 && !streamed.strippedPlanningLeak && isLastEndpoint;
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
- throw new AIError.ProviderResponseError("Cloud Code Assist API returned an empty response", {
1055
- provider: model.provider,
1056
- kind: "empty-body",
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 TIER_LEGACY = "legacy-tier";
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
- function readProjectId(value: string | { id?: string } | undefined): string | undefined {
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
- if (value && typeof value === "object" && typeof value.id === "string" && value.id.length > 0) {
57
- return value.id;
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(allowedTiers?: Array<{ id?: string; isDefault?: boolean }>): string {
63
- if (!allowedTiers || allowedTiers.length === 0) {
64
- return TIER_LEGACY;
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
- const defaultTier = allowedTiers.find(tier => tier.isDefault && typeof tier.id === "string" && tier.id.length > 0);
67
- if (defaultTier?.id) {
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 TIER_LEGACY;
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: { tierId: string; metadata: typeof ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA },
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 = readProjectId(operation.response?.cloudaicompanionProject);
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
- throwIfLoginCancelled(signal);
134
- const loadResponse = await oauthFetch(
135
- `${endpoint}/v1internal:loadCodeAssist`,
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
- if (!loadResponse.ok) {
147
- const errorText = await loadResponse.text();
148
- throw new AIError.OAuthError(
149
- `loadCodeAssist failed: ${loadResponse.status} ${loadResponse.statusText}: ${errorText}`,
150
- { kind: "discovery", status: loadResponse.status },
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
- const loadPayload = (await loadResponse.json()) as LoadCodeAssistPayload;
155
- const existingProject = readProjectId(loadPayload.cloudaicompanionProject);
156
- if (existingProject) {
157
- return existingProject;
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
- tierId,
164
- metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
207
+ tier_id: fallbackTierId,
208
+ metadata: getAntigravityOnboardMetadata(),
165
209
  };
166
- const provisionedProject = await onboardProjectWithRetries(endpoint, headers, onboardBody, signal, onProgress);
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) {
@@ -1,4 +1,4 @@
1
- import { getGeminiCliHeaders } from "../providers/google-gemini-cli";
1
+ import { getGeminiCliHeaders } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
2
2
  import type {
3
3
  UsageAmount,
4
4
  UsageFetchContext,