Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/commands/quota/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { requestJson } from '../../client/http';
import { quotaEndpoint, usageEndpoint } from '../../client/endpoints';
import { formatOutput, detectOutputFormat } from '../../output/formatter';
import { renderUsage } from '../../output/usage';
import { resolveQuotaCounts } from '../../utils/quota';
import type { Config } from '../../config/schema';
import type { GlobalFlags } from '../../types/flags';
import type { AccountBalanceResponse, QuotaModelRemain } from '../../types/api';
Expand Down Expand Up @@ -50,8 +51,22 @@ export default defineCommand({

if (config.quiet) {
for (const m of models) {
const remaining = m.current_interval_total_count - m.current_interval_usage_count;
console.log(`${m.model_name}\t${m.current_interval_usage_count}\t${m.current_interval_total_count}\t${remaining}`);
const counts = resolveQuotaCounts(
m.current_interval_usage_count,
m.current_interval_total_count,
m.current_interval_remaining_percent,
);

if (counts) {
console.log(`${m.model_name}\t${counts.used}\t${counts.total}\t${counts.remaining}`);
continue;
}

const remaining = m.current_interval_remaining_percent === undefined
|| m.current_interval_remaining_percent === null
? '-'
: `${m.current_interval_remaining_percent}%`;
console.log(`${m.model_name}\t-\t${m.current_interval_total_count}\t${remaining}`);
}
return;
}
Expand Down
12 changes: 7 additions & 5 deletions src/output/quota-table.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolveQuotaCounts } from '../utils/quota';
import type { Config } from '../config/schema';
import type { QuotaModelRemain } from '../types/api';

Expand Down Expand Up @@ -151,7 +152,7 @@ const UNLIMITED_LABEL_EN = 'unlimited';

function renderMetric(
label: string,
remaining: number,
reportedCount: number,
total: number,
percent: number | undefined | null,
color: boolean,
Expand All @@ -169,10 +170,11 @@ function renderMetric(
const bar = `[${'█'.repeat(COMPACT_BAR_WIDTH)}]`;
return `${label} ${bar} ${ulStr}`;
}
const pct = remainingPct(percent, remaining, total, boostPermille);
const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, total <= 0);
if (total > 0) {
const count = `${remaining.toLocaleString()} / ${total.toLocaleString()}`;
const pct = remainingPct(percent, reportedCount, total, boostPermille);
const counts = resolveQuotaCounts(reportedCount, total, percent);
const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, counts === undefined);
if (counts) {
const count = `${counts.remaining.toLocaleString()} / ${counts.total.toLocaleString()}`;
return color ? `${D}${label}${R} ${bar} ${remainingColors(pct)[0]}${count}${R}` : `${label} ${bar} ${count}`;
}
return `${label} ${bar}`;
Expand Down
51 changes: 51 additions & 0 deletions src/utils/quota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export interface ResolvedQuotaCounts {
used: number;
remaining: number;
total: number;
}

const PERCENT_MATCH_TOLERANCE = 1;

/**
* Resolve the ambiguous `*_usage_count` fields returned by the quota API.
*
* Older responses use the fields as remaining counts, while newer responses
* may use them as consumed counts. When the server also returns an explicit
* remaining percentage, use it to select the interpretation that agrees with
* the authoritative percentage. Without a percentage, preserve the legacy
* remaining-count interpretation.
*/
export function resolveQuotaCounts(
reportedCount: number,
total: number,
remainingPercent?: number | null,
): ResolvedQuotaCounts | undefined {
if (!Number.isFinite(reportedCount)
|| !Number.isFinite(total)
|| total <= 0
|| reportedCount < 0
|| reportedCount > total) {
return undefined;
}

let remaining = reportedCount;

if (remainingPercent !== undefined
&& remainingPercent !== null
&& Number.isFinite(remainingPercent)) {
const reportedAsRemaining = (reportedCount / total) * 100;
const reportedAsUsed = ((total - reportedCount) / total) * 100;
const remainingDistance = Math.abs(reportedAsRemaining - remainingPercent);
const usedDistance = Math.abs(reportedAsUsed - remainingPercent);
const closestDistance = Math.min(remainingDistance, usedDistance);

if (closestDistance > PERCENT_MATCH_TOLERANCE) return undefined;
if (usedDistance < remainingDistance) remaining = total - reportedCount;
}

return {
used: total - remaining,
remaining,
total,
};
}
52 changes: 52 additions & 0 deletions test/commands/quota/show.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,56 @@ describe('quota show command', () => {
}
});

it('normalizes ambiguous quota counts in quiet output', async () => {
server = createMockServer({
routes: {
'/v1/token_plan/remains': () => jsonResponse({
model_remains: [
{
model_name: 'legacy-video',
current_interval_total_count: 3,
current_interval_usage_count: 3,
current_interval_remaining_percent: 100,
},
{
model_name: 'current-video',
current_interval_total_count: 5,
current_interval_usage_count: 0,
current_interval_remaining_percent: 100,
},
{
model_name: 'general',
current_interval_total_count: 0,
current_interval_usage_count: 0,
current_interval_remaining_percent: 99,
},
],
}),
},
});

const output: string[] = [];
const origLog = console.log;
console.log = (msg: string) => { output.push(msg); };

try {
await showCommand.execute(
{
...baseConfig,
baseUrl: server.url,
quiet: true,
},
{ ...baseFlags, quiet: true },
);
} finally {
console.log = origLog;
}

expect(output).toEqual([
'legacy-video\t0\t3\t3',
'current-video\t0\t5\t5',
'general\t-\t0\t99%',
]);
});

});
65 changes: 65 additions & 0 deletions test/output/quota-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,71 @@ describe('renderQuotaTable', () => {
expect(output).not.toContain('0 / 3');
});

it('uses remaining percent to disambiguate newer used-count responses', () => {
const lines: string[] = [];
const originalLog = console.log;

console.log = (message?: unknown) => {
lines.push(String(message ?? ''));
};

try {
renderQuotaTable(
[
{
...createModel(),
model_name: 'video',
current_interval_total_count: 5,
current_interval_usage_count: 0,
current_interval_remaining_percent: 100,
current_weekly_total_count: 35,
current_weekly_usage_count: 0,
current_weekly_remaining_percent: 100,
},
],
{ ...createConfig(), noColor: true },
);
} finally {
console.log = originalLog;
}

const output = lines.join('\n');
expect(output).toContain('5 / 5');
expect(output).toContain('35 / 35');
expect(output).not.toContain('0 / 5');
expect(output).not.toContain('0 / 35');
});

it('falls back to the authoritative percent when counts cannot be reconciled', () => {
const lines: string[] = [];
const originalLog = console.log;

console.log = (message?: unknown) => {
lines.push(String(message ?? ''));
};

try {
renderQuotaTable(
[
{
...createModel(),
current_interval_total_count: 10,
current_interval_usage_count: 7,
current_interval_remaining_percent: 40,
},
],
{ ...createConfig(), noColor: true },
);
} finally {
console.log = originalLog;
}

const output = lines.join('\n');
expect(output).toContain('Left [████......] 40%');
expect(output).not.toContain('7 / 10');
expect(output).not.toContain('3 / 10');
});

it('renders the reset countdown in a boxed column with the window duration tag', () => {
const lines: string[] = [];
const originalLog = console.log;
Expand Down
37 changes: 37 additions & 0 deletions test/utils/quota.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'bun:test';
import { resolveQuotaCounts } from '../../src/utils/quota';

describe('resolveQuotaCounts', () => {
it('preserves legacy responses where usage_count means remaining', () => {
expect(resolveQuotaCounts(3, 3, 100)).toEqual({
used: 0,
remaining: 3,
total: 3,
});
});

it('supports newer responses where usage_count means used', () => {
expect(resolveQuotaCounts(0, 5, 100)).toEqual({
used: 0,
remaining: 5,
total: 5,
});
});

it('preserves legacy semantics when no percentage is available', () => {
expect(resolveQuotaCounts(4, 10)).toEqual({
used: 6,
remaining: 4,
total: 10,
});
});

it('returns undefined when neither interpretation matches the percentage', () => {
expect(resolveQuotaCounts(7, 10, 40)).toBeUndefined();
});

it('returns undefined for zero-sized or invalid buckets', () => {
expect(resolveQuotaCounts(0, 0, 99)).toBeUndefined();
expect(resolveQuotaCounts(6, 5, 0)).toBeUndefined();
});
});
Loading