fix(cliproxy): preserve entitlement evidence on agy failures

This commit is contained in:
Tam Nhu Tran
2026-04-10 14:28:32 -04:00
parent 506b61eca6
commit b56b58c067
3 changed files with 168 additions and 22 deletions
+36
View File
@@ -352,6 +352,28 @@ function buildAntigravityFailure(
};
}
function mergeAntigravityTierEvidence(
entitlement: ProviderEntitlementEvidence | undefined,
tier: AccountTier,
rawTierId: string | null,
rawTierLabel: string | null
): ProviderEntitlementEvidence | undefined {
if (tier === 'unknown' && !entitlement) {
return undefined;
}
return buildProviderEntitlementEvidence({
normalizedTier: tier,
rawTierId,
rawTierLabel,
source: rawTierId ? 'runtime_api' : (entitlement?.source ?? 'runtime_inference'),
confidence: rawTierId ? 'high' : (entitlement?.confidence ?? 'medium'),
accessState: entitlement?.accessState ?? 'unknown',
capacityState: entitlement?.capacityState ?? 'unknown',
notes: entitlement?.notes ?? null,
});
}
async function readManagedResponse(
response: Response,
viaManagement: boolean
@@ -620,6 +642,14 @@ async function getProjectId(accountId: string, accessToken: string): Promise<Pro
error: 'Invalid quota response from provider',
errorCode: 'provider_unavailable',
retryable: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'temporarily_unavailable',
notes: 'Provider returned a 2xx response with an empty or invalid project payload.',
}),
};
}
@@ -862,6 +892,12 @@ export async function fetchAccountQuota(
} else {
result.isExpired = authData.isExpired;
result.expiresAt = authData.expiresAt || undefined;
result.entitlement = mergeAntigravityTierEvidence(
result.entitlement,
apiTier,
rawTierId,
rawTierLabel
);
}
if (verbose && result.error) {
@@ -82,4 +82,134 @@ describe('Antigravity quota failure metadata', () => {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('attaches entitlement evidence when project lookup returns an invalid 2xx payload', async () => {
const moduleId = Date.now() + Math.random();
const { fetchAccountQuota } = await import(
`../../../src/cliproxy/quota-fetcher?agy-invalid-project=${moduleId}`
);
const { getProviderAuthDir } = await import(
`../../../src/cliproxy/config-generator?agy-config=${moduleId}`
);
const fs = await import('node:fs');
const os = await import('node:os');
const path = await import('node:path');
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-agy-invalid-project-'));
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
try {
const authDir = getProviderAuthDir('agy');
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(
path.join(authDir, 'antigravity-user@example.com.json'),
JSON.stringify({
type: 'antigravity',
email: 'user@example.com',
access_token: 'token',
})
);
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response('', {
status: 200,
headers: { 'Content-Type': 'application/json' },
})) as typeof fetch;
try {
const result = await fetchAccountQuota('agy', 'user@example.com');
expect(result.success).toBe(false);
expect(result.errorCode).toBe('provider_unavailable');
expect(result.entitlement).toMatchObject({
accessState: 'unknown',
capacityState: 'temporarily_unavailable',
});
} finally {
globalThis.fetch = originalFetch;
}
} finally {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('preserves live tier evidence when quota fetch fails after a successful project lookup', async () => {
const moduleId = Date.now() + Math.random();
const { fetchAccountQuota } = await import(
`../../../src/cliproxy/quota-fetcher?agy-invalid-models=${moduleId}`
);
const { getProviderAuthDir } = await import(
`../../../src/cliproxy/config-generator?agy-config=${moduleId}`
);
const fs = await import('node:fs');
const os = await import('node:os');
const path = await import('node:path');
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-agy-invalid-models-'));
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
try {
const authDir = getProviderAuthDir('agy');
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(
path.join(authDir, 'antigravity-user@example.com.json'),
JSON.stringify({
type: 'antigravity',
email: 'user@example.com',
access_token: 'token',
})
);
const originalFetch = globalThis.fetch;
let requestCount = 0;
globalThis.fetch = (async () => {
requestCount += 1;
if (requestCount === 1) {
return new Response(
JSON.stringify({
cloudaicompanionProject: { id: 'project-x' },
paidTier: { id: 'g1-pro-tier' },
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
return new Response('', {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}) as typeof fetch;
try {
const result = await fetchAccountQuota('agy', 'user@example.com');
expect(result.success).toBe(false);
expect(result.entitlement).toMatchObject({
normalizedTier: 'pro',
rawTierId: 'g1-pro-tier',
rawTierLabel: 'Pro',
accessState: 'unknown',
capacityState: 'temporarily_unavailable',
});
} finally {
globalThis.fetch = originalFetch;
}
} finally {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
});
+2 -22
View File
@@ -9,9 +9,11 @@ import type {
ListAiProvidersResult,
UpsertAiProviderEntryInput,
} from '../../../src/cliproxy/ai-providers';
import type { ProviderEntitlementEvidence } from '../../../src/cliproxy/provider-entitlement-types';
export const API_BASE_URL = '/api';
export const API_CONFLICT_ERROR_CODE = 'CONFLICT';
export type { ProviderEntitlementEvidence };
export class ApiConflictError extends Error {
readonly code = API_CONFLICT_ERROR_CODE;
@@ -566,28 +568,6 @@ export interface QuotaResult {
entitlement?: ProviderEntitlementEvidence;
}
export interface ProviderEntitlementEvidence {
normalizedTier: 'free' | 'pro' | 'ultra' | 'unknown';
rawTierId: string | null;
rawTierLabel: string | null;
source: 'runtime_api' | 'runtime_inference' | 'registry_cache' | 'official_docs';
confidence: 'high' | 'medium' | 'low';
accessState:
| 'entitled'
| 'not_entitled'
| 'capacity_exhausted'
| 'temporarily_unavailable'
| 'unknown';
capacityState:
| 'available'
| 'capacity_exhausted'
| 'rate_limited'
| 'temporarily_unavailable'
| 'unknown';
lastVerifiedAt: number;
notes?: string | null;
}
/** Codex rate limit window */
export interface CodexQuotaWindow {
/** Window label: "Primary", "Secondary", "Code Review (Primary)", "Code Review (Secondary)" */