feat: add DeepSeek quota provider
This commit is contained in:
@@ -11,6 +11,7 @@ const AUTH = JSON.stringify({
|
||||
crof: { key: 'test-token' },
|
||||
neuralwatt: { key: 'test-token' },
|
||||
'zai-coding-plan': { key: 'test-token' },
|
||||
deepseek: { key: 'test-token' },
|
||||
});
|
||||
((fs as unknown) as { existsSync: () => boolean }).existsSync = () => true;
|
||||
((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH;
|
||||
@@ -419,3 +420,72 @@ describe('NeuralWatt quota provider (VS Code parity)', () => {
|
||||
fsMock.readFileSync = ORIGINAL_FS.readFileSync;
|
||||
});
|
||||
});
|
||||
|
||||
describe('DeepSeek quota provider (VS Code parity)', () => {
|
||||
test('builds credits_balance window from documented USD payload (string balance)', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{ currency: 'USD', total_balance: '7.54', granted_balance: '0.00', topped_up_balance: '7.54' },
|
||||
],
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('deepseek');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.providerId, 'deepseek');
|
||||
assert.equal(result.usage!.windows.credits_balance!.valueLabel, '$7.54');
|
||||
assert.equal(result.usage!.windows.credits_balance!.usedPercent, null);
|
||||
assert.equal(result.usage!.windows.credits_balance!.windowSeconds, null);
|
||||
assert.equal(result.usage!.windows.credits_balance!.resetAt, null);
|
||||
});
|
||||
|
||||
test('falls back to CNY entry with ¥ symbol when no USD entry is present', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
is_available: true,
|
||||
balance_infos: [
|
||||
{ currency: 'CNY', total_balance: '100.00', granted_balance: '0.00', topped_up_balance: '100.00' },
|
||||
],
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('deepseek');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.usage!.windows.credits_balance!.valueLabel, '¥100.00');
|
||||
});
|
||||
|
||||
test('maps 401 to session-expired', async () => {
|
||||
stubFetchFailing(async () => ({}), { ok: false, status: 401 });
|
||||
|
||||
const result = await fetchQuotaForProvider('deepseek');
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error, 'Session expired — please re-authenticate with DeepSeek');
|
||||
});
|
||||
|
||||
test('returns no-quota-data on a 200 payload with no usable balance', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({
|
||||
is_available: true,
|
||||
balance_infos: [{ currency: 'USD', total_balance: '', granted_balance: '0.00', topped_up_balance: '0.00' }],
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('deepseek');
|
||||
|
||||
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({
|
||||
is_available: true,
|
||||
balance_infos: [{ currency: 'USD', total_balance: '0.00', granted_balance: '0.00', topped_up_balance: '0.00' }],
|
||||
})));
|
||||
|
||||
const result = await fetchQuotaForProvider('deepseek');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.usage!.windows.credits_balance!.valueLabel, '$0.00');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,6 +124,16 @@ type CrofPayload = {
|
||||
credits?: number | string;
|
||||
};
|
||||
|
||||
type DeepseekPayload = {
|
||||
is_available?: boolean;
|
||||
balance_infos?: Array<{
|
||||
currency?: string;
|
||||
total_balance?: number | string;
|
||||
granted_balance?: number | string;
|
||||
topped_up_balance?: number | string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type NeuralwattPayload = {
|
||||
balance?: {
|
||||
credits_remaining_usd?: number | string;
|
||||
@@ -492,6 +502,11 @@ export const listConfiguredQuotaProviders = () => {
|
||||
configured.add('neuralwatt');
|
||||
}
|
||||
|
||||
const deepseekAuth = normalizeAuthEntry(getAuthEntry(auth, ['deepseek']));
|
||||
if (deepseekAuth && ((deepseekAuth as Record<string, unknown>).key || (deepseekAuth as Record<string, unknown>).token)) {
|
||||
configured.add('deepseek');
|
||||
}
|
||||
|
||||
return Array.from(configured);
|
||||
};
|
||||
|
||||
@@ -2175,6 +2190,101 @@ const fetchCrofQuota = async (): Promise<ProviderResult> => {
|
||||
}
|
||||
};
|
||||
|
||||
const DEEPSEEK_QUOTA_URL = 'https://api.deepseek.com/user/balance';
|
||||
|
||||
const fetchDeepseekQuota = async (): Promise<ProviderResult> => {
|
||||
const auth = readAuthFile();
|
||||
const entry = normalizeAuthEntry(getAuthEntry(auth, ['deepseek'])) as Record<string, unknown> | null;
|
||||
const apiKey = (entry?.key as string | undefined) ?? (entry?.token as string | undefined);
|
||||
|
||||
if (!apiKey) {
|
||||
return buildResult({
|
||||
providerId: 'deepseek',
|
||||
providerName: 'DeepSeek',
|
||||
ok: false,
|
||||
configured: false,
|
||||
error: 'Not configured',
|
||||
});
|
||||
}
|
||||
|
||||
const timeoutSignal = AbortSignal.timeout(15_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(DEEPSEEK_QUOTA_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Accept-Encoding': 'identity',
|
||||
},
|
||||
signal: timeoutSignal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return buildResult({
|
||||
providerId: 'deepseek',
|
||||
providerName: 'DeepSeek',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: response.status === 401 || response.status === 403
|
||||
? 'Session expired — please re-authenticate with DeepSeek'
|
||||
: `API error: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const payload = await response.json() as DeepseekPayload;
|
||||
const balanceInfos = Array.isArray(payload?.balance_infos) ? payload.balance_infos : [];
|
||||
const balanceInfo = balanceInfos.find((info) => info?.currency === 'USD')
|
||||
?? balanceInfos.find((info) => info?.currency === 'CNY')
|
||||
?? null;
|
||||
const rawBalance = balanceInfo?.total_balance;
|
||||
const totalBalance = (typeof rawBalance === 'number' || (typeof rawBalance === 'string' && rawBalance.trim() !== ''))
|
||||
? toNumber(rawBalance)
|
||||
: null;
|
||||
|
||||
if (totalBalance === null) {
|
||||
return buildResult({
|
||||
providerId: 'deepseek',
|
||||
providerName: 'DeepSeek',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: 'No quota data in response',
|
||||
});
|
||||
}
|
||||
|
||||
const symbol = balanceInfo?.currency === 'CNY' ? '¥' : '$';
|
||||
const windows: Record<string, UsageWindow> = {
|
||||
credits_balance: toUsageWindow({
|
||||
usedPercent: null,
|
||||
windowSeconds: null,
|
||||
resetAt: null,
|
||||
valueLabel: `${symbol}${formatMoney(totalBalance)}`,
|
||||
}),
|
||||
};
|
||||
|
||||
return buildResult({
|
||||
providerId: 'deepseek',
|
||||
providerName: 'DeepSeek',
|
||||
ok: true,
|
||||
configured: true,
|
||||
usage: { windows },
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeout = error instanceof DOMException && error.name === 'AbortError' && timeoutSignal.aborted;
|
||||
const isParseError = error instanceof SyntaxError;
|
||||
return buildResult({
|
||||
providerId: 'deepseek',
|
||||
providerName: 'DeepSeek',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: isTimeout
|
||||
? 'Request timed out'
|
||||
: isParseError
|
||||
? 'Invalid response from provider'
|
||||
: (error instanceof Error ? error.message : 'Request failed'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchQuotaForProvider = async (providerId: string): Promise<ProviderResult> => {
|
||||
switch (providerId) {
|
||||
case 'claude':
|
||||
@@ -2218,6 +2328,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
|
||||
return fetchCursorQuota();
|
||||
case 'crof':
|
||||
return fetchCrofQuota();
|
||||
case 'deepseek':
|
||||
return fetchDeepseekQuota();
|
||||
case 'neuralwatt':
|
||||
return fetchNeuralwattQuota();
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user