dsh-plugin-subscription-oauth 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auth/ccswitch.d.ts +26 -1
- package/lib/auth/ccswitch.js +60 -3
- package/lib/auth/rpc.d.ts +2 -0
- package/lib/auth/rpc.js +13 -7
- package/lib/index.d.ts +12 -6
- package/lib/index.js +29 -8
- package/package.json +1 -1
package/lib/auth/ccswitch.d.ts
CHANGED
|
@@ -24,13 +24,38 @@ export interface CcSwitchBalance {
|
|
|
24
24
|
/** Whether the account reports itself as usable. */
|
|
25
25
|
available: boolean;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Query the balance endpoint some OpenAI-compatible providers expose at `{base}/v1/usage`.
|
|
29
|
+
*
|
|
30
|
+
* huanling is one: it answers `GET {base}/v1/usage` with `{ remaining, unit, … }`, which is
|
|
31
|
+
* the figure cc-switch shows for it. This is the provider's own shape, not cc-switch's
|
|
32
|
+
* `balance_infos`, so it is read separately. The base has any trailing `/v1` stripped before
|
|
33
|
+
* the path is added, so both `https://api.huanling.icu` and `https://api.huanling.icu/v1`
|
|
34
|
+
* resolve to the same `{base}/v1/usage`.
|
|
35
|
+
*
|
|
36
|
+
* @param input - the provider's API key and base URL.
|
|
37
|
+
* @returns the balance, or undefined when the provider did not answer with one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function queryUsageBalance(input: {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
fetchFn?: FetchFn;
|
|
43
|
+
}): Promise<CcSwitchBalance | undefined>;
|
|
27
44
|
/**
|
|
28
45
|
* Query one provider's balance the way cc-switch does.
|
|
29
46
|
*
|
|
30
|
-
*
|
|
47
|
+
* cc-switch reads a DeepSeek-compatible provider's balance with `GET <base>/user/balance`
|
|
48
|
+
* and a bearer API key. For the official DeepSeek that base is `api.deepseek.com`, so the
|
|
49
|
+
* path is effectively hardcoded there; for a provider the user added through a custom
|
|
50
|
+
* endpoint (like huanling), the same path is read against that provider's own base URL,
|
|
51
|
+
* which is the only place its balance can live.
|
|
52
|
+
*
|
|
53
|
+
* @param input - the provider's API key and base URL.
|
|
31
54
|
* @returns the balance, or undefined when the provider did not answer with one.
|
|
32
55
|
*/
|
|
33
56
|
export declare function queryCcSwitchBalance(input: {
|
|
34
57
|
apiKey: string;
|
|
58
|
+
/** The provider's own base URL; `https://api.deepseek.com` for the official one. */
|
|
59
|
+
baseUrl: string;
|
|
35
60
|
fetchFn?: FetchFn;
|
|
36
61
|
}): Promise<CcSwitchBalance | undefined>;
|
package/lib/auth/ccswitch.js
CHANGED
|
@@ -23,18 +23,75 @@ function num(value) {
|
|
|
23
23
|
return value.trim();
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Query the balance endpoint some OpenAI-compatible providers expose at `{base}/v1/usage`.
|
|
28
|
+
*
|
|
29
|
+
* huanling is one: it answers `GET {base}/v1/usage` with `{ remaining, unit, … }`, which is
|
|
30
|
+
* the figure cc-switch shows for it. This is the provider's own shape, not cc-switch's
|
|
31
|
+
* `balance_infos`, so it is read separately. The base has any trailing `/v1` stripped before
|
|
32
|
+
* the path is added, so both `https://api.huanling.icu` and `https://api.huanling.icu/v1`
|
|
33
|
+
* resolve to the same `{base}/v1/usage`.
|
|
34
|
+
*
|
|
35
|
+
* @param input - the provider's API key and base URL.
|
|
36
|
+
* @returns the balance, or undefined when the provider did not answer with one.
|
|
37
|
+
*/
|
|
38
|
+
export async function queryUsageBalance(input) {
|
|
39
|
+
const fetchFn = input.fetchFn ?? proxiedFetch;
|
|
40
|
+
const base = input.baseUrl.replace(/\/+$/, '').replace(/\/v1$/i, '');
|
|
41
|
+
const url = `${base}/v1/usage`;
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetchFn(url, {
|
|
45
|
+
headers: { authorization: `Bearer ${input.apiKey}`, accept: 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(15_000),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return undefined;
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = await response.json();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (typeof payload !== 'object' || payload === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
const record = payload;
|
|
64
|
+
const total = num(record.remaining);
|
|
65
|
+
if (total === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const currency = typeof record.unit === 'string' && record.unit.trim() !== '' ? record.unit.trim() : 'USD';
|
|
68
|
+
return {
|
|
69
|
+
currency,
|
|
70
|
+
total,
|
|
71
|
+
available: record.isValid !== false && record.is_active !== false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
26
74
|
/**
|
|
27
75
|
* Query one provider's balance the way cc-switch does.
|
|
28
76
|
*
|
|
29
|
-
*
|
|
77
|
+
* cc-switch reads a DeepSeek-compatible provider's balance with `GET <base>/user/balance`
|
|
78
|
+
* and a bearer API key. For the official DeepSeek that base is `api.deepseek.com`, so the
|
|
79
|
+
* path is effectively hardcoded there; for a provider the user added through a custom
|
|
80
|
+
* endpoint (like huanling), the same path is read against that provider's own base URL,
|
|
81
|
+
* which is the only place its balance can live.
|
|
82
|
+
*
|
|
83
|
+
* @param input - the provider's API key and base URL.
|
|
30
84
|
* @returns the balance, or undefined when the provider did not answer with one.
|
|
31
85
|
*/
|
|
32
86
|
export async function queryCcSwitchBalance(input) {
|
|
33
87
|
const fetchFn = input.fetchFn ?? proxiedFetch;
|
|
88
|
+
// Resolve `<base>/user/balance` without doubling or dropping the path: a base that
|
|
89
|
+
// already ends in `/v1` keeps it, since that is the path the OpenAI-compatible wire
|
|
90
|
+
// uses and the balance endpoint lives beside it.
|
|
91
|
+
const url = `${input.baseUrl.replace(/\/+$/, '')}/user/balance`;
|
|
34
92
|
let response;
|
|
35
93
|
try {
|
|
36
|
-
|
|
37
|
-
response = await fetchFn('https://api.deepseek.com/user/balance', {
|
|
94
|
+
response = await fetchFn(url, {
|
|
38
95
|
headers: { authorization: `Bearer ${input.apiKey}`, accept: 'application/json' },
|
|
39
96
|
signal: AbortSignal.timeout(15_000),
|
|
40
97
|
});
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CredentialResolver = (ref: string) => Promise<string | undefined>;
|
|
|
16
16
|
export interface BalanceContext {
|
|
17
17
|
/** The credential reference each provider id's balance query uses. */
|
|
18
18
|
readonly credentialRefs: Readonly<Record<string, string>>;
|
|
19
|
+
/** The base URL each provider id's balance query reads, when it is not DeepSeek's own. */
|
|
20
|
+
readonly baseUrls: Readonly<Record<string, string>>;
|
|
19
21
|
/** Resolve one credential reference to its key. */
|
|
20
22
|
readonly resolveCredential?: CredentialResolver;
|
|
21
23
|
/**
|
package/lib/auth/rpc.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BAD_REQUEST, ProviderError } from '../errors.js';
|
|
2
|
-
import { queryCcSwitchBalance } from './ccswitch.js';
|
|
2
|
+
import { queryCcSwitchBalance, queryUsageBalance } from './ccswitch.js';
|
|
3
3
|
import { readDeepSeekBalance } from './deepseek-balance.js';
|
|
4
4
|
import { RPC_ENDPOINTS, RPC_PREFIX } from '../protocol.js';
|
|
5
5
|
/** Wrap a value as a successful envelope result. */
|
|
@@ -165,11 +165,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
165
165
|
return await readDeepSeekBalance(balance?.resolveDeepSeekAccount?.(), client);
|
|
166
166
|
}
|
|
167
167
|
case 'ccswitch-balance': {
|
|
168
|
-
// A non-subscription provider's balance, by cc-switch's query
|
|
169
|
-
// the provider; the configured map turns that into
|
|
170
|
-
// provider's profile uses
|
|
171
|
-
//
|
|
172
|
-
//
|
|
168
|
+
// A non-subscription provider's balance, by cc-switch's query against the provider's
|
|
169
|
+
// own base URL. The caller names the provider; the configured map turns that into
|
|
170
|
+
// the credential reference the provider's profile uses and the base URL that profile
|
|
171
|
+
// connects to, and the host's credential service resolves the reference. No entry,
|
|
172
|
+
// no key, or a key the provider does not answer for means no balance — never a
|
|
173
|
+
// fabricated zero.
|
|
173
174
|
const provider = requiredProvider(payload, endpoint);
|
|
174
175
|
const credentialRef = balance?.credentialRefs[provider];
|
|
175
176
|
if (credentialRef === undefined || balance?.resolveCredential === undefined)
|
|
@@ -177,7 +178,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
177
178
|
const apiKey = await balance.resolveCredential(credentialRef);
|
|
178
179
|
if (apiKey === undefined)
|
|
179
180
|
return undefined;
|
|
180
|
-
|
|
181
|
+
const baseUrl = balance?.baseUrls[provider] ?? 'https://api.deepseek.com';
|
|
182
|
+
// A provider that answers the OpenAI-compatible `/v1/usage` (the shape cc-switch's
|
|
183
|
+
// custom template reads, `{ remaining, unit }`) is asked that first; one that
|
|
184
|
+
// answers the DeepSeek `balance_infos` shape is asked that as a fallback.
|
|
185
|
+
return await queryUsageBalance({ apiKey, baseUrl })
|
|
186
|
+
?? await queryCcSwitchBalance({ apiKey, baseUrl });
|
|
181
187
|
}
|
|
182
188
|
case 'reload':
|
|
183
189
|
return registry.reload();
|
package/lib/index.d.ts
CHANGED
|
@@ -45,16 +45,22 @@ export interface Config {
|
|
|
45
45
|
*/
|
|
46
46
|
auto?: boolean;
|
|
47
47
|
/**
|
|
48
|
-
* The
|
|
49
|
-
* provider id.
|
|
48
|
+
* The balance source a non-subscription provider uses, by provider id.
|
|
50
49
|
*
|
|
51
50
|
* A provider that is not a subscription this plugin serves has no usage windows to
|
|
52
51
|
* meter, so the chip shows its balance instead — read by cc-switch's query, which needs
|
|
53
|
-
* the provider's API key
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* the provider's API key and its own base URL (the balance endpoint lives beside the
|
|
53
|
+
* provider's OpenAI-compatible wire, not on DeepSeek's). Each entry names the
|
|
54
|
+
* credential reference the provider's profile uses (`apiKeyEnv`) and the base URL that
|
|
55
|
+
* profile connects to; the host's credential service resolves the reference. A provider
|
|
56
|
+
* with no entry, or whose key does not resolve, gets no balance and the chip stays
|
|
57
|
+
* silent.
|
|
57
58
|
*/
|
|
59
|
+
balanceSources?: Record<string, {
|
|
60
|
+
credentialRef: string;
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** @deprecated Use {@link balanceSources}, which carries the base URL too. */
|
|
58
64
|
balanceCredentialRefs?: Record<string, string>;
|
|
59
65
|
}
|
|
60
66
|
/** Configuration schema, with the config file's own defaults applied. */
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,8 @@ export const Config = z.object({
|
|
|
42
42
|
defaultModel: z.string().default(KIMI_DEFAULT_MODEL),
|
|
43
43
|
extraModels: z.array(z.string()).default([]),
|
|
44
44
|
auto: z.boolean().default(true),
|
|
45
|
+
balanceSources: z.dict(z.object({ credentialRef: z.string(), baseUrl: z.string() })).default({}),
|
|
46
|
+
balanceCredentialRefs: z.dict(z.string()).default({}),
|
|
45
47
|
});
|
|
46
48
|
/**
|
|
47
49
|
* Register every configured provider route, its credential store, and — when
|
|
@@ -188,13 +190,18 @@ export function apply(ctx, config = {}) {
|
|
|
188
190
|
handles.set(route.id, handle);
|
|
189
191
|
ctx.effect(() => () => handle(), `${name}: ${route.id} provider route`);
|
|
190
192
|
}
|
|
191
|
-
// A non-subscription provider's balance is read by cc-switch's query
|
|
192
|
-
// provider's
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const credentialRefs = config.balanceCredentialRefs
|
|
193
|
+
// A non-subscription provider's balance is read by cc-switch's query against the
|
|
194
|
+
// provider's own base URL (the balance endpoint lives beside its OpenAI-compatible
|
|
195
|
+
// wire, not on DeepSeek's), which needs the provider's API key. Each configured source
|
|
196
|
+
// names the credential reference the provider's profile uses (`apiKeyEnv`) and the base
|
|
197
|
+
// URL that profile connects to; the host's credential service resolves the reference.
|
|
198
|
+
const balanceSources = config.balanceSources ?? {};
|
|
199
|
+
const credentialRefs = { ...config.balanceCredentialRefs };
|
|
200
|
+
const baseUrls = {};
|
|
201
|
+
for (const [provider, source] of Object.entries(balanceSources)) {
|
|
202
|
+
credentialRefs[provider] = source.credentialRef;
|
|
203
|
+
baseUrls[provider] = source.baseUrl;
|
|
204
|
+
}
|
|
198
205
|
// The DeepSeek account's balance is read on the node half through the host's
|
|
199
206
|
// `deepseekAccount` service; the browser reaches it through the plugin's own RPC, the
|
|
200
207
|
// same channel as the rest of the chip's data. The service is read lazily, per request
|
|
@@ -202,6 +209,7 @@ export function apply(ctx, config = {}) {
|
|
|
202
209
|
// without it still serves every other endpoint, and the balance endpoint answers nothing.
|
|
203
210
|
const balanceContext = {
|
|
204
211
|
credentialRefs,
|
|
212
|
+
baseUrls,
|
|
205
213
|
resolveDeepSeekAccount: () => {
|
|
206
214
|
try {
|
|
207
215
|
return ctx.get('deepseekAccount');
|
|
@@ -213,7 +221,20 @@ export function apply(ctx, config = {}) {
|
|
|
213
221
|
resolveCredential: async (ref) => {
|
|
214
222
|
try {
|
|
215
223
|
const credentials = ctx.get('credentials');
|
|
216
|
-
|
|
224
|
+
const hit = await credentials?.resolve?.(ref);
|
|
225
|
+
// The resolve answer is `{ value, source }`; an env-var source may name the var
|
|
226
|
+
// rather than carry the value, so both are considered before giving up.
|
|
227
|
+
if (typeof hit === 'object' && hit !== null) {
|
|
228
|
+
const record = hit;
|
|
229
|
+
if (typeof record.value === 'string' && record.value.trim() !== '')
|
|
230
|
+
return record.value;
|
|
231
|
+
if (typeof record.source === 'string') {
|
|
232
|
+
const fromEnv = process.env[record.source];
|
|
233
|
+
if (typeof fromEnv === 'string' && fromEnv.trim() !== '')
|
|
234
|
+
return fromEnv;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
217
238
|
}
|
|
218
239
|
catch {
|
|
219
240
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "EricFL1998",
|