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
package/lib/http.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound HTTP for the Kimi routes.
|
|
3
|
+
*
|
|
4
|
+
* The plugin respects the conventional proxy environment variables so a user
|
|
5
|
+
* behind a corporate relay can still reach `auth.kimi.com`. `NO_PROXY` is
|
|
6
|
+
* honoured, and a loopback destination is never proxied.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-plugin-subscription-oauth/http
|
|
9
|
+
*/
|
|
10
|
+
/** One cached dispatcher per proxy URL. */
|
|
11
|
+
const agents = new Map();
|
|
12
|
+
/** Hosts that always bypass a proxy. */
|
|
13
|
+
const ALWAYS_DIRECT = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
14
|
+
/** Read the first non-empty value among the given environment names. */
|
|
15
|
+
function envValue(env, ...names) {
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const value = env[name];
|
|
18
|
+
if (value !== undefined && value.trim() !== '')
|
|
19
|
+
return value.trim();
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Whether `host` matches one `NO_PROXY` entry (exact host or dotted suffix). */
|
|
24
|
+
function matchesNoProxy(host, noProxy) {
|
|
25
|
+
const target = host.toLowerCase();
|
|
26
|
+
for (const raw of noProxy.split(',')) {
|
|
27
|
+
const entry = raw.trim().toLowerCase().replace(/^\./, '');
|
|
28
|
+
if (entry === '')
|
|
29
|
+
continue;
|
|
30
|
+
if (entry === '*')
|
|
31
|
+
return true;
|
|
32
|
+
if (target === entry || target.endsWith(`.${entry}`))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/** Proxy URL that applies to `url`, or undefined for a direct connection. */
|
|
38
|
+
function proxyFor(url, env) {
|
|
39
|
+
const host = url.hostname.toLowerCase();
|
|
40
|
+
if (ALWAYS_DIRECT.has(host))
|
|
41
|
+
return undefined;
|
|
42
|
+
const noProxy = envValue(env, 'NO_PROXY', 'no_proxy');
|
|
43
|
+
if (noProxy !== undefined && matchesNoProxy(host, noProxy))
|
|
44
|
+
return undefined;
|
|
45
|
+
if (url.protocol === 'https:')
|
|
46
|
+
return envValue(env, 'HTTPS_PROXY', 'https_proxy', 'ALL_PROXY', 'all_proxy');
|
|
47
|
+
return envValue(env, 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy');
|
|
48
|
+
}
|
|
49
|
+
/** Lazily create (and cache) the dispatcher for one proxy URL. */
|
|
50
|
+
async function dispatcherFor(proxyUrl) {
|
|
51
|
+
const cached = agents.get(proxyUrl);
|
|
52
|
+
if (cached !== undefined)
|
|
53
|
+
return cached;
|
|
54
|
+
const { ProxyAgent } = await import('undici');
|
|
55
|
+
const agent = new ProxyAgent(proxyUrl);
|
|
56
|
+
agents.set(proxyUrl, agent);
|
|
57
|
+
return agent;
|
|
58
|
+
}
|
|
59
|
+
/** The absolute URL of a fetch input, or undefined when it cannot be parsed. */
|
|
60
|
+
function requestUrl(input) {
|
|
61
|
+
try {
|
|
62
|
+
if (input instanceof URL)
|
|
63
|
+
return input;
|
|
64
|
+
if (typeof input === 'string')
|
|
65
|
+
return new URL(input);
|
|
66
|
+
return new URL(input.url);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch an absolute URL, honouring the proxy environment.
|
|
74
|
+
*
|
|
75
|
+
* Falls back to a direct request when no proxy applies or when the dispatcher
|
|
76
|
+
* cannot be constructed, so a malformed proxy setting degrades to a direct
|
|
77
|
+
* attempt instead of breaking every call.
|
|
78
|
+
*
|
|
79
|
+
* @param input - absolute request URL (a `Request` is inspected for its URL).
|
|
80
|
+
* @param init - standard request options.
|
|
81
|
+
* @param env - environment to read proxy settings from.
|
|
82
|
+
* @returns the response.
|
|
83
|
+
*/
|
|
84
|
+
export async function proxiedFetch(input, init, env = process.env) {
|
|
85
|
+
const url = requestUrl(input);
|
|
86
|
+
if (url === undefined)
|
|
87
|
+
return fetch(input, init);
|
|
88
|
+
const proxyUrl = proxyFor(url, env);
|
|
89
|
+
if (proxyUrl === undefined)
|
|
90
|
+
return fetch(input, init);
|
|
91
|
+
try {
|
|
92
|
+
const dispatcher = await dispatcherFor(proxyUrl);
|
|
93
|
+
return await fetch(input, { ...init, dispatcher });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return fetch(input, init);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi Code, Grok (X Premium) and Google Antigravity subscription access for
|
|
3
|
+
* DeepSeek Harness.
|
|
4
|
+
*
|
|
5
|
+
* Each provider route authorizes once through its own OAuth flow (Settings →
|
|
6
|
+
* Subscriptions) and then streams over the wire that provider's subscription
|
|
7
|
+
* exposes. Tokens live below `~/.dsh/plugins/kimi-oauth/`, one provider section
|
|
8
|
+
* per route.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
/** Plugin id; also the name every effect label and log line is prefixed with. */
|
|
15
|
+
export declare const name = "dsh-plugin-subscription-oauth";
|
|
16
|
+
/** The provider routes need the LLM runtime; everything else is optional. */
|
|
17
|
+
export declare const inject: string[];
|
|
18
|
+
/** Every route key this plugin accepts in configuration. */
|
|
19
|
+
export declare const PROVIDER_IDS: readonly ["kimi", "grok", "antigravity", "glm"];
|
|
20
|
+
/** The routes this build can actually register. */
|
|
21
|
+
export declare const IMPLEMENTED_PROVIDER_IDS: readonly ["kimi", "grok", "antigravity", "glm"];
|
|
22
|
+
/** One route key. */
|
|
23
|
+
export type ProviderId = typeof PROVIDER_IDS[number];
|
|
24
|
+
/** Plugin configuration, as accepted from `cordis.patch.yml`. */
|
|
25
|
+
export interface Config {
|
|
26
|
+
/** Routes to register; defaults to every implemented provider. */
|
|
27
|
+
providers?: ProviderId[];
|
|
28
|
+
/** Register the routes at all. Defaults to true. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** OAuth host serving the Kimi Code device flow. */
|
|
31
|
+
oauthHost?: string;
|
|
32
|
+
/** Kimi Code request base; `<base>/responses` is the endpoint. */
|
|
33
|
+
apiBase?: string;
|
|
34
|
+
/** Consult each endpoint's live model list. Defaults to true. */
|
|
35
|
+
discovery?: boolean;
|
|
36
|
+
/** Abandon a stream after this much silence. Defaults to 300000. */
|
|
37
|
+
streamIdleTimeoutMs?: number;
|
|
38
|
+
/** Model the picker preselects on the Kimi Code route. */
|
|
39
|
+
defaultModel?: string;
|
|
40
|
+
/** Kimi Code model ids to publish without shipped metadata. */
|
|
41
|
+
extraModels?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Register the `auto` provider, which stands in for every enabled model across
|
|
44
|
+
* the other routes. Defaults to true.
|
|
45
|
+
*/
|
|
46
|
+
auto?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Configuration schema, with the config file's own defaults applied. */
|
|
49
|
+
export declare const Config: z<Config>;
|
|
50
|
+
/**
|
|
51
|
+
* Register every configured provider route, its credential store, and — when
|
|
52
|
+
* the host has a connection service — the Settings page's RPC routes.
|
|
53
|
+
*
|
|
54
|
+
* @param ctx - the plugin's context.
|
|
55
|
+
* @param config - validated plugin configuration.
|
|
56
|
+
*/
|
|
57
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi Code, Grok (X Premium) and Google Antigravity subscription access for
|
|
3
|
+
* DeepSeek Harness.
|
|
4
|
+
*
|
|
5
|
+
* Each provider route authorizes once through its own OAuth flow (Settings →
|
|
6
|
+
* Subscriptions) and then streams over the wire that provider's subscription
|
|
7
|
+
* exposes. Tokens live below `~/.dsh/plugins/kimi-oauth/`, one provider section
|
|
8
|
+
* per route.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
import { registerSubscriptionRpc } from './auth/rpc.js';
|
|
16
|
+
import { SubscriptionRegistry } from './auth/registry.js';
|
|
17
|
+
import { KIMI_DATA_DIR, KIMI_DEFAULT_MODEL, KIMI_STREAM_IDLE_TIMEOUT_MS, normalizeApiBase, normalizeOAuthHost, resolveApiBase, resolveOAuthHost, } from './constants.js';
|
|
18
|
+
import { proxiedFetch } from './http.js';
|
|
19
|
+
import { AUTO_PROVIDER_ID, createAutoProvider } from './providers/auto.js';
|
|
20
|
+
import { createAntigravityProvider } from './providers/antigravity.js';
|
|
21
|
+
import { createGrokProvider } from './providers/grok.js';
|
|
22
|
+
import { createKimiProvider } from './providers/kimi-provider.js';
|
|
23
|
+
import { createGlmProvider } from './providers/glm-provider.js';
|
|
24
|
+
import { ModelPreferences } from './settings.js';
|
|
25
|
+
/** Plugin id; also the name every effect label and log line is prefixed with. */
|
|
26
|
+
export const name = 'dsh-plugin-subscription-oauth';
|
|
27
|
+
/** The provider routes need the LLM runtime; everything else is optional. */
|
|
28
|
+
export const inject = ['llm'];
|
|
29
|
+
/** Every route key this plugin accepts in configuration. */
|
|
30
|
+
export const PROVIDER_IDS = ['kimi', 'grok', 'antigravity', 'glm'];
|
|
31
|
+
/** The routes this build can actually register. */
|
|
32
|
+
export const IMPLEMENTED_PROVIDER_IDS = ['kimi', 'grok', 'antigravity', 'glm'];
|
|
33
|
+
/** Configuration schema, with the config file's own defaults applied. */
|
|
34
|
+
export const Config = z.object({
|
|
35
|
+
providers: z.array(z.union([...PROVIDER_IDS])).default([...IMPLEMENTED_PROVIDER_IDS]),
|
|
36
|
+
enabled: z.boolean().default(true),
|
|
37
|
+
oauthHost: z.string(),
|
|
38
|
+
apiBase: z.string(),
|
|
39
|
+
discovery: z.boolean().default(true),
|
|
40
|
+
streamIdleTimeoutMs: z.number().default(KIMI_STREAM_IDLE_TIMEOUT_MS),
|
|
41
|
+
defaultModel: z.string().default(KIMI_DEFAULT_MODEL),
|
|
42
|
+
extraModels: z.array(z.string()).default([]),
|
|
43
|
+
auto: z.boolean().default(true),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Register every configured provider route, its credential store, and — when
|
|
47
|
+
* the host has a connection service — the Settings page's RPC routes.
|
|
48
|
+
*
|
|
49
|
+
* @param ctx - the plugin's context.
|
|
50
|
+
* @param config - validated plugin configuration.
|
|
51
|
+
*/
|
|
52
|
+
export function apply(ctx, config = {}) {
|
|
53
|
+
if (config.enabled === false)
|
|
54
|
+
return;
|
|
55
|
+
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? KIMI_STREAM_IDLE_TIMEOUT_MS;
|
|
56
|
+
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) {
|
|
57
|
+
throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
|
|
58
|
+
}
|
|
59
|
+
const selected = new Set(config.providers ?? [...IMPLEMENTED_PROVIDER_IDS]);
|
|
60
|
+
const onWarn = (message) => {
|
|
61
|
+
ctx.logger.warn(`${name}: ${message}`);
|
|
62
|
+
};
|
|
63
|
+
const dataDir = dshHomePath(...KIMI_DATA_DIR);
|
|
64
|
+
const credentialFile = path.join(dataDir, 'auth.json');
|
|
65
|
+
const preferences = new ModelPreferences(path.join(dataDir, 'preferences.json'));
|
|
66
|
+
/** Registration handles, so a catalog change can re-announce one route. */
|
|
67
|
+
const handles = new Map();
|
|
68
|
+
const reannounce = (id) => { handles.get(id)?.replace([id]); };
|
|
69
|
+
const routes = [];
|
|
70
|
+
for (const id of selected) {
|
|
71
|
+
switch (id) {
|
|
72
|
+
case 'kimi':
|
|
73
|
+
routes.push(createKimiProvider({
|
|
74
|
+
dataDir,
|
|
75
|
+
credentialFile,
|
|
76
|
+
oauthHost: normalizeOAuthHost(config.oauthHost ?? resolveOAuthHost()),
|
|
77
|
+
apiBase: normalizeApiBase(config.apiBase ?? resolveApiBase()),
|
|
78
|
+
discovery: config.discovery ?? true,
|
|
79
|
+
streamIdleTimeoutMs,
|
|
80
|
+
...config.defaultModel === undefined ? {} : { defaultModel: config.defaultModel },
|
|
81
|
+
...config.extraModels === undefined ? {} : { extraModels: config.extraModels },
|
|
82
|
+
preferences,
|
|
83
|
+
// Resolved per request: the attachment service may start after this plugin.
|
|
84
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
85
|
+
onCatalogChanged: () => { reannounce('kimi'); },
|
|
86
|
+
onWarn,
|
|
87
|
+
fetchFn: proxiedFetch,
|
|
88
|
+
}));
|
|
89
|
+
break;
|
|
90
|
+
case 'grok':
|
|
91
|
+
routes.push(createGrokProvider({
|
|
92
|
+
credentialFile,
|
|
93
|
+
discovery: config.discovery ?? true,
|
|
94
|
+
streamIdleTimeoutMs,
|
|
95
|
+
preferences,
|
|
96
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
97
|
+
onCatalogChanged: () => { reannounce('grok'); },
|
|
98
|
+
onWarn,
|
|
99
|
+
fetchFn: proxiedFetch,
|
|
100
|
+
}));
|
|
101
|
+
break;
|
|
102
|
+
case 'antigravity':
|
|
103
|
+
routes.push(createAntigravityProvider({
|
|
104
|
+
credentialFile,
|
|
105
|
+
discovery: config.discovery ?? true,
|
|
106
|
+
streamIdleTimeoutMs,
|
|
107
|
+
preferences,
|
|
108
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
109
|
+
onCatalogChanged: () => { reannounce('antigravity'); },
|
|
110
|
+
onWarn,
|
|
111
|
+
fetchFn: proxiedFetch,
|
|
112
|
+
}));
|
|
113
|
+
break;
|
|
114
|
+
case 'glm':
|
|
115
|
+
routes.push(createGlmProvider({
|
|
116
|
+
credentialFile,
|
|
117
|
+
discovery: config.discovery ?? true,
|
|
118
|
+
streamIdleTimeoutMs,
|
|
119
|
+
preferences,
|
|
120
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
121
|
+
onCatalogChanged: () => { reannounce('glm'); },
|
|
122
|
+
onWarn,
|
|
123
|
+
fetchFn: proxiedFetch,
|
|
124
|
+
}));
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
// A configured route with no module yet must not silently disappear.
|
|
128
|
+
onWarn(`provider ${id} is configured but not implemented in this build`);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const registry = new SubscriptionRegistry(routes);
|
|
133
|
+
// `auto` stands in for the other routes, so it is built after them and added
|
|
134
|
+
// to the same registry it reads. Its list spans routes, which is why it is a
|
|
135
|
+
// provider of its own rather than a model inside each one.
|
|
136
|
+
if (config.auto ?? true) {
|
|
137
|
+
const auto = createAutoProvider({
|
|
138
|
+
preferences,
|
|
139
|
+
registry,
|
|
140
|
+
// Auto spans every provider the harness serves — the built-in DeepSeek
|
|
141
|
+
// routes and any provider the user adds included — so it is enumerated
|
|
142
|
+
// through the runtime rather than through this plugin's own registry. The
|
|
143
|
+
// runtime is read per call, so a provider that registers later (another
|
|
144
|
+
// plugin, a route the user configures) joins the chain without a restart.
|
|
145
|
+
host: {
|
|
146
|
+
providers: () => ctx.llm.listProviders().map((entry) => ({ id: entry.id, name: entry.name })),
|
|
147
|
+
models: (provider) => ctx.llm.listModels(provider),
|
|
148
|
+
resolve: async (provider, model) => {
|
|
149
|
+
const resolved = await ctx.llm.resolveModelInfo(provider, model);
|
|
150
|
+
return {
|
|
151
|
+
...resolved.context?.contextWindow === undefined ? {} : { contextWindow: resolved.context.contextWindow },
|
|
152
|
+
...resolved.reasoning === undefined
|
|
153
|
+
? {}
|
|
154
|
+
: { reasoningEfforts: resolved.reasoning.efforts.map((effort) => String(effort.id)) },
|
|
155
|
+
...resolved.inputModalities === undefined ? {} : { inputModalities: resolved.inputModalities },
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
// Dispatch through the harness, which selects the adapter that owns the
|
|
159
|
+
// provider. Auto's own adapter is never asked to guess.
|
|
160
|
+
stream: (options) => ctx.llm.stream(options),
|
|
161
|
+
},
|
|
162
|
+
onCatalogChanged: () => { reannounce(AUTO_PROVIDER_ID); },
|
|
163
|
+
onWarn,
|
|
164
|
+
});
|
|
165
|
+
registry.register(auto);
|
|
166
|
+
routes.push(auto);
|
|
167
|
+
}
|
|
168
|
+
// Registered in the page's reading order, not construction order: the model
|
|
169
|
+
// picker groups by the order the adapters arrive, and Auto leads it.
|
|
170
|
+
for (const id of registry.ids()) {
|
|
171
|
+
const route = registry.get(id);
|
|
172
|
+
if (route === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const handle = ctx.llm.registerAdapter([route.id], route.adapter);
|
|
175
|
+
handles.set(route.id, handle);
|
|
176
|
+
ctx.effect(() => () => handle(), `${name}: ${route.id} provider route`);
|
|
177
|
+
}
|
|
178
|
+
registerSubscriptionRpc(ctx, registry);
|
|
179
|
+
// Warm each route's live catalog for an install that is already signed in.
|
|
180
|
+
for (const id of registry.ids()) {
|
|
181
|
+
void registry.discover(id, false).catch((error) => {
|
|
182
|
+
onWarn(`${id} startup discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract shared by the plugin's node half (the RPC routes) and its
|
|
3
|
+
* browser half (the Settings page).
|
|
4
|
+
*
|
|
5
|
+
* Only this module crosses the boundary: the client bundle imports the types
|
|
6
|
+
* and the channel name, and every call travels as one JSON envelope over a
|
|
7
|
+
* `POST /api/<prefix><endpoint>` route. One status document describes every
|
|
8
|
+
* provider route the plugin serves, so the page renders one card per entry.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/protocol
|
|
11
|
+
*/
|
|
12
|
+
/** Prefix of every RPC method this plugin serves. */
|
|
13
|
+
export declare const RPC_PREFIX = "subscription-oauth.";
|
|
14
|
+
/** Endpoints the Settings page may call. */
|
|
15
|
+
export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload"];
|
|
16
|
+
/** One endpoint name. */
|
|
17
|
+
export type RpcEndpoint = typeof RPC_ENDPOINTS[number];
|
|
18
|
+
/** Client → host envelope. */
|
|
19
|
+
export interface RpcRequest {
|
|
20
|
+
type: 'client-request';
|
|
21
|
+
rpcId: string;
|
|
22
|
+
method: string;
|
|
23
|
+
payload: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** Host → client envelope, carrying either the value or the failure. */
|
|
26
|
+
export interface RpcResponse {
|
|
27
|
+
type: 'server-response';
|
|
28
|
+
rpcId: string;
|
|
29
|
+
result: RpcResult;
|
|
30
|
+
}
|
|
31
|
+
/** Discriminated result inside {@link RpcResponse}. */
|
|
32
|
+
export type RpcResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
value: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
error: RpcError;
|
|
38
|
+
};
|
|
39
|
+
/** Failure detail, with the provider-neutral code when one applies. */
|
|
40
|
+
export interface RpcError {
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
/**
|
|
44
|
+
* Carrier-owned structured detail. The Connection transport models a failure
|
|
45
|
+
* as `{code, message, details}`, so an empty object travels here rather than
|
|
46
|
+
* an absent field the client would have to repair.
|
|
47
|
+
*/
|
|
48
|
+
details: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One provider-owned connection fact the manage dialog shows.
|
|
52
|
+
*
|
|
53
|
+
* The key is a stable identifier (`oauthHost`, `apiBase`, …) that the browser
|
|
54
|
+
* half localizes; the host never sends user-facing prose.
|
|
55
|
+
*/
|
|
56
|
+
export interface DetailRow {
|
|
57
|
+
key: string;
|
|
58
|
+
value: string;
|
|
59
|
+
}
|
|
60
|
+
/** How the browser should present one pending login. */
|
|
61
|
+
export type LoginMode =
|
|
62
|
+
/** Open the URL in a new tab; the redirect completes the login. */
|
|
63
|
+
'open-url'
|
|
64
|
+
/** Show the code (and a link) while the host polls the token endpoint. */
|
|
65
|
+
| 'device-code';
|
|
66
|
+
/** What the Settings page has to show to finish a login. */
|
|
67
|
+
export interface LoginPrompt {
|
|
68
|
+
/** URL the user opens (or that was opened for them). */
|
|
69
|
+
readonly url: string;
|
|
70
|
+
/** Code the user confirms in the browser; device flows only. */
|
|
71
|
+
readonly userCode?: string;
|
|
72
|
+
/** Epoch milliseconds after which the attempt lapses. */
|
|
73
|
+
readonly expiresAt: number;
|
|
74
|
+
readonly mode: LoginMode;
|
|
75
|
+
}
|
|
76
|
+
/** One signed-in account as the page displays it. */
|
|
77
|
+
export interface AccountStatus {
|
|
78
|
+
/** Account key the store is keyed by. */
|
|
79
|
+
account: string;
|
|
80
|
+
/** Display label (email, user id, or the key's digest). */
|
|
81
|
+
label: string;
|
|
82
|
+
/** Epoch milliseconds when the access token lapses. */
|
|
83
|
+
expiresAt: number;
|
|
84
|
+
/** Whether the access token is already past its expiry. */
|
|
85
|
+
expired: boolean;
|
|
86
|
+
/** Whether this account serves requests by default. */
|
|
87
|
+
isDefault: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether the provider refused this account's grant outright.
|
|
90
|
+
*
|
|
91
|
+
* A refresh answered with `invalid_grant` means the authorization is gone — the
|
|
92
|
+
* stored token can never be renewed — so the page asks for a new sign-in rather
|
|
93
|
+
* than reporting the account as connected.
|
|
94
|
+
*/
|
|
95
|
+
needsLogin?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** One selectable model, as the page lists it. */
|
|
98
|
+
export interface ModelSummary {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
contextWindow: number;
|
|
103
|
+
inputModalities?: readonly string[];
|
|
104
|
+
defaultEffort?: string;
|
|
105
|
+
/** Whether the session model picker offers this model. */
|
|
106
|
+
visible: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Route that owns this model, when the listing spans routes.
|
|
109
|
+
*
|
|
110
|
+
* The uto provider mixes models from every route, so its rows name the
|
|
111
|
+
* owning route; a single-route catalog leaves it out.
|
|
112
|
+
*/
|
|
113
|
+
source?: string;
|
|
114
|
+
/** Whether the user added this id by hand, so it can be removed again. */
|
|
115
|
+
userAdded?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Whether this entry is fixed at the head of the list.
|
|
118
|
+
*
|
|
119
|
+
* The synthetic `auto` entry follows the user's own order, so it cannot be
|
|
120
|
+
* hidden or moved; the page renders it without those controls.
|
|
121
|
+
*/
|
|
122
|
+
pinned?: boolean;
|
|
123
|
+
}
|
|
124
|
+
/** One metered allowance of a subscription plan. */
|
|
125
|
+
export interface UsageWindow {
|
|
126
|
+
/** What the window meters, e.g. `weekly`, `5h`, `credits`. */
|
|
127
|
+
label: string;
|
|
128
|
+
/** How much of the allowance is spent, when the provider states it. */
|
|
129
|
+
usedPercent?: number;
|
|
130
|
+
/** Amount already spent, when the provider states an absolute figure. */
|
|
131
|
+
used?: number;
|
|
132
|
+
/** Total allowance, when the provider states one. */
|
|
133
|
+
limit?: number;
|
|
134
|
+
/** Epoch milliseconds when the window resets, when the provider states it. */
|
|
135
|
+
resetsAt?: number;
|
|
136
|
+
}
|
|
137
|
+
/** One account's plan and metered allowances. */
|
|
138
|
+
export interface ProviderUsage {
|
|
139
|
+
/** Account the figures belong to. */
|
|
140
|
+
account: string;
|
|
141
|
+
/** Plan name or tier, when the provider states one. */
|
|
142
|
+
plan?: string;
|
|
143
|
+
/** Whether the plan renews on a date the provider states. */
|
|
144
|
+
renewsAt?: number;
|
|
145
|
+
/** Metered allowances, most relevant first. */
|
|
146
|
+
windows: readonly UsageWindow[];
|
|
147
|
+
/** Epoch milliseconds these figures were read. */
|
|
148
|
+
fetchedAt: number;
|
|
149
|
+
/**
|
|
150
|
+
* Whether these figures are the route's substitute rather than its own accounting.
|
|
151
|
+
*
|
|
152
|
+
* A substitute is shown when the primary source cannot answer, and it is never
|
|
153
|
+
* reused as if it were a reading: the next request asks again.
|
|
154
|
+
*/
|
|
155
|
+
substitute?: boolean;
|
|
156
|
+
}
|
|
157
|
+
/** Result of reading one route's usage. */
|
|
158
|
+
export interface UsageResult {
|
|
159
|
+
provider: string;
|
|
160
|
+
/** Usage per account the endpoint could answer for. */
|
|
161
|
+
usage: readonly ProviderUsage[];
|
|
162
|
+
/** Why an account's usage is missing, when the read failed. */
|
|
163
|
+
error?: string;
|
|
164
|
+
status: SubscriptionStatus;
|
|
165
|
+
}
|
|
166
|
+
/** The full state of one provider route. */
|
|
167
|
+
export interface ProviderStatus {
|
|
168
|
+
/** Provider route key, e.g. `kimi`. */
|
|
169
|
+
provider: string;
|
|
170
|
+
displayName: string;
|
|
171
|
+
/** Where a user manages the underlying subscription. */
|
|
172
|
+
codeUrl?: string;
|
|
173
|
+
/** Whether at least one account is stored. */
|
|
174
|
+
loggedIn: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Whether this route signs in at all.
|
|
177
|
+
*
|
|
178
|
+
* uto has no account of its own — it delegates to routes that do — so the
|
|
179
|
+
* page hides the connect and manage controls for it.
|
|
180
|
+
*/
|
|
181
|
+
connectable?: boolean;
|
|
182
|
+
/** Whether a login attempt is being polled. */
|
|
183
|
+
busy: boolean;
|
|
184
|
+
/** The pending attempt, when `busy`. */
|
|
185
|
+
pending?: LoginPrompt;
|
|
186
|
+
accounts: AccountStatus[];
|
|
187
|
+
/** Account key serving requests, when one does. */
|
|
188
|
+
activeAccount?: string;
|
|
189
|
+
/** Absolute path of the credential file, for support requests. */
|
|
190
|
+
credentialFile: string;
|
|
191
|
+
/** Whether the live model list is consulted. */
|
|
192
|
+
discovery: boolean;
|
|
193
|
+
/** Live catalog ids, when discovery has succeeded this session. */
|
|
194
|
+
discovered?: readonly string[];
|
|
195
|
+
models: readonly ModelSummary[];
|
|
196
|
+
/** Model the picker preselects. */
|
|
197
|
+
defaultModel?: string;
|
|
198
|
+
/** Last usage figures read for this route, when they were requested. */
|
|
199
|
+
usage?: readonly ProviderUsage[];
|
|
200
|
+
/** Why the last usage read failed, when it did. */
|
|
201
|
+
usageError?: string;
|
|
202
|
+
/** Provider-owned connection facts shown in the manage dialog. */
|
|
203
|
+
details: readonly DetailRow[];
|
|
204
|
+
/** Last background failure (a login that could not complete). */
|
|
205
|
+
lastError?: string;
|
|
206
|
+
}
|
|
207
|
+
/** Every provider route's state, in display order. */
|
|
208
|
+
export interface SubscriptionStatus {
|
|
209
|
+
providers: ProviderStatus[];
|
|
210
|
+
}
|
|
211
|
+
/** Result of starting a login. */
|
|
212
|
+
export interface LoginStart {
|
|
213
|
+
prompt: LoginPrompt;
|
|
214
|
+
status: SubscriptionStatus;
|
|
215
|
+
}
|
|
216
|
+
/** Result of a token refresh. */
|
|
217
|
+
export interface RefreshResult {
|
|
218
|
+
account: string;
|
|
219
|
+
expiresAt: number;
|
|
220
|
+
status: SubscriptionStatus;
|
|
221
|
+
}
|
|
222
|
+
/** Payload fields naming one provider route and optionally one account. */
|
|
223
|
+
export interface AccountRequest {
|
|
224
|
+
provider: string;
|
|
225
|
+
account?: string;
|
|
226
|
+
}
|
|
227
|
+
/** Payload of `setDefault`. */
|
|
228
|
+
export interface SetDefaultRequest {
|
|
229
|
+
provider: string;
|
|
230
|
+
account: string;
|
|
231
|
+
}
|
|
232
|
+
/** Payload of `discover`. */
|
|
233
|
+
export interface DiscoverRequest {
|
|
234
|
+
provider: string;
|
|
235
|
+
/** Ask the endpoint for its live model list even when one is cached. */
|
|
236
|
+
force?: boolean;
|
|
237
|
+
}
|
|
238
|
+
/** Payload of `setModelVisible`. */
|
|
239
|
+
export interface SetModelVisibleRequest {
|
|
240
|
+
provider: string;
|
|
241
|
+
/** Model id as the catalog reports it. */
|
|
242
|
+
id: string;
|
|
243
|
+
/** Whether the session model picker should offer it. */
|
|
244
|
+
visible: boolean;
|
|
245
|
+
}
|
|
246
|
+
/** Payload of `addModel` and `removeModel`. */
|
|
247
|
+
export interface ModelIdRequest {
|
|
248
|
+
provider: string;
|
|
249
|
+
/** Model id as the endpoint accepts it. */
|
|
250
|
+
id: string;
|
|
251
|
+
}
|
|
252
|
+
/** Payload of `setModelOrder`. */
|
|
253
|
+
export interface SetModelOrderRequest {
|
|
254
|
+
provider: string;
|
|
255
|
+
/** Every catalog model id, in the order the user arranged them. */
|
|
256
|
+
ids: readonly string[];
|
|
257
|
+
}
|
|
258
|
+
/** Payload naming only a provider route. */
|
|
259
|
+
export interface ProviderRequest {
|
|
260
|
+
provider: string;
|
|
261
|
+
}
|
|
262
|
+
/** Map an endpoint to its payload and value types. */
|
|
263
|
+
export interface RpcMap {
|
|
264
|
+
status: {
|
|
265
|
+
payload: undefined;
|
|
266
|
+
value: SubscriptionStatus;
|
|
267
|
+
};
|
|
268
|
+
login: {
|
|
269
|
+
payload: AccountRequest;
|
|
270
|
+
value: LoginStart;
|
|
271
|
+
};
|
|
272
|
+
cancel: {
|
|
273
|
+
payload: ProviderRequest;
|
|
274
|
+
value: SubscriptionStatus;
|
|
275
|
+
};
|
|
276
|
+
logout: {
|
|
277
|
+
payload: AccountRequest;
|
|
278
|
+
value: SubscriptionStatus;
|
|
279
|
+
};
|
|
280
|
+
setDefault: {
|
|
281
|
+
payload: SetDefaultRequest;
|
|
282
|
+
value: SubscriptionStatus;
|
|
283
|
+
};
|
|
284
|
+
refresh: {
|
|
285
|
+
payload: AccountRequest;
|
|
286
|
+
value: RefreshResult;
|
|
287
|
+
};
|
|
288
|
+
discover: {
|
|
289
|
+
payload: DiscoverRequest;
|
|
290
|
+
value: SubscriptionStatus;
|
|
291
|
+
};
|
|
292
|
+
setModelVisible: {
|
|
293
|
+
payload: SetModelVisibleRequest;
|
|
294
|
+
value: SubscriptionStatus;
|
|
295
|
+
};
|
|
296
|
+
setModelOrder: {
|
|
297
|
+
payload: SetModelOrderRequest;
|
|
298
|
+
value: SubscriptionStatus;
|
|
299
|
+
};
|
|
300
|
+
addModel: {
|
|
301
|
+
payload: ModelIdRequest;
|
|
302
|
+
value: SubscriptionStatus;
|
|
303
|
+
};
|
|
304
|
+
removeModel: {
|
|
305
|
+
payload: ModelIdRequest;
|
|
306
|
+
value: SubscriptionStatus;
|
|
307
|
+
};
|
|
308
|
+
usage: {
|
|
309
|
+
payload: DiscoverRequest;
|
|
310
|
+
value: UsageResult;
|
|
311
|
+
};
|
|
312
|
+
reload: {
|
|
313
|
+
payload: undefined;
|
|
314
|
+
value: SubscriptionStatus;
|
|
315
|
+
};
|
|
316
|
+
}
|
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire contract shared by the plugin's node half (the RPC routes) and its
|
|
3
|
+
* browser half (the Settings page).
|
|
4
|
+
*
|
|
5
|
+
* Only this module crosses the boundary: the client bundle imports the types
|
|
6
|
+
* and the channel name, and every call travels as one JSON envelope over a
|
|
7
|
+
* `POST /api/<prefix><endpoint>` route. One status document describes every
|
|
8
|
+
* provider route the plugin serves, so the page renders one card per entry.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/protocol
|
|
11
|
+
*/
|
|
12
|
+
/** Prefix of every RPC method this plugin serves. */
|
|
13
|
+
export const RPC_PREFIX = 'subscription-oauth.';
|
|
14
|
+
/** Endpoints the Settings page may call. */
|
|
15
|
+
export const RPC_ENDPOINTS = [
|
|
16
|
+
'status',
|
|
17
|
+
'login',
|
|
18
|
+
'cancel',
|
|
19
|
+
'logout',
|
|
20
|
+
'setDefault',
|
|
21
|
+
'refresh',
|
|
22
|
+
'discover',
|
|
23
|
+
'setModelVisible',
|
|
24
|
+
'setModelOrder',
|
|
25
|
+
'addModel',
|
|
26
|
+
'removeModel',
|
|
27
|
+
'usage',
|
|
28
|
+
'reload',
|
|
29
|
+
];
|