dsh-plugin-subscription-oauth 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +413 -2
- package/cordis.patch.yml +13 -0
- package/lib/auth/device-flow.d.ts +112 -0
- package/lib/auth/device-flow.js +319 -0
- package/lib/auth/device-id.d.ts +31 -0
- package/lib/auth/device-id.js +58 -0
- package/lib/auth/flow.d.ts +33 -0
- package/lib/auth/flow.js +12 -0
- package/lib/auth/glm-flow.d.ts +99 -0
- package/lib/auth/glm-flow.js +194 -0
- package/lib/auth/headers.d.ts +61 -0
- package/lib/auth/headers.js +106 -0
- package/lib/auth/pkce-flow.d.ts +78 -0
- package/lib/auth/pkce-flow.js +205 -0
- package/lib/auth/provider-controller.d.ts +241 -0
- package/lib/auth/provider-controller.js +502 -0
- package/lib/auth/registry.d.ts +91 -0
- package/lib/auth/registry.js +149 -0
- package/lib/auth/rpc.d.ts +31 -0
- package/lib/auth/rpc.js +200 -0
- package/lib/auth/store.d.ts +99 -0
- package/lib/auth/store.js +213 -0
- package/lib/auth/tokens.d.ts +76 -0
- package/lib/auth/tokens.js +103 -0
- package/lib/client/SubscriptionsSection.d.ts +34 -0
- package/lib/client/SubscriptionsSection.js +742 -0
- package/lib/client/index.d.ts +36 -0
- package/lib/client/index.js +47 -0
- package/lib/client/locales.d.ts +187 -0
- package/lib/client/locales.js +185 -0
- package/lib/client/styles.d.ts +18 -0
- package/lib/client/styles.js +257 -0
- package/lib/client/subscriptions-rpc.d.ts +24 -0
- package/lib/client/subscriptions-rpc.js +44 -0
- package/lib/client.js +2078 -0
- package/lib/client.js.map +1 -0
- package/lib/constants.d.ts +79 -0
- package/lib/constants.js +91 -0
- package/lib/errors.d.ts +28 -0
- package/lib/errors.js +34 -0
- package/lib/http.d.ts +24 -0
- package/lib/http.js +98 -0
- package/lib/index.d.ts +57 -0
- package/lib/index.js +185 -0
- package/lib/protocol.d.ts +316 -0
- package/lib/protocol.js +29 -0
- package/lib/providers/antigravity-catalog.d.ts +53 -0
- package/lib/providers/antigravity-catalog.js +269 -0
- package/lib/providers/antigravity.d.ts +331 -0
- package/lib/providers/antigravity.js +821 -0
- package/lib/providers/auto.d.ts +151 -0
- package/lib/providers/auto.js +351 -0
- package/lib/providers/catalog.d.ts +69 -0
- package/lib/providers/catalog.js +78 -0
- package/lib/providers/common.d.ts +51 -0
- package/lib/providers/common.js +169 -0
- package/lib/providers/glm-provider.d.ts +41 -0
- package/lib/providers/glm-provider.js +165 -0
- package/lib/providers/glm.d.ts +159 -0
- package/lib/providers/glm.js +445 -0
- package/lib/providers/grok.d.ts +224 -0
- package/lib/providers/grok.js +700 -0
- package/lib/providers/kimi-provider.d.ts +45 -0
- package/lib/providers/kimi-provider.js +148 -0
- package/lib/providers/kimi.d.ts +151 -0
- package/lib/providers/kimi.js +400 -0
- package/lib/providers/responses-stream.d.ts +58 -0
- package/lib/providers/responses-stream.js +115 -0
- package/lib/providers/usage.d.ts +65 -0
- package/lib/providers/usage.js +309 -0
- package/lib/settings.d.ts +122 -0
- package/lib/settings.js +333 -0
- package/lib/translate/antigravity-schema.d.ts +24 -0
- package/lib/translate/antigravity-schema.js +174 -0
- package/lib/translate/antigravity.d.ts +184 -0
- package/lib/translate/antigravity.js +419 -0
- package/lib/translate/responses.d.ts +156 -0
- package/lib/translate/responses.js +395 -0
- package/lib/translate/sse.d.ts +27 -0
- package/lib/translate/sse.js +82 -0
- package/package.json +126 -4
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `glm` route: BigModel's coding plan over the OpenAI Responses wire.
|
|
3
|
+
*
|
|
4
|
+
* Sign-in is ZCode's own OAuth flow (see `auth/glm-flow.ts`). What the flow hands
|
|
5
|
+
* back is a BigModel access token and the ZCode JWT the subscription is bound to;
|
|
6
|
+
* requests and the model list then go to the coding-plan host, whose
|
|
7
|
+
* `/responses` route answers the same wire the `kimi` and `grok` routes use.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-plugin-subscription-oauth/providers/glm
|
|
10
|
+
*/
|
|
11
|
+
import { LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
13
|
+
import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
14
|
+
import { proxiedFetch } from '../http.js';
|
|
15
|
+
import { findEntry, resolveCatalog } from './catalog.js';
|
|
16
|
+
import { streamProviderResponses } from './responses-stream.js';
|
|
17
|
+
import { toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
18
|
+
/** Provider route this module serves. */
|
|
19
|
+
export const GLM_PROVIDER_ID = 'glm';
|
|
20
|
+
/** Display name for the picker group and the Settings card. */
|
|
21
|
+
export const GLM_PROVIDER_NAME = 'GLM (BigModel)';
|
|
22
|
+
/** Page a user opens to manage the coding plan. */
|
|
23
|
+
export const GLM_CODE_URL = 'https://bigmodel.cn/coding-plan/personal/overview';
|
|
24
|
+
/** Coding-plan host; the Responses endpoint is `<base>/responses`. */
|
|
25
|
+
export const GLM_API_BASE = 'https://open.bigmodel.cn/api/coding/paas/v4';
|
|
26
|
+
/** Origin the account-level endpoints hang off, without the coding path. */
|
|
27
|
+
export const GLM_ORIGIN = 'https://open.bigmodel.cn';
|
|
28
|
+
/** Path appended to the API base for a streaming request. */
|
|
29
|
+
export const GLM_RESPONSES_PATH = '/responses';
|
|
30
|
+
/** Path appended to the API base to list the models an account can reach. */
|
|
31
|
+
export const GLM_MODELS_PATH = '/models';
|
|
32
|
+
/**
|
|
33
|
+
* Account allowance endpoint.
|
|
34
|
+
*
|
|
35
|
+
* It sits on the host root rather than under the coding prefix, and BigModel
|
|
36
|
+
* authenticates it with the bare key — no `Bearer` scheme, which the host refuses.
|
|
37
|
+
*/
|
|
38
|
+
export const GLM_QUOTA_PATH = '/api/monitor/usage/quota/limit';
|
|
39
|
+
/** Model the picker preselects. */
|
|
40
|
+
export const GLM_DEFAULT_MODEL = 'glm-5.3';
|
|
41
|
+
/** Context capacity of the GLM 5.x family. */
|
|
42
|
+
export const GLM_CONTEXT_WINDOW = 1_048_576;
|
|
43
|
+
/** Output budget used when the caller sets no cap. */
|
|
44
|
+
export const GLM_DEFAULT_MAX_TOKENS = 131_072;
|
|
45
|
+
/** Reasoning ladder the GLM 5.3 family accepts. */
|
|
46
|
+
const GLM_53_EFFORTS = ['low', 'high', 'max'];
|
|
47
|
+
/**
|
|
48
|
+
* The catalog this build ships, so a signed-in route is usable with no discovery.
|
|
49
|
+
*
|
|
50
|
+
* Ids are the wire ids, lowercase: the coding plan answers `glm-5.3`, not the
|
|
51
|
+
* `GLM-5.3` display name its own client shows. `glm-5.3-flash` is the multimodal
|
|
52
|
+
* member of the family; the rest read text only.
|
|
53
|
+
*/
|
|
54
|
+
export const GLM_MODELS = [
|
|
55
|
+
{
|
|
56
|
+
id: 'glm-5.3',
|
|
57
|
+
name: 'GLM-5.3',
|
|
58
|
+
description: 'Coding plan flagship, text only',
|
|
59
|
+
contextWindow: GLM_CONTEXT_WINDOW,
|
|
60
|
+
inputModalities: ['text'],
|
|
61
|
+
reasoningEfforts: [...GLM_53_EFFORTS],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'glm-5.3-flash',
|
|
65
|
+
name: 'GLM-5.3 Flash',
|
|
66
|
+
description: 'Fast, reads images',
|
|
67
|
+
contextWindow: GLM_CONTEXT_WINDOW,
|
|
68
|
+
inputModalities: ['text', 'image'],
|
|
69
|
+
reasoningEfforts: [...GLM_53_EFFORTS],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'glm-5.2',
|
|
73
|
+
name: 'GLM-5.2',
|
|
74
|
+
description: 'Previous flagship',
|
|
75
|
+
contextWindow: GLM_CONTEXT_WINDOW,
|
|
76
|
+
inputModalities: ['text'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'glm-5.1',
|
|
80
|
+
name: 'GLM-5.1',
|
|
81
|
+
description: 'Earlier 5.x release',
|
|
82
|
+
contextWindow: GLM_CONTEXT_WINDOW,
|
|
83
|
+
inputModalities: ['text'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'glm-5',
|
|
87
|
+
name: 'GLM-5',
|
|
88
|
+
description: 'First 5.x release',
|
|
89
|
+
contextWindow: GLM_CONTEXT_WINDOW,
|
|
90
|
+
inputModalities: ['text'],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* Coerce one decoded credential entry into a GLM session.
|
|
95
|
+
*
|
|
96
|
+
* @param value - decoded JSON value for one account.
|
|
97
|
+
* @returns the session, or undefined when required fields are missing.
|
|
98
|
+
*/
|
|
99
|
+
export function parseGlmSession(value) {
|
|
100
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
101
|
+
return undefined;
|
|
102
|
+
const record = value;
|
|
103
|
+
const accessToken = nonEmpty(record.accessToken);
|
|
104
|
+
const zcodeJwt = nonEmpty(record.zcodeJwt);
|
|
105
|
+
const expiresAt = finiteInstant(record.expiresAt);
|
|
106
|
+
if (accessToken === undefined || zcodeJwt === undefined || expiresAt === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const refreshToken = nonEmpty(record.refreshToken);
|
|
109
|
+
const apiBase = nonEmpty(record.apiBase);
|
|
110
|
+
const accountId = nonEmpty(record.accountId);
|
|
111
|
+
const email = nonEmpty(record.email);
|
|
112
|
+
return {
|
|
113
|
+
accessToken,
|
|
114
|
+
zcodeJwt,
|
|
115
|
+
expiresAt,
|
|
116
|
+
...refreshToken === undefined ? {} : { refreshToken },
|
|
117
|
+
...apiBase === undefined ? {} : { apiBase },
|
|
118
|
+
...accountId === undefined ? {} : { accountId },
|
|
119
|
+
...email === undefined ? {} : { email },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** A finite number from a wire field, or undefined. */
|
|
123
|
+
function finiteNumber(value) {
|
|
124
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
125
|
+
return value;
|
|
126
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
127
|
+
const parsed = Number(value);
|
|
128
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/** A reset instant from an RFC 3339 string or epoch milliseconds. */
|
|
133
|
+
function resetInstant(value) {
|
|
134
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
135
|
+
return value;
|
|
136
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
137
|
+
return undefined;
|
|
138
|
+
const parsed = Date.parse(value);
|
|
139
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the two windows a coding-plan allowance reports.
|
|
143
|
+
*
|
|
144
|
+
* The host lists one row per metered window and names it by a `unit`/`number`
|
|
145
|
+
* pair: `(3, 5)` is the rolling five-hour window and `(6, 1)` the weekly one.
|
|
146
|
+
* Rows of any other type — MCP tool usage, for instance — are not model quota and
|
|
147
|
+
* are skipped rather than shown as an allowance.
|
|
148
|
+
*
|
|
149
|
+
* @param payload - the decoded `quota/limit` body.
|
|
150
|
+
* @returns one window per metered allowance, rolling first.
|
|
151
|
+
*/
|
|
152
|
+
export function parseGlmQuota(payload) {
|
|
153
|
+
if (typeof payload !== 'object' || payload === null)
|
|
154
|
+
return [];
|
|
155
|
+
const body = payload;
|
|
156
|
+
const data = typeof body.data === 'object' && body.data !== null ? body.data : body;
|
|
157
|
+
const limits = Array.isArray(data.limits) ? data.limits : [];
|
|
158
|
+
const found = new Map();
|
|
159
|
+
for (const row of limits) {
|
|
160
|
+
if (typeof row !== 'object' || row === null)
|
|
161
|
+
continue;
|
|
162
|
+
const type = typeof row.type === 'string' ? row.type : '';
|
|
163
|
+
if (type !== 'TOKENS_LIMIT' && type !== 'CREDIT_LIMIT')
|
|
164
|
+
continue;
|
|
165
|
+
let usedPercent = finiteNumber(row.percentage);
|
|
166
|
+
if (usedPercent === undefined) {
|
|
167
|
+
const used = finiteNumber(row.currentValue);
|
|
168
|
+
const total = finiteNumber(row.usage);
|
|
169
|
+
if (used !== undefined && total !== undefined && total > 0)
|
|
170
|
+
usedPercent = (used / total) * 100;
|
|
171
|
+
}
|
|
172
|
+
if (usedPercent === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const unit = finiteNumber(row.unit);
|
|
175
|
+
const number = finiteNumber(row.number);
|
|
176
|
+
const label = unit === 3 && number === 5 ? '5h' : unit === 6 && number === 1 ? 'weekly' : undefined;
|
|
177
|
+
if (label === undefined || found.has(label))
|
|
178
|
+
continue;
|
|
179
|
+
const resetsAt = resetInstant(row.nextResetTime);
|
|
180
|
+
found.set(label, {
|
|
181
|
+
label,
|
|
182
|
+
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
183
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return ['5h', 'weekly'].flatMap((label) => {
|
|
187
|
+
const window = found.get(label);
|
|
188
|
+
return window === undefined ? [] : [window];
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Read an account's coding-plan allowance.
|
|
193
|
+
*
|
|
194
|
+
* @param input - host, bearer, and fetch.
|
|
195
|
+
* @returns the windows the host reported; an empty list when it stated none.
|
|
196
|
+
*/
|
|
197
|
+
export async function readGlmUsage(input) {
|
|
198
|
+
const response = await input.fetchFn(`${input.origin}${GLM_QUOTA_PATH}`, {
|
|
199
|
+
headers: {
|
|
200
|
+
// BigModel wants the key itself here, not a Bearer scheme.
|
|
201
|
+
authorization: input.accessToken,
|
|
202
|
+
accept: 'application/json',
|
|
203
|
+
},
|
|
204
|
+
...input.signal === undefined ? {} : { signal: input.signal },
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok)
|
|
207
|
+
throw await httpFailure(response, 'GLM allowance');
|
|
208
|
+
return { windows: parseGlmQuota(await response.json()) };
|
|
209
|
+
}
|
|
210
|
+
/** Map a refused allowance request onto a harness error. */
|
|
211
|
+
async function httpFailure(response, label) {
|
|
212
|
+
const text = await response.text().catch(() => '');
|
|
213
|
+
const detail = text.trim() === '' ? undefined : text.slice(0, 200);
|
|
214
|
+
return new LlmError(detail === undefined
|
|
215
|
+
? `${label} request failed (HTTP ${response.status})`
|
|
216
|
+
: `${label} request failed (HTTP ${response.status}): ${detail}`, response.status === 401 || response.status === 403 ? 'INVALID_CREDENTIAL' : 'PROVIDER_ERROR', { status: response.status });
|
|
217
|
+
}
|
|
218
|
+
/** The adapter behind the `glm` route. */
|
|
219
|
+
export class GlmAdapter extends LlmAdapter {
|
|
220
|
+
options;
|
|
221
|
+
/** Live catalog ids, once discovery succeeded this session. */
|
|
222
|
+
live;
|
|
223
|
+
constructor(options) {
|
|
224
|
+
super();
|
|
225
|
+
this.options = options;
|
|
226
|
+
}
|
|
227
|
+
providerInfo(provider) {
|
|
228
|
+
return { id: provider, name: GLM_PROVIDER_NAME };
|
|
229
|
+
}
|
|
230
|
+
providerRetryPolicy() {
|
|
231
|
+
// The coding host states its own pacing through 429; a local ladder would
|
|
232
|
+
// fight the account-level limiter.
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
async listModels(_provider) {
|
|
236
|
+
// Nothing to offer without a signed-in account: a model that cannot be called
|
|
237
|
+
// must not appear in the picker, and Auto must not try it.
|
|
238
|
+
if (await this.options.tokens.session() === undefined)
|
|
239
|
+
return [];
|
|
240
|
+
const hidden = await this.hidden();
|
|
241
|
+
return resolveCatalog(this.catalog(), [], hidden).entries.map((entry) => ({
|
|
242
|
+
provider: GLM_PROVIDER_ID,
|
|
243
|
+
id: entry.id,
|
|
244
|
+
name: entry.name,
|
|
245
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
246
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
async resolveModel(_provider, model) {
|
|
250
|
+
const entry = findEntry(this.catalog(), model);
|
|
251
|
+
if (entry === undefined) {
|
|
252
|
+
// An id this build has never heard of is still dispatchable: the endpoint is
|
|
253
|
+
// the authority on what it serves, and a newer model should not be refused.
|
|
254
|
+
const template = findEntry(this.catalog(), GLM_DEFAULT_MODEL) ?? this.catalog()[0];
|
|
255
|
+
return {
|
|
256
|
+
provider: GLM_PROVIDER_ID,
|
|
257
|
+
id: model,
|
|
258
|
+
name: model,
|
|
259
|
+
context: { contextWindow: template?.contextWindow ?? GLM_CONTEXT_WINDOW },
|
|
260
|
+
defaultMaxTokens: GLM_DEFAULT_MAX_TOKENS,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const reasoning = reasoningInfo(entry);
|
|
264
|
+
return {
|
|
265
|
+
provider: GLM_PROVIDER_ID,
|
|
266
|
+
id: entry.id,
|
|
267
|
+
name: entry.name,
|
|
268
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
269
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
270
|
+
context: { contextWindow: entry.contextWindow },
|
|
271
|
+
defaultMaxTokens: GLM_DEFAULT_MAX_TOKENS,
|
|
272
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
stream(options) {
|
|
276
|
+
return streamProviderResponses(options, {
|
|
277
|
+
label: 'GLM API',
|
|
278
|
+
tokens: this.options.tokens,
|
|
279
|
+
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
280
|
+
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
281
|
+
...this.options.onCredentialRejected === undefined
|
|
282
|
+
? {}
|
|
283
|
+
: { onCredentialRejected: this.options.onCredentialRejected },
|
|
284
|
+
onFallback: (from, to, reason) => {
|
|
285
|
+
this.options.onWarn?.(`glm: model ${from} unavailable (${reason}); falling back to ${to}`);
|
|
286
|
+
},
|
|
287
|
+
describeFailure: (error) => this.refreshHint(error),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Translate an auth failure into the message a user can act on. */
|
|
291
|
+
refreshHint(error) {
|
|
292
|
+
if (error instanceof ProviderError && error.code === REAUTH_REQUIRED) {
|
|
293
|
+
return new LlmError(`GLM sign-in expired; sign in again from Settings → Subscriptions (${GLM_CODE_URL})`, 'INVALID_CREDENTIAL', { status: 401 });
|
|
294
|
+
}
|
|
295
|
+
return error;
|
|
296
|
+
}
|
|
297
|
+
/** The catalog served: the shipped list, the live one, then hand-added ids. */
|
|
298
|
+
catalog() {
|
|
299
|
+
return [...this.shippedCatalog(), ...this.addedEntries()];
|
|
300
|
+
}
|
|
301
|
+
/** The route's own catalog, without ids the user added by hand. */
|
|
302
|
+
shippedCatalog() {
|
|
303
|
+
const shipped = this.options.extraModels === undefined
|
|
304
|
+
? [...GLM_MODELS]
|
|
305
|
+
: [...GLM_MODELS, ...this.options.extraModels.map((id) => extraEntry(id))];
|
|
306
|
+
const live = this.live;
|
|
307
|
+
if (live === undefined)
|
|
308
|
+
return shipped;
|
|
309
|
+
const merged = [...shipped];
|
|
310
|
+
for (const id of live) {
|
|
311
|
+
if (findEntry(merged, id) === undefined)
|
|
312
|
+
merged.push(extraEntry(id));
|
|
313
|
+
}
|
|
314
|
+
return merged;
|
|
315
|
+
}
|
|
316
|
+
/** Ids the user published by hand that this route does not already serve. */
|
|
317
|
+
addedEntries() {
|
|
318
|
+
const own = this.shippedCatalog();
|
|
319
|
+
const added = this.options.userModels?.();
|
|
320
|
+
if (added === undefined)
|
|
321
|
+
return [];
|
|
322
|
+
return (Array.isArray(added) ? added : [])
|
|
323
|
+
.filter((id) => findEntry(own, id) === undefined)
|
|
324
|
+
.map((id) => extraEntry(id));
|
|
325
|
+
}
|
|
326
|
+
/** The ids the picker must currently omit. */
|
|
327
|
+
async hidden() {
|
|
328
|
+
if (this.options.hiddenModels === undefined)
|
|
329
|
+
return new Set();
|
|
330
|
+
try {
|
|
331
|
+
return await this.options.hiddenModels();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// A preferences read failure must not empty the picker.
|
|
335
|
+
return new Set();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Adopt the endpoint's live catalog.
|
|
340
|
+
*
|
|
341
|
+
* @param ids - model ids the endpoint reported.
|
|
342
|
+
*/
|
|
343
|
+
discover(ids) {
|
|
344
|
+
this.live = [...ids];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Read the live model list.
|
|
348
|
+
*
|
|
349
|
+
* @param accessToken - bearer for the account.
|
|
350
|
+
* @param signal - optional cancellation.
|
|
351
|
+
* @returns the ids the endpoint serves, or undefined when it cannot be read.
|
|
352
|
+
*/
|
|
353
|
+
async fetchCatalog(accessToken, signal) {
|
|
354
|
+
if (!this.options.discovery)
|
|
355
|
+
return undefined;
|
|
356
|
+
const fetchFn = this.options.fetchFn ?? proxiedFetch;
|
|
357
|
+
try {
|
|
358
|
+
const response = await fetchFn(`${this.options.apiBase}${GLM_MODELS_PATH}`, {
|
|
359
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
|
|
360
|
+
...signal === undefined ? {} : { signal },
|
|
361
|
+
});
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
this.options.onWarn?.(`GLM model list unavailable (HTTP ${response.status})`);
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
const body = await response.json();
|
|
367
|
+
const ids = (body.data ?? [])
|
|
368
|
+
.map((entry) => (typeof entry?.id === 'string' ? entry.id.trim() : ''))
|
|
369
|
+
.filter((id) => id !== '');
|
|
370
|
+
return ids.length === 0 ? undefined : ids;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
this.options.onWarn?.(`GLM model list unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Send one streaming request over the Responses wire. */
|
|
378
|
+
async request(options, model, accessToken, signal) {
|
|
379
|
+
const resolveImage = this.imageResolver(signal);
|
|
380
|
+
const resolved = await toResponsesInput(options.messages, options.system, resolveImage);
|
|
381
|
+
const body = {
|
|
382
|
+
model,
|
|
383
|
+
...resolved.instructions === undefined ? {} : { instructions: resolved.instructions },
|
|
384
|
+
input: resolved.input,
|
|
385
|
+
...options.tools === undefined || options.tools.length === 0
|
|
386
|
+
? {}
|
|
387
|
+
: { tools: toResponsesTools(options.tools), tool_choice: 'auto', parallel_tool_calls: true },
|
|
388
|
+
...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
|
|
389
|
+
...options.reasoningEffort === undefined ? {} : { reasoning: { effort: String(options.reasoningEffort) } },
|
|
390
|
+
...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
|
|
391
|
+
include: ['reasoning.encrypted_content'],
|
|
392
|
+
store: false,
|
|
393
|
+
stream: true,
|
|
394
|
+
};
|
|
395
|
+
const fetchFn = this.options.fetchFn ?? proxiedFetch;
|
|
396
|
+
return fetchFn(`${this.options.apiBase}${GLM_RESPONSES_PATH}`, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: {
|
|
399
|
+
'authorization': `Bearer ${accessToken}`,
|
|
400
|
+
'accept': 'text/event-stream',
|
|
401
|
+
'content-type': 'application/json',
|
|
402
|
+
},
|
|
403
|
+
body: JSON.stringify(body),
|
|
404
|
+
signal,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/** Resolve image blocks through the attachment service, when it is present. */
|
|
408
|
+
imageResolver(signal) {
|
|
409
|
+
const store = this.options.resolveAttachments?.();
|
|
410
|
+
if (store === undefined || store === null)
|
|
411
|
+
return undefined;
|
|
412
|
+
return async (ref) => {
|
|
413
|
+
try {
|
|
414
|
+
const stored = await store.readImage(ref, signal);
|
|
415
|
+
return { mediaType: ref.mediaType, base64: Buffer.from(stored.data).toString('base64') };
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.options.onWarn?.(`image attachment unavailable (${error instanceof Error ? error.message : String(error)})`);
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** The catalog the controller publishes without hand-added ids. */
|
|
424
|
+
ownCatalogEntries() {
|
|
425
|
+
return this.shippedCatalog();
|
|
426
|
+
}
|
|
427
|
+
/** The catalog the controller publishes. */
|
|
428
|
+
catalogEntries() {
|
|
429
|
+
return this.catalog();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** An entry for an id this build ships no metadata for. */
|
|
433
|
+
function extraEntry(id) {
|
|
434
|
+
return { id, name: id, contextWindow: GLM_CONTEXT_WINDOW };
|
|
435
|
+
}
|
|
436
|
+
/** The reasoning block for a catalog entry, or undefined when it supports none. */
|
|
437
|
+
function reasoningInfo(entry) {
|
|
438
|
+
const efforts = entry.reasoningEfforts;
|
|
439
|
+
if (efforts === undefined || efforts.length === 0)
|
|
440
|
+
return undefined;
|
|
441
|
+
return {
|
|
442
|
+
efforts: efforts.map((effort) => ({ id: ReasoningEffortId(effort), name: String(effort), description: '' })),
|
|
443
|
+
...entry.defaultEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(entry.defaultEffort) },
|
|
444
|
+
};
|
|
445
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `grok` provider route: xAI / X Premium (SuperGrok) subscription access.
|
|
3
|
+
*
|
|
4
|
+
* Login is an OIDC authorization-code flow with PKCE against `auth.x.ai`,
|
|
5
|
+
* discovered from the issuer's well-known document and completed on a loopback
|
|
6
|
+
* callback the CLI's registered client allows. Requests then go over xAI's
|
|
7
|
+
* Responses endpoint, which the shared Responses translator already speaks.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-plugin-subscription-oauth/providers/grok
|
|
10
|
+
*/
|
|
11
|
+
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
14
|
+
import type { PkcePair } from '../auth/pkce-flow.js';
|
|
15
|
+
import type { SessionBase } from '../auth/store.js';
|
|
16
|
+
import { TokenManager } from '../auth/tokens.js';
|
|
17
|
+
import type { ProviderRoute } from '../auth/registry.js';
|
|
18
|
+
import type { ModelPreferences } from '../settings.js';
|
|
19
|
+
import type { FetchFn } from '../http.js';
|
|
20
|
+
import type { CatalogEntry } from './catalog.js';
|
|
21
|
+
/** Route key this module registers. */
|
|
22
|
+
export declare const GROK_PROVIDER_ID = "grok";
|
|
23
|
+
/** Display name for selectors and diagnostics. */
|
|
24
|
+
export declare const GROK_PROVIDER_NAME = "Grok (X Premium)";
|
|
25
|
+
/** Where a user manages the underlying subscription. */
|
|
26
|
+
export declare const GROK_CODE_URL = "https://x.ai/grok";
|
|
27
|
+
/** OIDC issuer whose well-known document names the endpoints. */
|
|
28
|
+
export declare const GROK_ISSUER = "https://auth.x.ai";
|
|
29
|
+
/** Discovery document URL. */
|
|
30
|
+
export declare const GROK_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
31
|
+
/** The Grok CLI's registered client id. */
|
|
32
|
+
export declare const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
33
|
+
/** Scopes the CLI requests; `grok-cli:access` is what admits subscription traffic. */
|
|
34
|
+
export declare const GROK_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
35
|
+
/** The one loopback port the registered client redirects to. */
|
|
36
|
+
export declare const GROK_CALLBACK_PORT = 56121;
|
|
37
|
+
/** Path the client redirects to. */
|
|
38
|
+
export declare const GROK_CALLBACK_PATH = "/callback";
|
|
39
|
+
/** API base; `<base>/responses` streams and `<base>/models` lists. */
|
|
40
|
+
export declare const GROK_API_BASE = "https://api.x.ai/v1";
|
|
41
|
+
/** Optional CLI catalog that discloses per-model context and reasoning levels. */
|
|
42
|
+
export declare const GROK_CLI_MODELS_URL = "https://cli-chat-proxy.grok.com/v1/models";
|
|
43
|
+
/** Context window assumed when neither the CLI catalog nor a seed states one. */
|
|
44
|
+
export declare const GROK_CONTEXT_WINDOW = 256000;
|
|
45
|
+
/** Default per-request output cap. */
|
|
46
|
+
export declare const GROK_DEFAULT_MAX_TOKENS = 32000;
|
|
47
|
+
/** Refresh when the access token has less than this much life left. */
|
|
48
|
+
export declare const GROK_PREEMPT_MS: number;
|
|
49
|
+
/**
|
|
50
|
+
* Curated catalog entries.
|
|
51
|
+
*
|
|
52
|
+
* Seeds only what the provider's own documentation names; everything else the
|
|
53
|
+
* account is entitled to arrives through live discovery, so a new model does not
|
|
54
|
+
* need a plugin release.
|
|
55
|
+
*/
|
|
56
|
+
export declare const GROK_MODELS: readonly CatalogEntry[];
|
|
57
|
+
/** A stored Grok (X Premium) login. */
|
|
58
|
+
export interface GrokSession extends SessionBase {
|
|
59
|
+
/** Token endpoint discovered at login time; used for later refresh. */
|
|
60
|
+
tokenEndpoint: string;
|
|
61
|
+
/** Issuer the session was created against. */
|
|
62
|
+
issuer: string;
|
|
63
|
+
/** Scopes the tokens were issued with, echoed on refresh. */
|
|
64
|
+
scopes?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Coerce one decoded credential entry into a Grok session.
|
|
68
|
+
* @param value - decoded JSON value for one account.
|
|
69
|
+
* @returns the session, or undefined when required fields are missing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseGrokSession(value: unknown): GrokSession | undefined;
|
|
72
|
+
/** The discovered authorization-server endpoints. */
|
|
73
|
+
interface GrokEndpoints {
|
|
74
|
+
authorizationEndpoint: string;
|
|
75
|
+
tokenEndpoint: string;
|
|
76
|
+
}
|
|
77
|
+
/** Account identity from the id token, falling back to the access token. */
|
|
78
|
+
export declare function identityFromGrokTokens(accessToken: string, idToken?: string): {
|
|
79
|
+
accountId?: string;
|
|
80
|
+
email?: string;
|
|
81
|
+
};
|
|
82
|
+
/** One token-endpoint answer, normalised. */
|
|
83
|
+
interface GrokTokens {
|
|
84
|
+
accessToken: string;
|
|
85
|
+
refreshToken?: string;
|
|
86
|
+
expiresAt: number;
|
|
87
|
+
idToken?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Turn a token-endpoint payload into tokens with an absolute expiry. */
|
|
90
|
+
export declare function parseGrokTokenPayload(payload: unknown, previousRefreshToken?: string): GrokTokens;
|
|
91
|
+
/** OIDC discovery, cached for the process. */
|
|
92
|
+
export declare function createGrokDiscovery(fetchFn: FetchFn, onWarn?: (message: string) => void): () => Promise<GrokEndpoints>;
|
|
93
|
+
/** Exchange an authorization code for tokens. */
|
|
94
|
+
export declare function exchangeGrokCode(input: {
|
|
95
|
+
fetchFn: FetchFn;
|
|
96
|
+
tokenEndpoint: string;
|
|
97
|
+
code: string;
|
|
98
|
+
redirectUri: string;
|
|
99
|
+
pkce: PkcePair;
|
|
100
|
+
signal: AbortSignal;
|
|
101
|
+
}): Promise<GrokTokens>;
|
|
102
|
+
/** Redeem a refresh token. */
|
|
103
|
+
export declare function refreshGrokToken(input: {
|
|
104
|
+
fetchFn: FetchFn;
|
|
105
|
+
tokenEndpoint: string;
|
|
106
|
+
refreshToken: string;
|
|
107
|
+
}): Promise<GrokTokens>;
|
|
108
|
+
/** One CLI-catalog entry's capability metadata. */
|
|
109
|
+
interface GrokCliMetadata {
|
|
110
|
+
contextWindow?: number;
|
|
111
|
+
reasoningEfforts?: readonly string[];
|
|
112
|
+
defaultEffort?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Constructor dependencies for {@link GrokAdapter}. */
|
|
115
|
+
export interface GrokAdapterOptions {
|
|
116
|
+
/** API base; `<base>/responses` and `<base>/models`. */
|
|
117
|
+
apiBase: string;
|
|
118
|
+
/** Whether the live catalog is consulted. */
|
|
119
|
+
discovery: boolean;
|
|
120
|
+
/** Inactivity budget before a stalled stream is abandoned. */
|
|
121
|
+
streamIdleTimeoutMs: number;
|
|
122
|
+
tokens: TokenManager<GrokSession>;
|
|
123
|
+
onWarn?: (message: string) => void;
|
|
124
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
125
|
+
hiddenModels?: () => Promise<ReadonlySet<string>>;
|
|
126
|
+
/**
|
|
127
|
+
* Model ids the user added by hand.
|
|
128
|
+
*
|
|
129
|
+
* Read per catalog query, so the add button takes effect at the picker's next
|
|
130
|
+
* refresh. A hand-added id is published with conservative metadata: the
|
|
131
|
+
* endpoint is the authority on what it serves, not this build.
|
|
132
|
+
*/
|
|
133
|
+
userModels?: () => Promise<readonly string[]>;
|
|
134
|
+
/** The user's model rank for this route, most preferred first. */
|
|
135
|
+
modelOrder?: () => Promise<readonly string[]>;
|
|
136
|
+
fetchFn?: FetchFn;
|
|
137
|
+
onCredentialRejected?: (account: string) => void;
|
|
138
|
+
}
|
|
139
|
+
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
140
|
+
export declare class GrokAdapter extends LlmAdapter {
|
|
141
|
+
private readonly options;
|
|
142
|
+
/** Live model ids seen this session. */
|
|
143
|
+
private discovered;
|
|
144
|
+
/** Capability metadata from the CLI catalog, keyed by model id. */
|
|
145
|
+
private readonly metadata;
|
|
146
|
+
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
147
|
+
private userAddedIds;
|
|
148
|
+
constructor(options: GrokAdapterOptions);
|
|
149
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
150
|
+
providerRetryPolicy(): undefined;
|
|
151
|
+
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
|
|
152
|
+
private hidden;
|
|
153
|
+
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
154
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
155
|
+
/**
|
|
156
|
+
* The catalog the route serves: its own entries, then the ids the user added
|
|
157
|
+
* by hand. The two are built separately so the hand-added check can look at
|
|
158
|
+
* the shipped list without recursing.
|
|
159
|
+
*/
|
|
160
|
+
private catalog;
|
|
161
|
+
/**
|
|
162
|
+
* The catalog to present: curated seeds with their shipped metadata, then any
|
|
163
|
+
* live id this build does not know, enriched from the CLI catalog when it
|
|
164
|
+
* disclosed that model's context window or reasoning ladder.
|
|
165
|
+
*/
|
|
166
|
+
private shippedCatalog;
|
|
167
|
+
/** Re-read the ids the user added by hand. */
|
|
168
|
+
private syncUserModels;
|
|
169
|
+
/**
|
|
170
|
+
* The entries for ids the user added by hand.
|
|
171
|
+
*
|
|
172
|
+
* Published without shipped metadata: this build has never seen the model, so
|
|
173
|
+
* the endpoint's own defaults are the honest description.
|
|
174
|
+
*/
|
|
175
|
+
private addedEntries;
|
|
176
|
+
/** Record the live catalog for the picker. */
|
|
177
|
+
discover(ids: readonly string[], metadata?: ReadonlyMap<string, GrokCliMetadata>): void;
|
|
178
|
+
/** The curated catalog, for the Settings page's model list. */
|
|
179
|
+
catalogEntries(): readonly CatalogEntry[];
|
|
180
|
+
/** What this route itself serves, without the ids added by hand. */
|
|
181
|
+
ownCatalogEntries(): readonly CatalogEntry[];
|
|
182
|
+
/** Build and send one request for one model. */
|
|
183
|
+
private request;
|
|
184
|
+
/** Resolve image blocks through the attachment service, when it is present. */
|
|
185
|
+
private imageResolver;
|
|
186
|
+
/**
|
|
187
|
+
* Read the live model list, and the CLI catalog's capability metadata.
|
|
188
|
+
* @param accessToken - bearer for the account's models.
|
|
189
|
+
* @returns the model ids, or undefined when discovery is off or unavailable.
|
|
190
|
+
*/
|
|
191
|
+
fetchCatalog(accessToken: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
|
|
192
|
+
/** Read per-model capabilities from the CLI catalog; failures are silent. */
|
|
193
|
+
private fetchCliMetadata;
|
|
194
|
+
}
|
|
195
|
+
/** Everything the `grok` route needs from the plugin configuration. */
|
|
196
|
+
export interface GrokProviderOptions {
|
|
197
|
+
/** Absolute path of `auth.json`. */
|
|
198
|
+
credentialFile: string;
|
|
199
|
+
/** API base; `<base>/responses` and `<base>/models`. */
|
|
200
|
+
apiBase?: string;
|
|
201
|
+
/** Consult the endpoint's live model list. */
|
|
202
|
+
discovery: boolean;
|
|
203
|
+
/** Abandon a stream after this much silence. */
|
|
204
|
+
streamIdleTimeoutMs: number;
|
|
205
|
+
/** Shared model-visibility preferences. */
|
|
206
|
+
preferences: ModelPreferences;
|
|
207
|
+
/** Resolve the attachment service per request. */
|
|
208
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
209
|
+
/** Invoked when the catalog or its visibility changed. */
|
|
210
|
+
onCatalogChanged?: () => void;
|
|
211
|
+
onWarn?: (message: string) => void;
|
|
212
|
+
/** Fetch implementation; defaults to the proxy-aware fetch. */
|
|
213
|
+
fetchFn?: FetchFn;
|
|
214
|
+
/** Discovery fetch override, for tests. */
|
|
215
|
+
discoveryFetchFn?: FetchFn;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Build the `grok` route: its adapter and its login/lifecycle controller.
|
|
219
|
+
*
|
|
220
|
+
* @param options - resolved paths and shared services.
|
|
221
|
+
* @returns the route, ready to register and to serve over RPC.
|
|
222
|
+
*/
|
|
223
|
+
export declare function createGrokProvider(options: GrokProviderOptions): ProviderRoute;
|
|
224
|
+
export {};
|