dsh-subscriptions 0.1.4 → 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 (62) hide show
  1. package/README.md +31 -5
  2. package/lib/auth/device-flow.js +6 -3
  3. package/lib/auth/oauth-flow.js +41 -8
  4. package/lib/auth/rpc.d.ts +73 -2
  5. package/lib/auth/rpc.js +53 -3
  6. package/lib/client/ExternalUsageCards.js +1 -1
  7. package/lib/client/OllamaCard.d.ts +13 -0
  8. package/lib/client/OllamaCard.js +76 -0
  9. package/lib/client/OllamaSettingsManager.d.ts +24 -0
  10. package/lib/client/OllamaSettingsManager.js +246 -0
  11. package/lib/client/ProviderModelEditor.js +4 -2
  12. package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
  13. package/lib/client/SubscriptionUsageBadge.js +3 -2
  14. package/lib/client/SubscriptionsSection.d.ts +2 -74
  15. package/lib/client/SubscriptionsSection.js +4 -67
  16. package/lib/client/UsageMeter.js +11 -7
  17. package/lib/client/locales.d.ts +68 -0
  18. package/lib/client/locales.js +77 -0
  19. package/lib/client/ollama-draft.d.ts +229 -0
  20. package/lib/client/ollama-draft.js +246 -0
  21. package/lib/client/usage-pace.d.ts +7 -0
  22. package/lib/client/usage-pace.js +15 -0
  23. package/lib/client.js +1385 -244
  24. package/lib/client.js.map +1 -1
  25. package/lib/index.d.ts +25 -0
  26. package/lib/index.js +11060 -9733
  27. package/lib/model-defaults.d.ts +4 -5
  28. package/lib/model-defaults.js +18 -14
  29. package/lib/provider-settings.d.ts +2 -1
  30. package/lib/provider-settings.js +6 -4
  31. package/lib/providers/antigravity.d.ts +0 -2
  32. package/lib/providers/antigravity.js +5 -9
  33. package/lib/providers/claude.d.ts +14 -1
  34. package/lib/providers/claude.js +25 -13
  35. package/lib/providers/codex-search.js +5 -2
  36. package/lib/providers/codex.js +7 -7
  37. package/lib/providers/common.d.ts +27 -0
  38. package/lib/providers/common.js +70 -0
  39. package/lib/providers/copilot.js +3 -3
  40. package/lib/providers/cursor-adapter.d.ts +35 -0
  41. package/lib/providers/cursor-adapter.js +245 -4
  42. package/lib/providers/cursor-auth.js +11 -4
  43. package/lib/providers/cursor-usage.js +2 -1
  44. package/lib/providers/external-usage-controller.d.ts +9 -2
  45. package/lib/providers/external-usage-controller.js +19 -5
  46. package/lib/providers/external-usage.d.ts +8 -0
  47. package/lib/providers/external-usage.js +93 -8
  48. package/lib/providers/grok.js +14 -9
  49. package/lib/providers/minimax-usage.js +2 -1
  50. package/lib/providers/ollama-settings.d.ts +90 -0
  51. package/lib/providers/ollama-settings.js +197 -0
  52. package/lib/providers/ollama-web.d.ts +46 -0
  53. package/lib/providers/ollama-web.js +213 -0
  54. package/lib/providers/ollama.d.ts +107 -0
  55. package/lib/providers/ollama.js +545 -0
  56. package/lib/tools/image-generate.js +2 -1
  57. package/lib/tools/video-generate.d.ts +0 -5
  58. package/lib/tools/video-generate.js +25 -14
  59. package/lib/tools/x-search.js +2 -2
  60. package/lib/translate/resolved.d.ts +5 -2
  61. package/lib/translate/resolved.js +5 -2
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,24 @@ reported quota in **Settings → Subscriptions**.
13
13
  - **Six subscription model sources:** Connect ChatGPT/Codex, Claude, Grok,
14
14
  GitHub Copilot, Google Antigravity, or Cursor and choose their models in
15
15
  DSH's picker. These routes use account sign-in, without provider API keys.
16
+ - **Ollama Cloud key route:** Add the `ollama-cloud` route with an
17
+ `OLLAMA_API_KEY` credential. The route ships no model catalog:
18
+ configure it on the Settings → Subscriptions card, which fetches the
19
+ cloud list, ticks the models to keep, and edits each one's display
20
+ name, order, default reasoning effort, and local context window.
21
+ The picker shows exactly the ticked list, so an unconfigured route
22
+ offers no model. The card also edits the base URL, the credential
23
+ reference, and the web search/fetch switch, and persists them under
24
+ the DSH home directory. No OAuth sign-in. A hand-written `llm-pi-ai`
25
+ route with the same id collides with this one: delete the manual
26
+ block when enabling the plugin route. Stop sequences are unsupported
27
+ on this route, consistent with the other adapters. Web search/fetch
28
+ stay off until the card's switch is on (or a profile sets
29
+ `ollama.web`), so untouched installs keep their existing web
30
+ provider; a profile still pins them to `ollama-cloud`. Not ported
31
+ from the standalone plugin: its transient-error reclassification,
32
+ per-image request quotas, and per-model usage counts. See the
33
+ [key-route note](.agents/notes/implemented/provider/2026-10-10-ollama-cloud-key-route.md).
16
34
  - **Account and model control:** Refresh catalogs and choose visible models.
17
35
  For Codex, Claude, Grok, Copilot, and Antigravity, manage multiple accounts
18
36
  and same-provider pools. Pools can use available quota to select an account
@@ -20,8 +38,9 @@ reported quota in **Settings → Subscriptions**.
20
38
  - **Quota while you work:** See reported usage windows and reset times on
21
39
  provider cards and in an optional session-footer pill. The same view reads
22
40
  OpenCode Go and Kimi Code usage through keys already configured in
23
- **Settings → Models**. Their model routes remain built into DSH. GitHub
24
- Copilot has no usage endpoint.
41
+ **Settings → Models** (their model routes remain built into DSH), and
42
+ Ollama Cloud usage through its `OLLAMA_API_KEY` credential (its route
43
+ ships in this plugin). GitHub Copilot has no usage endpoint.
25
44
  - **Provider tools:** Use Codex web search, Grok X search, ChatGPT or Grok
26
45
  image generation and editing, and Grok video generation when the matching
27
46
  provider is enabled.
@@ -60,7 +79,9 @@ in the composer's tool row: Standard or Fast, per session.
60
79
 
61
80
  **Settings → Subscriptions → provider → Manage** edits account aliases,
62
81
  pool participation, model allowlists, and the per-model effort, context,
63
- and tool settings.
82
+ and tool settings. A context value only lowers the window a session plans
83
+ against, never above what the provider advertises; Codex and Claude support
84
+ it, and it does not enlarge the API's capacity.
64
85
 
65
86
  ![Model settings](docs/images/model-settings.png)
66
87
 
@@ -175,8 +196,8 @@ allowBuilds:
175
196
  Restart `dsh web` after installation. Open **Settings → Subscriptions** to
176
197
  connect providers, manage accounts and model lists, and inspect available
177
198
  quota. The session footer quota pill can be shown or hidden there for the
178
- current browser. OpenCode Go and Kimi Code cards show usage when their API
179
- keys are configured in **Settings → Models**.
199
+ current browser. OpenCode Go, Kimi Code, and Ollama Cloud cards show
200
+ usage when their API keys are configured.
180
201
 
181
202
  To update an installation, run the same `dsh plugin --profile web add` command again and restart `dsh web`.
182
203
 
@@ -209,6 +230,11 @@ that a provider still accepts live sign-in or model requests. See
209
230
  - **OpenCode Go and Kimi Code:** Live usage responses were verified. Their API
210
231
  keys stay on the DSH host; the browser receives configured status and quota
211
232
  windows, not the keys.
233
+ - **Ollama Cloud:** Unit and fixture coverage only (chat wire, discovery
234
+ mapping and wire-id normalization, the settings store and its RPC
235
+ endpoints, usage parsing, web providers, host E2E fixtures). The card's
236
+ rendering has no browser test. Live chat, listing, usage, and web calls
237
+ against `ollama.com` remain to be verified with a real key.
212
238
 
213
239
  Cursor uses one connected account and a live model catalog. Its model
214
240
  visibility selection persists under the DSH home directory, and automatic
@@ -8,6 +8,7 @@
8
8
  * engines uniformly.
9
9
  */
10
10
  import { hostFetch } from '../http.js';
11
+ import { parseProviderJson } from '../providers/common.js';
11
12
  /** Default poll interval when the device-code response omits one. */
12
13
  const DEFAULT_INTERVAL_SEC = 5;
13
14
  /** Default device-code lifetime when the response omits one (GitHub: 15 minutes). */
@@ -77,7 +78,7 @@ export class DeviceFlowManager {
77
78
  if (!response.ok) {
78
79
  throw new Error(`${provider} device-code request failed (HTTP ${String(response.status)})`);
79
80
  }
80
- const wire = await response.json();
81
+ const wire = await parseProviderJson(response, 'device code request');
81
82
  if (typeof wire.device_code !== 'string' || wire.device_code.length === 0
82
83
  || typeof wire.user_code !== 'string' || wire.user_code.length === 0
83
84
  || typeof wire.verification_uri !== 'string' || wire.verification_uri.length === 0) {
@@ -133,7 +134,7 @@ export class DeviceFlowManager {
133
134
  }).toString(),
134
135
  signal: controller.signal,
135
136
  });
136
- const result = await pollResponse.json();
137
+ const result = await parseProviderJson(pollResponse, 'device token poll');
137
138
  if (typeof result.access_token === 'string' && result.access_token.length > 0) {
138
139
  settle(undefined, result.access_token);
139
140
  return;
@@ -152,7 +153,9 @@ export class DeviceFlowManager {
152
153
  settle(new Error('the device code expired before authorization completed'));
153
154
  return;
154
155
  default:
155
- settle(new Error(`${provider} device-flow polling failed: ${result.error_description ?? result.error ?? `HTTP ${String(pollResponse.status)}`}`));
156
+ // Safe means this failure path carries no provider-controlled free text,
157
+ // not that no secret can exist elsewhere in the system.
158
+ settle(new Error(`${provider} device-flow polling failed (HTTP ${String(pollResponse.status)}): [provider response body omitted]`));
156
159
  return;
157
160
  }
158
161
  }
@@ -12,6 +12,38 @@ export const DEFAULT_FLOW_TIMEOUT_MS = 180_000;
12
12
  const SUCCESS_PAGE = '<!doctype html><html><head><meta charset="utf-8"><title>Login successful</title></head>'
13
13
  + '<body style="font-family:sans-serif"><h1>Login successful</h1>'
14
14
  + '<p>You can close this tab and return to DeepSeek Harness.</p></body></html>';
15
+ function authorizationFailure(error) {
16
+ // Safe means this failure path carries no provider-controlled free text,
17
+ // not that no secret can exist elsewhere in the system.
18
+ // RFC 6749 §4.1.2.1 categories select local literals, never provider prose.
19
+ let category;
20
+ switch (error) {
21
+ case 'invalid_request':
22
+ category = 'invalid_request';
23
+ break;
24
+ case 'unauthorized_client':
25
+ category = 'unauthorized_client';
26
+ break;
27
+ case 'access_denied':
28
+ category = 'access_denied';
29
+ break;
30
+ case 'unsupported_response_type':
31
+ category = 'unsupported_response_type';
32
+ break;
33
+ case 'invalid_scope':
34
+ category = 'invalid_scope';
35
+ break;
36
+ case 'server_error':
37
+ category = 'server_error';
38
+ break;
39
+ case 'temporarily_unavailable':
40
+ category = 'temporarily_unavailable';
41
+ break;
42
+ }
43
+ return 'authorization failed'
44
+ + (category === undefined ? '' : ` (${category})`)
45
+ + ': [provider message omitted]';
46
+ }
15
47
  function failurePage(detail) {
16
48
  return '<!doctype html><html><head><meta charset="utf-8"><title>Login failed</title></head>'
17
49
  + `<body style="font-family:sans-serif"><h1>Login failed</h1><p>${detail.replace(/[<>&]/g, '')}</p></body></html>`;
@@ -138,19 +170,20 @@ export class OAuthFlowManager {
138
170
  response.end('not found');
139
171
  return;
140
172
  }
141
- const errorDescription = url.searchParams.get('error_description') ?? url.searchParams.get('error');
142
- if (errorDescription !== null) {
143
- response.writeHead(200, { 'content-type': 'text/html' });
144
- response.end(failurePage(errorDescription));
145
- settle(new Error(`authorization failed: ${errorDescription}`));
146
- return;
147
- }
148
173
  if (url.searchParams.get('state') !== input.state) {
149
- // A stray or replayed redirect must not kill the real attempt.
174
+ // A stray or replayed redirect must not kill the real attempt, even on error.
175
+ // RFC 6749 §4.1.2.1 requires errors to echo the state sent in the request.
150
176
  response.writeHead(400, { 'content-type': 'text/plain' });
151
177
  response.end('state mismatch');
152
178
  return;
153
179
  }
180
+ if (url.searchParams.has('error_description') || url.searchParams.has('error')) {
181
+ const detail = authorizationFailure(url.searchParams.get('error'));
182
+ response.writeHead(200, { 'content-type': 'text/html' });
183
+ response.end(failurePage(detail));
184
+ settle(new Error(detail));
185
+ return;
186
+ }
154
187
  const code = url.searchParams.get('code');
155
188
  if (code === null || code.length === 0) {
156
189
  response.writeHead(400, { 'content-type': 'text/plain' });
package/lib/auth/rpc.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const SUBSCRIPTIONS_AUTH_PREFIX = "subscriptions-auth.";
23
23
  * Every endpoint {@link dispatch} answers; each gets one exact Fetch route.
24
24
  * Kept in one place so the route table and the switch cannot drift apart.
25
25
  */
26
- export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
26
+ export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "ollamaSettings", "ollamaSetSettings", "ollamaModels", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
27
27
  /** Decoded image bytes returned by the `image` endpoint. */
28
28
  export interface ImageBytesResult {
29
29
  mediaType: string;
@@ -201,11 +201,82 @@ export interface CursorRpcController {
201
201
  settings(force: boolean, signal: AbortSignal): Promise<unknown>;
202
202
  setSettings(input: unknown): Promise<void>;
203
203
  }
204
+ /** One model in the Ollama Cloud picker catalog, with its capability flags. */
205
+ export interface OllamaModelView {
206
+ id: string;
207
+ name: string;
208
+ /** The endpoint advertised the `vision` capability. */
209
+ vision: boolean;
210
+ /** The endpoint advertised the `thinking` capability. */
211
+ thinking: boolean;
212
+ contextWindow?: number;
213
+ /** Advertised reasoning levels, when the model reasons. */
214
+ efforts?: {
215
+ id: string;
216
+ name: string;
217
+ }[];
218
+ /** The level preselected for the model, when one is known. */
219
+ defaultEffort?: string;
220
+ }
221
+ /** One candidate returned by the cloud listing; adopting it stores `id`. */
222
+ export interface OllamaCloudCandidate {
223
+ /** Normalized wire id, e.g. `glm-5.3:cloud`. */
224
+ id: string;
225
+ /** The id exactly as the listing published it, e.g. `glm-5.3`. */
226
+ rawId: string;
227
+ /** Display basename for the checkbox row. */
228
+ name: string;
229
+ vision: boolean;
230
+ thinking: boolean;
231
+ contextWindow?: number;
232
+ efforts: string[];
233
+ defaultEffort?: string;
234
+ }
235
+ /**
236
+ * What the Ollama card renders: the stored overrides, the effective values
237
+ * after config/default fallback, and the catalog the picker serves.
238
+ */
239
+ export interface OllamaSettingsView {
240
+ provider: 'ollama-cloud';
241
+ /** The stored document; absent fields mean "no override". */
242
+ settings: unknown;
243
+ effective: {
244
+ baseURL: string;
245
+ apiKeyRef: string;
246
+ web: boolean;
247
+ };
248
+ /** The effective catalog, in picker order (empty hides the picker entry). */
249
+ models: OllamaModelView[];
250
+ /** A non-empty `config.ollama.models` wins over the store, so card edits are inert. */
251
+ configOverridesModels: boolean;
252
+ }
253
+ /** Ollama Cloud catalog and connection settings; the API key stays in DSH credentials. */
254
+ export interface OllamaRpcController {
255
+ settings(): Promise<OllamaSettingsView>;
256
+ /** Validate and persist the store document, then re-announce the route. */
257
+ setSettings(input: unknown): Promise<void>;
258
+ /**
259
+ * List the endpoint's Cloud models for the adoption checklist.
260
+ * @param input - an optional one-time API key and base URL override; the
261
+ * key is used for this listing only and never persisted.
262
+ * @param signal - caller cancellation from the RPC transport.
263
+ */
264
+ models(input: {
265
+ apiKey?: string;
266
+ baseURL?: string;
267
+ }, signal: AbortSignal): Promise<{
268
+ models: OllamaCloudCandidate[];
269
+ }>;
270
+ }
204
271
  /**
205
272
  * Register the `/subscriptions-auth` RPC channel when a host connection exists.
206
273
  * @param ctx - the plugin context (headless profiles have no `connection`).
207
274
  * @param controller - the auth operations backing the endpoints.
208
275
  * @param speed - the per-session speed-tier state backing the Speed toggle.
209
276
  * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
277
+ * @param providerSettings - optional OAuth provider/account preferences.
278
+ * @param externalUsage - optional credential-backed quota readers.
279
+ * @param cursor - optional Cursor account and model-visibility actions.
280
+ * @param ollama - optional Ollama Cloud catalog and connection settings.
210
281
  */
211
- export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined): void;
282
+ export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, modelDefaults?: ModelDefaultsController | undefined, providerSettings?: ProviderSettingsController | undefined, externalUsage?: ExternalUsageRpcController | undefined, cursor?: CursorRpcController | undefined, ollama?: OllamaRpcController | undefined): void;
package/lib/auth/rpc.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { AttachmentId } from '@deepseek-ai/dsh-attachment';
10
10
  import { PROVIDER_IDS } from './store.js';
11
11
  import { EXTERNAL_USAGE_SOURCES } from '../providers/external-usage-controller.js';
12
+ import { validateOllamaBaseURL } from '../providers/ollama-settings.js';
12
13
  /**
13
14
  * Endpoint-name prefix under the shared `/api` channel: endpoint `status`
14
15
  * is served at `/api/subscriptions-auth.status`. The browser half calls
@@ -25,6 +26,7 @@ export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
25
26
  'externalStatus', 'externalUsage',
26
27
  'cursorStatus', 'cursorLogin', 'cursorCancel', 'cursorLogout', 'cursorUsage', 'cursorModels',
27
28
  'cursorSettings', 'cursorSetSettings',
29
+ 'ollamaSettings', 'ollamaSetSettings', 'ollamaModels',
28
30
  'image', 'video', 'prepareReset', 'consumeReset',
29
31
  'speed', 'setSpeed',
30
32
  'modelDefaults', 'setModelDefault',
@@ -121,6 +123,18 @@ function readString(payload, field) {
121
123
  }
122
124
  return value;
123
125
  }
126
+ /** Read a field that may be omitted, rejecting an empty or non-string value. */
127
+ function readOptionalString(payload, field) {
128
+ if (typeof payload !== 'object' || payload === null)
129
+ throw new BadRequest('payload must be an object');
130
+ const value = payload[field];
131
+ if (value === undefined)
132
+ return undefined;
133
+ if (typeof value !== 'string' || value.trim().length === 0) {
134
+ throw new BadRequest(`payload.${field} must be a non-empty string when present`);
135
+ }
136
+ return value;
137
+ }
124
138
  /** Validate the `setModelDefault` endpoint's payload. */
125
139
  function readModelDefaultInput(payload) {
126
140
  const provider = readProvider(payload);
@@ -222,7 +236,7 @@ function readSessionId(payload) {
222
236
  throw new BadRequest('payload must be an object');
223
237
  return readString(payload, 'sessionId');
224
238
  }
225
- async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor) {
239
+ async function dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama) {
226
240
  switch (endpoint) {
227
241
  case 'providerSettings':
228
242
  if (!providerSettings)
@@ -322,6 +336,38 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
322
336
  throw new BadRequest('payload must be an object');
323
337
  await cursor.setSettings(payload.settings);
324
338
  return ok({ ok: true });
339
+ case 'ollamaSettings':
340
+ if (!ollama)
341
+ throw new BadRequest('Ollama settings are unavailable');
342
+ return ok(await ollama.settings());
343
+ case 'ollamaSetSettings':
344
+ if (!ollama)
345
+ throw new BadRequest('Ollama settings are unavailable');
346
+ if (payload === null || typeof payload !== 'object')
347
+ throw new BadRequest('payload must be an object');
348
+ await ollama.setSettings(payload.settings);
349
+ return ok({ ok: true });
350
+ case 'ollamaModels': {
351
+ if (!ollama)
352
+ throw new BadRequest('Ollama settings are unavailable');
353
+ const apiKey = readOptionalString(payload, 'apiKey');
354
+ const rawBaseURL = readOptionalString(payload, 'baseURL');
355
+ let baseURL;
356
+ if (rawBaseURL !== undefined) {
357
+ // The listing may run against a not-yet-saved draft address, so it
358
+ // passes the same URL check the store applies.
359
+ try {
360
+ baseURL = validateOllamaBaseURL(rawBaseURL);
361
+ }
362
+ catch (error) {
363
+ throw new BadRequest(error instanceof Error ? error.message : String(error));
364
+ }
365
+ }
366
+ return ok(await ollama.models({
367
+ ...apiKey === undefined ? {} : { apiKey },
368
+ ...baseURL === undefined ? {} : { baseURL },
369
+ }, signal));
370
+ }
325
371
  case 'prepareReset':
326
372
  if (!controller.prepareReset)
327
373
  throw new BadRequest('Reset redemption unavailable');
@@ -363,8 +409,12 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
363
409
  * @param controller - the auth operations backing the endpoints.
364
410
  * @param speed - the per-session speed-tier state backing the Speed toggle.
365
411
  * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
412
+ * @param providerSettings - optional OAuth provider/account preferences.
413
+ * @param externalUsage - optional credential-backed quota readers.
414
+ * @param cursor - optional Cursor account and model-visibility actions.
415
+ * @param ollama - optional Ollama Cloud catalog and connection settings.
366
416
  */
367
- export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined) {
417
+ export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefined, providerSettings = undefined, externalUsage = undefined, cursor = undefined, ollama = undefined) {
368
418
  // `connection` is not in this plugin's inject list (headless compositions
369
419
  // lack it), so its startup order is unconstrained: defer registration until
370
420
  // the service exists instead of probing once at apply time.
@@ -381,7 +431,7 @@ export function registerAuthRpc(ctx, controller, speed, modelDefaults = undefine
381
431
  const connection = ctx.get('connection');
382
432
  const handler = async (endpoint, payload, signal) => {
383
433
  try {
384
- return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor);
434
+ return await dispatch(controller, speed, modelDefaults, endpoint, payload, signal, providerSettings, externalUsage, cursor, ollama);
385
435
  }
386
436
  catch (error) {
387
437
  return failure(error);
@@ -77,7 +77,7 @@ export function ExternalUsageCards({ rpc, t }) {
77
77
  const snapshot = usage[id];
78
78
  return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: configured
79
79
  ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: name })] }), !(status === undefined && statusError !== undefined) && _jsx("p", { style: styles.status, children: status === undefined ? t('checking')
80
- : configured ? t('externalUsageConnected') : t('externalUsageNotConfigured') }), statusError !== undefined && _jsx("p", { style: styles.error, children: t('externalUsageUnavailable', { message: statusError }) }), status !== undefined && !configured && _jsx("p", { style: styles.status, children: t('externalUsageConfigure', { ref }) }), configured && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: t('externalUsageApiKey') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), snapshot?.plan !== undefined && _jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: snapshot.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading[id] === true, onClick: () => { void refresh(id); }, children: t('usageRefresh') })] }), loading[id] === true && snapshot === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), errors[id] !== undefined && _jsx("p", { style: styles.error, children: t('usageError', { message: errors[id] }) }), snapshot?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), snapshot?.windows?.map((window, index) => {
80
+ : configured ? t('externalUsageConnected') : t('externalUsageNotConfigured') }), statusError !== undefined && _jsx("p", { style: styles.error, children: t('externalUsageUnavailable', { message: statusError }) }), status !== undefined && !configured && _jsx("p", { style: styles.status, children: t('externalUsageConfigure', { ref }) }), configured && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: t('externalUsageApiKey') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), snapshot?.plan !== undefined && _jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: snapshot.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading[id] === true, onClick: () => { void refresh(id); }, children: t('usageRefresh') })] }), loading[id] === true && snapshot === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), errors[id] !== undefined && _jsx("p", { style: styles.error, children: t('usageError', { message: errors[id] }) }), snapshot !== undefined && snapshot.supported === false && _jsx("p", { style: styles.status, children: t('usageUnsupported') }), snapshot?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), snapshot?.windows?.map((window, index) => {
81
81
  const percent = displayUsedPercent(window.usedPercent);
82
82
  const label = externalUsageWindowLabel(t, window);
83
83
  return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt === undefined ? ''
@@ -0,0 +1,13 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
3
+ type Translate = SubscriptionsSectionInjected['t'];
4
+ /**
5
+ * Ollama Cloud route card: the catalog the model picker shows, the web
6
+ * capability toggle, and the API-key usage row. The settings dialog owns
7
+ * every write; the card only reads, so a save is reflected by re-reading.
8
+ */
9
+ export declare function OllamaCard({ rpc, t }: {
10
+ rpc: ConnectionHandle['rpc'];
11
+ t: Translate;
12
+ }): import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
+ import { externalUsageWindowLabel } from './ExternalUsageCards.js';
5
+ import { OllamaSettingsManager } from './OllamaSettingsManager.js';
6
+ import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
7
+ import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
8
+ import { UsageMeter } from './UsageMeter.js';
9
+ import { displayUsedPercent } from './usage-pace.js';
10
+ function message(error) {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+ /**
14
+ * Ollama Cloud route card: the catalog the model picker shows, the web
15
+ * capability toggle, and the API-key usage row. The settings dialog owns
16
+ * every write; the card only reads, so a save is reflected by re-reading.
17
+ */
18
+ export function OllamaCard({ rpc, t }) {
19
+ const [view, setView] = useState();
20
+ const [configured, setConfigured] = useState();
21
+ const [usage, setUsage] = useState();
22
+ const [observedAt, setObservedAt] = useState();
23
+ const [usageStale, setUsageStale] = useState(false);
24
+ const [loading, setLoading] = useState(false);
25
+ // Two failure surfaces: the card's own load and the usage lookup. They
26
+ // render in different places so a stale usage reading cannot masquerade as
27
+ // a settings failure.
28
+ const [statusError, setStatusError] = useState();
29
+ const [usageError, setUsageError] = useState();
30
+ const [manage, setManage] = useState(false);
31
+ const refreshUsage = useCallback(async () => {
32
+ setLoading(true);
33
+ try {
34
+ const value = await callSubscriptionsAuth(rpc, 'externalUsage', { source: 'ollama-cloud' });
35
+ setUsage(value);
36
+ // Prefer the server's observation time; a cached reading must not look fresh.
37
+ setObservedAt(value.observedAt ?? Date.now());
38
+ setUsageStale(value.stale === true);
39
+ setUsageError(undefined);
40
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
41
+ }
42
+ catch (failure) {
43
+ setUsageError(message(failure));
44
+ setUsageStale(true);
45
+ }
46
+ finally {
47
+ setLoading(false);
48
+ }
49
+ }, [rpc]);
50
+ const refreshStatus = useCallback(async () => {
51
+ try {
52
+ const [settings, status] = await Promise.all([
53
+ callSubscriptionsAuth(rpc, 'ollamaSettings', {}),
54
+ callSubscriptionsAuth(rpc, 'externalStatus', {}),
55
+ ]);
56
+ setView(settings);
57
+ setStatusError(undefined);
58
+ const isConfigured = status['ollama-cloud']?.configured === true;
59
+ setConfigured(isConfigured);
60
+ if (isConfigured)
61
+ void refreshUsage();
62
+ }
63
+ catch (failure) {
64
+ setStatusError(message(failure));
65
+ }
66
+ }, [rpc, refreshUsage]);
67
+ useEffect(() => { void refreshStatus(); }, [refreshStatus]);
68
+ return _jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.header, children: [_jsx("span", { style: { ...styles.dot, background: configured === true
69
+ ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: t('ollamaTitle') })] }), _jsx("p", { style: styles.status, children: view === undefined ? t('checking')
70
+ : view.models.length > 0 ? t('ollamaStatus', { count: view.models.length }) : t('ollamaStatusNone') }), view !== undefined && _jsx("p", { style: styles.status, children: view.effective.web ? t('ollamaWebOn') : t('ollamaWebOff') }), statusError !== undefined && _jsx("p", { style: styles.error, children: t('ollamaModelsLoadFailed', { message: statusError }) }), view !== undefined && configured === false && _jsx("p", { style: styles.status, children: t('ollamaConfigureHint', { ref: view.effective.apiKeyRef }) }), configured === true && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: t('externalUsageApiKey') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && _jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading, onClick: () => { void refreshUsage(); }, children: t('usageRefresh') })] }), loading && usage === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), usageError !== undefined && _jsx("p", { style: styles.error, children: t('usageError', { message: usageError }) }), usage !== undefined && usage.supported === false && _jsx("p", { style: styles.status, children: t('usageUnsupported') }), usage?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), usage?.windows?.map((window, index) => {
71
+ const percent = displayUsedPercent(window.usedPercent);
72
+ const label = externalUsageWindowLabel(t, window);
73
+ return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt === undefined ? ''
74
+ : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt, stale: usageStale })] }, index);
75
+ })] })] }), view?.configOverridesModels === true && _jsx("p", { style: styles.status, children: t('ollamaCatalogOverride') }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManage(true), children: t('accountsManage') }), configured !== true && _jsx("button", { type: "button", style: styles.button, onClick: () => { void refreshStatus(); }, children: t('usageRefresh') })] }), manage && _jsx(OllamaSettingsManager, { rpc: rpc, t: t, onClose: () => setManage(false), onSaved: () => { void refreshStatus(); } })] });
76
+ }
@@ -0,0 +1,24 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
3
+ type Translate = SubscriptionsSectionInjected['t'];
4
+ interface Props {
5
+ rpc: ConnectionHandle['rpc'];
6
+ t: Translate;
7
+ onClose: () => void;
8
+ /** Called after a successful save or reset, so the card re-reads its status. */
9
+ onSaved: () => void;
10
+ }
11
+ /**
12
+ * Ollama Cloud settings dialog. It owns one draft (model list, web toggle,
13
+ * base URL and credential reference) that Save writes in a single
14
+ * `ollamaSetSettings` call, so a failure keeps every edit. Fetching the cloud
15
+ * catalog is a read-only side trip, and a key typed for the fetch never
16
+ * leaves this component. The model list follows the shared model editor: a
17
+ * filter, select all/none over the filtered rows, and one checkbox per
18
+ * model, where ticking edits the draft and enables Save. Ollama adds what the
19
+ * shared editor lacks: a display name, ordering, and a local context window.
20
+ * Unticking keeps the row's full settings in a dialog-local stash, so ticking
21
+ * it again restores them even when the cloud listing no longer returns it.
22
+ */
23
+ export declare function OllamaSettingsManager({ rpc, t, onClose, onSaved }: Props): import("react").JSX.Element;
24
+ export {};