fix(antigravity): bootstrap project via loadCodeAssist + fetchAvailableModels fallback - #2219
Conversation
…tchAvailableModels fallback (#12) * fix(antigravity): bootstrap project assignment via loadCodeAssist before discovery All three antigravity discovery endpoints (prod, daily, sandbox) were 404-ing because :models requires a prior :loadCodeAssist call to assign a project id to the OAuth token. Without that bootstrap step, the server has no context for the request and returns 404. Adds open-sse/services/antigravityProjectBootstrap.ts with an idempotent ensureAntigravityProjectAssigned() helper that calls :loadCodeAssist before the first :models request for each access token, with per-token memoization to avoid repeated bootstraps across concurrent or sequential discovery calls. Falls through all three base URLs on failure (non-fatal). Wired into fetchAntigravityDiscoveryModelsCached() in the models route immediately after resolveAntigravityVersion() and before the discovery URL loop. Mirrors the loadCodeAssist flow already used in: - src/lib/oauth/services/antigravity.ts (OAuth connect flow) - open-sse/executors/gemini-cli.ts (Gemini CLI project refresh) Fixes: 3x antigravity discovery 404s on container start, 2026-05-13. * fix(antigravity): try fetchAvailableModels before models in discovery loop The /v1internal:models endpoint returns 404 for free-tier accounts while /v1internal:fetchAvailableModels returns 200 with the full model list. The normalizer already handles the fetchAvailableModels response format (object map keyed by model id). Adding fetchAvailableModels URLs first in the discovery loop ensures successful discovery for free-tier accounts, with models URLs retained as fallback for accounts that may support them. --------- Co-authored-by: OmniRoute Ops <ops@nomenak.dev>
There was a problem hiding this comment.
Code Review
This pull request introduces a bootstrap mechanism for the Google Cloud Code Assist API to ensure a project context is assigned to OAuth tokens before model discovery. It adds a new service, antigravityProjectBootstrap.ts, which handles memoized project assignment and integrates this step into the model discovery route. Comprehensive unit tests were also added to verify the bootstrap logic and execution order. Review feedback recommends using AbortSignal.timeout() for more concise timeout management and adopting the Headers constructor in tests to improve type safety when accessing request headers.
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), BOOTSTRAP_TIMEOUT_MS); | ||
| try { | ||
| const response = await fetchImpl(url, { | ||
| method: "POST", | ||
| headers: getAntigravityHeaders("loadCodeAssist", accessToken), | ||
| body: JSON.stringify({ metadata: getAntigravityLoadCodeAssistMetadata() }), | ||
| signal: controller.signal, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| console.warn( | ||
| `[models] antigravity loadCodeAssist failed at ${url} (${response.status}) — trying next` | ||
| ); | ||
| continue; | ||
| } | ||
|
|
||
| const data = (await response.json()) as Record<string, unknown>; | ||
|
|
||
| // cloudaicompanionProject may be a plain string or an object with an id field. | ||
| const raw = data.cloudaicompanionProject; | ||
| let projectId = | ||
| typeof raw === "string" | ||
| ? raw.trim() | ||
| : raw && | ||
| typeof raw === "object" && | ||
| typeof (raw as Record<string, unknown>).id === "string" | ||
| ? ((raw as Record<string, unknown>).id as string).trim() | ||
| : ""; | ||
|
|
||
| if (projectId) { | ||
| return projectId; | ||
| } | ||
|
|
||
| console.warn( | ||
| `[models] antigravity loadCodeAssist at ${url} returned no project id — trying next` | ||
| ); | ||
| } catch (error) { | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
| console.warn(`[models] antigravity loadCodeAssist threw for ${url}: ${msg} — trying next`); | ||
| } finally { | ||
| clearTimeout(timeoutId); | ||
| } |
There was a problem hiding this comment.
For improved readability and to leverage modern platform APIs, you can simplify the timeout logic by using AbortSignal.timeout(). This avoids the need for manual setTimeout and clearTimeout management, making the code more concise and less error-prone.
try {
const response = await fetchImpl(url, {
method: "POST",
headers: getAntigravityHeaders("loadCodeAssist", accessToken),
body: JSON.stringify({ metadata: getAntigravityLoadCodeAssistMetadata() }),
signal: AbortSignal.timeout(BOOTSTRAP_TIMEOUT_MS),
});
if (!response.ok) {
console.warn(
`[models] antigravity loadCodeAssist failed at ${url} (${response.status}) — trying next`
);
continue;
}
const data = (await response.json()) as Record<string, unknown>;
// cloudaicompanionProject may be a plain string or an object with an id field.
const raw = data.cloudaicompanionProject;
let projectId =
typeof raw === "string"
? raw.trim()
: raw &&
typeof raw === "object" &&
typeof (raw as Record<string, unknown>).id === "string"
? ((raw as Record<string, unknown>).id as string).trim()
: "";
if (projectId) {
return projectId;
}
console.warn(
`[models] antigravity loadCodeAssist at ${url} returned no project id — trying next`
);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
console.warn(`[models] antigravity loadCodeAssist threw for ${url}: ${msg} — trying next`);
}There was a problem hiding this comment.
Already addressed in 950aab2 (pushed after this review) — swapped the manual setTimeout / clearTimeout for AbortSignal.timeout(8_000). Thanks for the suggestion.
| const calledFor: string[] = []; | ||
|
|
||
| const mockFetch = async (url: string, init?: RequestInit): Promise<Response> => { | ||
| const auth = (init?.headers as Record<string, string> | undefined)?.Authorization ?? ""; |
There was a problem hiding this comment.
The type assertion as Record<string, string> is not entirely safe because init.headers can have other types (e.g., a Headers object or string[][]). Using the Headers constructor is a more robust and type-safe way to parse the headers.
| const auth = (init?.headers as Record<string, string> | undefined)?.Authorization ?? ""; | |
| const auth = new Headers(init?.headers).get("Authorization") ?? ""; |
There was a problem hiding this comment.
Already addressed in 950aab2 — both headers assertions in the test now use new Headers(init.headers) and .get(...) instead of the unsafe Record<string, string> cast. Thanks.
| let capturedAuth: string | null = null; | ||
|
|
||
| const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => { | ||
| capturedAuth = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null; |
There was a problem hiding this comment.
To improve type safety and robustness, it's better to use the Headers constructor to parse the headers rather than relying on a potentially unsafe type assertion. This ensures the code correctly handles all possible types for init.headers.
| capturedAuth = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null; | |
| capturedAuth = new Headers(init?.headers).get("Authorization") ?? null; |
There was a problem hiding this comment.
Same fix in 950aab2 — second Headers ctor swap applied at the same time. Thanks for the pair of catches.
…r per review Apply gemini-code-assist suggestions from PR diegosouzapw#2219: - antigravityProjectBootstrap.ts: replace manual AbortController + setTimeout/clearTimeout pair with AbortSignal.timeout() — drops the finally block. - antigravity-discovery-bootstrap.test.ts: parse Authorization via new Headers(init?.headers).get() instead of unsafe Record cast. All 9 tests in tests/unit/antigravity-discovery-bootstrap.test.ts still pass.
|
All 3 suggestions applied in 950aab2:
Tests still pass (9/9 in antigravity-discovery-bootstrap.test.ts). Thanks for the review. |
|
Thanks @NomenAK! Your contribution has been integrated into The PR branch was synced with the latest Reviewed and merged via the |
After merging PRs #2221 (ModelSync shared loopback readiness gate + IPv4 force) and #2219 (Antigravity loadCodeAssist bootstrap + fetchAvailableModels fallback) into release/v3.8.0, two test suites needed updates to match the new routing: - tests/unit/model-sync-route.test.ts: * resetStorage() now calls __resetLoopbackReadinessForTests() so the module-level __loopbackReadyPromise cache does not leak between tests. * Every fetch mock now answers the /__readiness_probe__/ URL with 404 so the gate opens immediately (any HTTP response satisfies the probe). * Self-fetch target URL assertions updated from http://localhost/... to http://127.0.0.1:20128/... per PR #2221's IPv4-force. - tests/unit/provider-models-route.test.ts: * The Antigravity discovery-retry test now treats loadCodeAssist calls as non-fatal failures so the discovery path is still exercised. * The expected discovery URL sequence is updated to the new fetchAvailableModels-first order introduced by PR #2219.
Deep audit of all 320 commits since v3.7.9 found: - 18 merged PRs not documented in CHANGELOG (4 features, 10 bug fixes, 1 security, 2 chores, 1 debug improvement) - 3 contributors entirely missing from credits table (@NomenAK with 12 PRs, @kang-heewon, @one-vs) - 4 existing contributors with inaccurate PR counts (@oyi77 8→12, @ddarkr 2→3, @andrewmunsell 2→3, @nickwizard 2→3) New entries added: - feat: #2135 (1proxy settings), #2227 (antigravity project ID), #2238 (Z.AI Search), #2240 (CLI Suite) - fix: #2217, #2218, #2219, #2221, #2222, #2223, #2224, #2231, #2233, #2236, #2242, #2243 - security: #2209 (stack trace exposure) - chore: #2228, #2234 Total contributors updated from 50+ to 55+.
…uzapw#5193 regression of diegosouzapw#2541) PR diegosouzapw#5193 changed onboarding from inline await to fire-and-forget gated by if (projectId), which never fires when projectId is empty — the exact case that needs onboarding. This re-introduced the diegosouzapw#2541 catch-22. Add an else-if branch that attempts onboarding inline (bounded by AbortSignal.timeout) when projectId is empty, then retries loadCodeAssist to discover the newly created project. - Existing accounts with projectId: unchanged (fire-and-forget) - New accounts without projectId: now onboarded within login flow - Timeout bounded: +8s worst case (onboardUser + retry loadCodeAssist) - Graceful degradation: if onboarding fails, lazy retry handles it Tests: - 3 new tests covering empty-projectId onboarding path (RED→GREEN) - Existing 2 tests preserved and passing - Adjusted timeout assertion for the stall test (now includes onboardUser stall) - 5/5 passing on Node 24 Fixes diegosouzapw#7814 Related: diegosouzapw#5193, diegosouzapw#2569, diegosouzapw#2541, diegosouzapw#2219
…uzapw#5193 regression of diegosouzapw#2541) PR diegosouzapw#5193 changed onboarding from inline await to fire-and-forget gated by if (projectId), which never fires when projectId is empty — the exact case that needs onboarding. This re-introduced the diegosouzapw#2541 catch-22. Add an else-if branch that attempts onboarding inline (bounded by AbortSignal.timeout) when projectId is empty, then retries loadCodeAssist to discover the newly created project. - Existing accounts with projectId: unchanged (fire-and-forget) - New accounts without projectId: now onboarded within login flow - Timeout bounded: +8s worst case (onboardUser + retry loadCodeAssist) - Graceful degradation: if onboarding fails, lazy retry handles it Tests: - 3 new tests covering empty-projectId onboarding path (RED→GREEN) - Existing 2 tests preserved and passing - Adjusted timeout assertion for the stall test (now includes onboardUser stall) - 5/5 passing on Node 24 Fixes diegosouzapw#7814 Related: diegosouzapw#5193, diegosouzapw#2569, diegosouzapw#2541, diegosouzapw#2219
…egression of #2541) (#7815) * fix(antigravity): attempt onboarding when projectId is empty (#5193 regression of #2541) PR #5193 changed onboarding from inline await to fire-and-forget gated by if (projectId), which never fires when projectId is empty — the exact case that needs onboarding. This re-introduced the #2541 catch-22. Add an else-if branch that attempts onboarding inline (bounded by AbortSignal.timeout) when projectId is empty, then retries loadCodeAssist to discover the newly created project. - Existing accounts with projectId: unchanged (fire-and-forget) - New accounts without projectId: now onboarded within login flow - Timeout bounded: +8s worst case (onboardUser + retry loadCodeAssist) - Graceful degradation: if onboarding fails, lazy retry handles it Tests: - 3 new tests covering empty-projectId onboarding path (RED→GREEN) - Existing 2 tests preserved and passing - Adjusted timeout assertion for the stall test (now includes onboardUser stall) - 5/5 passing on Node 24 Fixes #7814 Related: #5193, #2569, #2541, #2219 * docs(changelog): add fragment for antigravity onboarding empty-projectId fix (#7814) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: Rafael Dias Zendron <rafaumeu@users.noreply.github.com>
…leModels fallback (diegosouzapw#2219) Integrated into release/v3.8.0
After merging PRs diegosouzapw#2221 (ModelSync shared loopback readiness gate + IPv4 force) and diegosouzapw#2219 (Antigravity loadCodeAssist bootstrap + fetchAvailableModels fallback) into release/v3.8.0, two test suites needed updates to match the new routing: - tests/unit/model-sync-route.test.ts: * resetStorage() now calls __resetLoopbackReadinessForTests() so the module-level __loopbackReadyPromise cache does not leak between tests. * Every fetch mock now answers the /__readiness_probe__/ URL with 404 so the gate opens immediately (any HTTP response satisfies the probe). * Self-fetch target URL assertions updated from http://localhost/... to http://127.0.0.1:20128/... per PR diegosouzapw#2221's IPv4-force. - tests/unit/provider-models-route.test.ts: * The Antigravity discovery-retry test now treats loadCodeAssist calls as non-fatal failures so the discovery path is still exercised. * The expected discovery URL sequence is updated to the new fetchAvailableModels-first order introduced by PR diegosouzapw#2219.
Deep audit of all 320 commits since v3.7.9 found: - 18 merged PRs not documented in CHANGELOG (4 features, 10 bug fixes, 1 security, 2 chores, 1 debug improvement) - 3 contributors entirely missing from credits table (@NomenAK with 12 PRs, @kang-heewon, @one-vs) - 4 existing contributors with inaccurate PR counts (@oyi77 8→12, @ddarkr 2→3, @andrewmunsell 2→3, @nickwizard 2→3) New entries added: - feat: diegosouzapw#2135 (1proxy settings), diegosouzapw#2227 (antigravity project ID), diegosouzapw#2238 (Z.AI Search), diegosouzapw#2240 (CLI Suite) - fix: diegosouzapw#2217, diegosouzapw#2218, diegosouzapw#2219, diegosouzapw#2221, diegosouzapw#2222, diegosouzapw#2223, diegosouzapw#2224, diegosouzapw#2231, diegosouzapw#2233, diegosouzapw#2236, diegosouzapw#2242, diegosouzapw#2243 - security: diegosouzapw#2209 (stack trace exposure) - chore: diegosouzapw#2228, diegosouzapw#2234 Total contributors updated from 50+ to 55+.
…uzapw#5193 regression of diegosouzapw#2541) (diegosouzapw#7815) * fix(antigravity): attempt onboarding when projectId is empty (diegosouzapw#5193 regression of diegosouzapw#2541) PR diegosouzapw#5193 changed onboarding from inline await to fire-and-forget gated by if (projectId), which never fires when projectId is empty — the exact case that needs onboarding. This re-introduced the diegosouzapw#2541 catch-22. Add an else-if branch that attempts onboarding inline (bounded by AbortSignal.timeout) when projectId is empty, then retries loadCodeAssist to discover the newly created project. - Existing accounts with projectId: unchanged (fire-and-forget) - New accounts without projectId: now onboarded within login flow - Timeout bounded: +8s worst case (onboardUser + retry loadCodeAssist) - Graceful degradation: if onboarding fails, lazy retry handles it Tests: - 3 new tests covering empty-projectId onboarding path (RED→GREEN) - Existing 2 tests preserved and passing - Adjusted timeout assertion for the stall test (now includes onboardUser stall) - 5/5 passing on Node 24 Fixes diegosouzapw#7814 Related: diegosouzapw#5193, diegosouzapw#2569, diegosouzapw#2541, diegosouzapw#2219 * docs(changelog): add fragment for antigravity onboarding empty-projectId fix (diegosouzapw#7814) Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com> Co-authored-by: Rafael Dias Zendron <rafaumeu@users.noreply.github.com>
…leModels fallback (diegosouzapw#2219) Integrated into release/v3.8.0
After merging PRs diegosouzapw#2221 (ModelSync shared loopback readiness gate + IPv4 force) and diegosouzapw#2219 (Antigravity loadCodeAssist bootstrap + fetchAvailableModels fallback) into release/v3.8.0, two test suites needed updates to match the new routing: - tests/unit/model-sync-route.test.ts: * resetStorage() now calls __resetLoopbackReadinessForTests() so the module-level __loopbackReadyPromise cache does not leak between tests. * Every fetch mock now answers the /__readiness_probe__/ URL with 404 so the gate opens immediately (any HTTP response satisfies the probe). * Self-fetch target URL assertions updated from http://localhost/... to http://127.0.0.1:20128/... per PR diegosouzapw#2221's IPv4-force. - tests/unit/provider-models-route.test.ts: * The Antigravity discovery-retry test now treats loadCodeAssist calls as non-fatal failures so the discovery path is still exercised. * The expected discovery URL sequence is updated to the new fetchAvailableModels-first order introduced by PR diegosouzapw#2219.
Deep audit of all 320 commits since v3.7.9 found: - 18 merged PRs not documented in CHANGELOG (4 features, 10 bug fixes, 1 security, 2 chores, 1 debug improvement) - 3 contributors entirely missing from credits table (@NomenAK with 12 PRs, @kang-heewon, @one-vs) - 4 existing contributors with inaccurate PR counts (@oyi77 8→12, @ddarkr 2→3, @andrewmunsell 2→3, @nickwizard 2→3) New entries added: - feat: diegosouzapw#2135 (1proxy settings), diegosouzapw#2227 (antigravity project ID), diegosouzapw#2238 (Z.AI Search), diegosouzapw#2240 (CLI Suite) - fix: diegosouzapw#2217, diegosouzapw#2218, diegosouzapw#2219, diegosouzapw#2221, diegosouzapw#2222, diegosouzapw#2223, diegosouzapw#2224, diegosouzapw#2231, diegosouzapw#2233, diegosouzapw#2236, diegosouzapw#2242, diegosouzapw#2243 - security: diegosouzapw#2209 (stack trace exposure) - chore: diegosouzapw#2228, diegosouzapw#2234 Total contributors updated from 50+ to 55+.
What
Bootstrap Antigravity project assignment via
:loadCodeAssistbefore model discovery, and prefer/v1internal:fetchAvailableModelsover/v1internal:models(which 404s for free-tier accounts).Why
Two root causes were observed when antigravity discovery 404'd on every container boot:
/v1internal:modelsrequires a prior/v1internal:loadCodeAssistcall to assign a project id to the OAuth token. Without that bootstrap, the server has no context and returns 404 for all three base URLs (prod, daily, sandbox)./v1internal:modelsis dead for free-tier accounts. The correct endpoint is/v1internal:fetchAvailableModels, which returns 200 with the full model list.Both gaps confirmed by direct probing with a live token.
How
open-sse/services/antigravityProjectBootstrap.ts(~129 lines):ensureAntigravityProjectAssigned(accessToken, opts?)— idempotent loadCodeAssist call with per-token memoization, 8s timeout per URL, tries all 3 base URLs, non-fatal on failuresrc/app/api/providers/[id]/models/route.ts:ensureAntigravityProjectAssigned(accessToken)immediately afterresolveAntigravityVersion()[...fetchAvailableModelsUrls, ...modelsDiscoveryUrls]— fetchAvailableModels first,:modelskept as paid-tier fallbacktests/unit/antigravity-discovery-bootstrap.test.ts: memoization, fallthrough, non-fatal errors, auth headers, ordering, etc.clearAntigravityProjectCacheandgetAntigravityProjectFromCachetest helpersNotes
The fix keeps
:modelsin the fallback list because paid-tier accounts may still use it — happy to remove it entirely if you prefer the cleaner code path. Also happy to revise the cache eviction strategy or the timeout values if they don't fit upstream defaults.