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
|
@@ -12,8 +12,49 @@
|
|
|
12
12
|
* @module dsh-plugin-subscription-oauth/providers/responses-stream
|
|
13
13
|
*/
|
|
14
14
|
import { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
|
|
15
|
+
import { allowanceScheduler } from '../auth/allowance-scheduler.js';
|
|
15
16
|
import { streamResponses } from '../translate/responses.js';
|
|
16
17
|
import { httpLlmError, idleWatchdog, mapFetchFailure } from './common.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 function accountPoolHooks(input) {
|
|
31
|
+
const { provider, tokens, readUsage } = input;
|
|
32
|
+
return {
|
|
33
|
+
accounts: () => tokens.accounts(),
|
|
34
|
+
// Steer away from an account that has no allowance: read its live figures when a
|
|
35
|
+
// reader is wired (so a 100 % window is avoided before it fails there), else fall
|
|
36
|
+
// back to the remembered reset time from a past failure.
|
|
37
|
+
isAccountSpent: async (account) => {
|
|
38
|
+
if (readUsage !== undefined) {
|
|
39
|
+
try {
|
|
40
|
+
const windows = await readUsage(account);
|
|
41
|
+
const spentWindow = windows.find((window) => (window.usedPercent ?? 0) >= 100);
|
|
42
|
+
if (spentWindow !== undefined) {
|
|
43
|
+
allowanceScheduler.noteReset(provider, account, spentWindow.resetsAt);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A failed read is not a reason to avoid the account; fall through to memory.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return allowanceScheduler.isSpentUntilReset(provider, account);
|
|
53
|
+
},
|
|
54
|
+
noteAccountSpent: (account, resetsAt) => { allowanceScheduler.noteReset(provider, account, resetsAt); },
|
|
55
|
+
resetsAt: async (account) => allowanceScheduler.resetAt(provider, account),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
17
58
|
/**
|
|
18
59
|
* Whether a refusal says "this model cannot serve you now", as opposed to "this
|
|
19
60
|
* request is wrong".
|
|
@@ -60,20 +101,92 @@ export async function* streamProviderResponses(options, input) {
|
|
|
60
101
|
if (models.length === 0) {
|
|
61
102
|
throw new LlmError(`${label} has no enabled model to dispatch to`, EMPTY_RESPONSE_CODE);
|
|
62
103
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
104
|
+
// When the provider meters per account and an account's allowance can run out,
|
|
105
|
+
// every account is a candidate: a spent one falls through to the next, so one
|
|
106
|
+
// model id draws on the whole pool's allowance. Accounts are tried in order;
|
|
107
|
+
// the first that serves the request wins.
|
|
108
|
+
let accounts = input.accounts === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: [...await input.accounts()];
|
|
111
|
+
// Send work straight to an account that has allowance: one already known to be
|
|
112
|
+
// spent until a future reset is moved to the back, so a fresh request does not
|
|
113
|
+
// fail against it once to find out. This is a preference, not a skip — a lone
|
|
114
|
+
// account still serves, and the spent-retry below catches a stale record.
|
|
115
|
+
if (accounts !== undefined && accounts.length > 1 && input.isAccountSpent !== undefined) {
|
|
116
|
+
const fresh = [];
|
|
117
|
+
const tired = [];
|
|
118
|
+
for (const account of accounts) {
|
|
119
|
+
if (await input.isAccountSpent(account))
|
|
120
|
+
tired.push(account);
|
|
121
|
+
else
|
|
122
|
+
fresh.push(account);
|
|
123
|
+
}
|
|
124
|
+
if (tired.length > 0 && fresh.length > 0) {
|
|
125
|
+
accounts = [...fresh, ...tired];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const tryAccounts = accounts === undefined || accounts.length === 0
|
|
129
|
+
? [undefined]
|
|
130
|
+
: accounts;
|
|
131
|
+
for (const accountKey of tryAccounts) {
|
|
132
|
+
let token = await tokens.resolve(accountKey);
|
|
133
|
+
// Track which accounts a spent-allowance already moved past, so a later model
|
|
134
|
+
// in the chain does not re-try an account known to be exhausted.
|
|
135
|
+
const spent = new Set();
|
|
136
|
+
for (const [index, model] of models.entries()) {
|
|
137
|
+
try {
|
|
138
|
+
if (spent.has(token.account))
|
|
139
|
+
continue;
|
|
140
|
+
let response = await request(options, model, token.accessToken, watchdog.signal);
|
|
70
141
|
if (response.status === 401 || response.status === 403) {
|
|
71
|
-
|
|
142
|
+
token = await tokens.resolve(token.account, true);
|
|
143
|
+
response = await request(options, model, token.accessToken, watchdog.signal);
|
|
144
|
+
if (response.status === 401 || response.status === 403) {
|
|
145
|
+
input.onCredentialRejected?.(token.account);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// A spent allowance moves to the next account (when the provider supports
|
|
149
|
+
// it), not to the next model: the model is fine, the account is not.
|
|
150
|
+
if (!response.ok
|
|
151
|
+
&& input.isAllowanceExhausted !== undefined
|
|
152
|
+
&& input.accounts !== undefined
|
|
153
|
+
&& input.isAllowanceExhausted(response.status)
|
|
154
|
+
&& tryAccounts.length > 1) {
|
|
155
|
+
spent.add(token.account);
|
|
156
|
+
// Remember when this account's allowance refills, so later requests go
|
|
157
|
+
// straight to another account until then instead of failing here again.
|
|
158
|
+
input.noteAccountSpent?.(token.account, await input.resetsAt?.(token.account));
|
|
159
|
+
const next = tryAccounts.find((candidate) => candidate !== undefined && !spent.has(candidate));
|
|
160
|
+
if (next !== undefined) {
|
|
161
|
+
lastError = await httpLlmError(response, label);
|
|
162
|
+
input.onAccountSwitch?.(token.account, next, reasonOf(lastError));
|
|
163
|
+
token = await tokens.resolve(next);
|
|
164
|
+
// Re-try this same model on the fresh account.
|
|
165
|
+
response = await request(options, model, token.accessToken, watchdog.signal);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const error = await httpLlmError(response, label);
|
|
170
|
+
if (models.length > 1 && isModelFallbackWorthy(response.status) && index < models.length - 1) {
|
|
171
|
+
const next = models[index + 1];
|
|
172
|
+
if (next !== undefined) {
|
|
173
|
+
lastError = error;
|
|
174
|
+
input.onFallback?.(model, next, reasonOf(error));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
72
179
|
}
|
|
180
|
+
if (response.body === null) {
|
|
181
|
+
throw new LlmError(`${label} returned no response body`, EMPTY_RESPONSE_CODE);
|
|
182
|
+
}
|
|
183
|
+
yield* streamResponses(response.body, () => { watchdog.pulse(); });
|
|
184
|
+
return;
|
|
73
185
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
186
|
+
catch (error) {
|
|
187
|
+
// A transport failure on a chain is also worth another model.
|
|
188
|
+
const transport = error instanceof TypeError || error.name === 'AbortError';
|
|
189
|
+
if (transport && options.signal?.aborted !== true && index < models.length - 1) {
|
|
77
190
|
const next = models[index + 1];
|
|
78
191
|
if (next !== undefined) {
|
|
79
192
|
lastError = error;
|
|
@@ -81,26 +194,8 @@ export async function* streamProviderResponses(options, input) {
|
|
|
81
194
|
continue;
|
|
82
195
|
}
|
|
83
196
|
}
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
if (response.body === null) {
|
|
87
|
-
throw new LlmError(`${label} returned no response body`, EMPTY_RESPONSE_CODE);
|
|
88
|
-
}
|
|
89
|
-
yield* streamResponses(response.body, () => { watchdog.pulse(); });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
// A transport failure on a chain is also worth another model.
|
|
94
|
-
const transport = error instanceof TypeError || error.name === 'AbortError';
|
|
95
|
-
if (transport && options.signal?.aborted !== true && index < models.length - 1) {
|
|
96
|
-
const next = models[index + 1];
|
|
97
|
-
if (next !== undefined) {
|
|
98
|
-
lastError = error;
|
|
99
|
-
input.onFallback?.(model, next, reasonOf(error));
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
197
|
+
throw input.describeFailure === undefined ? error : input.describeFailure(error);
|
|
102
198
|
}
|
|
103
|
-
throw input.describeFailure === undefined ? error : input.describeFailure(error);
|
|
104
199
|
}
|
|
105
200
|
}
|
|
106
201
|
throw lastError ?? new LlmError(`${label} served none of the requested models`, EMPTY_RESPONSE_CODE);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|