feat: add Charm Hyper quota provider (#3368)

This commit is contained in:
Howon Lee
2026-09-05 23:16:13 +03:00
committed by GitHub
parent 7b42208b8c
commit 5e7c147785
9 changed files with 454 additions and 0 deletions
@@ -19,6 +19,7 @@ const AUTH = JSON.stringify({
'opencode-go': { key: 'test-token' },
'zai-coding-plan': { key: 'test-token' },
deepseek: { key: 'test-token' },
hyper: { key: 'test-token' },
'github-copilot': { access: 'test-token' },
anthropic: { access: 'test-token', refresh: 'test-refresh' },
});
@@ -714,3 +715,87 @@ describe('DeepSeek quota provider (VS Code parity)', () => {
fsMock.readFileSync = ORIGINAL_FS.readFileSync;
});
});
describe('Charm Hyper quota provider (VS Code parity)', () => {
beforeEach(() => {
const fsMock = fs as unknown as { existsSync: () => boolean; readFileSync: () => string };
fsMock.existsSync = () => true;
fsMock.readFileSync = () => AUTH;
});
test('builds credits and credits_balance windows from documented payload (numeric balance)', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({ balance: 100 })));
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, true);
assert.equal(result.providerId, 'hyper');
const balanceWindow = result.usage!.windows.credits_balance!;
assert.equal(balanceWindow.valueLabel, '$5.00');
assert.equal(balanceWindow.usedPercent, null);
assert.equal(balanceWindow.windowSeconds, null);
assert.equal(balanceWindow.resetAt, null);
const creditsWindow = result.usage!.windows.credits!;
assert.equal(creditsWindow.valueLabel, '100 credits');
assert.equal(creditsWindow.usedPercent, null);
assert.equal(creditsWindow.windowSeconds, null);
assert.equal(creditsWindow.resetAt, null);
});
test('tolerates a string balance', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({ balance: '50' })));
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, true);
assert.equal(result.usage!.windows.credits!.valueLabel, '50 credits');
assert.equal(result.usage!.windows.credits_balance!.valueLabel, '$2.50');
});
test('maps 401 to session-expired', async () => {
stubFetchFailing(async () => ({}), { ok: false, status: 401 });
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, false);
assert.equal(result.error, 'Session expired — please re-authenticate with Charm Hyper');
});
test('reports a normalized timeout error', async () => {
stubFetchReturning(() => Promise.reject(new DOMException('The operation timed out.', 'TimeoutError')));
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, false);
assert.equal(result.error, 'Request timed out');
});
test('returns no-quota-data on a 200 payload with no balance', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({})));
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, false);
assert.equal(result.configured, true);
assert.equal(result.error, 'No quota data in response');
assert.equal(result.usage, null);
});
test('keeps a literal zero balance as a valid valueLabel', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({ balance: 0 })));
const result = await fetchQuotaForProvider('hyper');
assert.equal(result.ok, true);
assert.equal(result.usage!.windows.credits!.valueLabel, '0 credits');
assert.equal(result.usage!.windows.credits_balance!.valueLabel, '$0.00');
});
test('teardown: restore fs', () => {
const fsMock = fs as unknown as { existsSync: unknown; readFileSync: unknown };
fsMock.existsSync = ORIGINAL_FS.existsSync;
fsMock.readFileSync = ORIGINAL_FS.readFileSync;
});
});
+111
View File
@@ -155,6 +155,10 @@ type DeepseekPayload = {
}>;
};
type HyperPayload = {
balance?: number | string;
};
type NeuralwattPayload = {
balance?: {
credits_remaining_usd?: number | string;
@@ -853,6 +857,11 @@ export const listConfiguredQuotaProviders = () => {
configured.add('deepseek');
}
const hyperAuth = normalizeAuthEntry(getAuthEntry(auth, ['hyper']));
if (hyperAuth && ((hyperAuth as Record<string, unknown>).key || (hyperAuth as Record<string, unknown>).token)) {
configured.add('hyper');
}
let xaiAuth: XaiAuthEntry | null = null;
try {
xaiAuth = resolveXaiAuth();
@@ -2786,6 +2795,106 @@ const fetchDeepseekQuota = async (): Promise<ProviderResult> => {
}
};
const HYPER_QUOTA_URL = 'https://hyper.charm.land/v1/credits';
const HYPER_CREDIT_TO_USD = 0.05;
const fetchHyperQuota = async (): Promise<ProviderResult> => {
const auth = readAuthFile();
const entry = normalizeAuthEntry(getAuthEntry(auth, ['hyper'])) as Record<string, unknown> | null;
const apiKey = (entry?.key as string | undefined) ?? (entry?.token as string | undefined);
if (!apiKey) {
return buildResult({
providerId: 'hyper',
providerName: 'Charm Hyper',
ok: false,
configured: false,
error: 'Not configured',
});
}
const timeoutSignal = AbortSignal.timeout(15_000);
try {
const response = await fetch(HYPER_QUOTA_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${apiKey}`,
'Accept-Encoding': 'identity',
},
signal: timeoutSignal,
});
if (!response.ok) {
return buildResult({
providerId: 'hyper',
providerName: 'Charm Hyper',
ok: false,
configured: true,
error: response.status === 401 || response.status === 403
? 'Session expired — please re-authenticate with Charm Hyper'
: `API error: ${response.status}`,
});
}
const payload = await response.json() as HyperPayload;
const rawBalance = payload?.balance;
const balance = (typeof rawBalance === 'number' || (typeof rawBalance === 'string' && rawBalance.trim() !== ''))
? toNumber(rawBalance)
: null;
if (balance === null) {
return buildResult({
providerId: 'hyper',
providerName: 'Charm Hyper',
ok: false,
configured: true,
error: 'No quota data in response',
});
}
const creditsLabel = Number.isInteger(balance) ? String(balance) : formatMoney(balance);
const windows: Record<string, UsageWindow> = {
credits_balance: toUsageWindow({
usedPercent: null,
windowSeconds: null,
resetAt: null,
valueLabel: `$${formatMoney(balance * HYPER_CREDIT_TO_USD)}`,
}),
credits: toUsageWindow({
usedPercent: null,
windowSeconds: null,
resetAt: null,
valueLabel: `${creditsLabel} credits`,
}),
};
return buildResult({
providerId: 'hyper',
providerName: 'Charm Hyper',
ok: true,
configured: true,
usage: { windows },
});
} catch (error) {
const isTimeout = error instanceof DOMException && (
error.name === 'TimeoutError' || (error.name === 'AbortError' && timeoutSignal.aborted)
);
const isParseError = error instanceof SyntaxError;
return buildResult({
providerId: 'hyper',
providerName: 'Charm Hyper',
ok: false,
configured: true,
error: isTimeout
? 'Request timed out'
: isParseError
? 'Invalid response from provider'
: (error instanceof Error ? error.message : 'Request failed'),
});
}
};
const fetchXaiQuota = async (): Promise<ProviderResult> => {
try {
const entry = resolveXaiAuth();
@@ -2907,6 +3016,8 @@ const fetchQuotaForProviderUncoalesced = async (providerId: string): Promise<Pro
return fetchCrofQuota();
case 'deepseek':
return fetchDeepseekQuota();
case 'hyper':
return fetchHyperQuota();
case 'neuralwatt':
return fetchNeuralwattQuota();
case 'xai':