@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 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
@@ -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.0",
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.0",
42
- "@oh-my-pi/pi-catalog": "17.3.0",
43
- "@oh-my-pi/pi-utils": "17.3.0",
44
- "@oh-my-pi/pi-wire": "17.3.0"
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",
@@ -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,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
- !started &&
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 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) {
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
  },
@@ -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,