feat: add DeepSeek quota provider #2594
This commit is contained in:
@@ -22,5 +22,6 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
|
|||||||
{ id: 'wafer', name: 'Wafer.ai' },
|
{ id: 'wafer', name: 'Wafer.ai' },
|
||||||
{ id: 'opencode-go', name: 'OpenCode Go' },
|
{ id: 'opencode-go', name: 'OpenCode Go' },
|
||||||
{ id: 'crof', name: 'CrofAI' },
|
{ id: 'crof', name: 'CrofAI' },
|
||||||
|
{ id: 'deepseek', name: 'DeepSeek' },
|
||||||
{ id: 'neuralwatt', name: 'NeuralWatt' },
|
{ id: 'neuralwatt', name: 'NeuralWatt' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type QuotaProviderId =
|
|||||||
| 'wafer'
|
| 'wafer'
|
||||||
| 'opencode-go'
|
| 'opencode-go'
|
||||||
| 'crof'
|
| 'crof'
|
||||||
|
| 'deepseek'
|
||||||
| 'neuralwatt';
|
| 'neuralwatt';
|
||||||
|
|
||||||
export interface UsageWindow {
|
export interface UsageWindow {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const AUTH = JSON.stringify({
|
|||||||
crof: { key: 'test-token' },
|
crof: { key: 'test-token' },
|
||||||
neuralwatt: { key: 'test-token' },
|
neuralwatt: { key: 'test-token' },
|
||||||
'zai-coding-plan': { 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 { existsSync: () => boolean }).existsSync = () => true;
|
||||||
((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH;
|
((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH;
|
||||||
@@ -419,3 +420,72 @@ describe('NeuralWatt quota provider (VS Code parity)', () => {
|
|||||||
fsMock.readFileSync = ORIGINAL_FS.readFileSync;
|
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;
|
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 = {
|
type NeuralwattPayload = {
|
||||||
balance?: {
|
balance?: {
|
||||||
credits_remaining_usd?: number | string;
|
credits_remaining_usd?: number | string;
|
||||||
@@ -492,6 +502,11 @@ export const listConfiguredQuotaProviders = () => {
|
|||||||
configured.add('neuralwatt');
|
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);
|
return Array.from(configured);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2191,6 +2206,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> => {
|
export const fetchQuotaForProvider = async (providerId: string): Promise<ProviderResult> => {
|
||||||
switch (providerId) {
|
switch (providerId) {
|
||||||
case 'claude':
|
case 'claude':
|
||||||
@@ -2234,6 +2344,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
|
|||||||
return fetchCursorQuota();
|
return fetchCursorQuota();
|
||||||
case 'crof':
|
case 'crof':
|
||||||
return fetchCrofQuota();
|
return fetchCrofQuota();
|
||||||
|
case 'deepseek':
|
||||||
|
return fetchDeepseekQuota();
|
||||||
case 'neuralwatt':
|
case 'neuralwatt':
|
||||||
return fetchNeuralwattQuota();
|
return fetchNeuralwattQuota();
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
|
|||||||
| `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
|
| `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
|
||||||
| `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
|
| `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
|
||||||
| `crof` | CrofAI | `providers/crof.js` | `crof` (API key under `key` or `token`) |
|
| `crof` | CrofAI | `providers/crof.js` | `crof` (API key under `key` or `token`) |
|
||||||
|
| `deepseek` | DeepSeek | `providers/deepseek.js` | `deepseek` (API key under `key` or `token`) |
|
||||||
| `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
|
| `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
|
||||||
| `github-copilot` | GitHub Copilot | `providers/copilot.js` | `github-copilot`, `copilot` |
|
| `github-copilot` | GitHub Copilot | `providers/copilot.js` | `github-copilot`, `copilot` |
|
||||||
| `github-copilot-addon` | GitHub Copilot Add-on | `providers/copilot.js` | `github-copilot`, `copilot` |
|
| `github-copilot-addon` | GitHub Copilot Add-on | `providers/copilot.js` | `github-copilot`, `copilot` |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export {
|
|||||||
fetchGoogleQuota,
|
fetchGoogleQuota,
|
||||||
fetchCodexQuota,
|
fetchCodexQuota,
|
||||||
fetchCursorQuota,
|
fetchCursorQuota,
|
||||||
|
fetchDeepseekQuota,
|
||||||
fetchCopilotQuota,
|
fetchCopilotQuota,
|
||||||
fetchCopilotAddonQuota,
|
fetchCopilotAddonQuota,
|
||||||
fetchKimiQuota,
|
fetchKimiQuota,
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { readAuthFile } from '../../opencode/auth.js';
|
||||||
|
import {
|
||||||
|
getAuthEntry,
|
||||||
|
normalizeAuthEntry,
|
||||||
|
buildResult,
|
||||||
|
toUsageWindow,
|
||||||
|
toNumber,
|
||||||
|
formatMoney
|
||||||
|
} from '../utils/index.js';
|
||||||
|
|
||||||
|
export const providerId = 'deepseek';
|
||||||
|
export const providerName = 'DeepSeek';
|
||||||
|
const aliases = ['deepseek'];
|
||||||
|
const DEEPSEEK_QUOTA_URL = 'https://api.deepseek.com/user/balance';
|
||||||
|
|
||||||
|
export const isConfigured = () => {
|
||||||
|
const auth = readAuthFile();
|
||||||
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
||||||
|
return Boolean(entry?.key || entry?.token);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchQuota = async () => {
|
||||||
|
const auth = readAuthFile();
|
||||||
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
||||||
|
const apiKey = entry?.key ?? entry?.token;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return buildResult({
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
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,
|
||||||
|
providerName,
|
||||||
|
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();
|
||||||
|
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,
|
||||||
|
providerName,
|
||||||
|
ok: false,
|
||||||
|
configured: true,
|
||||||
|
error: 'No quota data in response'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCny = balanceInfo?.currency === 'CNY';
|
||||||
|
const symbol = isCny ? '¥' : '$';
|
||||||
|
const valueLabel = `${symbol}${formatMoney(totalBalance)}`;
|
||||||
|
|
||||||
|
const windows = {
|
||||||
|
credits_balance: toUsageWindow({
|
||||||
|
usedPercent: null,
|
||||||
|
windowSeconds: null,
|
||||||
|
resetAt: null,
|
||||||
|
valueLabel
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildResult({
|
||||||
|
providerId,
|
||||||
|
providerName,
|
||||||
|
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,
|
||||||
|
providerName,
|
||||||
|
ok: false,
|
||||||
|
configured: true,
|
||||||
|
error: isTimeout
|
||||||
|
? 'Request timed out'
|
||||||
|
: isParseError
|
||||||
|
? 'Invalid response from provider'
|
||||||
|
: (error instanceof Error ? error.message : 'Request failed')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../../opencode/auth.js', () => ({
|
||||||
|
readAuthFile: () => ({ deepseek: { key: 'test-token' } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { fetchQuota } from './deepseek.js';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockResponse = (body, init = {}) => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => body,
|
||||||
|
...init,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Documented payload shape from https://api.deepseek.com/user/balance
|
||||||
|
const DOCUMENTED_PAYLOAD = {
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [
|
||||||
|
{
|
||||||
|
currency: 'USD',
|
||||||
|
total_balance: '7.54',
|
||||||
|
granted_balance: '0.00',
|
||||||
|
topped_up_balance: '7.54'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('DeepSeek quota provider', () => {
|
||||||
|
it('builds credits_balance window from documented USD payload (string balance)', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(DOCUMENTED_PAYLOAD)));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.providerId).toBe('deepseek');
|
||||||
|
|
||||||
|
const window = result.usage.windows.credits_balance;
|
||||||
|
expect(window).toBeDefined();
|
||||||
|
expect(window.valueLabel).toBe('$7.54');
|
||||||
|
expect(window.usedPercent).toBeNull();
|
||||||
|
expect(window.windowSeconds).toBeNull();
|
||||||
|
expect(window.resetAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to CNY entry when no USD entry is present', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [
|
||||||
|
{ currency: 'CNY', total_balance: '100.00', granted_balance: '0.00', topped_up_balance: '100.00' }
|
||||||
|
]
|
||||||
|
})));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('¥100.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers the USD entry when both USD and CNY are present', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [
|
||||||
|
{ currency: 'CNY', total_balance: '100.00', granted_balance: '0.00', topped_up_balance: '100.00' },
|
||||||
|
{ currency: 'USD', total_balance: '3.55', granted_balance: '0.00', topped_up_balance: '3.55' }
|
||||||
|
]
|
||||||
|
})));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$3.55');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tolerates a numeric total_balance', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [{ currency: 'USD', total_balance: 12.5, granted_balance: 0, topped_up_balance: 12.5 }]
|
||||||
|
})));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$12.50');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps 401 to session-expired error', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 401, json: async () => ({}) }));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.error).toBe('Session expired — please re-authenticate with DeepSeek');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps 403 to session-expired error', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 403, json: async () => ({}) }));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.error).toBe('Session expired — please re-authenticate with DeepSeek');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports invalid-response on JSON parse failure', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => { throw new SyntaxError('Unexpected token'); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid response from provider');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no-quota-data on a 200 payload with no usable balance', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [{ currency: 'USD', total_balance: '', granted_balance: '0.00', topped_up_balance: '0.00' }]
|
||||||
|
})));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect(result.configured).toBe(true);
|
||||||
|
expect(result.error).toBe('No quota data in response');
|
||||||
|
expect(result.usage).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a literal zero balance as a valid valueLabel', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse({
|
||||||
|
is_available: true,
|
||||||
|
balance_infos: [{ currency: 'USD', total_balance: '0.00', granted_balance: '0.00', topped_up_balance: '0.00' }]
|
||||||
|
})));
|
||||||
|
|
||||||
|
const result = await fetchQuota();
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$0.00');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ import * as codex from './codex.js';
|
|||||||
import * as copilot from './copilot.js';
|
import * as copilot from './copilot.js';
|
||||||
import * as crof from './crof.js';
|
import * as crof from './crof.js';
|
||||||
import * as cursor from './cursor.js';
|
import * as cursor from './cursor.js';
|
||||||
|
import * as deepseek from './deepseek.js';
|
||||||
import * as google from './google/index.js';
|
import * as google from './google/index.js';
|
||||||
import * as kimi from './kimi.js';
|
import * as kimi from './kimi.js';
|
||||||
import * as nanogpt from './nanogpt.js';
|
import * as nanogpt from './nanogpt.js';
|
||||||
@@ -51,6 +52,12 @@ const registry = {
|
|||||||
isConfigured: cursor.isConfigured,
|
isConfigured: cursor.isConfigured,
|
||||||
fetchQuota: cursor.fetchQuota
|
fetchQuota: cursor.fetchQuota
|
||||||
},
|
},
|
||||||
|
deepseek: {
|
||||||
|
providerId: deepseek.providerId,
|
||||||
|
providerName: deepseek.providerName,
|
||||||
|
isConfigured: deepseek.isConfigured,
|
||||||
|
fetchQuota: deepseek.fetchQuota
|
||||||
|
},
|
||||||
google: {
|
google: {
|
||||||
providerId: google.providerId,
|
providerId: google.providerId,
|
||||||
providerName: google.providerName,
|
providerName: google.providerName,
|
||||||
@@ -184,6 +191,7 @@ export const fetchOpenaiQuota = openai.fetchQuota;
|
|||||||
export const fetchGoogleQuota = google.fetchGoogleQuota;
|
export const fetchGoogleQuota = google.fetchGoogleQuota;
|
||||||
export const fetchCodexQuota = codex.fetchQuota;
|
export const fetchCodexQuota = codex.fetchQuota;
|
||||||
export const fetchCursorQuota = cursor.fetchQuota;
|
export const fetchCursorQuota = cursor.fetchQuota;
|
||||||
|
export const fetchDeepseekQuota = deepseek.fetchQuota;
|
||||||
export const fetchCopilotQuota = copilot.fetchQuota;
|
export const fetchCopilotQuota = copilot.fetchQuota;
|
||||||
export const fetchCopilotAddonQuota = copilot.fetchQuotaAddon;
|
export const fetchCopilotAddonQuota = copilot.fetchQuotaAddon;
|
||||||
export const fetchKimiQuota = kimi.fetchQuota;
|
export const fetchKimiQuota = kimi.fetchQuota;
|
||||||
|
|||||||
Reference in New Issue
Block a user