mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 10:23:16 +00:00
feat(cliproxy): add entitlement evidence for gemini and agy
This commit is contained in:
@@ -86,7 +86,9 @@ export function AccountSurfaceCard({
|
||||
const title = displayEmail || identity.email || accountId;
|
||||
const normalizedProvider = provider.toLowerCase();
|
||||
const showTierBadge =
|
||||
(normalizedProvider === 'agy' || normalizedProvider === 'antigravity') &&
|
||||
(normalizedProvider === 'agy' ||
|
||||
normalizedProvider === 'antigravity' ||
|
||||
normalizedProvider === 'gemini') &&
|
||||
tier &&
|
||||
tier !== 'unknown' &&
|
||||
tier !== 'free';
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type ModelTier,
|
||||
type UnifiedQuotaResult,
|
||||
} from '@/lib/utils';
|
||||
import type { ProviderEntitlementEvidence } from '@/lib/api-client';
|
||||
|
||||
interface QuotaTooltipContentProps {
|
||||
quota: UnifiedQuotaResult | null | undefined;
|
||||
@@ -77,6 +78,35 @@ function getClaudeWindowDisplayLabel(rateLimitType: string, fallback: string): s
|
||||
}
|
||||
}
|
||||
|
||||
function renderEntitlementRows(entitlement: ProviderEntitlementEvidence | undefined) {
|
||||
if (!entitlement) return null;
|
||||
|
||||
const rows: Array<{ label: string; value: string | null }> = [];
|
||||
if (entitlement.rawTierLabel) {
|
||||
rows.push({ label: 'Tier', value: entitlement.rawTierLabel });
|
||||
} else if (entitlement.normalizedTier !== 'unknown') {
|
||||
rows.push({ label: 'Tier', value: entitlement.normalizedTier });
|
||||
}
|
||||
if (entitlement.rawTierId) {
|
||||
rows.push({ label: 'Tier ID', value: entitlement.rawTierId });
|
||||
}
|
||||
if (entitlement.accessState !== 'entitled' || entitlement.capacityState !== 'available') {
|
||||
rows.push({
|
||||
label: 'State',
|
||||
value: `${entitlement.accessState.replaceAll('_', ' ')} / ${entitlement.capacityState.replaceAll('_', ' ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
return rows.map((row) => (
|
||||
<div key={row.label} className="flex justify-between gap-4">
|
||||
<span className="text-muted-foreground">{row.label}</span>
|
||||
<span className="font-mono">{row.value}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders provider-specific quota tooltip content
|
||||
* Uses type guards for proper TypeScript narrowing
|
||||
@@ -132,6 +162,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
||||
|
||||
return (
|
||||
<div className="text-xs space-y-1.5">
|
||||
{renderEntitlementRows(quota.entitlement)}
|
||||
<p className="font-medium">Model Quotas:</p>
|
||||
{tierOrder.map((tier, idx) => {
|
||||
const models = groups.get(tier);
|
||||
@@ -268,10 +299,13 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
||||
// Gemini provider tooltip
|
||||
if (isGeminiQuotaResult(quota)) {
|
||||
const hasBucketResetTime = quota.buckets.some((bucket) => !!bucket.resetTime);
|
||||
const hasEntitlementTier =
|
||||
!!quota.entitlement?.rawTierLabel || quota.entitlement?.normalizedTier !== 'unknown';
|
||||
|
||||
return (
|
||||
<div className="text-xs space-y-1.5">
|
||||
{quota.tierLabel && (
|
||||
{renderEntitlementRows(quota.entitlement)}
|
||||
{!hasEntitlementTier && quota.tierLabel && (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-muted-foreground">Tier</span>
|
||||
<span className="font-mono">{quota.tierLabel}</span>
|
||||
|
||||
@@ -562,6 +562,30 @@ export interface QuotaResult {
|
||||
retryable?: boolean;
|
||||
/** True if token is expired and needs re-authentication */
|
||||
needsReauth?: boolean;
|
||||
/** Richer provider entitlement evidence derived from live/runtime signals */
|
||||
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 */
|
||||
@@ -725,6 +749,8 @@ export interface GeminiCliQuotaResult {
|
||||
tierId?: string | null;
|
||||
/** Available Google One AI credits when reported by the API */
|
||||
creditBalance?: number | null;
|
||||
/** Richer provider entitlement evidence derived from live/runtime signals */
|
||||
entitlement?: ProviderEntitlementEvidence;
|
||||
/** Timestamp of fetch */
|
||||
lastUpdated: number;
|
||||
/** Upstream HTTP status when available */
|
||||
|
||||
@@ -771,6 +771,25 @@ export function getQuotaFailureInfo(
|
||||
const technicalDetail = buildQuotaTechnicalDetail(quota);
|
||||
const rawDetail = buildQuotaRawDetail(quota, summary, technicalDetail);
|
||||
const lowerSummary = summary.toLowerCase();
|
||||
const entitlement = 'entitlement' in quota ? quota.entitlement : undefined;
|
||||
|
||||
if (
|
||||
quota.errorCode === 'capacity_exhausted' ||
|
||||
entitlement?.capacityState === 'capacity_exhausted' ||
|
||||
lowerSummary.includes('no capacity available') ||
|
||||
lowerSummary.includes('capacity exhausted')
|
||||
) {
|
||||
return {
|
||||
label: 'Capacity',
|
||||
summary,
|
||||
actionHint:
|
||||
actionHint ||
|
||||
'Retry later or switch to another model. This is a temporary provider capacity issue.',
|
||||
technicalDetail,
|
||||
rawDetail,
|
||||
tone: 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
quota.needsReauth ||
|
||||
@@ -808,6 +827,7 @@ export function getQuotaFailureInfo(
|
||||
}
|
||||
|
||||
if (
|
||||
entitlement?.accessState === 'not_entitled' ||
|
||||
quota.isForbidden ||
|
||||
quota.httpStatus === 403 ||
|
||||
errorCode === 'quota_api_forbidden' ||
|
||||
|
||||
Reference in New Issue
Block a user