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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider plumbing: an idle watchdog for streams, wire-error
|
|
3
|
+
* classification, and transport-failure mapping.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-plugin-subscription-oauth/providers/common
|
|
6
|
+
*/
|
|
7
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
/** Watchdog handle used around one streaming request. */
|
|
9
|
+
export interface IdleWatchdog {
|
|
10
|
+
/** Abort signal to hand to `fetch`; also fires when the caller's signal does. */
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
/** Report inbound transport activity, resetting the idle timer. */
|
|
13
|
+
pulse(): void;
|
|
14
|
+
/** Stop the timer; safe to call more than once. */
|
|
15
|
+
stop(): void;
|
|
16
|
+
/** Whether the idle budget (rather than the caller) aborted the request. */
|
|
17
|
+
readonly timedOut: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Guard one stream with an inactivity budget.
|
|
21
|
+
*
|
|
22
|
+
* The timer aborts the request when no chunk arrives for `idleMs`, so a dead
|
|
23
|
+
* connection fails the turn instead of hanging it forever.
|
|
24
|
+
*
|
|
25
|
+
* @param signal - the caller's cancellation signal, if any.
|
|
26
|
+
* @param idleMs - inactivity budget in milliseconds; a non-positive value disables it.
|
|
27
|
+
* @returns the watchdog.
|
|
28
|
+
*/
|
|
29
|
+
export declare function idleWatchdog(signal: AbortSignal | undefined, idleMs: number): IdleWatchdog;
|
|
30
|
+
/**
|
|
31
|
+
* Turn a non-2xx provider response into an LlmError carrying the status and
|
|
32
|
+
* any rate-limit hint the headers disclosed.
|
|
33
|
+
*
|
|
34
|
+
* @param response - the failed response.
|
|
35
|
+
* @param label - human name of the endpoint, used in the message.
|
|
36
|
+
* @returns the error to throw.
|
|
37
|
+
*/
|
|
38
|
+
export declare function httpLlmError(response: Response, label: string): Promise<LlmError>;
|
|
39
|
+
/** Whether a failure means the stored credential no longer works. */
|
|
40
|
+
export declare function isMissingOrInvalidCredential(error: unknown): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Map a thrown transport failure onto a harness error, distinguishing caller
|
|
43
|
+
* cancellation from an idle timeout from a genuine network fault.
|
|
44
|
+
*
|
|
45
|
+
* @param label - human name of the endpoint.
|
|
46
|
+
* @param error - the thrown value.
|
|
47
|
+
* @param watchdog - the request's watchdog, when one was installed.
|
|
48
|
+
* @param signal - the caller's signal.
|
|
49
|
+
* @returns the error to throw.
|
|
50
|
+
*/
|
|
51
|
+
export declare function mapFetchFailure(label: string, error: unknown, watchdog?: Pick<IdleWatchdog, 'timedOut'>, signal?: AbortSignal): unknown;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared provider plumbing: an idle watchdog for streams, wire-error
|
|
3
|
+
* classification, and transport-failure mapping.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-plugin-subscription-oauth/providers/common
|
|
6
|
+
*/
|
|
7
|
+
import { errorChain, LlmError } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
/**
|
|
9
|
+
* Guard one stream with an inactivity budget.
|
|
10
|
+
*
|
|
11
|
+
* The timer aborts the request when no chunk arrives for `idleMs`, so a dead
|
|
12
|
+
* connection fails the turn instead of hanging it forever.
|
|
13
|
+
*
|
|
14
|
+
* @param signal - the caller's cancellation signal, if any.
|
|
15
|
+
* @param idleMs - inactivity budget in milliseconds; a non-positive value disables it.
|
|
16
|
+
* @returns the watchdog.
|
|
17
|
+
*/
|
|
18
|
+
export function idleWatchdog(signal, idleMs) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
let timedOut = false;
|
|
21
|
+
let timer;
|
|
22
|
+
const onCallerAbort = () => controller.abort(signal?.reason);
|
|
23
|
+
if (signal !== undefined) {
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
controller.abort(signal.reason);
|
|
26
|
+
else
|
|
27
|
+
signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
28
|
+
}
|
|
29
|
+
const arm = () => {
|
|
30
|
+
if (idleMs <= 0)
|
|
31
|
+
return;
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
timedOut = true;
|
|
34
|
+
controller.abort(new Error(`Kimi stream idle for more than ${idleMs}ms`));
|
|
35
|
+
}, idleMs);
|
|
36
|
+
timer.unref?.();
|
|
37
|
+
};
|
|
38
|
+
const clear = () => {
|
|
39
|
+
if (timer !== undefined)
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
timer = undefined;
|
|
42
|
+
};
|
|
43
|
+
const stop = () => {
|
|
44
|
+
clear();
|
|
45
|
+
signal?.removeEventListener('abort', onCallerAbort);
|
|
46
|
+
};
|
|
47
|
+
arm();
|
|
48
|
+
return {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
pulse: () => {
|
|
51
|
+
clear();
|
|
52
|
+
arm();
|
|
53
|
+
},
|
|
54
|
+
stop,
|
|
55
|
+
get timedOut() {
|
|
56
|
+
return timedOut;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Read a `retry-after` header as an absolute delay in milliseconds. */
|
|
61
|
+
function retryAfterMs(response) {
|
|
62
|
+
const raw = response.headers.get('retry-after');
|
|
63
|
+
if (raw === null)
|
|
64
|
+
return undefined;
|
|
65
|
+
const seconds = Number(raw);
|
|
66
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
67
|
+
return Math.round(seconds * 1000);
|
|
68
|
+
const at = Date.parse(raw);
|
|
69
|
+
if (!Number.isNaN(at))
|
|
70
|
+
return Math.max(0, at - Date.now());
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Provider-neutral failure code for one HTTP status. */
|
|
74
|
+
function statusCode(status) {
|
|
75
|
+
if (status === 401 || status === 403)
|
|
76
|
+
return 'INVALID_CREDENTIAL';
|
|
77
|
+
if (status === 404)
|
|
78
|
+
return 'MODEL_NOT_FOUND';
|
|
79
|
+
if (status === 429)
|
|
80
|
+
return 'RATE_LIMIT';
|
|
81
|
+
if (status === 400 || status === 422)
|
|
82
|
+
return 'INVALID_REQUEST';
|
|
83
|
+
if (status >= 500)
|
|
84
|
+
return 'SERVER';
|
|
85
|
+
return 'HTTP_ERROR';
|
|
86
|
+
}
|
|
87
|
+
/** Best-effort provider message out of an error response body. */
|
|
88
|
+
async function errorBodyMessage(response) {
|
|
89
|
+
try {
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
if (text.trim() === '')
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(text);
|
|
95
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
96
|
+
const record = parsed;
|
|
97
|
+
const error = record.error;
|
|
98
|
+
if (typeof error === 'object' && error !== null) {
|
|
99
|
+
const message = error.message;
|
|
100
|
+
if (typeof message === 'string' && message.trim() !== '')
|
|
101
|
+
return message.trim();
|
|
102
|
+
}
|
|
103
|
+
if (typeof record.message === 'string' && record.message.trim() !== '')
|
|
104
|
+
return record.message.trim();
|
|
105
|
+
if (typeof error === 'string' && error.trim() !== '')
|
|
106
|
+
return error.trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Not JSON: fall through to the raw body.
|
|
111
|
+
}
|
|
112
|
+
return text.slice(0, 400).trim();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Turn a non-2xx provider response into an LlmError carrying the status and
|
|
120
|
+
* any rate-limit hint the headers disclosed.
|
|
121
|
+
*
|
|
122
|
+
* @param response - the failed response.
|
|
123
|
+
* @param label - human name of the endpoint, used in the message.
|
|
124
|
+
* @returns the error to throw.
|
|
125
|
+
*/
|
|
126
|
+
export async function httpLlmError(response, label) {
|
|
127
|
+
const detail = await errorBodyMessage(response);
|
|
128
|
+
const retryAfter = retryAfterMs(response);
|
|
129
|
+
const message = detail === undefined
|
|
130
|
+
? `${label} request failed (HTTP ${response.status} ${response.statusText})`.trim()
|
|
131
|
+
: `${label} request failed (HTTP ${response.status}): ${detail}`;
|
|
132
|
+
return new LlmError(message, statusCode(response.status), {
|
|
133
|
+
status: response.status,
|
|
134
|
+
...retryAfter === undefined ? {} : { providerRetryAfterMs: retryAfter },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Whether a failure means the stored credential no longer works. */
|
|
138
|
+
export function isMissingOrInvalidCredential(error) {
|
|
139
|
+
if (error instanceof LlmError) {
|
|
140
|
+
return error.failure.code === 'INVALID_CREDENTIAL' || error.failure.status === 401 || error.failure.status === 403;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Map a thrown transport failure onto a harness error, distinguishing caller
|
|
146
|
+
* cancellation from an idle timeout from a genuine network fault.
|
|
147
|
+
*
|
|
148
|
+
* @param label - human name of the endpoint.
|
|
149
|
+
* @param error - the thrown value.
|
|
150
|
+
* @param watchdog - the request's watchdog, when one was installed.
|
|
151
|
+
* @param signal - the caller's signal.
|
|
152
|
+
* @returns the error to throw.
|
|
153
|
+
*/
|
|
154
|
+
export function mapFetchFailure(label, error, watchdog, signal) {
|
|
155
|
+
if (error instanceof LlmError)
|
|
156
|
+
return error;
|
|
157
|
+
if (signal?.aborted === true)
|
|
158
|
+
return error;
|
|
159
|
+
if (watchdog?.timedOut === true) {
|
|
160
|
+
return new LlmError(`${label} stream timed out waiting for data`, 'TIMEOUT');
|
|
161
|
+
}
|
|
162
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
163
|
+
if (cause !== undefined && cause !== error) {
|
|
164
|
+
return new LlmError(`${label} request failed: ${errorChain(error)}`, 'NETWORK', { cause: error });
|
|
165
|
+
}
|
|
166
|
+
return error instanceof Error
|
|
167
|
+
? new LlmError(`${label} request failed: ${error.message}`, 'NETWORK', { cause: error })
|
|
168
|
+
: new LlmError(`${label} request failed: ${String(error)}`, 'NETWORK');
|
|
169
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `glm` provider route's wiring: credential store, ZCode OAuth login, model
|
|
3
|
+
* catalog, and its Settings card facts.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-plugin-subscription-oauth/providers/glm-provider
|
|
6
|
+
*/
|
|
7
|
+
import type { ProviderRoute } from '../auth/registry.js';
|
|
8
|
+
import type { ModelPreferences } from '../settings.js';
|
|
9
|
+
import type { FetchFn } from '../http.js';
|
|
10
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
|
+
/** Everything the GLM route needs from the plugin configuration. */
|
|
12
|
+
export interface GlmProviderOptions {
|
|
13
|
+
/** Absolute path of `auth.json`. */
|
|
14
|
+
credentialFile: string;
|
|
15
|
+
/** Request base; `<base>/responses` is the endpoint. */
|
|
16
|
+
apiBase?: string;
|
|
17
|
+
/** Consult the endpoint's live model list. */
|
|
18
|
+
discovery: boolean;
|
|
19
|
+
/** Abandon a stream after this much silence. */
|
|
20
|
+
streamIdleTimeoutMs: number;
|
|
21
|
+
/** Model the picker preselects. */
|
|
22
|
+
defaultModel?: string;
|
|
23
|
+
/** Extra model ids published without shipped metadata. */
|
|
24
|
+
extraModels?: readonly string[];
|
|
25
|
+
/** Shared model-visibility preferences. */
|
|
26
|
+
preferences: ModelPreferences;
|
|
27
|
+
/** Resolve the attachment service per request. */
|
|
28
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
29
|
+
/** Invoked when the catalog or its visibility changed. */
|
|
30
|
+
onCatalogChanged?: () => void;
|
|
31
|
+
onWarn?: (message: string) => void;
|
|
32
|
+
/** Fetch implementation; defaults to the proxy-aware fetch. */
|
|
33
|
+
fetchFn?: FetchFn;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the `glm` route: its adapter and its login/lifecycle controller.
|
|
37
|
+
*
|
|
38
|
+
* @param options - resolved endpoints, paths, and shared services.
|
|
39
|
+
* @returns the route, ready to register and to serve over RPC.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createGlmProvider(options: GlmProviderOptions): ProviderRoute;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `glm` provider route's wiring: credential store, ZCode OAuth login, model
|
|
3
|
+
* catalog, and its Settings card facts.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-plugin-subscription-oauth/providers/glm-provider
|
|
6
|
+
*/
|
|
7
|
+
import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
8
|
+
import { TokenStore } from '../auth/store.js';
|
|
9
|
+
import { TokenManager } from '../auth/tokens.js';
|
|
10
|
+
import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
|
|
11
|
+
import { startGlmFlow, pollGlmFlow } from '../auth/glm-flow.js';
|
|
12
|
+
import { GlmAdapter, GLM_API_BASE, GLM_CODE_URL, GLM_CONTEXT_WINDOW, GLM_DEFAULT_MODEL, GLM_ORIGIN, GLM_PROVIDER_ID, GLM_PROVIDER_NAME, parseGlmSession, readGlmUsage } from './glm.js';
|
|
13
|
+
import { ProviderController } from '../auth/provider-controller.js';
|
|
14
|
+
import { proxiedFetch } from '../http.js';
|
|
15
|
+
/** Wait, honouring cancellation. */
|
|
16
|
+
function sleep(ms, signal) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
if (signal.aborted) {
|
|
19
|
+
reject(new Error(LOGIN_CANCELLED_MESSAGE));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
signal.removeEventListener('abort', onAbort);
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
function onAbort() {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(new Error(LOGIN_CANCELLED_MESSAGE));
|
|
29
|
+
}
|
|
30
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the `glm` route: its adapter and its login/lifecycle controller.
|
|
35
|
+
*
|
|
36
|
+
* @param options - resolved endpoints, paths, and shared services.
|
|
37
|
+
* @returns the route, ready to register and to serve over RPC.
|
|
38
|
+
*/
|
|
39
|
+
export function createGlmProvider(options) {
|
|
40
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
41
|
+
const apiBase = options.apiBase ?? GLM_API_BASE;
|
|
42
|
+
const store = new TokenStore(options.credentialFile, GLM_PROVIDER_ID, parseGlmSession);
|
|
43
|
+
const toSession = (tokens) => ({
|
|
44
|
+
accessToken: tokens.accessToken,
|
|
45
|
+
zcodeJwt: tokens.zcodeJwt,
|
|
46
|
+
expiresAt: tokens.expiresAt,
|
|
47
|
+
...tokens.refreshToken === undefined ? {} : { refreshToken: tokens.refreshToken },
|
|
48
|
+
apiBase,
|
|
49
|
+
...tokens.accountId === undefined ? {} : { accountId: tokens.accountId },
|
|
50
|
+
...tokens.label === undefined ? {} : { email: tokens.label },
|
|
51
|
+
});
|
|
52
|
+
/** The in-flight attempt, so `pending()` is stable across calls. */
|
|
53
|
+
let current;
|
|
54
|
+
const flow = {
|
|
55
|
+
async start() {
|
|
56
|
+
const start = await startGlmFlow({ fetchFn });
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const session = (async () => {
|
|
59
|
+
// The browser sign-in completes on the broker's own callback, so this side
|
|
60
|
+
// only has to keep asking until the flow is ready or lapses.
|
|
61
|
+
while (true) {
|
|
62
|
+
if (controller.signal.aborted)
|
|
63
|
+
throw new Error(LOGIN_CANCELLED_MESSAGE);
|
|
64
|
+
const tokens = await pollGlmFlow({ fetchFn }, start, controller.signal);
|
|
65
|
+
if (tokens !== undefined)
|
|
66
|
+
return toSession(tokens);
|
|
67
|
+
await sleep(start.pollIntervalMs, controller.signal);
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
const attempt = {
|
|
71
|
+
prompt: {
|
|
72
|
+
url: start.authorizeUrl,
|
|
73
|
+
expiresAt: start.expiresAt,
|
|
74
|
+
mode: 'open-url',
|
|
75
|
+
},
|
|
76
|
+
session,
|
|
77
|
+
cancel: () => { controller.abort(); },
|
|
78
|
+
};
|
|
79
|
+
current = attempt;
|
|
80
|
+
// Drop the record once the attempt settles, so `pending()` cannot report a
|
|
81
|
+
// login that already finished or failed.
|
|
82
|
+
void session.catch(() => undefined).finally(() => {
|
|
83
|
+
if (current === attempt)
|
|
84
|
+
current = undefined;
|
|
85
|
+
});
|
|
86
|
+
return attempt;
|
|
87
|
+
},
|
|
88
|
+
pending() {
|
|
89
|
+
return current;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const tokens = new TokenManager({
|
|
93
|
+
store,
|
|
94
|
+
displayName: GLM_PROVIDER_NAME,
|
|
95
|
+
// The ZCode flow has no refresh grant, so there is nothing to pre-empt: asking
|
|
96
|
+
// five minutes early would refuse a token that is still perfectly usable and
|
|
97
|
+
// send the user to sign in again for no reason. Renewal happens when the token
|
|
98
|
+
// has actually lapsed, which is also what the official client does.
|
|
99
|
+
preemptMs: 0,
|
|
100
|
+
refresh: () => {
|
|
101
|
+
throw new ProviderError(`${GLM_PROVIDER_NAME} sign-in has expired; connect the account again`, REAUTH_REQUIRED);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const adapter = new GlmAdapter({
|
|
105
|
+
apiBase,
|
|
106
|
+
discovery: options.discovery,
|
|
107
|
+
streamIdleTimeoutMs: options.streamIdleTimeoutMs,
|
|
108
|
+
tokens,
|
|
109
|
+
...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
|
|
110
|
+
...options.resolveAttachments === undefined ? {} : { resolveAttachments: options.resolveAttachments },
|
|
111
|
+
hiddenModels: () => options.preferences.hiddenIds(GLM_PROVIDER_ID),
|
|
112
|
+
userModels: () => options.preferences.extraIds(GLM_PROVIDER_ID),
|
|
113
|
+
...options.extraModels === undefined ? {} : { extraModels: options.extraModels },
|
|
114
|
+
fetchFn,
|
|
115
|
+
});
|
|
116
|
+
const spec = {
|
|
117
|
+
id: GLM_PROVIDER_ID,
|
|
118
|
+
displayName: GLM_PROVIDER_NAME,
|
|
119
|
+
codeUrl: GLM_CODE_URL,
|
|
120
|
+
credentialFile: options.credentialFile,
|
|
121
|
+
discovery: options.discovery,
|
|
122
|
+
defaultModel: options.defaultModel ?? GLM_DEFAULT_MODEL,
|
|
123
|
+
catalog: () => adapter.catalogEntries(),
|
|
124
|
+
catalogOwn: () => adapter.ownCatalogEntries(),
|
|
125
|
+
fallbackContextWindow: GLM_CONTEXT_WINDOW,
|
|
126
|
+
discover: (accessToken) => adapter.fetchCatalog(accessToken),
|
|
127
|
+
usage: (accessToken) => readGlmUsage({ origin: GLM_ORIGIN, accessToken, fetchFn }),
|
|
128
|
+
details: () => [
|
|
129
|
+
{ key: 'apiBase', value: apiBase },
|
|
130
|
+
{ key: 'oauthBroker', value: 'https://zcode.z.ai' },
|
|
131
|
+
{ key: 'credentialFile', value: options.credentialFile },
|
|
132
|
+
],
|
|
133
|
+
applyDiscovery: (ids) => { adapter.discover(ids); },
|
|
134
|
+
};
|
|
135
|
+
const controller = new ProviderController({
|
|
136
|
+
spec,
|
|
137
|
+
store,
|
|
138
|
+
tokens,
|
|
139
|
+
flow,
|
|
140
|
+
preferences: options.preferences,
|
|
141
|
+
...options.onCatalogChanged === undefined ? {} : { onCatalogChanged: options.onCatalogChanged },
|
|
142
|
+
...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
|
|
143
|
+
});
|
|
144
|
+
// An explicit façade, not a spread: a spread would copy own properties and drop
|
|
145
|
+
// the class prototype, leaving every method undefined.
|
|
146
|
+
return {
|
|
147
|
+
id: controller.id,
|
|
148
|
+
displayName: controller.displayName,
|
|
149
|
+
credentialFile: controller.credentialFile,
|
|
150
|
+
adapter,
|
|
151
|
+
status: () => controller.status(),
|
|
152
|
+
login: (account) => controller.login(account),
|
|
153
|
+
cancel: () => controller.cancel(),
|
|
154
|
+
logout: (account) => controller.logout(account),
|
|
155
|
+
setDefault: (account) => controller.setDefault(account),
|
|
156
|
+
refresh: (account) => controller.refresh(account),
|
|
157
|
+
discover: (force) => controller.discover(force),
|
|
158
|
+
setModelVisible: (id, visible) => controller.setModelVisible(id, visible),
|
|
159
|
+
setModelOrder: (ids) => controller.setModelOrder(ids),
|
|
160
|
+
addModel: (id) => controller.addModel(id),
|
|
161
|
+
removeModel: (id) => controller.removeModel(id),
|
|
162
|
+
usage: (force) => controller.usage(force),
|
|
163
|
+
reload: () => controller.reload(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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 } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import type { SessionBase } from '../auth/store.js';
|
|
14
|
+
import type { TokenManager } from '../auth/tokens.js';
|
|
15
|
+
import type { FetchFn } from '../http.js';
|
|
16
|
+
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
17
|
+
import type { UsageWindow } from '../protocol.js';
|
|
18
|
+
import type { CatalogEntry } from './catalog.js';
|
|
19
|
+
import type { UsageRead } from './usage.js';
|
|
20
|
+
/** Provider route this module serves. */
|
|
21
|
+
export declare const GLM_PROVIDER_ID = "glm";
|
|
22
|
+
/** Display name for the picker group and the Settings card. */
|
|
23
|
+
export declare const GLM_PROVIDER_NAME = "GLM (BigModel)";
|
|
24
|
+
/** Page a user opens to manage the coding plan. */
|
|
25
|
+
export declare const GLM_CODE_URL = "https://bigmodel.cn/coding-plan/personal/overview";
|
|
26
|
+
/** Coding-plan host; the Responses endpoint is `<base>/responses`. */
|
|
27
|
+
export declare const GLM_API_BASE = "https://open.bigmodel.cn/api/coding/paas/v4";
|
|
28
|
+
/** Origin the account-level endpoints hang off, without the coding path. */
|
|
29
|
+
export declare const GLM_ORIGIN = "https://open.bigmodel.cn";
|
|
30
|
+
/** Path appended to the API base for a streaming request. */
|
|
31
|
+
export declare const GLM_RESPONSES_PATH = "/responses";
|
|
32
|
+
/** Path appended to the API base to list the models an account can reach. */
|
|
33
|
+
export declare const GLM_MODELS_PATH = "/models";
|
|
34
|
+
/**
|
|
35
|
+
* Account allowance endpoint.
|
|
36
|
+
*
|
|
37
|
+
* It sits on the host root rather than under the coding prefix, and BigModel
|
|
38
|
+
* authenticates it with the bare key — no `Bearer` scheme, which the host refuses.
|
|
39
|
+
*/
|
|
40
|
+
export declare const GLM_QUOTA_PATH = "/api/monitor/usage/quota/limit";
|
|
41
|
+
/** Model the picker preselects. */
|
|
42
|
+
export declare const GLM_DEFAULT_MODEL = "glm-5.3";
|
|
43
|
+
/** Context capacity of the GLM 5.x family. */
|
|
44
|
+
export declare const GLM_CONTEXT_WINDOW = 1048576;
|
|
45
|
+
/** Output budget used when the caller sets no cap. */
|
|
46
|
+
export declare const GLM_DEFAULT_MAX_TOKENS = 131072;
|
|
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 declare const GLM_MODELS: readonly CatalogEntry[];
|
|
55
|
+
/** One signed-in BigModel account. */
|
|
56
|
+
export interface GlmSession extends SessionBase {
|
|
57
|
+
/** ZCode JWT the coding plan is bound to; presented as the API key. */
|
|
58
|
+
zcodeJwt: string;
|
|
59
|
+
/** API base recorded at login time (diagnostics only). */
|
|
60
|
+
apiBase?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Coerce one decoded credential entry into a GLM session.
|
|
64
|
+
*
|
|
65
|
+
* @param value - decoded JSON value for one account.
|
|
66
|
+
* @returns the session, or undefined when required fields are missing.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseGlmSession(value: unknown): GlmSession | undefined;
|
|
69
|
+
/** Everything the adapter needs from the route. */
|
|
70
|
+
export interface GlmAdapterOptions {
|
|
71
|
+
/** Request base; `<base>/responses` is the endpoint. */
|
|
72
|
+
apiBase: string;
|
|
73
|
+
/** Consult the endpoint's live model list. */
|
|
74
|
+
discovery: boolean;
|
|
75
|
+
/** Abandon a stream after this much silence. */
|
|
76
|
+
streamIdleTimeoutMs: number;
|
|
77
|
+
/** Token manager owning refresh. */
|
|
78
|
+
tokens: TokenManager<GlmSession>;
|
|
79
|
+
/** Model ids the user hid from the picker. */
|
|
80
|
+
hiddenModels?: () => Promise<ReadonlySet<string>> | ReadonlySet<string>;
|
|
81
|
+
/** Ids the user added by hand. */
|
|
82
|
+
userModels?: () => Promise<readonly string[]> | readonly string[];
|
|
83
|
+
/** Extra ids published without shipped metadata. */
|
|
84
|
+
extraModels?: readonly string[];
|
|
85
|
+
/** Resolve the attachment service per request. */
|
|
86
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
87
|
+
onWarn?: (message: string) => void;
|
|
88
|
+
onCredentialRejected?: (account: string) => void;
|
|
89
|
+
/** Fetch implementation; defaults to the proxy-aware fetch. */
|
|
90
|
+
fetchFn?: FetchFn;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the two windows a coding-plan allowance reports.
|
|
94
|
+
*
|
|
95
|
+
* The host lists one row per metered window and names it by a `unit`/`number`
|
|
96
|
+
* pair: `(3, 5)` is the rolling five-hour window and `(6, 1)` the weekly one.
|
|
97
|
+
* Rows of any other type — MCP tool usage, for instance — are not model quota and
|
|
98
|
+
* are skipped rather than shown as an allowance.
|
|
99
|
+
*
|
|
100
|
+
* @param payload - the decoded `quota/limit` body.
|
|
101
|
+
* @returns one window per metered allowance, rolling first.
|
|
102
|
+
*/
|
|
103
|
+
export declare function parseGlmQuota(payload: unknown): UsageWindow[];
|
|
104
|
+
/**
|
|
105
|
+
* Read an account's coding-plan allowance.
|
|
106
|
+
*
|
|
107
|
+
* @param input - host, bearer, and fetch.
|
|
108
|
+
* @returns the windows the host reported; an empty list when it stated none.
|
|
109
|
+
*/
|
|
110
|
+
export declare function readGlmUsage(input: {
|
|
111
|
+
origin: string;
|
|
112
|
+
accessToken: string;
|
|
113
|
+
fetchFn: FetchFn;
|
|
114
|
+
signal?: AbortSignal;
|
|
115
|
+
}): Promise<UsageRead>;
|
|
116
|
+
/** The adapter behind the `glm` route. */
|
|
117
|
+
export declare class GlmAdapter extends LlmAdapter {
|
|
118
|
+
private readonly options;
|
|
119
|
+
/** Live catalog ids, once discovery succeeded this session. */
|
|
120
|
+
private live;
|
|
121
|
+
constructor(options: GlmAdapterOptions);
|
|
122
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
123
|
+
providerRetryPolicy(): undefined;
|
|
124
|
+
listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
|
|
125
|
+
resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
126
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
127
|
+
/** Translate an auth failure into the message a user can act on. */
|
|
128
|
+
private refreshHint;
|
|
129
|
+
/** The catalog served: the shipped list, the live one, then hand-added ids. */
|
|
130
|
+
private catalog;
|
|
131
|
+
/** The route's own catalog, without ids the user added by hand. */
|
|
132
|
+
private shippedCatalog;
|
|
133
|
+
/** Ids the user published by hand that this route does not already serve. */
|
|
134
|
+
private addedEntries;
|
|
135
|
+
/** The ids the picker must currently omit. */
|
|
136
|
+
private hidden;
|
|
137
|
+
/**
|
|
138
|
+
* Adopt the endpoint's live catalog.
|
|
139
|
+
*
|
|
140
|
+
* @param ids - model ids the endpoint reported.
|
|
141
|
+
*/
|
|
142
|
+
discover(ids: readonly string[]): void;
|
|
143
|
+
/**
|
|
144
|
+
* Read the live model list.
|
|
145
|
+
*
|
|
146
|
+
* @param accessToken - bearer for the account.
|
|
147
|
+
* @param signal - optional cancellation.
|
|
148
|
+
* @returns the ids the endpoint serves, or undefined when it cannot be read.
|
|
149
|
+
*/
|
|
150
|
+
fetchCatalog(accessToken: string, signal?: AbortSignal): Promise<readonly string[] | undefined>;
|
|
151
|
+
/** Send one streaming request over the Responses wire. */
|
|
152
|
+
private request;
|
|
153
|
+
/** Resolve image blocks through the attachment service, when it is present. */
|
|
154
|
+
private imageResolver;
|
|
155
|
+
/** The catalog the controller publishes without hand-added ids. */
|
|
156
|
+
ownCatalogEntries(): readonly CatalogEntry[];
|
|
157
|
+
/** The catalog the controller publishes. */
|
|
158
|
+
catalogEntries(): readonly CatalogEntry[];
|
|
159
|
+
}
|