fix(cliproxy): attach entitlement evidence to agy failures

This commit is contained in:
Tam Nhu Tran
2026-04-10 13:42:51 -04:00
parent 7330fdbf28
commit 7c4545eb1b
2 changed files with 101 additions and 1 deletions
+70 -1
View File
@@ -185,6 +185,7 @@ interface ProjectLookupResult {
tier?: AccountTier; tier?: AccountTier;
rawTierId?: string | null; rawTierId?: string | null;
rawTierLabel?: string | null; rawTierLabel?: string | null;
entitlement?: ProviderEntitlementEvidence;
error?: string; error?: string;
errorCode?: string; errorCode?: string;
errorDetail?: string; errorDetail?: string;
@@ -219,7 +220,14 @@ function buildAntigravityFailure(
bodyText?: string bodyText?: string
): Pick< ): Pick<
QuotaResult, QuotaResult,
'error' | 'errorCode' | 'errorDetail' | 'actionHint' | 'retryable' | 'httpStatus' | 'needsReauth' | 'error'
| 'errorCode'
| 'errorDetail'
| 'actionHint'
| 'retryable'
| 'httpStatus'
| 'needsReauth'
| 'entitlement'
> & { isForbidden?: boolean } { > & { isForbidden?: boolean } {
const detail = normalizeErrorDetail(bodyText || ''); const detail = normalizeErrorDetail(bodyText || '');
@@ -232,6 +240,13 @@ function buildAntigravityFailure(
'Re-authenticate this account. If CLIProxy is running, retry after the proxy finishes refreshing the token.', 'Re-authenticate this account. If CLIProxy is running, retry after the proxy finishes refreshing the token.',
needsReauth: true, needsReauth: true,
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'unknown',
capacityState: 'unknown',
}),
}; };
} }
@@ -243,6 +258,13 @@ function buildAntigravityFailure(
actionHint: 'This account does not have Gemini Code Assist quota access.', actionHint: 'This account does not have Gemini Code Assist quota access.',
isForbidden: true, isForbidden: true,
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'not_entitled',
capacityState: 'unknown',
}),
}; };
} }
@@ -254,6 +276,13 @@ function buildAntigravityFailure(
actionHint: 'Retry later. This looks temporary.', actionHint: 'Retry later. This looks temporary.',
retryable: true, retryable: true,
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'rate_limited',
}),
}; };
} }
@@ -265,6 +294,13 @@ function buildAntigravityFailure(
actionHint: 'Retry later. This looks temporary.', actionHint: 'Retry later. This looks temporary.',
retryable: true, retryable: true,
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'temporarily_unavailable',
}),
}; };
} }
@@ -276,6 +312,13 @@ function buildAntigravityFailure(
actionHint: 'Retry later. The provider appears unavailable.', actionHint: 'Retry later. The provider appears unavailable.',
retryable: true, retryable: true,
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'temporarily_unavailable',
}),
}; };
} }
@@ -285,6 +328,13 @@ function buildAntigravityFailure(
error: `API error: ${status}`, error: `API error: ${status}`,
errorCode: 'quota_request_failed', errorCode: 'quota_request_failed',
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'unknown',
}),
}; };
} }
@@ -292,6 +342,13 @@ function buildAntigravityFailure(
error: 'Quota request failed', error: 'Quota request failed',
errorCode: 'quota_request_failed', errorCode: 'quota_request_failed',
errorDetail: detail, errorDetail: detail,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'low',
accessState: 'unknown',
capacityState: 'unknown',
}),
}; };
} }
@@ -581,6 +638,14 @@ async function getProjectId(accountId: string, accessToken: string): Promise<Pro
errorCode: 'account_unprovisioned', errorCode: 'account_unprovisioned',
actionHint: 'Complete sign-in in the Antigravity app, then retry quota refresh.', actionHint: 'Complete sign-in in the Antigravity app, then retry quota refresh.',
isUnprovisioned: true, isUnprovisioned: true,
entitlement: buildProviderEntitlementEvidence({
normalizedTier: 'unknown',
source: 'runtime_inference',
confidence: 'medium',
accessState: 'unknown',
capacityState: 'unknown',
notes: 'Project provisioning is incomplete for this account.',
}),
}; };
} }
@@ -881,6 +946,10 @@ export async function fetchAllProviderQuotas(
return results; return results;
} }
export const __testExports = {
buildAntigravityFailure,
};
/** /**
* Find available account with remaining quota * Find available account with remaining quota
* Used by preflight check for auto-switching * Used by preflight check for auto-switching
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'bun:test';
async function loadAntigravityQuotaTestExports() {
const moduleId = Date.now() + Math.random();
const mod = await import(`../../../src/cliproxy/quota-fetcher?agy-quota-fetcher=${moduleId}`);
return mod.__testExports;
}
describe('Antigravity quota failure metadata', () => {
it('marks 403 failures as not entitled', async () => {
const { buildAntigravityFailure } = await loadAntigravityQuotaTestExports();
const result = buildAntigravityFailure(403, 'forbidden');
expect(result.entitlement).toMatchObject({
accessState: 'not_entitled',
capacityState: 'unknown',
});
});
it('marks 429 failures as rate limited', async () => {
const { buildAntigravityFailure } = await loadAntigravityQuotaTestExports();
const result = buildAntigravityFailure(429, 'rate limited');
expect(result.entitlement).toMatchObject({
accessState: 'unknown',
capacityState: 'rate_limited',
});
});
});