Repository navigation
fix(antigravity): consolidate multi-account discovery and model-denial routing #7835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30a7330
84b15d9
5ddaa7b
20237e4
9f4d777
cc14ebf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { getConfigDir } from "../../config/paths"; | ||
| import { getValidAccessSnapshotForAccount, type OAuthAccessSnapshot } from "../../oauth"; | ||
| import { credentialGeneration, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store"; | ||
| import { providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound"; | ||
| import { isRegistryModelDiscoveryUrl, readBoundedDiscoveryJson } from "../../providers/model-discovery"; | ||
| import { parseAntigravityAvailableModels, type AntigravityAvailableModel } from "../../providers/antigravity-models"; | ||
| import { captureAntigravityModelObservation, recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster"; | ||
| import type { OcxProviderConfig } from "../../types"; | ||
| import { materializeCapturedHeaders, type CapturedModelsRequest } from "./gather-capture"; | ||
|
|
||
| /** A sibling contributes only while the exact snapshot sent is still usable. */ | ||
| export async function discoverAntigravitySiblings( | ||
| name: string, provider: OcxProviderConfig, activeAccountId: string, | ||
| request: CapturedModelsRequest, maxBytes: number, maxModels: number, | ||
| root: string, | ||
| ): Promise<() => AntigravityAvailableModel[]> { | ||
| if (root !== getConfigDir()) return () => []; | ||
| const results: Array<{ | ||
| snapshot: OAuthAccessSnapshot; models: AntigravityAvailableModel[]; | ||
| observation: ReturnType<typeof captureAntigravityModelObservation>; | ||
| }> = []; | ||
| const current = (snapshot: OAuthAccessSnapshot): boolean => { | ||
| if (getConfigDir() !== root) return false; | ||
| const row = getAccountCredentialWithStatus(name, snapshot.accountId); | ||
| return !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now() | ||
| && credentialGeneration(row.credential) === snapshot.generation | ||
| && row.credential.access === snapshot.accessToken; | ||
| }; | ||
| // Bound concurrency and preserve stable account order; a failed sibling is fail-soft. | ||
| for (const sibling of getAccountSet(name)?.accounts ?? []) { | ||
| if (sibling.id === activeAccountId || sibling.paused || sibling.needsReauth) continue; | ||
| try { | ||
| if (getConfigDir() !== root) break; | ||
| const snapshot = await getValidAccessSnapshotForAccount(name, sibling.id, { requireUsableAccount: true }); | ||
| const project = provider.project ?? snapshot.projectId; | ||
| if (!project || !current(snapshot)) continue; | ||
| const observation = captureAntigravityModelObservation(); | ||
| const response = await providerOutboundPost(name, provider, request.url, { | ||
| headers: materializeCapturedHeaders(request, snapshot.accessToken), | ||
| body: JSON.stringify({ project }), signal: AbortSignal.timeout(8000), | ||
| }, { isCanonicalUrl: isRegistryModelDiscoveryUrl, beforeSend: () => current(snapshot) }); | ||
| if (!response.ok || await providerRedirectError(response, request.url)) continue; | ||
| const bounded = await readBoundedDiscoveryJson(response, maxBytes); | ||
| if (!bounded.ok) continue; | ||
| const models = parseAntigravityAvailableModels(bounded.value, maxModels); | ||
| if (models && current(snapshot)) results.push({ snapshot, models, observation }); | ||
| } catch { /* A sibling cannot invalidate successful active-account discovery. */ } | ||
| } | ||
| // Called synchronously with the catalog publication, after all outbound awaits. | ||
| return () => { | ||
| const union: AntigravityAvailableModel[] = []; | ||
| for (const { snapshot, models, observation } of results) { | ||
| if (!current(snapshot)) continue; | ||
| recordAntigravityAccountRoster(name, snapshot.accountId, models, Date.now(), observation); | ||
| union.push(...models); | ||
| } | ||
| return union; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,7 +53,8 @@ import { | |
| import type { FastPolicyAuthority } from "../../providers/fastwire"; | ||
| import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry"; | ||
| import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models"; | ||
| import { recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster"; | ||
| import { captureAntigravityModelObservation, recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster"; | ||
| import { discoverAntigravitySiblings } from "./antigravity-discovery"; | ||
| import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; | ||
| import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"; | ||
| import { effectiveModelAliases } from "../../providers/default-aliases"; | ||
|
|
@@ -729,6 +730,7 @@ export async function fetchProviderModelsWithAuth( | |
| // fetch a different URL and keep the rejection. | ||
| if (!maySendAnthropicDiscovery()) return observed(withConfiguredRetention(failedDiscoveryConfigured), "degraded"); | ||
| const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl, beforeSend: maySendAnthropicDiscovery }; | ||
| const antigravityObservation = cloudCodeAssist ? captureAntigravityModelObservation() : undefined; | ||
| const res = request.method === "POST" | ||
| ? await providerOutboundPost(name, prov, url, { | ||
| headers, | ||
|
|
@@ -790,7 +792,21 @@ export async function fetchProviderModelsWithAuth( | |
| return observed(models, "degraded"); | ||
| } | ||
| if (antigravity) { | ||
| const upstream: CatalogModel[] = antigravity.map(model => ({ | ||
| if (getConfigDir() !== metadataConfigDir) return observed(withConfiguredRetention(configured), "degraded"); | ||
| const admitSiblings = resolveAuth.kind === "refreshing" && name === "google-antigravity" | ||
| && prov.authMode === "oauth" && auth.oauthAccountId | ||
| ? await discoverAntigravitySiblings(name, prov, auth.oauthAccountId, request, discovery.maxResponseBytes, discovery.maxModels, metadataConfigDir) | ||
| : () => []; | ||
| if (!isCurrentCacheGeneration() || getConfigDir() !== metadataConfigDir) return observed(withConfiguredRetention(configured), "degraded"); | ||
| const activeAccount = auth.oauthAccountId ? getAccountCredentialWithStatus(name, auth.oauthAccountId) : null; | ||
| if (auth.oauthAccountId && (!activeAccount || activeAccount.paused || activeAccount.needsReauth | ||
| || activeAccount.credential.expires <= Date.now() || activeAccount.credential.access !== apiKey | ||
| || credentialGeneration(activeAccount.credential) !== auth.oauthGeneration)) | ||
| return observed(withConfiguredRetention(configured), "degraded"); | ||
| const byId = new Map(antigravity.map(model => [model.id, model])); | ||
| for (const model of admitSiblings()) if (!byId.has(model.id)) byId.set(model.id, model); | ||
| const merged = [...byId.values()]; | ||
| const upstream: CatalogModel[] = merged.map(model => ({ | ||
| id: model.id, | ||
| provider: name, | ||
| // Only an exact discovered wire map proves a new model's selectable effort ladder. | ||
|
|
@@ -810,13 +826,13 @@ export async function fetchProviderModelsWithAuth( | |
| } | ||
| // Save exact family routes before publishing synthetic IDs. These synchronous writes | ||
| // cannot interleave with a credential-generation change; failure keeps the prior cache. | ||
| registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, { | ||
| registerAntigravityDiscoveredWireModels(prov.baseUrl, merged, { | ||
| provider: name, | ||
| cacheGeneration, | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| // Entitlement is per account (Claude 4.6 vs 5.5, Argon): remember what THIS account | ||
| // lists so account selection can prefer one that serves the requested model. | ||
| if (auth.oauthAccountId) recordAntigravityAccountRoster(name, auth.oauthAccountId, antigravity); | ||
| if (auth.oauthAccountId) recordAntigravityAccountRoster(name, auth.oauthAccountId, antigravity, Date.now(), antigravityObservation); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reject older roster observations before publication. When distinct catalog gather flights finish out of order, Line 835 can pass an older observation after a newer roster has been stored. As per coding guidelines, “Preserve existing public exports and configuration compatibility unless the task explicitly changes them.” Keep the roster API compatible while enforcing observation order.
|
||
| if (!setCached(name, forCache, Date.now(), cacheGeneration, catalogAuthority)) { | ||
| return observed(withConfiguredRetention(configured), "degraded"); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: lidge-jun/opencodex
Length of output: 41740
Script executed:
Repository: lidge-jun/opencodex
Length of output: 35192
Script executed:
Repository: lidge-jun/opencodex
Length of output: 13335
Script executed:
Repository: lidge-jun/opencodex
Length of output: 2454
Reject malformed UTF-8 before recording a model denial.
When invalid UTF-8 occurs inside a JSON string,
readBoundedResponseBodyreplaces it and still returnsdisplaySafe: true.parseUpstreamJsonPayloadcan then parse the body, and a matchingResourceInfocan mark the normalized 404 as model-specific. Recovery records that denial, which can exclude the account for 24 hours.Suggested fix