fix(core): fix quota footer for non-auto models and improve display (#25121)

This commit is contained in:
Jack Wotherspoon
2026-04-13 13:03:41 -04:00
committed by GitHub
parent 0179726222
commit 6b6ea56437
12 changed files with 153 additions and 75 deletions
+72
View File
@@ -3006,6 +3006,78 @@ describe('Config Quota & Preview Model Access', () => {
// Never set => stays null (unknown); getter returns true so UI shows preview
expect(config.getHasAccessToPreviewModel()).toBe(true);
});
it('should derive quota from remainingFraction when remainingAmount is missing', async () => {
mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
buckets: [
{
modelId: 'gemini-3-flash-preview',
remainingFraction: 0.96,
},
],
});
config.setModel('gemini-3-flash-preview');
mockCoreEvents.emitQuotaChanged.mockClear();
await config.refreshUserQuota();
// Normalized: limit=100, remaining=96
expect(mockCoreEvents.emitQuotaChanged).toHaveBeenCalledWith(
96,
100,
undefined,
);
expect(config.getQuotaRemaining()).toBe(96);
expect(config.getQuotaLimit()).toBe(100);
});
it('should store quota from remainingFraction when remainingFraction is 0', async () => {
mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
buckets: [
{
modelId: 'gemini-3-pro-preview',
remainingFraction: 0,
},
],
});
config.setModel('gemini-3-pro-preview');
mockCoreEvents.emitQuotaChanged.mockClear();
await config.refreshUserQuota();
// remaining=0, limit=100 but limit>0 check still passes
// however remaining=0 means 0% remaining = 100% used
expect(config.getQuotaRemaining()).toBe(0);
expect(config.getQuotaLimit()).toBe(100);
});
it('should emit QuotaChanged when model is switched via setModel', async () => {
mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
buckets: [
{
modelId: 'gemini-2.5-pro',
remainingAmount: '10',
remainingFraction: 0.2,
},
{
modelId: 'gemini-2.5-flash',
remainingAmount: '80',
remainingFraction: 0.8,
},
],
});
config.setModel('auto-gemini-2.5');
await config.refreshUserQuota();
mockCoreEvents.emitQuotaChanged.mockClear();
// Switch to a specific model — should re-emit quota for that model
config.setModel('gemini-2.5-pro');
expect(mockCoreEvents.emitQuotaChanged).toHaveBeenCalledWith(
10,
50,
undefined,
);
});
});
describe('refreshUserQuotaIfStale', () => {
+32 -24
View File
@@ -832,18 +832,16 @@ export class Config implements McpContext, AgentLoopContext {
private lastEmittedQuotaLimit: number | undefined;
private emitQuotaChangedEvent(): void {
const pooled = this.getPooledQuota();
const remaining = this.getQuotaRemaining();
const limit = this.getQuotaLimit();
const resetTime = this.getQuotaResetTime();
if (
this.lastEmittedQuotaRemaining !== pooled.remaining ||
this.lastEmittedQuotaLimit !== pooled.limit
this.lastEmittedQuotaRemaining !== remaining ||
this.lastEmittedQuotaLimit !== limit
) {
this.lastEmittedQuotaRemaining = pooled.remaining;
this.lastEmittedQuotaLimit = pooled.limit;
coreEvents.emitQuotaChanged(
pooled.remaining,
pooled.limit,
pooled.resetTime,
);
this.lastEmittedQuotaRemaining = remaining;
this.lastEmittedQuotaLimit = limit;
coreEvents.emitQuotaChanged(remaining, limit, resetTime);
}
}
@@ -1819,6 +1817,9 @@ export class Config implements McpContext, AgentLoopContext {
// When the user explicitly sets a model, that becomes the active model.
this._activeModel = newModel;
coreEvents.emitModelChanged(newModel);
this.lastEmittedQuotaRemaining = undefined;
this.lastEmittedQuotaLimit = undefined;
this.emitQuotaChangedEvent();
}
if (this.onModelChange && !isTemporary) {
this.onModelChange(newModel);
@@ -2112,24 +2113,31 @@ export class Config implements McpContext, AgentLoopContext {
this.lastQuotaFetchTime = Date.now();
for (const bucket of quota.buckets) {
if (
bucket.modelId &&
bucket.remainingAmount &&
bucket.remainingFraction != null
) {
const remaining = parseInt(bucket.remainingAmount, 10);
const limit =
if (!bucket.modelId || bucket.remainingFraction == null) {
continue;
}
let remaining: number;
let limit: number;
if (bucket.remainingAmount) {
remaining = parseInt(bucket.remainingAmount, 10);
limit =
bucket.remainingFraction > 0
? Math.round(remaining / bucket.remainingFraction)
: (this.modelQuotas.get(bucket.modelId)?.limit ?? 0);
} else {
// Server only sent remainingFraction — use a normalized scale.
limit = 100;
remaining = Math.round(bucket.remainingFraction * limit);
}
if (!isNaN(remaining) && Number.isFinite(limit) && limit > 0) {
this.modelQuotas.set(bucket.modelId, {
remaining,
limit,
resetTime: bucket.resetTime,
});
}
if (!isNaN(remaining) && Number.isFinite(limit) && limit > 0) {
this.modelQuotas.set(bucket.modelId, {
remaining,
limit,
resetTime: bucket.resetTime,
});
}
}
this.emitQuotaChangedEvent();