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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -2
  3. package/cordis.patch.yml +13 -0
  4. package/lib/auth/device-flow.d.ts +112 -0
  5. package/lib/auth/device-flow.js +319 -0
  6. package/lib/auth/device-id.d.ts +31 -0
  7. package/lib/auth/device-id.js +58 -0
  8. package/lib/auth/flow.d.ts +33 -0
  9. package/lib/auth/flow.js +12 -0
  10. package/lib/auth/glm-flow.d.ts +99 -0
  11. package/lib/auth/glm-flow.js +194 -0
  12. package/lib/auth/headers.d.ts +61 -0
  13. package/lib/auth/headers.js +106 -0
  14. package/lib/auth/pkce-flow.d.ts +78 -0
  15. package/lib/auth/pkce-flow.js +205 -0
  16. package/lib/auth/provider-controller.d.ts +241 -0
  17. package/lib/auth/provider-controller.js +502 -0
  18. package/lib/auth/registry.d.ts +91 -0
  19. package/lib/auth/registry.js +149 -0
  20. package/lib/auth/rpc.d.ts +31 -0
  21. package/lib/auth/rpc.js +200 -0
  22. package/lib/auth/store.d.ts +99 -0
  23. package/lib/auth/store.js +213 -0
  24. package/lib/auth/tokens.d.ts +76 -0
  25. package/lib/auth/tokens.js +103 -0
  26. package/lib/client/SubscriptionsSection.d.ts +34 -0
  27. package/lib/client/SubscriptionsSection.js +742 -0
  28. package/lib/client/index.d.ts +36 -0
  29. package/lib/client/index.js +47 -0
  30. package/lib/client/locales.d.ts +187 -0
  31. package/lib/client/locales.js +185 -0
  32. package/lib/client/styles.d.ts +18 -0
  33. package/lib/client/styles.js +257 -0
  34. package/lib/client/subscriptions-rpc.d.ts +24 -0
  35. package/lib/client/subscriptions-rpc.js +44 -0
  36. package/lib/client.js +2078 -0
  37. package/lib/client.js.map +1 -0
  38. package/lib/constants.d.ts +79 -0
  39. package/lib/constants.js +91 -0
  40. package/lib/errors.d.ts +28 -0
  41. package/lib/errors.js +34 -0
  42. package/lib/http.d.ts +24 -0
  43. package/lib/http.js +98 -0
  44. package/lib/index.d.ts +57 -0
  45. package/lib/index.js +185 -0
  46. package/lib/protocol.d.ts +316 -0
  47. package/lib/protocol.js +29 -0
  48. package/lib/providers/antigravity-catalog.d.ts +53 -0
  49. package/lib/providers/antigravity-catalog.js +269 -0
  50. package/lib/providers/antigravity.d.ts +331 -0
  51. package/lib/providers/antigravity.js +821 -0
  52. package/lib/providers/auto.d.ts +151 -0
  53. package/lib/providers/auto.js +351 -0
  54. package/lib/providers/catalog.d.ts +69 -0
  55. package/lib/providers/catalog.js +78 -0
  56. package/lib/providers/common.d.ts +51 -0
  57. package/lib/providers/common.js +169 -0
  58. package/lib/providers/glm-provider.d.ts +41 -0
  59. package/lib/providers/glm-provider.js +165 -0
  60. package/lib/providers/glm.d.ts +159 -0
  61. package/lib/providers/glm.js +445 -0
  62. package/lib/providers/grok.d.ts +224 -0
  63. package/lib/providers/grok.js +700 -0
  64. package/lib/providers/kimi-provider.d.ts +45 -0
  65. package/lib/providers/kimi-provider.js +148 -0
  66. package/lib/providers/kimi.d.ts +151 -0
  67. package/lib/providers/kimi.js +400 -0
  68. package/lib/providers/responses-stream.d.ts +58 -0
  69. package/lib/providers/responses-stream.js +115 -0
  70. package/lib/providers/usage.d.ts +65 -0
  71. package/lib/providers/usage.js +309 -0
  72. package/lib/settings.d.ts +122 -0
  73. package/lib/settings.js +333 -0
  74. package/lib/translate/antigravity-schema.d.ts +24 -0
  75. package/lib/translate/antigravity-schema.js +174 -0
  76. package/lib/translate/antigravity.d.ts +184 -0
  77. package/lib/translate/antigravity.js +419 -0
  78. package/lib/translate/responses.d.ts +156 -0
  79. package/lib/translate/responses.js +395 -0
  80. package/lib/translate/sse.d.ts +27 -0
  81. package/lib/translate/sse.js +82 -0
  82. package/package.json +126 -4
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The `auto` provider route: one model that stands in for the user's arrangement
3
+ * of every enabled model, across every other route this plugin serves.
4
+ *
5
+ * `auto` is a provider rather than a model inside each route because its list is
6
+ * not any one route's catalog: the user mixes Kimi, Grok and Antigravity models
7
+ * in a single order, and switches individual ones out of the fallback without
8
+ * hiding them from the picker. A request for `auto` is delegated, in that order,
9
+ * to the route that owns each model — so `auto` holds no credentials of its own
10
+ * and its card has nothing to sign into.
11
+ *
12
+ * @module dsh-plugin-subscription-oauth/providers/auto
13
+ */
14
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
15
+ import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ModelModality, StreamChunk } from '@deepseek-ai/dsh-llm';
16
+ import type { ModelSummary } from '../protocol.js';
17
+ import type { ModelPreferences } from '../settings.js';
18
+ import type { ProviderRoute, SubscriptionRegistry } from '../auth/registry.js';
19
+ /** Route key the `auto` provider registers under. */
20
+ export declare const AUTO_PROVIDER_ID = "auto";
21
+ /** Display name for the picker group and the Settings card. */
22
+ export declare const AUTO_PROVIDER_NAME = "Auto";
23
+ /** Context advertised before any candidate is known. */
24
+ export declare const AUTO_FALLBACK_CONTEXT_WINDOW = 262144;
25
+ /** One candidate: a model of one route. */
26
+ export interface AutoCandidate {
27
+ /** Owning route key. */
28
+ provider: string;
29
+ /** Model id within that route. */
30
+ model: string;
31
+ /** Display name, for the Settings card. */
32
+ name: string;
33
+ /** Context window the owning route advertises. */
34
+ contextWindow: number;
35
+ /** Reasoning efforts the owning route advertises, in display order. */
36
+ reasoningEfforts?: readonly string[];
37
+ /** Input modalities the owning route advertises. */
38
+ inputModalities?: readonly ModelModality[];
39
+ }
40
+ /** The key `auto` stores for one candidate, stable across restarts. */
41
+ export declare function autoKey(provider: string, model: string): string;
42
+ /** Split one `provider/model` key back into its parts. */
43
+ export declare function parseAutoKey(key: string): {
44
+ provider: string;
45
+ model: string;
46
+ } | undefined;
47
+ /**
48
+ * The harness's own provider roster.
49
+ *
50
+ * `auto` spans everything the session can pick, not only the routes this plugin
51
+ * registered: the built-in DeepSeek providers and any provider the user added are
52
+ * models Auto is expected to fall back to. The runtime offers exactly the three
53
+ * facts that needs — which providers exist, what each one serves, and the call
54
+ * entry point that selects the owning adapter — so it is injected as a port rather
55
+ * than reached for directly.
56
+ */
57
+ export interface AutoHost {
58
+ /** Every provider the harness serves, in picker order. */
59
+ providers(): readonly {
60
+ id: string;
61
+ name: string;
62
+ }[];
63
+ /** Models one provider currently offers. */
64
+ models(provider: string): Promise<readonly {
65
+ id: string;
66
+ name?: string;
67
+ }[]>;
68
+ /** One model's context window, reasoning ladder and input kinds, when stated. */
69
+ resolve(provider: string, model: string): Promise<{
70
+ contextWindow?: number;
71
+ reasoningEfforts?: readonly string[];
72
+ inputModalities?: readonly ModelModality[];
73
+ }>;
74
+ /** Run one call, letting the harness select that provider's adapter. */
75
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
76
+ }
77
+ /** Everything the `auto` route needs. */
78
+ export interface AutoProviderOptions {
79
+ /** Shared model preferences, where `auto`'s own order and switches live. */
80
+ preferences: ModelPreferences;
81
+ /** Every other route, read per call so registration order stays free. */
82
+ registry: SubscriptionRegistry;
83
+ /**
84
+ * The harness's own roster, when the route is mounted in a running harness.
85
+ *
86
+ * Present, this is what `auto` enumerates — it already contains this plugin's
87
+ * routes as well, so enumerating both would list every model twice. Absent (a
88
+ * unit test with no runtime), the registry alone is used.
89
+ */
90
+ host?: AutoHost;
91
+ onWarn?: (message: string) => void;
92
+ /** Invoked when the list changed, so the picker re-queries. */
93
+ onCatalogChanged?: () => void;
94
+ }
95
+ /**
96
+ * The `auto` provider's wire adapter.
97
+ *
98
+ * Its own list is the single `auto` entry; every request is delegated to the
99
+ * route that owns the chosen model.
100
+ */
101
+ export declare class AutoAdapter extends LlmAdapter {
102
+ private readonly options;
103
+ /** The route this adapter serves, built alongside it. */
104
+ route?: ProviderRoute;
105
+ constructor(options: AutoProviderOptions);
106
+ providerInfo(provider: string): LlmProviderInfo;
107
+ providerRetryPolicy(): undefined;
108
+ listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
109
+ resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
110
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
111
+ /**
112
+ * The candidates `auto` will try, in order.
113
+ *
114
+ * Every model a route currently offers the picker is a candidate — that is what
115
+ * "enabled" means here — minus the ones switched out of Auto, arranged by the
116
+ * user's own order and then by each route's catalog order.
117
+ */
118
+ chain(): Promise<readonly AutoCandidate[]>;
119
+ /**
120
+ * Every candidate in the user's order, switched-out ones included.
121
+ *
122
+ * The card renders these rows and the drop sends back their ids, so the order
123
+ * applied here is the one the page must show: reading the raw candidate list
124
+ * instead would persist a reorder and then redraw the old arrangement.
125
+ */
126
+ private arranged;
127
+ /**
128
+ * Every model the picker currently offers, from every provider.
129
+ *
130
+ * In a running harness that means the whole roster, which is where the built-in
131
+ * DeepSeek providers and any provider the user added come from. Without a host —
132
+ * a unit test — this plugin's own routes are the roster.
133
+ */
134
+ private candidates;
135
+ /** The same list built from this plugin's own routes, for a host-less build. */
136
+ private registryCandidates;
137
+ /** The Settings card's rows: every candidate, switched-out ones included. */
138
+ summaries(): Promise<ModelSummary[]>;
139
+ }
140
+ /**
141
+ * Build the `auto` route: its adapter and a Settings façade over shared
142
+ * preferences.
143
+ *
144
+ * `auto` has no credentials, so the credential-shaped endpoints are refused here
145
+ * rather than half-answered: the page hides their controls for a route that
146
+ * reports itself as not connectable.
147
+ *
148
+ * @param options - shared preferences and the registry it stands in for.
149
+ * @returns the route, ready to register and to serve over RPC.
150
+ */
151
+ export declare function createAutoProvider(options: AutoProviderOptions): ProviderRoute;
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The `auto` provider route: one model that stands in for the user's arrangement
3
+ * of every enabled model, across every other route this plugin serves.
4
+ *
5
+ * `auto` is a provider rather than a model inside each route because its list is
6
+ * not any one route's catalog: the user mixes Kimi, Grok and Antigravity models
7
+ * in a single order, and switches individual ones out of the fallback without
8
+ * hiding them from the picker. A request for `auto` is delegated, in that order,
9
+ * to the route that owns each model — so `auto` holds no credentials of its own
10
+ * and its card has nothing to sign into.
11
+ *
12
+ * @module dsh-plugin-subscription-oauth/providers/auto
13
+ */
14
+ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
15
+ import { BAD_REQUEST, ProviderError } from '../errors.js';
16
+ /** Route key the `auto` provider registers under. */
17
+ export const AUTO_PROVIDER_ID = 'auto';
18
+ /** Display name for the picker group and the Settings card. */
19
+ export const AUTO_PROVIDER_NAME = 'Auto';
20
+ /** Context advertised before any candidate is known. */
21
+ export const AUTO_FALLBACK_CONTEXT_WINDOW = 262_144;
22
+ /** The key `auto` stores for one candidate, stable across restarts. */
23
+ export function autoKey(provider, model) {
24
+ return `${provider}/${model}`;
25
+ }
26
+ /** Split one `provider/model` key back into its parts. */
27
+ export function parseAutoKey(key) {
28
+ const at = key.indexOf('/');
29
+ if (at <= 0 || at === key.length - 1)
30
+ return undefined;
31
+ return { provider: key.slice(0, at), model: key.slice(at + 1) };
32
+ }
33
+ /** One short reason for a log line. */
34
+ function reasonOf(error) {
35
+ if (error instanceof LlmError)
36
+ return `${error.failure.code}: ${error.message}`;
37
+ return error instanceof Error ? error.message : String(error);
38
+ }
39
+ /** Whether a refusal means "this candidate cannot serve you now". */
40
+ function isCandidateFallbackWorthy(status) {
41
+ // Gone or renamed model, exhausted allowance, rate limit, provider trouble.
42
+ return status === 402 || status === 404 || status === 408 || status === 429 || status >= 500;
43
+ }
44
+ /**
45
+ * The `auto` provider's wire adapter.
46
+ *
47
+ * Its own list is the single `auto` entry; every request is delegated to the
48
+ * route that owns the chosen model.
49
+ */
50
+ export class AutoAdapter extends LlmAdapter {
51
+ options;
52
+ /** The route this adapter serves, built alongside it. */
53
+ route;
54
+ constructor(options) {
55
+ super();
56
+ this.options = options;
57
+ }
58
+ providerInfo(provider) {
59
+ return { id: provider, name: AUTO_PROVIDER_NAME };
60
+ }
61
+ providerRetryPolicy() {
62
+ // The routes `auto` walks own their pacing; retrying here would multiply
63
+ // their own retry-after handling.
64
+ return undefined;
65
+ }
66
+ async listModels(_provider) {
67
+ const chain = await this.chain();
68
+ // An `auto` with nothing to try is not offered at all, rather than offered
69
+ // and guaranteed to fail.
70
+ if (chain.length === 0)
71
+ return [];
72
+ return [{
73
+ provider: AUTO_PROVIDER_ID,
74
+ id: AUTO_PROVIDER_ID,
75
+ name: AUTO_PROVIDER_NAME,
76
+ description: `Tries ${String(chain.length)} enabled model(s) in your order, falling back when one is unavailable`,
77
+ }];
78
+ }
79
+ async resolveModel(_provider, model) {
80
+ if (model !== AUTO_PROVIDER_ID) {
81
+ throw new ProviderError(`auto serves only its own entry, not ${model}`, BAD_REQUEST);
82
+ }
83
+ const first = (await this.chain())[0];
84
+ const efforts = first?.reasoningEfforts ?? [];
85
+ return {
86
+ provider: AUTO_PROVIDER_ID,
87
+ id: AUTO_PROVIDER_ID,
88
+ name: AUTO_PROVIDER_NAME,
89
+ context: { contextWindow: first?.contextWindow ?? AUTO_FALLBACK_CONTEXT_WINDOW },
90
+ ...efforts.length === 0 ? {} : {
91
+ reasoning: {
92
+ efforts: efforts.map((effort) => ({
93
+ id: ReasoningEffortId(effort),
94
+ name: effort[0]?.toUpperCase() + effort.slice(1),
95
+ })),
96
+ },
97
+ },
98
+ };
99
+ }
100
+ async *stream(options) {
101
+ if (options.model !== AUTO_PROVIDER_ID) {
102
+ throw new ProviderError(`auto serves only its own entry, not ${options.model}`, BAD_REQUEST);
103
+ }
104
+ const chain = await this.chain();
105
+ if (chain.length === 0) {
106
+ throw new LlmError('Auto has no enabled model to try; turn one on in Settings → Subscriptions', EMPTY_RESPONSE_CODE);
107
+ }
108
+ let lastError;
109
+ for (const [index, candidate] of chain.entries()) {
110
+ const route = this.options.registry.get(candidate.provider);
111
+ const next = chain[index + 1];
112
+ // A chain may advance only while nothing has been produced: once a chunk
113
+ // has been yielded the turn belongs to that model, and a later failure is
114
+ // reported rather than papered over by a second model's text.
115
+ let produced = false;
116
+ try {
117
+ // A route this plugin owns streams through its own adapter. Anything else
118
+ // — a built-in provider, a provider the user added — goes through the
119
+ // harness, which is the only thing that knows which adapter serves it.
120
+ const source = route !== undefined
121
+ ? route.adapter.stream({ ...options, provider: candidate.provider, model: candidate.model })
122
+ : this.options.host?.stream({ ...options, provider: candidate.provider, model: candidate.model });
123
+ if (source === undefined) {
124
+ lastError = new LlmError(`Auto refers to the unknown provider ${candidate.provider}`, EMPTY_RESPONSE_CODE);
125
+ if (next === undefined)
126
+ break;
127
+ continue;
128
+ }
129
+ for await (const chunk of source) {
130
+ produced = true;
131
+ yield chunk;
132
+ }
133
+ return;
134
+ }
135
+ catch (error) {
136
+ lastError = error;
137
+ if (produced || next === undefined || options.signal?.aborted === true)
138
+ throw error;
139
+ this.options.onWarn?.(`auto: ${candidate.provider}/${candidate.model} failed (${reasonOf(error)}); trying ${next.provider}/${next.model}`);
140
+ }
141
+ }
142
+ throw lastError ?? new LlmError('Auto served none of its models', EMPTY_RESPONSE_CODE);
143
+ }
144
+ /**
145
+ * The candidates `auto` will try, in order.
146
+ *
147
+ * Every model a route currently offers the picker is a candidate — that is what
148
+ * "enabled" means here — minus the ones switched out of Auto, arranged by the
149
+ * user's own order and then by each route's catalog order.
150
+ */
151
+ async chain() {
152
+ const { off } = await this.options.preferences.autoPreferences();
153
+ const skipped = new Set(off);
154
+ return (await this.arranged()).filter((candidate) => !skipped.has(autoKey(candidate.provider, candidate.model)));
155
+ }
156
+ /**
157
+ * Every candidate in the user's order, switched-out ones included.
158
+ *
159
+ * The card renders these rows and the drop sends back their ids, so the order
160
+ * applied here is the one the page must show: reading the raw candidate list
161
+ * instead would persist a reorder and then redraw the old arrangement.
162
+ */
163
+ async arranged() {
164
+ const { order } = await this.options.preferences.autoPreferences();
165
+ const candidates = await this.candidates();
166
+ const byKey = new Map(candidates.map((candidate) => [autoKey(candidate.provider, candidate.model), candidate]));
167
+ const out = [];
168
+ const seen = new Set();
169
+ for (const key of order) {
170
+ const candidate = byKey.get(key);
171
+ if (candidate === undefined || seen.has(key))
172
+ continue;
173
+ seen.add(key);
174
+ out.push(candidate);
175
+ }
176
+ for (const candidate of candidates) {
177
+ const key = autoKey(candidate.provider, candidate.model);
178
+ if (seen.has(key))
179
+ continue;
180
+ seen.add(key);
181
+ out.push(candidate);
182
+ }
183
+ return out;
184
+ }
185
+ /**
186
+ * Every model the picker currently offers, from every provider.
187
+ *
188
+ * In a running harness that means the whole roster, which is where the built-in
189
+ * DeepSeek providers and any provider the user added come from. Without a host —
190
+ * a unit test — this plugin's own routes are the roster.
191
+ */
192
+ async candidates() {
193
+ const host = this.options.host;
194
+ if (host === undefined)
195
+ return await this.registryCandidates();
196
+ const out = [];
197
+ for (const entry of host.providers()) {
198
+ // Auto cannot chain to itself; that would be a loop, not a fallback.
199
+ if (entry.id === AUTO_PROVIDER_ID)
200
+ continue;
201
+ try {
202
+ for (const model of await host.models(entry.id)) {
203
+ const resolved = await host.resolve(entry.id, model.id);
204
+ out.push({
205
+ provider: entry.id,
206
+ model: model.id,
207
+ name: model.name ?? model.id,
208
+ contextWindow: resolved.contextWindow ?? AUTO_FALLBACK_CONTEXT_WINDOW,
209
+ ...resolved.reasoningEfforts === undefined || resolved.reasoningEfforts.length === 0
210
+ ? {}
211
+ : { reasoningEfforts: resolved.reasoningEfforts },
212
+ ...resolved.inputModalities === undefined ? {} : { inputModalities: resolved.inputModalities },
213
+ });
214
+ }
215
+ }
216
+ catch (error) {
217
+ // One unreachable provider must not empty Auto: the others still stand in.
218
+ this.options.onWarn?.(`auto could not read ${entry.id}'s models (${errorChain(error)})`);
219
+ }
220
+ }
221
+ return out;
222
+ }
223
+ /** The same list built from this plugin's own routes, for a host-less build. */
224
+ async registryCandidates() {
225
+ const out = [];
226
+ for (const route of this.options.registry.all()) {
227
+ if (route.id === AUTO_PROVIDER_ID)
228
+ continue;
229
+ try {
230
+ for (const model of await route.adapter.listModels(route.id)) {
231
+ const resolved = await route.adapter.resolveModel(route.id, model.id);
232
+ out.push({
233
+ provider: route.id,
234
+ model: model.id,
235
+ name: model.name,
236
+ contextWindow: resolved.context?.contextWindow ?? AUTO_FALLBACK_CONTEXT_WINDOW,
237
+ ...resolved.reasoning === undefined
238
+ ? {}
239
+ : { reasoningEfforts: resolved.reasoning.efforts.map((effort) => String(effort.id)) },
240
+ ...resolved.inputModalities === undefined ? {} : { inputModalities: resolved.inputModalities },
241
+ });
242
+ }
243
+ }
244
+ catch (error) {
245
+ // One unreachable route must not empty Auto: the others still stand in.
246
+ this.options.onWarn?.(`auto could not read ${route.id}'s models (${errorChain(error)})`);
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+ /** The Settings card's rows: every candidate, switched-out ones included. */
252
+ async summaries() {
253
+ const { off } = await this.options.preferences.autoPreferences();
254
+ const skipped = new Set(off);
255
+ const chain = await this.chain();
256
+ const listed = new Set(chain.map((candidate) => autoKey(candidate.provider, candidate.model)));
257
+ // Switched-out candidates are not in the chain, so they are read from the full
258
+ // arrangement, which keeps the user's order for the rows the card renders.
259
+ const all = await this.arranged();
260
+ return all.map((candidate) => {
261
+ const key = autoKey(candidate.provider, candidate.model);
262
+ return {
263
+ id: key,
264
+ name: candidate.name,
265
+ description: `${candidate.provider} · ${candidate.model}`,
266
+ contextWindow: candidate.contextWindow,
267
+ ...candidate.inputModalities === undefined ? {} : { inputModalities: candidate.inputModalities },
268
+ visible: listed.has(key) && !skipped.has(key),
269
+ source: candidate.provider,
270
+ };
271
+ });
272
+ }
273
+ }
274
+ /**
275
+ * Build the `auto` route: its adapter and a Settings façade over shared
276
+ * preferences.
277
+ *
278
+ * `auto` has no credentials, so the credential-shaped endpoints are refused here
279
+ * rather than half-answered: the page hides their controls for a route that
280
+ * reports itself as not connectable.
281
+ *
282
+ * @param options - shared preferences and the registry it stands in for.
283
+ * @returns the route, ready to register and to serve over RPC.
284
+ */
285
+ export function createAutoProvider(options) {
286
+ const adapter = new AutoAdapter(options);
287
+ /** The state the page renders. */
288
+ const status = async () => ({
289
+ provider: AUTO_PROVIDER_ID,
290
+ displayName: AUTO_PROVIDER_NAME,
291
+ loggedIn: true,
292
+ connectable: false,
293
+ busy: false,
294
+ accounts: [],
295
+ credentialFile: '—',
296
+ discovery: true,
297
+ models: await adapter.summaries(),
298
+ details: [
299
+ { key: 'autoChain', value: String((await adapter.chain()).length) },
300
+ ],
301
+ });
302
+ /** Refuse a credential operation: `auto` owns no account. */
303
+ const noAccount = (operation) => {
304
+ throw new ProviderError(`auto has no account of its own; ${operation} one of its providers instead`, BAD_REQUEST);
305
+ };
306
+ const route = {
307
+ id: AUTO_PROVIDER_ID,
308
+ displayName: AUTO_PROVIDER_NAME,
309
+ credentialFile: '—',
310
+ adapter,
311
+ status,
312
+ login: async () => noAccount('connect'),
313
+ cancel: async () => status(),
314
+ logout: async () => noAccount('sign out'),
315
+ setDefault: async () => noAccount('select a default for'),
316
+ refresh: async () => noAccount('refresh'),
317
+ discover: async () => status(),
318
+ setModelVisible: async (id, visible) => {
319
+ if (parseAutoKey(id) === undefined) {
320
+ throw new ProviderError(`auto lists provider/model keys, not ${JSON.stringify(id)}`, BAD_REQUEST);
321
+ }
322
+ if (await options.preferences.setAutoEnabled(id, visible))
323
+ options.onCatalogChanged?.();
324
+ return status();
325
+ },
326
+ setModelOrder: async (ids) => {
327
+ for (const id of ids) {
328
+ if (parseAutoKey(id) === undefined) {
329
+ throw new ProviderError(`auto lists provider/model keys, not ${JSON.stringify(id)}`, BAD_REQUEST);
330
+ }
331
+ }
332
+ if (await options.preferences.setAutoOrder(ids))
333
+ options.onCatalogChanged?.();
334
+ return status();
335
+ },
336
+ // `auto` holds no catalog of its own: its rows are the other routes' models,
337
+ // and a hand-added id belongs to the route that serves it.
338
+ addModel: async () => noAccount('add a model to'),
339
+ removeModel: async () => noAccount('remove a model from'),
340
+ // `auto` states no allowance of its own: each candidate's own route does.
341
+ usage: async () => ({
342
+ usage: [],
343
+ status: await status(),
344
+ }),
345
+ reload: () => status(),
346
+ };
347
+ // The adapter needs no back-reference to the route, but exposing it keeps a
348
+ // future delegation from having to rebuild the pair.
349
+ adapter.route = route;
350
+ return route;
351
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Provider model catalogs.
3
+ *
4
+ * A provider ships a curated catalog — the ids it serves, their display names,
5
+ * context windows, reasoning ladder, and any aliases the endpoint also accepts.
6
+ * Live discovery may widen it; it never narrows it, because an endpoint's own
7
+ * listing omits variants it still serves.
8
+ *
9
+ * The `auto` provider is not part of any of these: it spans them, and lives in
10
+ * its own module.
11
+ *
12
+ * @module dsh-plugin-subscription-oauth/providers/catalog
13
+ */
14
+ import type { ModelModality } from '@deepseek-ai/dsh-llm';
15
+ /** One provider-owned model entry. */
16
+ export interface CatalogEntry {
17
+ /** Model id sent on the wire. */
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ /** Other ids the endpoint accepts for this entry. */
22
+ aliases?: readonly string[];
23
+ /** Combined request and response context, in tokens. */
24
+ contextWindow: number;
25
+ inputModalities?: readonly ModelModality[];
26
+ /** Selectable reasoning efforts, in display order. */
27
+ reasoningEfforts?: readonly string[];
28
+ /** Effort materialized when a caller picks none. */
29
+ defaultEffort?: string;
30
+ }
31
+ /** Whether one catalog entry covers a model id. */
32
+ export declare function entryMatches(entry: CatalogEntry, id: string): boolean;
33
+ /** The entry covering a model id, when the catalog has one. */
34
+ export declare function findEntry(entries: readonly CatalogEntry[], id: string): CatalogEntry | undefined;
35
+ /**
36
+ * Build an entry for a model the curated catalog does not describe.
37
+ *
38
+ * Used for ids the live list reports that this build has not seen, and for ids
39
+ * the plugin configuration adds: they are dispatchable, but their metadata is
40
+ * the conservative default rather than a guess.
41
+ *
42
+ * @param id - model id.
43
+ * @param description - why this entry exists, shown in the picker.
44
+ * @param contextWindow - context to advertise.
45
+ * @returns the entry.
46
+ */
47
+ export declare function fallbackEntry(id: string, description: string, contextWindow: number): CatalogEntry;
48
+ /** A catalog after the user's order and visibility choices are applied. */
49
+ export interface ResolvedCatalog {
50
+ /** Every entry the picker may offer, hidden ids omitted (the picker's list). */
51
+ entries: CatalogEntry[];
52
+ /** The enabled entries, in catalog order. */
53
+ enabled: CatalogEntry[];
54
+ /** Every entry in catalog order, hidden ids included (the Settings list). */
55
+ ranked: CatalogEntry[];
56
+ }
57
+ /**
58
+ * Apply the user's model order and visibility to one provider's catalog.
59
+ *
60
+ * Ranked ids come first in the user's order; ids the order does not mention keep
61
+ * their curated position after them, so a model added by a plugin update or by
62
+ * live discovery is listed without the user having to re-rank anything.
63
+ *
64
+ * @param entries - the provider's catalog, in curated order.
65
+ * @param order - the user's stored rank, as model ids.
66
+ * @param hidden - ids the picker must omit.
67
+ * @returns the listed entries, the full list, and the enabled entries.
68
+ */
69
+ export declare function resolveCatalog(entries: readonly CatalogEntry[], order: readonly string[], hidden: ReadonlySet<string>): ResolvedCatalog;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Provider model catalogs.
3
+ *
4
+ * A provider ships a curated catalog — the ids it serves, their display names,
5
+ * context windows, reasoning ladder, and any aliases the endpoint also accepts.
6
+ * Live discovery may widen it; it never narrows it, because an endpoint's own
7
+ * listing omits variants it still serves.
8
+ *
9
+ * The `auto` provider is not part of any of these: it spans them, and lives in
10
+ * its own module.
11
+ *
12
+ * @module dsh-plugin-subscription-oauth/providers/catalog
13
+ */
14
+ /** Whether one catalog entry covers a model id. */
15
+ export function entryMatches(entry, id) {
16
+ return entry.id === id || entry.aliases?.includes(id) === true;
17
+ }
18
+ /** The entry covering a model id, when the catalog has one. */
19
+ export function findEntry(entries, id) {
20
+ return entries.find((entry) => entryMatches(entry, id));
21
+ }
22
+ /**
23
+ * Build an entry for a model the curated catalog does not describe.
24
+ *
25
+ * Used for ids the live list reports that this build has not seen, and for ids
26
+ * the plugin configuration adds: they are dispatchable, but their metadata is
27
+ * the conservative default rather than a guess.
28
+ *
29
+ * @param id - model id.
30
+ * @param description - why this entry exists, shown in the picker.
31
+ * @param contextWindow - context to advertise.
32
+ * @returns the entry.
33
+ */
34
+ export function fallbackEntry(id, description, contextWindow) {
35
+ return {
36
+ id,
37
+ name: id,
38
+ description,
39
+ contextWindow,
40
+ inputModalities: ['text', 'image'],
41
+ reasoningEfforts: ['low', 'medium', 'high', 'max'],
42
+ defaultEffort: 'max',
43
+ };
44
+ }
45
+ /**
46
+ * Apply the user's model order and visibility to one provider's catalog.
47
+ *
48
+ * Ranked ids come first in the user's order; ids the order does not mention keep
49
+ * their curated position after them, so a model added by a plugin update or by
50
+ * live discovery is listed without the user having to re-rank anything.
51
+ *
52
+ * @param entries - the provider's catalog, in curated order.
53
+ * @param order - the user's stored rank, as model ids.
54
+ * @param hidden - ids the picker must omit.
55
+ * @returns the listed entries, the full list, and the enabled entries.
56
+ */
57
+ export function resolveCatalog(entries, order, hidden) {
58
+ const available = new Map(entries.map((entry) => [entry.id, entry]));
59
+ const ranked = [];
60
+ const seen = new Set();
61
+ for (const id of order) {
62
+ if (seen.has(id))
63
+ continue;
64
+ const entry = available.get(id);
65
+ if (entry === undefined)
66
+ continue;
67
+ seen.add(id);
68
+ ranked.push(entry);
69
+ }
70
+ for (const entry of entries) {
71
+ if (seen.has(entry.id))
72
+ continue;
73
+ seen.add(entry.id);
74
+ ranked.push(entry);
75
+ }
76
+ const enabled = ranked.filter((entry) => !hidden.has(entry.id));
77
+ return { entries: enabled, ranked, enabled };
78
+ }