Files
OmniRoute/open-sse/services/antigravityProjectPersistence.ts
Andrew B. 9a99a39b33 fix(antigravity): quota-aware account selection and projectId persistence (#8891)
* fix(antigravity): per-model quota + 30min credits_exhausted reprobe

- accountFallback.ts: hasPerModelQuota() now treats antigravity/agy as
  per-model quota. A single-model 429 no longer cascades to all models
  in the provider.
- connectionRecovery.ts: credits_exhausted removed from terminal set;
  isCreditsExhaustedReprobeCandidate() with 30min default. Loads
  active+inactive rows so inactive credits_exhausted accounts can recover.
- tests/unit/quota-connection-recovery.test.ts: 6 cases covering pure
  helpers + tick wiring.

* fix(antigravity): persist projectId and prefer healthy accounts

Save Cloud Code projectId after runtime discovery, skip accounts missing
projectId when alternatives exist, and mark missing_project_id on 422.

* fix(antigravity): skip quota-exhausted models during account selection

Avoid repeatedly dispatching to Antigravity models that already report
exhausted quota, reducing wasted upstream calls and combo fallback latency.

---------

Co-authored-by: hermes <hermes@nous.local>
2026-08-11 04:25:13 -03:00

133 lines
4.3 KiB
TypeScript

/**
* @file antigravityProjectPersistence.ts
* @description Persist Antigravity Cloud Code projectId discovered at runtime and prefer
* healthy accounts during dynamic multi-account selection.
*
* @changes
* - [2026-07-24] [Composer] - Persist runtime loadCodeAssist projectId; filter broken accounts
*/
import { updateProviderConnection } from "@/lib/db/providers";
export type AntigravityProjectConnectionLike = {
projectId?: string | null;
providerSpecificData?: unknown;
errorCode?: string | null;
};
export function extractAntigravityProjectIdFromPayload(
data: Record<string, unknown> | null | undefined
): string | null {
if (!data || typeof data !== "object") return null;
const raw = data.cloudaicompanionProject;
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
const id = (raw as Record<string, unknown>).id;
if (typeof id === "string" && id.trim()) return id.trim();
}
return null;
}
export function getStoredAntigravityProjectId(
connection: Pick<AntigravityProjectConnectionLike, "projectId" | "providerSpecificData">
): string | null {
const column = typeof connection.projectId === "string" ? connection.projectId.trim() : "";
if (column) return column;
const psd = connection.providerSpecificData as Record<string, unknown> | undefined;
const fromPsd = typeof psd?.projectId === "string" ? psd.projectId.trim() : "";
return fromPsd || null;
}
const persistInFlight = new Set<string>();
export function persistDiscoveredAntigravityProjectId(
connectionId: string | null | undefined,
projectId: string,
existingProviderSpecificData?: Record<string, unknown> | null
): void {
const trimmed = projectId.trim();
if (!connectionId || !trimmed) return;
const dedupeKey = `${connectionId}:${trimmed}`;
if (persistInFlight.has(dedupeKey)) return;
persistInFlight.add(dedupeKey);
const providerSpecificData = {
...(existingProviderSpecificData || {}),
projectId: trimmed,
};
void updateProviderConnection(connectionId, {
projectId: trimmed,
errorCode: null,
lastError: null,
lastErrorType: null,
providerSpecificData,
})
.catch(() => {})
.finally(() => {
persistInFlight.delete(dedupeKey);
});
}
export function markAntigravityMissingCloudCodeProject(
connectionId: string | null | undefined
): void {
if (!connectionId) return;
void updateProviderConnection(connectionId, {
errorCode: "missing_project_id",
lastError:
"Missing Google projectId for Antigravity account. Reconnect OAuth after completing Gemini Code Assist onboarding.",
lastErrorType: "oauth_missing_project_id",
}).catch(() => {});
}
/**
* When dynamic routing spans multiple Antigravity accounts, prefer connections that
* already have a stored Cloud Code projectId. Accounts confirmed missing a project
* (422) are skipped when alternatives exist. If every account lacks a stored project,
* keep the full pool so request-time loadCodeAssist discovery can still recover (#2334).
*/
export function preferAntigravityConnectionsWithStoredProject<T extends object>(
connections: T[]
): T[] {
if (connections.length <= 1) return connections;
const hasStoredProject = (connection: T): boolean => {
const record = connection as Record<string, unknown>;
if (typeof record.projectId === "string" && record.projectId.trim()) return true;
let psd = record.providerSpecificData;
if (typeof psd === "string") {
try {
psd = JSON.parse(psd);
} catch {
return false;
}
}
if (!psd || typeof psd !== "object") return false;
const projectId = (psd as Record<string, unknown>).projectId;
return typeof projectId === "string" && projectId.trim().length > 0;
};
const withoutKnownMissing = connections.filter(
(connection) =>
(connection as Record<string, unknown>).errorCode !== "missing_project_id" ||
hasStoredProject(connection)
);
const pool = withoutKnownMissing.length > 0 ? withoutKnownMissing : connections;
const withStored = pool.filter(hasStoredProject);
if (withStored.length > 0 && withStored.length < pool.length) {
return withStored;
}
return pool;
}
/** Test helper — reset in-flight dedupe guards. */
export function clearAntigravityProjectPersistenceInFlight(): void {
persistInFlight.clear();
}