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.
- package/README.md +31 -5
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +73 -2
- package/lib/auth/rpc.js +53 -3
- package/lib/client/ExternalUsageCards.js +1 -1
- package/lib/client/OllamaCard.d.ts +13 -0
- package/lib/client/OllamaCard.js +76 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +246 -0
- package/lib/client/ProviderModelEditor.js +4 -2
- package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
- package/lib/client/SubscriptionUsageBadge.js +3 -2
- package/lib/client/SubscriptionsSection.d.ts +2 -74
- package/lib/client/SubscriptionsSection.js +4 -67
- package/lib/client/UsageMeter.js +11 -7
- package/lib/client/locales.d.ts +68 -0
- package/lib/client/locales.js +77 -0
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +246 -0
- package/lib/client/usage-pace.d.ts +7 -0
- package/lib/client/usage-pace.js +15 -0
- package/lib/client.js +1385 -244
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +25 -0
- package/lib/index.js +11060 -9733
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/provider-settings.d.ts +2 -1
- package/lib/provider-settings.js +6 -4
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/claude.d.ts +14 -1
- package/lib/providers/claude.js +25 -13
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.js +7 -7
- package/lib/providers/common.d.ts +27 -0
- package/lib/providers/common.js +70 -0
- package/lib/providers/copilot.js +3 -3
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage-controller.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +19 -5
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +93 -8
- package/lib/providers/grok.js +14 -9
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/ollama-settings.d.ts +90 -0
- package/lib/providers/ollama-settings.js +197 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +213 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +545 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/resolved.d.ts +5 -2
- package/lib/translate/resolved.js +5 -2
- 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
|
|
24
|
-
|
|
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
|

|
|
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
|
|
179
|
-
|
|
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
|
package/lib/auth/device-flow.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/lib/auth/oauth-flow.js
CHANGED
|
@@ -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 {};
|