dsh-plugin-subscription-oauth 0.6.1 → 0.8.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/allowance-scheduler.d.ts +58 -0
- package/lib/auth/allowance-scheduler.js +72 -0
- 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/client/UsageChip.d.ts +58 -2
- package/lib/client/UsageChip.js +104 -22
- package/lib/client.js +83 -54
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +12 -6
- package/lib/index.js +29 -8
- package/lib/providers/antigravity.d.ts +6 -0
- package/lib/providers/antigravity.js +65 -14
- package/lib/providers/ark-provider.js +8 -0
- package/lib/providers/ark.d.ts +3 -0
- package/lib/providers/ark.js +4 -1
- package/lib/providers/glm-provider.js +5 -0
- package/lib/providers/glm.d.ts +2 -0
- package/lib/providers/glm.js +4 -1
- package/lib/providers/grok.d.ts +3 -0
- package/lib/providers/grok.js +9 -1
- package/lib/providers/kimi-provider.js +5 -0
- package/lib/providers/kimi.d.ts +9 -0
- package/lib/providers/kimi.js +8 -1
- package/lib/providers/responses-stream.d.ts +44 -0
- package/lib/providers/responses-stream.js +125 -30
- package/package.json +1 -1
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;
|
|
@@ -218,6 +218,12 @@ export declare class AntigravityAdapter extends LlmAdapter {
|
|
|
218
218
|
private shippedCatalog;
|
|
219
219
|
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
220
220
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
221
|
+
/** Whether an account is remembered as spent until a future reset. */
|
|
222
|
+
private isSpent;
|
|
223
|
+
/** The moment an account's allowance is known to reset, when recorded. */
|
|
224
|
+
private resetAt;
|
|
225
|
+
/** Remember an account as spent (its exact reset is learned separately). */
|
|
226
|
+
private noteSpent;
|
|
221
227
|
/** Translate an auth failure into the message a user can act on. */
|
|
222
228
|
private refreshHint;
|
|
223
229
|
/** Build and send one streaming request for one model. */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, } from '@deepseek-ai/dsh-llm';
|
|
13
13
|
import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
14
14
|
import { attributedHeaders } from '../auth/headers.js';
|
|
15
|
+
import { allowanceScheduler } from '../auth/allowance-scheduler.js';
|
|
15
16
|
import { LoopbackFlow } from '../auth/pkce-flow.js';
|
|
16
17
|
import { TokenStore, finiteInstant, nonEmpty } from '../auth/store.js';
|
|
17
18
|
import { TokenManager } from '../auth/tokens.js';
|
|
@@ -415,25 +416,53 @@ export class AntigravityAdapter extends LlmAdapter {
|
|
|
415
416
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
416
417
|
try {
|
|
417
418
|
// `auto` owns the fallback chain now, so this route serves exactly the
|
|
418
|
-
// model it was asked for
|
|
419
|
+
// model it was asked for — but it draws on every account the provider holds, so a
|
|
420
|
+
// spent allowance moves to the next account rather than failing the call.
|
|
419
421
|
const model = options.model;
|
|
422
|
+
// Accounts come from the manager when it can list them; a minimal manager (or a
|
|
423
|
+
// test double) without `accounts()` means a single account, which is just the
|
|
424
|
+
// default one.
|
|
425
|
+
const accounts = typeof this.options.tokens.accounts === 'function'
|
|
426
|
+
? await this.options.tokens.accounts()
|
|
427
|
+
: [];
|
|
428
|
+
const tried = new Set();
|
|
420
429
|
let token = await this.options.tokens.resolve();
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
430
|
+
// Move an account known to be spent (from a past failure) to the back, so a fresh
|
|
431
|
+
// request starts on one that has allowance. A lone account still serves.
|
|
432
|
+
const order = [...accounts.filter((a) => a !== token.account && !this.isSpent(a)), token.account,
|
|
433
|
+
...accounts.filter((a) => a !== token.account && this.isSpent(a))];
|
|
434
|
+
for (const accountKey of order) {
|
|
435
|
+
if (tried.has(accountKey))
|
|
436
|
+
continue;
|
|
437
|
+
tried.add(accountKey);
|
|
438
|
+
token = await this.options.tokens.resolve(accountKey);
|
|
439
|
+
let response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
|
|
427
440
|
if (response.status === 401 || response.status === 403) {
|
|
428
|
-
|
|
441
|
+
// The stored token was accepted locally but refused on the wire; force
|
|
442
|
+
// one refresh, then report the credential as dead if it still fails.
|
|
443
|
+
token = await this.options.tokens.resolve(token.account, true);
|
|
444
|
+
response = await this.request(options, model, token.account, token.accessToken, watchdog.signal);
|
|
445
|
+
if (response.status === 401 || response.status === 403) {
|
|
446
|
+
this.options.onCredentialRejected?.(token.account);
|
|
447
|
+
}
|
|
429
448
|
}
|
|
449
|
+
// A spent allowance (402/429) moves to the next account, remembering when this
|
|
450
|
+
// one refills so later requests skip it until then.
|
|
451
|
+
if (!response.ok && (response.status === 402 || response.status === 429) && order.length > 1) {
|
|
452
|
+
this.noteSpent(token.account);
|
|
453
|
+
const resetAt = await this.resetAt(token.account);
|
|
454
|
+
allowanceScheduler.noteReset(ANTIGRAVITY_PROVIDER_ID, token.account, resetAt);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (!response.ok)
|
|
458
|
+
throw await httpLlmError(response, 'Antigravity API');
|
|
459
|
+
if (response.body === null) {
|
|
460
|
+
throw new LlmError('Antigravity API returned no response body', EMPTY_RESPONSE_CODE);
|
|
461
|
+
}
|
|
462
|
+
yield* streamAntigravity(response.body, () => { watchdog.pulse(); });
|
|
463
|
+
return;
|
|
430
464
|
}
|
|
431
|
-
|
|
432
|
-
throw await httpLlmError(response, 'Antigravity API');
|
|
433
|
-
if (response.body === null) {
|
|
434
|
-
throw new LlmError('Antigravity API returned no response body', EMPTY_RESPONSE_CODE);
|
|
435
|
-
}
|
|
436
|
-
yield* streamAntigravity(response.body, () => { watchdog.pulse(); });
|
|
465
|
+
throw new LlmError('Antigravity API: every account\'s allowance is spent', EMPTY_RESPONSE_CODE);
|
|
437
466
|
}
|
|
438
467
|
catch (error) {
|
|
439
468
|
throw mapFetchFailure('Antigravity API', this.refreshHint(error), watchdog, options.signal);
|
|
@@ -442,6 +471,28 @@ export class AntigravityAdapter extends LlmAdapter {
|
|
|
442
471
|
watchdog.stop();
|
|
443
472
|
}
|
|
444
473
|
}
|
|
474
|
+
/** Whether an account is remembered as spent until a future reset. */
|
|
475
|
+
isSpent(account) {
|
|
476
|
+
return allowanceScheduler.isSpentUntilReset(ANTIGRAVITY_PROVIDER_ID, account);
|
|
477
|
+
}
|
|
478
|
+
/** The moment an account's allowance is known to reset, when recorded. */
|
|
479
|
+
async resetAt(account) {
|
|
480
|
+
const held = await this.options.tokens.session(account);
|
|
481
|
+
if (held === undefined)
|
|
482
|
+
return undefined;
|
|
483
|
+
try {
|
|
484
|
+
const read = await this.usage(held.session.accessToken, held.session.projectId ?? '', held.session.email);
|
|
485
|
+
const spent = read.windows.find((window) => (window.usedPercent ?? 0) >= 100);
|
|
486
|
+
return spent?.resetsAt;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** Remember an account as spent (its exact reset is learned separately). */
|
|
493
|
+
noteSpent(account) {
|
|
494
|
+
this.options.onWarn?.(`antigravity: account ${account} allowance spent; moving to another account`);
|
|
495
|
+
}
|
|
445
496
|
/** Translate an auth failure into the message a user can act on. */
|
|
446
497
|
refreshHint(error) {
|
|
447
498
|
if (error instanceof ProviderError && error.code === REAUTH_REQUIRED) {
|
|
@@ -100,6 +100,14 @@ export function createArkProvider(options) {
|
|
|
100
100
|
discovery: options.discovery,
|
|
101
101
|
streamIdleTimeoutMs: options.streamIdleTimeoutMs,
|
|
102
102
|
tokens,
|
|
103
|
+
readUsage: async (account) => {
|
|
104
|
+
const held = await tokens.session(account);
|
|
105
|
+
const sts = held?.session.sts;
|
|
106
|
+
if (sts === undefined)
|
|
107
|
+
return [];
|
|
108
|
+
const read = await readArkUsage({ credentials: sts, fetchFn });
|
|
109
|
+
return read.windows;
|
|
110
|
+
},
|
|
103
111
|
...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
|
|
104
112
|
...options.resolveAttachments === undefined ? {} : { resolveAttachments: options.resolveAttachments },
|
|
105
113
|
hiddenModels: () => options.preferences.hiddenIds(ARK_PROVIDER_ID),
|
package/lib/providers/ark.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { TokenManager } from '../auth/tokens.js';
|
|
|
22
22
|
import type { ArkStsCredentials } from '../auth/ark-control.js';
|
|
23
23
|
import type { FetchFn } from '../http.js';
|
|
24
24
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
25
|
+
import type { UsageWindow } from '../protocol.js';
|
|
25
26
|
import type { CatalogEntry } from './catalog.js';
|
|
26
27
|
import type { UsageRead } from './usage.js';
|
|
27
28
|
/** Provider route this module serves. */
|
|
@@ -91,6 +92,8 @@ export interface ArkAdapterOptions {
|
|
|
91
92
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
92
93
|
onWarn?: (message: string) => void;
|
|
93
94
|
onCredentialRejected?: (account: string) => void;
|
|
95
|
+
/** Read one account's metered windows, so the streamer can steer away from a spent one. */
|
|
96
|
+
readUsage?: ((account: string) => Promise<readonly UsageWindow[]>) | undefined;
|
|
94
97
|
fetchFn?: FetchFn;
|
|
95
98
|
}
|
|
96
99
|
/** The adapter behind the `ark` route. */
|
package/lib/providers/ark.js
CHANGED
|
@@ -21,7 +21,7 @@ import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
|
21
21
|
import { callArkControl, parseArkFreeUsage, parseArkPlanUsage } from '../auth/ark-control.js';
|
|
22
22
|
import { proxiedFetch } from '../http.js';
|
|
23
23
|
import { findEntry, resolveCatalog } from './catalog.js';
|
|
24
|
-
import { streamProviderResponses } from './responses-stream.js';
|
|
24
|
+
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
25
25
|
import { toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
26
26
|
/** Provider route this module serves. */
|
|
27
27
|
export const ARK_PROVIDER_ID = 'ark';
|
|
@@ -167,6 +167,9 @@ export class ArkAdapter extends LlmAdapter {
|
|
|
167
167
|
tokens: this.options.tokens,
|
|
168
168
|
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
169
169
|
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
170
|
+
// A 402/429 means this account's allowance is spent: draw on the next account's.
|
|
171
|
+
isAllowanceExhausted: (status) => status === 402 || status === 429,
|
|
172
|
+
...accountPoolHooks({ provider: ARK_PROVIDER_ID, tokens: this.options.tokens, readUsage: this.options.readUsage }),
|
|
170
173
|
...this.options.onCredentialRejected === undefined
|
|
171
174
|
? {}
|
|
172
175
|
: { onCredentialRejected: this.options.onCredentialRejected },
|
|
@@ -110,6 +110,11 @@ export function createGlmProvider(options) {
|
|
|
110
110
|
...options.resolveAttachments === undefined ? {} : { resolveAttachments: options.resolveAttachments },
|
|
111
111
|
hiddenModels: () => options.preferences.hiddenIds(GLM_PROVIDER_ID),
|
|
112
112
|
userModels: () => options.preferences.extraIds(GLM_PROVIDER_ID),
|
|
113
|
+
readUsage: async (account) => {
|
|
114
|
+
const token = await tokens.resolve(account);
|
|
115
|
+
const read = await readGlmUsage({ origin: GLM_ORIGIN, accessToken: token.accessToken, fetchFn });
|
|
116
|
+
return read.windows;
|
|
117
|
+
},
|
|
113
118
|
...options.extraModels === undefined ? {} : { extraModels: options.extraModels },
|
|
114
119
|
fetchFn,
|
|
115
120
|
});
|
package/lib/providers/glm.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface GlmAdapterOptions {
|
|
|
86
86
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
87
87
|
onWarn?: (message: string) => void;
|
|
88
88
|
onCredentialRejected?: (account: string) => void;
|
|
89
|
+
/** Read one account's metered windows, so the streamer can steer away from a spent one. */
|
|
90
|
+
readUsage?: ((account: string) => Promise<readonly UsageWindow[]>) | undefined;
|
|
89
91
|
/** Fetch implementation; defaults to the proxy-aware fetch. */
|
|
90
92
|
fetchFn?: FetchFn;
|
|
91
93
|
}
|
package/lib/providers/glm.js
CHANGED
|
@@ -13,7 +13,7 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
|
13
13
|
import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
14
14
|
import { proxiedFetch } from '../http.js';
|
|
15
15
|
import { findEntry, resolveCatalog } from './catalog.js';
|
|
16
|
-
import { streamProviderResponses } from './responses-stream.js';
|
|
16
|
+
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
17
17
|
import { toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
18
18
|
/** Provider route this module serves. */
|
|
19
19
|
export const GLM_PROVIDER_ID = 'glm';
|
|
@@ -278,6 +278,9 @@ export class GlmAdapter extends LlmAdapter {
|
|
|
278
278
|
tokens: this.options.tokens,
|
|
279
279
|
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
280
280
|
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
281
|
+
// A 402/429 means this account's allowance is spent: draw on the next account's.
|
|
282
|
+
isAllowanceExhausted: (status) => status === 402 || status === 429,
|
|
283
|
+
...accountPoolHooks({ provider: GLM_PROVIDER_ID, tokens: this.options.tokens, readUsage: this.options.readUsage }),
|
|
281
284
|
...this.options.onCredentialRejected === undefined
|
|
282
285
|
? {}
|
|
283
286
|
: { onCredentialRejected: this.options.onCredentialRejected },
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { ProviderRoute } from '../auth/registry.js';
|
|
|
18
18
|
import type { ModelPreferences } from '../settings.js';
|
|
19
19
|
import type { FetchFn } from '../http.js';
|
|
20
20
|
import type { CatalogEntry } from './catalog.js';
|
|
21
|
+
import type { UsageWindow } from '../protocol.js';
|
|
21
22
|
/** Route key this module registers. */
|
|
22
23
|
export declare const GROK_PROVIDER_ID = "grok";
|
|
23
24
|
/** Display name for selectors and diagnostics. */
|
|
@@ -135,6 +136,8 @@ export interface GrokAdapterOptions {
|
|
|
135
136
|
modelOrder?: () => Promise<readonly string[]>;
|
|
136
137
|
fetchFn?: FetchFn;
|
|
137
138
|
onCredentialRejected?: (account: string) => void;
|
|
139
|
+
/** Read one account's metered windows, so the streamer can steer away from a spent one. */
|
|
140
|
+
readUsage?: ((account: string) => Promise<readonly UsageWindow[]>) | undefined;
|
|
138
141
|
}
|
|
139
142
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
140
143
|
export declare class GrokAdapter extends LlmAdapter {
|
package/lib/providers/grok.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ProviderController } from '../auth/provider-controller.js';
|
|
|
18
18
|
import { proxiedFetch } from '../http.js';
|
|
19
19
|
import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
|
|
20
20
|
import { isMissingOrInvalidCredential } from './common.js';
|
|
21
|
-
import { streamProviderResponses } from './responses-stream.js';
|
|
21
|
+
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
22
22
|
import { readGrokUsage } from './usage.js';
|
|
23
23
|
import { toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
24
24
|
/** Route key this module registers. */
|
|
@@ -334,6 +334,9 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
334
334
|
tokens: this.options.tokens,
|
|
335
335
|
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
336
336
|
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
337
|
+
// A 402/429 means this account's allowance is spent: draw on the next account's.
|
|
338
|
+
isAllowanceExhausted: (status) => status === 402 || status === 429,
|
|
339
|
+
...accountPoolHooks({ provider: GROK_PROVIDER_ID, tokens: this.options.tokens, readUsage: this.options.readUsage }),
|
|
337
340
|
...this.options.onCredentialRejected === undefined
|
|
338
341
|
? {}
|
|
339
342
|
: { onCredentialRejected: this.options.onCredentialRejected },
|
|
@@ -648,6 +651,11 @@ export function createGrokProvider(options) {
|
|
|
648
651
|
hiddenModels: () => options.preferences.hiddenIds(GROK_PROVIDER_ID),
|
|
649
652
|
userModels: () => options.preferences.extraIds(GROK_PROVIDER_ID),
|
|
650
653
|
modelOrder: () => options.preferences.orderOf(GROK_PROVIDER_ID),
|
|
654
|
+
readUsage: async (account) => {
|
|
655
|
+
const token = await tokens.resolve(account);
|
|
656
|
+
const read = await readGrokUsage({ accessToken: token.accessToken, fetchFn: options.discoveryFetchFn ?? proxiedFetch });
|
|
657
|
+
return read === undefined ? [] : read.windows;
|
|
658
|
+
},
|
|
651
659
|
...options.discoveryFetchFn === undefined ? {} : { fetchFn: options.discoveryFetchFn },
|
|
652
660
|
});
|
|
653
661
|
const spec = {
|
|
@@ -90,6 +90,11 @@ export function createKimiProvider(options) {
|
|
|
90
90
|
...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
|
|
91
91
|
...options.resolveAttachments === undefined ? {} : { resolveAttachments: options.resolveAttachments },
|
|
92
92
|
identityHeaders: async () => kimiIdentityHeaders(await loadOrCreateDeviceId(options.dataDir)),
|
|
93
|
+
readUsage: async (account) => {
|
|
94
|
+
const token = await tokens.resolve(account);
|
|
95
|
+
const read = await readKimiUsage({ apiBase: options.apiBase, accessToken: token.accessToken, fetchFn });
|
|
96
|
+
return read === undefined ? [] : read.windows;
|
|
97
|
+
},
|
|
93
98
|
hiddenModels: () => options.preferences.hiddenIds(KIMI_PROVIDER_ID),
|
|
94
99
|
userModels: () => options.preferences.extraIds(KIMI_PROVIDER_ID),
|
|
95
100
|
modelOrder: () => options.preferences.orderOf(KIMI_PROVIDER_ID),
|
package/lib/providers/kimi.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
|
10
10
|
import type { FetchFn } from '../http.js';
|
|
11
11
|
import type { TokenManager } from '../auth/tokens.js';
|
|
12
12
|
import type { SessionBase } from '../auth/store.js';
|
|
13
|
+
import type { UsageWindow } from '../protocol.js';
|
|
13
14
|
import type { CatalogEntry } from './catalog.js';
|
|
14
15
|
/** One catalog entry exposed as a selectable model. */
|
|
15
16
|
export type KimiModelEntry = CatalogEntry;
|
|
@@ -62,6 +63,14 @@ export interface KimiAdapterOptions {
|
|
|
62
63
|
fetchFn?: FetchFn;
|
|
63
64
|
/** Called when a call proved the stored credential unusable. */
|
|
64
65
|
onCredentialRejected?: (account: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Read one account's metered windows, so the streamer can steer away from an account
|
|
68
|
+
* whose allowance is spent before it fails there.
|
|
69
|
+
*
|
|
70
|
+
* Resolved per request; absence means the streamer only learns an account is spent by
|
|
71
|
+
* failing against it once.
|
|
72
|
+
*/
|
|
73
|
+
readUsage?: (account: string) => Promise<readonly UsageWindow[]>;
|
|
65
74
|
/** Extra model ids from the plugin configuration, appended to the catalog. */
|
|
66
75
|
extraModels?: readonly string[];
|
|
67
76
|
/**
|
package/lib/providers/kimi.js
CHANGED
|
@@ -11,7 +11,7 @@ import { kimiRequestHeaders } from '../auth/headers.js';
|
|
|
11
11
|
import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
12
12
|
import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
13
13
|
import { idleWatchdog, httpLlmError, isMissingOrInvalidCredential, mapFetchFailure } from './common.js';
|
|
14
|
-
import { streamProviderResponses } from './responses-stream.js';
|
|
14
|
+
import { streamProviderResponses, accountPoolHooks } from './responses-stream.js';
|
|
15
15
|
import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
|
|
16
16
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
17
17
|
import { prepareMoonshotTool } from '../translate/moonshot-schema.js';
|
|
@@ -228,6 +228,13 @@ export class KimiAdapter extends LlmAdapter {
|
|
|
228
228
|
tokens: this.options.tokens,
|
|
229
229
|
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
230
230
|
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
231
|
+
// A 402/429 means this account's allowance is spent: draw on the next account's
|
|
232
|
+
// before giving up, so one model id uses the whole pool's quota.
|
|
233
|
+
isAllowanceExhausted: (status) => status === 402 || status === 429,
|
|
234
|
+
...accountPoolHooks({ provider: KIMI_PROVIDER_ID, tokens: this.options.tokens, readUsage: this.options.readUsage }),
|
|
235
|
+
onAccountSwitch: (from, to, reason) => {
|
|
236
|
+
this.options.onWarn?.(`kimi: account ${from} allowance spent (${reason}); switching to ${to}`);
|
|
237
|
+
},
|
|
231
238
|
...this.options.onCredentialRejected === undefined
|
|
232
239
|
? {}
|
|
233
240
|
: { onCredentialRejected: this.options.onCredentialRejected },
|
|
@@ -14,6 +14,24 @@
|
|
|
14
14
|
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
15
15
|
import type { SessionBase } from '../auth/store.js';
|
|
16
16
|
import type { TokenManager } from '../auth/tokens.js';
|
|
17
|
+
import type { UsageWindow } from '../protocol.js';
|
|
18
|
+
/**
|
|
19
|
+
* Build the cross-account hooks one route needs to draw on its whole account pool.
|
|
20
|
+
*
|
|
21
|
+
* Every Responses-wire route (Kimi, Grok, GLM, Ark) meters per account and can run out;
|
|
22
|
+
* this wires the three account hooks identically, so a route only supplies how to read
|
|
23
|
+
* one account's allowance and its provider id. A route with no `readUsage` still rotates
|
|
24
|
+
* accounts on failure, it just cannot predict exhaustion before the first failure.
|
|
25
|
+
*
|
|
26
|
+
* @param provider - the provider route id, the scheduler's key.
|
|
27
|
+
* @param tokens - the route's token manager, enumerates accounts.
|
|
28
|
+
* @param readUsage - read one account's metered windows, when wired.
|
|
29
|
+
*/
|
|
30
|
+
export declare function accountPoolHooks<S extends SessionBase>(input: {
|
|
31
|
+
provider: string;
|
|
32
|
+
tokens: TokenManager<S>;
|
|
33
|
+
readUsage?: ((account: string) => Promise<readonly UsageWindow[]>) | undefined;
|
|
34
|
+
}): Pick<ResponsesStreamInput<S>, 'accounts' | 'isAccountSpent' | 'noteAccountSpent' | 'resetsAt'>;
|
|
17
35
|
/** Constructor inputs for {@link streamProviderResponses}. */
|
|
18
36
|
export interface ResponsesStreamInput<S extends SessionBase> {
|
|
19
37
|
/** Endpoint name used in failure messages, e.g. `Kimi API`. */
|
|
@@ -37,6 +55,32 @@ export interface ResponsesStreamInput<S extends SessionBase> {
|
|
|
37
55
|
describeFailure?(error: unknown): unknown;
|
|
38
56
|
/** Called when a model in the chain was skipped, with the reason. */
|
|
39
57
|
onFallback?: (from: string, to: string, reason: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a 402/429 on this provider means "this account's allowance is spent",
|
|
60
|
+
* so the call should move to the next account rather than fail.
|
|
61
|
+
*
|
|
62
|
+
* When omitted, an exhausted allowance fails the call (the old behaviour). When
|
|
63
|
+
* set, the caller enumerates the provider's other accounts and retries each until
|
|
64
|
+
* one serves the request or every account is spent.
|
|
65
|
+
*/
|
|
66
|
+
isAllowanceExhausted?: (status: number) => boolean;
|
|
67
|
+
/** Enumerate the provider's accounts, most preferred first; enables cross-account retry. */
|
|
68
|
+
accounts?: () => Promise<readonly string[]>;
|
|
69
|
+
/** Called when a call moved to another account because the first was spent. */
|
|
70
|
+
onAccountSwitch?: (from: string, to: string, reason: string) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Whether one account's allowance is best not used for a fresh request right now.
|
|
73
|
+
*
|
|
74
|
+
* The caller supplies this to steer a request away from an account that is nearly
|
|
75
|
+
* spent (or already spent until a remembered reset), so the request starts on a fuller
|
|
76
|
+
* account instead of failing once to find out. It may read the account's live figures;
|
|
77
|
+
* an account flagged here is tried last, not skipped, so a lone account still serves.
|
|
78
|
+
*/
|
|
79
|
+
isAccountSpent?: (account: string) => Promise<boolean>;
|
|
80
|
+
/** Remember that an account's allowance is spent until a given reset moment. */
|
|
81
|
+
noteAccountSpent?: (account: string, resetsAt: number | undefined) => void;
|
|
82
|
+
/** The moment one account's allowance is known to reset, when it was recorded. */
|
|
83
|
+
resetsAt?: (account: string) => Promise<number | undefined>;
|
|
40
84
|
}
|
|
41
85
|
/**
|
|
42
86
|
* Run one model call over the Responses wire, walking a model chain.
|