feat(usage): add ClinePass quota provider
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="24" height="24">
|
||||
<title>Cline</title>
|
||||
<path d="m39.06 22.594-2.403-4.826V14.99c0-4.606-3.697-8.336-8.257-8.336h-4.107c.297-.61.46-1.297.46-2.021 0-2.56-2.06-4.632-4.605-4.632s-4.606 2.072-4.606 4.632c0 .724.163 1.41.46 2.02h-4.107c-4.56 0-8.256 3.731-8.256 8.337v2.78l-2.454 4.81a1.7 1.7 0 0 0 0 1.545l2.454 4.758v2.78c0 4.605 3.697 8.336 8.256 8.336H28.4c4.56 0 8.257-3.73 8.257-8.337v-2.779l2.399-4.774a1.7 1.7 0 0 0 .004-1.516m-21.424 3.932c0 2.093-1.688 3.79-3.769 3.79-2.08 0-3.768-1.697-3.768-3.79V19.79c0-2.093 1.688-3.79 3.768-3.79s3.769 1.697 3.769 3.79zm12.142 0c0 2.093-1.688 3.79-3.769 3.79-2.08 0-3.768-1.697-3.768-3.79V19.79c0-2.093 1.688-3.79 3.768-3.79s3.769 1.697 3.769 3.79z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 773 B |
@@ -20,6 +20,7 @@ const LOGO_ALIAS = new Map<string, string>([
|
||||
['codex', 'openai'],
|
||||
['chatgpt', 'openai'],
|
||||
['claude', 'anthropic'],
|
||||
['cline-pass', 'cline'],
|
||||
['gemini', 'google'],
|
||||
['evroc-ai', 'evroc'],
|
||||
['evrocai', 'evroc'],
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface QuotaProviderMeta {
|
||||
|
||||
export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
|
||||
{ id: 'claude', name: 'Claude' },
|
||||
{ id: 'cline-pass', name: 'ClinePass' },
|
||||
{ id: 'codex', name: 'Codex' },
|
||||
{ id: 'cursor', name: 'Cursor' },
|
||||
{ id: 'github-copilot', name: 'GitHub Copilot' },
|
||||
|
||||
@@ -3,6 +3,7 @@ export type QuotaProviderId =
|
||||
| 'codex'
|
||||
| 'cursor'
|
||||
| 'claude'
|
||||
| 'cline-pass'
|
||||
| 'github-copilot'
|
||||
| 'github-copilot-addon'
|
||||
| 'google'
|
||||
|
||||
@@ -15,6 +15,7 @@ const ORIGINAL_FS = { ...fs };
|
||||
const AUTH = JSON.stringify({
|
||||
openai: { access: 'test-token' },
|
||||
crof: { key: 'test-token' },
|
||||
'cline-pass': { key: 'test-token' },
|
||||
neuralwatt: { key: 'test-token' },
|
||||
'opencode-go': { key: 'test-token' },
|
||||
openrouter: { key: 'test-token' },
|
||||
@@ -336,6 +337,67 @@ describe('Crof quota provider (VS Code parity)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClinePass quota provider (VS Code parity)', () => {
|
||||
// Live-verified response shape of
|
||||
// GET https://api.cline.bot/api/v1/users/me/plan/usage-limits
|
||||
const documentedPayload = {
|
||||
data: {
|
||||
limits: [
|
||||
{ type: 'five_hour', percentUsed: 43, resetsAt: '2026-09-08T17:00:44.598174595Z' },
|
||||
{ type: 'weekly', percentUsed: 17, resetsAt: '2026-09-13T17:00:44.598174595Z' },
|
||||
{ type: 'monthly', percentUsed: 8, resetsAt: '2026-10-01T00:00:00Z' },
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
test('maps documented limit kinds to 5h/weekly/monthly windows', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse(documentedPayload)));
|
||||
|
||||
const result = await fetchQuotaForProvider('cline-pass');
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.providerId, 'cline-pass');
|
||||
assert.equal(result.usage!.windows['5h']!.usedPercent, 43);
|
||||
assert.equal(result.usage!.windows['5h']!.windowSeconds, 18_000);
|
||||
assert.equal(result.usage!.windows['5h']!.resetAt, Date.parse('2026-09-08T17:00:44.598174595Z'));
|
||||
assert.equal(result.usage!.windows.weekly!.usedPercent, 17);
|
||||
assert.equal(result.usage!.windows.weekly!.windowSeconds, 604_800);
|
||||
assert.equal(result.usage!.windows.monthly!.usedPercent, 8);
|
||||
assert.equal(result.usage!.windows.monthly!.windowSeconds, null);
|
||||
});
|
||||
|
||||
test('ignores unknown limit types and rejects responses without quota data', async () => {
|
||||
stubFetchReturning(() => Promise.resolve(mockResponse({ data: { limits: [{ type: 'quarterly', percentUsed: 5 }] } })));
|
||||
|
||||
const result = await fetchQuotaForProvider('cline-pass');
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.configured, true);
|
||||
assert.equal(result.usage, null);
|
||||
assert.equal(result.error, 'No quota data in response');
|
||||
});
|
||||
|
||||
test('maps 401 to session-expired with ClinePass branding', async () => {
|
||||
stubFetchFailing(async () => ({}), { ok: false, status: 401 });
|
||||
|
||||
const result = await fetchQuotaForProvider('cline-pass');
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.configured, true);
|
||||
assert.equal(result.error, 'Session expired — please re-authenticate with ClinePass');
|
||||
});
|
||||
|
||||
test('reports invalid-response on JSON parse failure', async () => {
|
||||
stubFetchFailing(async () => { throw new SyntaxError('Unexpected token'); }, { ok: true, status: 200 });
|
||||
|
||||
const result = await fetchQuotaForProvider('cline-pass');
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.error, 'Invalid response from provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Codex quota provider (VS Code parity)', () => {
|
||||
test('coalesces concurrent refreshes for the same provider', async () => {
|
||||
let resolveResponse: ((response: Response) => void) | undefined;
|
||||
|
||||
@@ -146,6 +146,21 @@ type CrofPayload = {
|
||||
credits?: number | string;
|
||||
};
|
||||
|
||||
type ClinePayload = {
|
||||
data?: {
|
||||
limits?: Array<{
|
||||
type?: string;
|
||||
percentUsed?: number | string;
|
||||
resetsAt?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type ClineWindowKind = {
|
||||
key: string;
|
||||
windowSeconds: number | null;
|
||||
};
|
||||
|
||||
type DeepseekPayload = {
|
||||
is_available?: boolean;
|
||||
balance_infos?: Array<{
|
||||
@@ -844,6 +859,11 @@ export const listConfiguredQuotaProviders = () => {
|
||||
configured.add('crof');
|
||||
}
|
||||
|
||||
const clineAuth = normalizeAuthEntry(getAuthEntry(auth, ['cline-pass']));
|
||||
if (clineAuth && (asNonEmptyString(clineAuth.key) || asNonEmptyString(clineAuth.token))) {
|
||||
configured.add('cline-pass');
|
||||
}
|
||||
|
||||
const neuralwattAuth = normalizeAuthEntry(getAuthEntry(auth, ['neuralwatt']));
|
||||
if (neuralwattAuth && ((neuralwattAuth as Record<string, unknown>).key || (neuralwattAuth as Record<string, unknown>).token)) {
|
||||
configured.add('neuralwatt');
|
||||
@@ -2745,6 +2765,111 @@ const fetchCrofQuota = async (): Promise<ProviderResult> => {
|
||||
}
|
||||
};
|
||||
|
||||
const CLINE_PASS_USAGE_URL = 'https://api.cline.bot/api/v1/users/me/plan/usage-limits';
|
||||
|
||||
// Cline reports a rolling five-hour window, a rolling weekly window, and a
|
||||
// calendar-month limit. Each window carries its duration so consumers can rank
|
||||
// limits by how soon they run out; the calendar month has no fixed duration.
|
||||
const CLINE_WINDOW_KINDS = new Map<string, ClineWindowKind>([
|
||||
['five_hour', { key: '5h', windowSeconds: 5 * 60 * 60 }],
|
||||
['weekly', { key: 'weekly', windowSeconds: 7 * 24 * 60 * 60 }],
|
||||
['monthly', { key: 'monthly', windowSeconds: null }],
|
||||
]);
|
||||
|
||||
const fetchClinePassQuota = async (): Promise<ProviderResult> => {
|
||||
const auth = readAuthFile();
|
||||
const entry = normalizeAuthEntry(getAuthEntry(auth, ['cline-pass']));
|
||||
const apiKey = asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
|
||||
|
||||
if (!apiKey) {
|
||||
return buildResult({
|
||||
providerId: 'cline-pass',
|
||||
providerName: 'ClinePass',
|
||||
ok: false,
|
||||
configured: false,
|
||||
error: 'Not configured',
|
||||
});
|
||||
}
|
||||
|
||||
const timeoutSignal = AbortSignal.timeout(15_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(CLINE_PASS_USAGE_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Accept-Encoding': 'identity',
|
||||
},
|
||||
signal: timeoutSignal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return buildResult({
|
||||
providerId: 'cline-pass',
|
||||
providerName: 'ClinePass',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: response.status === 401
|
||||
? 'Session expired — please re-authenticate with ClinePass'
|
||||
: `API error: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
// SAFETY: the Cline usage endpoint returns this documented JSON shape;
|
||||
// every field is optional and validated before use below.
|
||||
const payload = await response.json() as ClinePayload;
|
||||
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
||||
|
||||
const windows: Record<string, UsageWindow> = {};
|
||||
for (const item of limits) {
|
||||
const limit = asObject(item);
|
||||
if (!limit) continue;
|
||||
const limitType = asNonEmptyString(limit.type);
|
||||
const kind = limitType === null ? undefined : CLINE_WINDOW_KINDS.get(limitType);
|
||||
if (!kind) continue;
|
||||
const usedPercent = toNumber(limit.percentUsed);
|
||||
if (usedPercent === null) continue;
|
||||
windows[kind.key] = toUsageWindow({
|
||||
usedPercent,
|
||||
windowSeconds: kind.windowSeconds,
|
||||
resetAt: toTimestamp(limit.resetsAt),
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(windows).length === 0) {
|
||||
return buildResult({
|
||||
providerId: 'cline-pass',
|
||||
providerName: 'ClinePass',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: 'No quota data in response',
|
||||
});
|
||||
}
|
||||
|
||||
return buildResult({
|
||||
providerId: 'cline-pass',
|
||||
providerName: 'ClinePass',
|
||||
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: 'cline-pass',
|
||||
providerName: 'ClinePass',
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: isTimeout
|
||||
? 'Request timed out'
|
||||
: isParseError
|
||||
? 'Invalid response from provider'
|
||||
: (error instanceof Error ? error.message : 'Request failed'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const DEEPSEEK_QUOTA_URL = 'https://api.deepseek.com/user/balance';
|
||||
|
||||
const fetchDeepseekQuota = async (): Promise<ProviderResult> => {
|
||||
@@ -3068,6 +3193,8 @@ const fetchQuotaForProviderUncoalesced = async (providerId: string): Promise<Pro
|
||||
return fetchCursorQuota();
|
||||
case 'crof':
|
||||
return fetchCrofQuota();
|
||||
case 'cline-pass':
|
||||
return fetchClinePassQuota();
|
||||
case 'deepseek':
|
||||
return fetchDeepseekQuota();
|
||||
case 'hyper':
|
||||
|
||||
@@ -23,6 +23,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
|
||||
| Provider ID | Display name | Module | Auth aliases/keys |
|
||||
| --- | --- | --- | --- |
|
||||
| `claude` | Claude | `providers/claude/` | Claude Code Keychain entry, Claude Code credentials file, OpenCode `auth.json` (`anthropic`, `claude`), `CLAUDE_CODE_OAUTH_TOKEN` |
|
||||
| `cline-pass` | ClinePass | `providers/cline-pass.js` | `cline-pass` (API key under `key` or `token`) |
|
||||
| `codex` | Codex | `providers/codex.js` | `openai`, `codex`, `chatgpt` |
|
||||
| `command-code` | Command Code | `providers/command-code.js` | `command-code` OAuth/API credential in OpenCode `auth.json`, or `COMMAND_CODE_API_KEY` |
|
||||
| `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { readAuthFile } from '../../opencode/auth.js';
|
||||
import {
|
||||
getAuthEntry,
|
||||
normalizeAuthEntry,
|
||||
asObject,
|
||||
buildResult,
|
||||
toUsageWindow,
|
||||
toNumber,
|
||||
toTimestamp
|
||||
} from '../utils/index.js';
|
||||
|
||||
export const providerId = 'cline-pass';
|
||||
export const providerName = 'ClinePass';
|
||||
const aliases = ['cline-pass'];
|
||||
const CLINE_USAGE_URL = 'https://api.cline.bot/api/v1/users/me/plan/usage-limits';
|
||||
|
||||
// Cline reports a rolling five-hour window, a rolling weekly window, and a
|
||||
// calendar-month limit. Each window carries its duration so consumers can rank
|
||||
// limits by how soon they run out; the calendar month has no fixed duration.
|
||||
const WINDOW_KINDS = {
|
||||
five_hour: { key: '5h', windowSeconds: 5 * 60 * 60 },
|
||||
weekly: { key: 'weekly', windowSeconds: 7 * 24 * 60 * 60 },
|
||||
monthly: { key: 'monthly', windowSeconds: null }
|
||||
};
|
||||
|
||||
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(CLINE_USAGE_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
|
||||
? 'Session expired — please re-authenticate with ClinePass'
|
||||
: `API error: ${response.status}`
|
||||
});
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
|
||||
|
||||
const windows = {};
|
||||
for (const item of limits) {
|
||||
const limit = asObject(item);
|
||||
if (!limit) continue;
|
||||
const kind = WINDOW_KINDS[limit.type];
|
||||
if (!kind) continue;
|
||||
const usedPercent = toNumber(limit.percentUsed);
|
||||
if (usedPercent === null) continue;
|
||||
windows[kind.key] = toUsageWindow({
|
||||
usedPercent,
|
||||
windowSeconds: kind.windowSeconds,
|
||||
resetAt: toTimestamp(limit.resetsAt)
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(windows).length === 0) {
|
||||
return buildResult({
|
||||
providerId,
|
||||
providerName,
|
||||
ok: false,
|
||||
configured: true,
|
||||
error: 'No quota data in response'
|
||||
});
|
||||
}
|
||||
|
||||
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,127 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../opencode/auth.js', () => ({
|
||||
readAuthFile: () => ({ 'cline-pass': { key: 'test-token' } }),
|
||||
}));
|
||||
|
||||
import { fetchQuota } from './cline-pass.js';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const mockResponse = (body, init = {}) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => body,
|
||||
...init,
|
||||
});
|
||||
|
||||
// Live-verified response shape of
|
||||
// GET https://api.cline.bot/api/v1/users/me/plan/usage-limits
|
||||
const documentedPayload = {
|
||||
data: {
|
||||
limits: [
|
||||
{ type: 'five_hour', percentUsed: 43, resetsAt: '2026-09-08T17:00:44.598174595Z' },
|
||||
{ type: 'weekly', percentUsed: 17, resetsAt: '2026-09-13T17:00:44.598174595Z' },
|
||||
{ type: 'monthly', percentUsed: 8, resetsAt: '2026-10-01T00:00:00Z' },
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
describe('ClinePass quota provider', () => {
|
||||
it('maps documented limit kinds to 5h/weekly/monthly windows', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(documentedPayload)));
|
||||
|
||||
const result = await fetchQuota();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.providerId).toBe('cline-pass');
|
||||
expect(Object.keys(result.usage.windows)).toEqual(['5h', 'weekly', 'monthly']);
|
||||
expect(result.usage.windows['5h'].usedPercent).toBe(43);
|
||||
expect(result.usage.windows['5h'].remainingPercent).toBe(57);
|
||||
expect(result.usage.windows['5h'].windowSeconds).toBe(18_000);
|
||||
expect(result.usage.windows['5h'].resetAt).toBe(Date.parse('2026-09-08T17:00:44.598174595Z'));
|
||||
expect(result.usage.windows.weekly.usedPercent).toBe(17);
|
||||
expect(result.usage.windows.weekly.windowSeconds).toBe(604_800);
|
||||
expect(result.usage.windows.monthly.usedPercent).toBe(8);
|
||||
expect(result.usage.windows.monthly.windowSeconds).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores unknown limit types', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
||||
mockResponse({ data: { limits: [{ type: 'quarterly', percentUsed: 5, resetsAt: '2026-10-01T00:00:00Z' }] } }),
|
||||
));
|
||||
|
||||
const result = await fetchQuota();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.usage).toBeNull();
|
||||
expect(result.error).toBe('No quota data in response');
|
||||
});
|
||||
|
||||
it('parses numeric-string percentUsed', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
||||
mockResponse({ data: { limits: [{ type: 'weekly', percentUsed: '51' }] } }),
|
||||
));
|
||||
|
||||
const result = await fetchQuota();
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.usage.windows.weekly.usedPercent).toBe(51);
|
||||
});
|
||||
|
||||
for (const payload of [{ data: { limits: [] } }, { data: {} }, {}]) {
|
||||
it(`rejects ${JSON.stringify(payload)} without quota data`, async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(payload)));
|
||||
|
||||
const result = await fetchQuota();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.configured).toBe(true);
|
||||
expect(result.usage).toBeNull();
|
||||
expect(result.error).toBe('No quota data in response');
|
||||
});
|
||||
}
|
||||
|
||||
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.configured).toBe(true);
|
||||
expect(result.error).toBe('Session expired — please re-authenticate with ClinePass');
|
||||
});
|
||||
|
||||
it('surfaces non-401 API errors with status', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: async () => ({}),
|
||||
}));
|
||||
|
||||
const result = await fetchQuota();
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('API error: 503');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
import { buildResult } from '../utils/index.js';
|
||||
|
||||
import * as claude from './claude/index.js';
|
||||
import * as clinePass from './cline-pass.js';
|
||||
import * as codex from './codex.js';
|
||||
import * as copilot from './copilot.js';
|
||||
import * as crof from './crof.js';
|
||||
@@ -37,6 +38,12 @@ const registry = {
|
||||
isConfigured: claude.isConfigured,
|
||||
fetchQuota: claude.fetchQuota
|
||||
},
|
||||
'cline-pass': {
|
||||
providerId: clinePass.providerId,
|
||||
providerName: clinePass.providerName,
|
||||
isConfigured: clinePass.isConfigured,
|
||||
fetchQuota: clinePass.fetchQuota
|
||||
},
|
||||
codex: {
|
||||
providerId: codex.providerId,
|
||||
providerName: codex.providerName,
|
||||
|
||||
Reference in New Issue
Block a user