dsh-subscriptions 0.1.5 → 0.2.2
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 +212 -154
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.d.ts +73 -2
- package/lib/auth/rpc.js +124 -23
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +13 -0
- package/lib/client/OllamaCard.js +94 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +329 -0
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +109 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +5 -11
- package/lib/client/SubscriptionUsageBadge.js +301 -112
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +259 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +69 -27
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +78 -0
- package/lib/client/locales.js +121 -34
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +255 -0
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.d.ts +7 -0
- package/lib/client/usage-pace.js +43 -10
- package/lib/client.js +1647 -266
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +28 -3
- package/lib/index.js +9329 -8217
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +14 -1
- package/lib/provider-settings.js +64 -20
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +23 -3
- package/lib/providers/claude.js +167 -92
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.d.ts +14 -0
- package/lib/providers/common.js +118 -38
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +30 -7
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +140 -20
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.d.ts +90 -0
- package/lib/providers/ollama-settings.js +213 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +231 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +608 -0
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +141 -50
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +26 -4
|
@@ -6,7 +6,10 @@ import { parseProviderJson } from './common.js';
|
|
|
6
6
|
* bar.
|
|
7
7
|
*/
|
|
8
8
|
function usedFromCounts(total, count) {
|
|
9
|
-
return typeof total === 'number' &&
|
|
9
|
+
return typeof total === 'number' &&
|
|
10
|
+
Number.isFinite(total) &&
|
|
11
|
+
total > 0 &&
|
|
12
|
+
typeof count === 'number'
|
|
10
13
|
? (1 - count / total) * 100
|
|
11
14
|
: undefined;
|
|
12
15
|
}
|
|
@@ -36,13 +39,18 @@ export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
|
|
|
36
39
|
if (key.startsWith('sk-api-'))
|
|
37
40
|
throw new Error('MiniMax requires a subscription key, not a pay-as-you-go key');
|
|
38
41
|
const response = await http(`https://www.minimax.${region === 'cn' ? 'cn' : 'io'}/v1/token_plan/remains`, {
|
|
39
|
-
headers: {
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: `Bearer ${key}`,
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
},
|
|
40
46
|
...(signal === undefined ? {} : { signal }),
|
|
41
47
|
});
|
|
42
48
|
if (!response.ok)
|
|
43
49
|
throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
|
|
44
|
-
const body = await parseProviderJson(response, 'MiniMax usage');
|
|
45
|
-
if ((body?.base_resp?.status_code !== undefined &&
|
|
50
|
+
const body = (await parseProviderJson(response, 'MiniMax usage'));
|
|
51
|
+
if ((body?.base_resp?.status_code !== undefined &&
|
|
52
|
+
body.base_resp.status_code !== 0) ||
|
|
53
|
+
!Array.isArray(body?.model_remains)) {
|
|
46
54
|
throw new Error('MiniMax usage response failed or has no quota records');
|
|
47
55
|
}
|
|
48
56
|
const windows = [];
|
|
@@ -60,22 +68,34 @@ export async function fetchMiniMaxUsage(key, region, http = fetch, signal) {
|
|
|
60
68
|
if (status === 3)
|
|
61
69
|
continue; // Unlimited pools cannot have a finite percentage.
|
|
62
70
|
const remaining = row[`${prefix}_remaining_percent`];
|
|
63
|
-
const boost = prefix === 'current_weekly' ? row.weekly_boost_permille ?? 1000 : 1000;
|
|
71
|
+
const boost = prefix === 'current_weekly' ? (row.weekly_boost_permille ?? 1000) : 1000;
|
|
64
72
|
if (typeof boost !== 'number' || !Number.isFinite(boost) || boost <= 0)
|
|
65
73
|
continue;
|
|
66
74
|
// An explicit percentage needs no counts: time-based plans send zero
|
|
67
75
|
// counts beside it. Official CLI compatibility: without a percentage, the
|
|
68
76
|
// legacy counts are the quota remaining.
|
|
69
|
-
const usedPercent = typeof remaining === 'number'
|
|
77
|
+
const usedPercent = typeof remaining === 'number'
|
|
78
|
+
? 100 - (remaining * boost) / 1000
|
|
70
79
|
: usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
|
|
71
|
-
if (usedPercent === undefined ||
|
|
80
|
+
if (usedPercent === undefined ||
|
|
81
|
+
!Number.isFinite(usedPercent) ||
|
|
82
|
+
usedPercent < 0 ||
|
|
83
|
+
usedPercent > 100)
|
|
72
84
|
continue;
|
|
73
85
|
const start = row[startKey];
|
|
74
86
|
const end = row[endKey];
|
|
75
|
-
const validBounds = typeof start === 'number' &&
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const validBounds = typeof start === 'number' &&
|
|
88
|
+
Number.isFinite(start) &&
|
|
89
|
+
start > 0 &&
|
|
90
|
+
typeof end === 'number' &&
|
|
91
|
+
Number.isFinite(end) &&
|
|
92
|
+
end > start;
|
|
93
|
+
windows.push({
|
|
94
|
+
kind: fallback === 'session' && validBounds && end - start !== 18_000_000
|
|
95
|
+
? 'other'
|
|
96
|
+
: fallback,
|
|
97
|
+
scope: row.model_name,
|
|
98
|
+
usedPercent,
|
|
79
99
|
...(validBounds ? { startsAt: start, resetsAt: end } : {}),
|
|
80
100
|
});
|
|
81
101
|
}
|
|
@@ -34,13 +34,17 @@ export class NpmCliVersionCache {
|
|
|
34
34
|
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
35
35
|
}
|
|
36
36
|
/** A manual catalog refresh also checks for a newly released CLI. */
|
|
37
|
-
invalidate() {
|
|
37
|
+
invalidate() {
|
|
38
|
+
this.expiresAt = 0;
|
|
39
|
+
}
|
|
38
40
|
resolve() {
|
|
39
41
|
if (this.pending !== undefined)
|
|
40
42
|
return this.pending;
|
|
41
43
|
if (this.presented !== undefined && this.now() < this.expiresAt)
|
|
42
44
|
return Promise.resolve(this.presented.version);
|
|
43
|
-
this.pending = this.refresh().finally(() => {
|
|
45
|
+
this.pending = this.refresh().finally(() => {
|
|
46
|
+
this.pending = undefined;
|
|
47
|
+
});
|
|
44
48
|
return this.pending;
|
|
45
49
|
}
|
|
46
50
|
/**
|
|
@@ -82,7 +86,10 @@ export class NpmCliVersionCache {
|
|
|
82
86
|
}, this.timeoutMs);
|
|
83
87
|
});
|
|
84
88
|
// Also bounded when an injected transport ignores cancellation.
|
|
85
|
-
this.presented = {
|
|
89
|
+
this.presented = {
|
|
90
|
+
version: await Promise.race([lookup(), timeout]),
|
|
91
|
+
source: 'npm',
|
|
92
|
+
};
|
|
86
93
|
this.expiresAt = this.now() + 6 * 60 * 60_000;
|
|
87
94
|
}
|
|
88
95
|
catch {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One model the Ollama Cloud route serves. The route ships no catalog of its
|
|
3
|
+
* own: this list is the catalog, in the order the picker shows it. Adoption
|
|
4
|
+
* fills the capability fields from the endpoint's `/api/tags` + `/api/show`
|
|
5
|
+
* reply; every field except `id` is optional so a hand-written row still
|
|
6
|
+
* works.
|
|
7
|
+
*/
|
|
8
|
+
export interface OllamaModelSetting {
|
|
9
|
+
/** Wire model id; must be non-empty and unique within the list. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Selector label; defaults to the id. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** Known combined request/response context capacity in tokens. */
|
|
14
|
+
contextWindow?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The user's local context-window choice. Unlike `contextWindow`, which
|
|
17
|
+
* snapshots what the endpoint advertised, this records only an explicit
|
|
18
|
+
* edit: absent follows the endpoint. The runtime caps it at the advertised
|
|
19
|
+
* window and it never widens the API's real capacity.
|
|
20
|
+
*/
|
|
21
|
+
contextOverride?: number;
|
|
22
|
+
/** Per-request output cap; defaults to the route's fallback. */
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
/** The endpoint advertised the `vision` capability. */
|
|
25
|
+
vision?: boolean;
|
|
26
|
+
/** The endpoint advertised the `thinking` capability. */
|
|
27
|
+
thinking?: boolean;
|
|
28
|
+
/** Advertised reasoning efforts, in picker order; empty means non-reasoning. */
|
|
29
|
+
efforts?: string[];
|
|
30
|
+
/** Effort preselected when the session picks none; must be one of `efforts`. */
|
|
31
|
+
defaultEffort?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Durable, non-secret Ollama Cloud configuration. Every field is optional and
|
|
35
|
+
* its absence means "no override": the effective value then falls back to the
|
|
36
|
+
* plugin config and, last, to the built-in default. The API key itself never
|
|
37
|
+
* appears here; `apiKeyRef` only names a DSH credential.
|
|
38
|
+
*/
|
|
39
|
+
export interface OllamaSettings {
|
|
40
|
+
/** The route's catalog, in picker order. `[]` hides every model. */
|
|
41
|
+
models?: OllamaModelSetting[];
|
|
42
|
+
/** Register the web search/fetch providers; the config value is the default. */
|
|
43
|
+
web?: boolean;
|
|
44
|
+
/** Native base URL; the config value is the default. */
|
|
45
|
+
baseURL?: string;
|
|
46
|
+
/** Credential reference naming the API key; the config value is the default. */
|
|
47
|
+
apiKeyRef?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Accept only an absolute http(s) base URL with no credentials, query, or
|
|
51
|
+
* fragment, and return its trimmed form. The route appends `/tags`, `/show`,
|
|
52
|
+
* and `/v1/chat/completions` to this value, so anything else would build a URL
|
|
53
|
+
* that cannot work — and a non-official host receives the referenced
|
|
54
|
+
* credential, which the dialog tells the user.
|
|
55
|
+
* @param value - the candidate URL.
|
|
56
|
+
* @returns the trimmed URL to store.
|
|
57
|
+
* @throws when the value is not a usable absolute http(s) URL.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateOllamaBaseURL(value: unknown): string;
|
|
60
|
+
/**
|
|
61
|
+
* Validate one settings document, dropping unknown keys and duplicate model
|
|
62
|
+
* ids (first wins, matching the Cursor store's visible-model handling).
|
|
63
|
+
* @param input - the parsed store document or an RPC payload.
|
|
64
|
+
* @throws when a known field has the wrong type or an unusable value.
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateOllamaSettings(input: unknown): OllamaSettings;
|
|
67
|
+
/**
|
|
68
|
+
* Durable Ollama Cloud configuration under the DSH home directory. Writes are
|
|
69
|
+
* serialized and atomic (temp file with mode 0600, then rename), so a crash
|
|
70
|
+
* never leaves a half-written document and a concurrent writer cannot lose an
|
|
71
|
+
* earlier save.
|
|
72
|
+
*/
|
|
73
|
+
export declare class OllamaSettingsStore {
|
|
74
|
+
readonly path: string;
|
|
75
|
+
private readonly onCorrupt?;
|
|
76
|
+
private current;
|
|
77
|
+
private writes;
|
|
78
|
+
/** Set when the stored file existed but was unreadable or invalid. */
|
|
79
|
+
private corrupt;
|
|
80
|
+
constructor(path?: string, onCorrupt?: ((message: string) => void) | undefined);
|
|
81
|
+
/** Why the stored file is ignored, when it is. */
|
|
82
|
+
corruption(): string | undefined;
|
|
83
|
+
get(): OllamaSettings;
|
|
84
|
+
/** The stored catalog, or `undefined` when the store carries no override. */
|
|
85
|
+
models(): readonly OllamaModelSetting[] | undefined;
|
|
86
|
+
web(): boolean | undefined;
|
|
87
|
+
baseURL(): string | undefined;
|
|
88
|
+
apiKeyRef(): string | undefined;
|
|
89
|
+
set(input: unknown): Promise<void>;
|
|
90
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
5
|
+
function fail(message) {
|
|
6
|
+
throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
function readNonEmptyString(value, field) {
|
|
9
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
10
|
+
fail(`${field} must be a non-empty string`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function readPositiveInteger(value, field) {
|
|
14
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
15
|
+
fail(`${field} must be a positive integer`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function readBoolean(value, field) {
|
|
20
|
+
if (typeof value !== 'boolean')
|
|
21
|
+
fail(`${field} must be a boolean`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Accept only an absolute http(s) base URL with no credentials, query, or
|
|
26
|
+
* fragment, and return its trimmed form. The route appends `/tags`, `/show`,
|
|
27
|
+
* and `/v1/chat/completions` to this value, so anything else would build a URL
|
|
28
|
+
* that cannot work — and a non-official host receives the referenced
|
|
29
|
+
* credential, which the dialog tells the user.
|
|
30
|
+
* @param value - the candidate URL.
|
|
31
|
+
* @returns the trimmed URL to store.
|
|
32
|
+
* @throws when the value is not a usable absolute http(s) URL.
|
|
33
|
+
*/
|
|
34
|
+
export function validateOllamaBaseURL(value) {
|
|
35
|
+
const raw = readNonEmptyString(value, 'baseURL');
|
|
36
|
+
let url;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
fail('baseURL must be an absolute URL');
|
|
42
|
+
}
|
|
43
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
44
|
+
fail('baseURL must use http or https');
|
|
45
|
+
if (url.username !== '' || url.password !== '')
|
|
46
|
+
fail('baseURL must not carry credentials');
|
|
47
|
+
if (url.search !== '' || url.hash !== '')
|
|
48
|
+
fail('baseURL must not carry a query or fragment');
|
|
49
|
+
return raw.trim();
|
|
50
|
+
}
|
|
51
|
+
/** Credential references are looked up by name, so whitespace is always a typo. */
|
|
52
|
+
function readApiKeyRef(value) {
|
|
53
|
+
const raw = readNonEmptyString(value, 'apiKeyRef');
|
|
54
|
+
if (/\s/.test(raw))
|
|
55
|
+
fail('apiKeyRef must not contain whitespace');
|
|
56
|
+
return raw;
|
|
57
|
+
}
|
|
58
|
+
function readEfforts(value, field) {
|
|
59
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
60
|
+
fail(`${field} must be a non-empty array`);
|
|
61
|
+
const efforts = [];
|
|
62
|
+
for (const entry of value) {
|
|
63
|
+
const effort = readNonEmptyString(entry, `${field} entries`);
|
|
64
|
+
if (!efforts.includes(effort))
|
|
65
|
+
efforts.push(effort);
|
|
66
|
+
}
|
|
67
|
+
return efforts;
|
|
68
|
+
}
|
|
69
|
+
function readModel(entry, index) {
|
|
70
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
71
|
+
fail(`models[${index}] must be an object`);
|
|
72
|
+
}
|
|
73
|
+
const record = entry;
|
|
74
|
+
// The id is the wire id, so surrounding whitespace is always a paste
|
|
75
|
+
// artifact; trim it before the duplicate check sees it.
|
|
76
|
+
const model = {
|
|
77
|
+
id: readNonEmptyString(record.id, `models[${index}].id`).trim(),
|
|
78
|
+
};
|
|
79
|
+
if (record.name !== undefined)
|
|
80
|
+
model.name = readNonEmptyString(record.name, `models[${index}].name`);
|
|
81
|
+
if (record.contextWindow !== undefined) {
|
|
82
|
+
model.contextWindow = readPositiveInteger(record.contextWindow, `models[${index}].contextWindow`);
|
|
83
|
+
}
|
|
84
|
+
if (record.contextOverride !== undefined) {
|
|
85
|
+
model.contextOverride = readPositiveInteger(record.contextOverride, `models[${index}].contextOverride`);
|
|
86
|
+
}
|
|
87
|
+
if (record.maxTokens !== undefined) {
|
|
88
|
+
model.maxTokens = readPositiveInteger(record.maxTokens, `models[${index}].maxTokens`);
|
|
89
|
+
}
|
|
90
|
+
if (record.vision !== undefined)
|
|
91
|
+
model.vision = readBoolean(record.vision, `models[${index}].vision`);
|
|
92
|
+
if (record.thinking !== undefined)
|
|
93
|
+
model.thinking = readBoolean(record.thinking, `models[${index}].thinking`);
|
|
94
|
+
if (record.efforts !== undefined)
|
|
95
|
+
model.efforts = readEfforts(record.efforts, `models[${index}].efforts`);
|
|
96
|
+
if (record.defaultEffort !== undefined) {
|
|
97
|
+
const effort = readNonEmptyString(record.defaultEffort, `models[${index}].defaultEffort`);
|
|
98
|
+
// The runtime rejects a default the model does not advertise, so a
|
|
99
|
+
// mismatch is a store bug rather than something to carry forward.
|
|
100
|
+
if (model.efforts !== undefined && !model.efforts.includes(effort)) {
|
|
101
|
+
fail(`models[${index}].defaultEffort must be one of its efforts`);
|
|
102
|
+
}
|
|
103
|
+
model.defaultEffort = effort;
|
|
104
|
+
}
|
|
105
|
+
return model;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate one settings document, dropping unknown keys and duplicate model
|
|
109
|
+
* ids (first wins, matching the Cursor store's visible-model handling).
|
|
110
|
+
* @param input - the parsed store document or an RPC payload.
|
|
111
|
+
* @throws when a known field has the wrong type or an unusable value.
|
|
112
|
+
*/
|
|
113
|
+
export function validateOllamaSettings(input) {
|
|
114
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
115
|
+
fail('Ollama settings must be an object');
|
|
116
|
+
}
|
|
117
|
+
const record = input;
|
|
118
|
+
const settings = {};
|
|
119
|
+
if (record.models !== undefined) {
|
|
120
|
+
if (!Array.isArray(record.models))
|
|
121
|
+
fail('models must be an array');
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const models = [];
|
|
124
|
+
for (const [index, entry] of record.models.entries()) {
|
|
125
|
+
const model = readModel(entry, index);
|
|
126
|
+
if (seen.has(model.id))
|
|
127
|
+
continue;
|
|
128
|
+
seen.add(model.id);
|
|
129
|
+
models.push(model);
|
|
130
|
+
}
|
|
131
|
+
settings.models = models;
|
|
132
|
+
}
|
|
133
|
+
if (record.web !== undefined)
|
|
134
|
+
settings.web = readBoolean(record.web, 'web');
|
|
135
|
+
if (record.baseURL !== undefined)
|
|
136
|
+
settings.baseURL = validateOllamaBaseURL(record.baseURL);
|
|
137
|
+
if (record.apiKeyRef !== undefined)
|
|
138
|
+
settings.apiKeyRef = readApiKeyRef(record.apiKeyRef);
|
|
139
|
+
return settings;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Durable Ollama Cloud configuration under the DSH home directory. Writes are
|
|
143
|
+
* serialized and atomic (temp file with mode 0600, then rename), so a crash
|
|
144
|
+
* never leaves a half-written document and a concurrent writer cannot lose an
|
|
145
|
+
* earlier save.
|
|
146
|
+
*/
|
|
147
|
+
export class OllamaSettingsStore {
|
|
148
|
+
path;
|
|
149
|
+
onCorrupt;
|
|
150
|
+
current = {};
|
|
151
|
+
writes = Promise.resolve();
|
|
152
|
+
/** Set when the stored file existed but was unreadable or invalid. */
|
|
153
|
+
corrupt;
|
|
154
|
+
constructor(path = dshHomePath('plugins', 'subscriptions', 'ollama-settings.json'), onCorrupt) {
|
|
155
|
+
this.path = path;
|
|
156
|
+
this.onCorrupt = onCorrupt;
|
|
157
|
+
try {
|
|
158
|
+
this.current = validateOllamaSettings(JSON.parse(readFileSync(path, 'utf8')));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.code !== 'ENOENT') {
|
|
162
|
+
// A broken file must not take the whole plugin down: degrade to
|
|
163
|
+
// defaults, keep the file untouched for manual recovery, and let
|
|
164
|
+
// the next save overwrite it.
|
|
165
|
+
this.corrupt = error instanceof Error ? error.message : String(error);
|
|
166
|
+
// The raw V8 message quotes the offending bytes; the log keeps a
|
|
167
|
+
// fixed diagnosis and corruption() carries the detail for callers
|
|
168
|
+
// that surface it outside log lines.
|
|
169
|
+
this.onCorrupt?.(`Ollama settings at ${path} are unreadable and ignored; defaults serve until the next save.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Why the stored file is ignored, when it is. */
|
|
174
|
+
corruption() {
|
|
175
|
+
return this.corrupt;
|
|
176
|
+
}
|
|
177
|
+
get() {
|
|
178
|
+
return structuredClone(this.current);
|
|
179
|
+
}
|
|
180
|
+
/** The stored catalog, or `undefined` when the store carries no override. */
|
|
181
|
+
models() {
|
|
182
|
+
return this.current.models;
|
|
183
|
+
}
|
|
184
|
+
web() {
|
|
185
|
+
return this.current.web;
|
|
186
|
+
}
|
|
187
|
+
baseURL() {
|
|
188
|
+
return this.current.baseURL;
|
|
189
|
+
}
|
|
190
|
+
apiKeyRef() {
|
|
191
|
+
return this.current.apiKeyRef;
|
|
192
|
+
}
|
|
193
|
+
set(input) {
|
|
194
|
+
const validated = validateOllamaSettings(input);
|
|
195
|
+
const run = this.writes.then(async () => {
|
|
196
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
197
|
+
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
198
|
+
try {
|
|
199
|
+
await writeFile(temporary, JSON.stringify(validated, null, 2), {
|
|
200
|
+
mode: 0o600,
|
|
201
|
+
});
|
|
202
|
+
await rename(temporary, this.path);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
await rm(temporary, { force: true });
|
|
206
|
+
}
|
|
207
|
+
this.current = validated;
|
|
208
|
+
this.corrupt = undefined;
|
|
209
|
+
});
|
|
210
|
+
this.writes = run.catch(() => undefined);
|
|
211
|
+
return run;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama-native web capabilities: search and fetch ride the Cloud
|
|
3
|
+
* `/api/web_search` + `/api/web_fetch` endpoints independently of the
|
|
4
|
+
* selected chat model. Both providers share the `ollama-cloud` id because
|
|
5
|
+
* the seam addresses each capability kind separately.
|
|
6
|
+
*/
|
|
7
|
+
import type { WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web';
|
|
8
|
+
import type { FetchFn } from './common.js';
|
|
9
|
+
/** Shared provider id for both web capabilities. */
|
|
10
|
+
export declare const OLLAMA_WEB_PROVIDER_ID = "ollama-cloud";
|
|
11
|
+
/** Per-attempt budget; the seam owns overall cancellation. */
|
|
12
|
+
export declare const OLLAMA_WEB_TIMEOUT_MS = 15000;
|
|
13
|
+
/** Search result cap applied at the request layer. */
|
|
14
|
+
export declare const OLLAMA_WEB_MAX_RESULTS = 10;
|
|
15
|
+
/** Response body bound for web payloads. */
|
|
16
|
+
export declare const OLLAMA_WEB_MAX_BYTES: number;
|
|
17
|
+
export interface OllamaWebOptions {
|
|
18
|
+
/** Resolve the API key per call; `undefined` means unconfigured. */
|
|
19
|
+
apiKey: () => Promise<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Native base URL; web endpoints ride it unchanged (no `/v1` mapping). A
|
|
22
|
+
* function form is read per call so a Settings save applies without a
|
|
23
|
+
* restart.
|
|
24
|
+
*/
|
|
25
|
+
baseURL: string | (() => string);
|
|
26
|
+
/** Local gate; absent counts as enabled. Read on every call. */
|
|
27
|
+
enabled?: () => boolean;
|
|
28
|
+
fetchFn?: FetchFn;
|
|
29
|
+
requestTimeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Search provider behind DSH's stock web_search tool. */
|
|
32
|
+
export declare class OllamaWebSearchProvider implements WebSearchProvider {
|
|
33
|
+
private readonly options;
|
|
34
|
+
readonly id = "ollama-cloud";
|
|
35
|
+
constructor(options: OllamaWebOptions);
|
|
36
|
+
available(): boolean;
|
|
37
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
38
|
+
}
|
|
39
|
+
/** Fetch provider behind DSH's stock web_fetch tool. */
|
|
40
|
+
export declare class OllamaWebFetchProvider implements WebFetchProvider {
|
|
41
|
+
private readonly options;
|
|
42
|
+
readonly id = "ollama-cloud";
|
|
43
|
+
constructor(options: OllamaWebOptions);
|
|
44
|
+
available(): boolean;
|
|
45
|
+
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
46
|
+
}
|