dsh-plugin-subscription-usage 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +38 -2
- package/cordis.patch.yml +5 -0
- package/icon.svg +9 -0
- package/lib/auth/claude-code-creds.d.ts +23 -0
- package/lib/auth/claude-code-creds.js +172 -0
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/jwt.d.ts +10 -0
- package/lib/auth/jwt.js +25 -0
- package/lib/auth/oauth-flow.d.ts +91 -0
- package/lib/auth/oauth-flow.js +227 -0
- package/lib/auth/pkce.d.ts +31 -0
- package/lib/auth/pkce.js +35 -0
- package/lib/auth/rpc.d.ts +233 -0
- package/lib/auth/rpc.js +501 -0
- package/lib/auth/store.d.ts +198 -0
- package/lib/auth/store.js +473 -0
- package/lib/client/Footer.d.ts +3 -0
- package/lib/client/Footer.js +73 -0
- package/lib/client/Gauge.d.ts +28 -0
- package/lib/client/Gauge.js +139 -0
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +48 -0
- package/lib/client/ImageGenerateToolview.js +130 -0
- package/lib/client/Notice.d.ts +15 -0
- package/lib/client/Notice.js +93 -0
- package/lib/client/ProviderCard.d.ts +35 -0
- package/lib/client/ProviderCard.js +240 -0
- package/lib/client/SpeedSelect.d.ts +72 -0
- package/lib/client/SpeedSelect.js +214 -0
- package/lib/client/Usage.d.ts +7 -0
- package/lib/client/Usage.js +83 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +172 -0
- package/lib/client/context.d.ts +16 -0
- package/lib/client/context.js +11 -0
- package/lib/client/fast-command.d.ts +2 -0
- package/lib/client/fast-command.js +4 -0
- package/lib/client/hooks.d.ts +2 -0
- package/lib/client/hooks.js +10 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client/index.js +113 -0
- package/lib/client/locales.d.ts +139 -0
- package/lib/client/locales.js +267 -0
- package/lib/client/meter.d.ts +91 -0
- package/lib/client/meter.js +196 -0
- package/lib/client/prefs.d.ts +51 -0
- package/lib/client/prefs.js +71 -0
- package/lib/client/rpc.d.ts +161 -0
- package/lib/client/rpc.js +31 -0
- package/lib/client/store.d.ts +67 -0
- package/lib/client/store.js +129 -0
- package/lib/client/styles.d.ts +7 -0
- package/lib/client/styles.js +176 -0
- package/lib/client.js +3933 -0
- package/lib/client.js.map +1 -0
- package/lib/compat.d.ts +55 -0
- package/lib/compat.js +20 -0
- package/lib/http.d.ts +134 -0
- package/lib/http.js +431 -0
- package/lib/index.d.ts +121 -0
- package/lib/index.js +13210 -0
- package/lib/json-text.d.ts +2 -0
- package/lib/json-text.js +4 -0
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +238 -0
- package/lib/model-speeds.d.ts +10 -0
- package/lib/model-speeds.js +48 -0
- package/lib/models-directory.d.ts +17 -0
- package/lib/models-directory.js +25 -0
- package/lib/provider-settings.d.ts +55 -0
- package/lib/provider-settings.js +215 -0
- package/lib/providers/account-preferences.d.ts +46 -0
- package/lib/providers/account-preferences.js +188 -0
- package/lib/providers/accounts.d.ts +113 -0
- package/lib/providers/accounts.js +142 -0
- package/lib/providers/antigravity-oauth-client.d.ts +7 -0
- package/lib/providers/antigravity-oauth-client.js +7 -0
- package/lib/providers/antigravity.d.ts +119 -0
- package/lib/providers/antigravity.js +552 -0
- package/lib/providers/catalog-refresh.d.ts +4 -0
- package/lib/providers/catalog-refresh.js +30 -0
- package/lib/providers/catalog-store.d.ts +47 -0
- package/lib/providers/catalog-store.js +283 -0
- package/lib/providers/claude-cli-version.d.ts +15 -0
- package/lib/providers/claude-cli-version.js +24 -0
- package/lib/providers/claude.d.ts +212 -0
- package/lib/providers/claude.js +726 -0
- package/lib/providers/codex-client-version.d.ts +8 -0
- package/lib/providers/codex-client-version.js +17 -0
- package/lib/providers/codex-search.d.ts +29 -0
- package/lib/providers/codex-search.js +198 -0
- package/lib/providers/codex.d.ts +253 -0
- package/lib/providers/codex.js +930 -0
- package/lib/providers/common.d.ts +415 -0
- package/lib/providers/common.js +679 -0
- package/lib/providers/copilot.d.ts +365 -0
- package/lib/providers/copilot.js +956 -0
- package/lib/providers/grok.d.ts +214 -0
- package/lib/providers/grok.js +750 -0
- package/lib/providers/image-pool.d.ts +30 -0
- package/lib/providers/image-pool.js +89 -0
- package/lib/providers/npm-cli-version.d.ts +56 -0
- package/lib/providers/npm-cli-version.js +98 -0
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +91 -0
- package/lib/providers/pool-health.js +164 -0
- package/lib/providers/pool-usage.d.ts +106 -0
- package/lib/providers/pool-usage.js +302 -0
- package/lib/providers/pool.d.ts +115 -0
- package/lib/providers/pool.js +461 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +37 -0
- package/lib/providers/reset-redemption.d.ts +20 -0
- package/lib/providers/reset-redemption.js +70 -0
- package/lib/tools/image-generate.d.ts +128 -0
- package/lib/tools/image-generate.js +438 -0
- package/lib/tools/registration.d.ts +15 -0
- package/lib/tools/registration.js +24 -0
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +257 -0
- package/lib/tools/x-search.d.ts +58 -0
- package/lib/tools/x-search.js +197 -0
- package/lib/translate/anthropic.d.ts +180 -0
- package/lib/translate/anthropic.js +534 -0
- package/lib/translate/antigravity-images.d.ts +11 -0
- package/lib/translate/antigravity-images.js +62 -0
- package/lib/translate/antigravity-schema.d.ts +4 -0
- package/lib/translate/antigravity-schema.js +145 -0
- package/lib/translate/antigravity-thinking.d.ts +5 -0
- package/lib/translate/antigravity-thinking.js +44 -0
- package/lib/translate/antigravity.d.ts +120 -0
- package/lib/translate/antigravity.js +412 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +382 -0
- package/lib/translate/resolved.d.ts +98 -0
- package/lib/translate/resolved.js +186 -0
- package/lib/translate/responses.d.ts +192 -0
- package/lib/translate/responses.js +443 -0
- package/lib/translate/sse.d.ts +21 -0
- package/lib/translate/sse.js +62 -0
- package/lib/usage/headers.d.ts +21 -0
- package/lib/usage/headers.js +64 -0
- package/lib/usage/pace.d.ts +53 -0
- package/lib/usage/pace.js +48 -0
- package/lib/usage/service.d.ts +64 -0
- package/lib/usage/service.js +104 -0
- package/lib/usage-providers/common.d.ts +23 -0
- package/lib/usage-providers/common.js +28 -0
- package/lib/usage-providers/index.d.ts +42 -0
- package/lib/usage-providers/index.js +71 -0
- package/lib/usage-providers/keys.d.ts +11 -0
- package/lib/usage-providers/keys.js +52 -0
- package/lib/usage-providers/kimi.d.ts +4 -0
- package/lib/usage-providers/kimi.js +52 -0
- package/lib/usage-providers/ollama-cloud.d.ts +4 -0
- package/lib/usage-providers/ollama-cloud.js +42 -0
- package/lib/usage-providers/zai.d.ts +5 -0
- package/lib/usage-providers/zai.js +53 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +115 -4
package/lib/json-text.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ProviderId } from './auth/store.js';
|
|
2
|
+
/** One model id → its configured default reasoning effort id. */
|
|
3
|
+
export type ModelDefaultMap = Readonly<Record<string, string>>;
|
|
4
|
+
/** Provider route → model defaults. */
|
|
5
|
+
export type ModelDefaults = Readonly<Partial<Record<ProviderId, ModelDefaultMap>>>;
|
|
6
|
+
/** Absolute path of the defaults file. */
|
|
7
|
+
export declare function modelDefaultsFilePath(): string;
|
|
8
|
+
/**
|
|
9
|
+
* The last load failure, or a warning about entries that were skipped while
|
|
10
|
+
* loading; consumers only use it for diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
export declare function modelDefaultsLoadError(): unknown;
|
|
13
|
+
/** A detached snapshot for the RPC surface (render + diffing). */
|
|
14
|
+
export declare function modelDefaultsSnapshot(): ModelDefaults;
|
|
15
|
+
/**
|
|
16
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
17
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
18
|
+
* write never leaves the live state ahead of the file.
|
|
19
|
+
* @param provider - the subscription provider route.
|
|
20
|
+
* @param model - the wire model id.
|
|
21
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
22
|
+
*/
|
|
23
|
+
export declare function setDefaultEffort(provider: ProviderId, model: string, effort: string | undefined): Promise<void>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model default reasoning effort overrides — the durable half of the
|
|
3
|
+
* Settings page's per-model "default effort" pickers.
|
|
4
|
+
*
|
|
5
|
+
* The file lives at `~/.dsh/plugins/subscriptions/model-defaults.json`
|
|
6
|
+
* (mode 0600, atomic replace). Shape: `{ "<provider>": { "<model id>": "<effort>" } }`.
|
|
7
|
+
* An absent entry means "follow the provider's own default": the `Default`
|
|
8
|
+
* chip the model picker shows when the discovered catalog advertises no
|
|
9
|
+
* default at all.
|
|
10
|
+
*
|
|
11
|
+
* Writes are single-process and atomic, but *not* as serialised as the rest
|
|
12
|
+
* of the page: the Settings page disables only the row being saved, so two
|
|
13
|
+
* rows saved back to back can overlap. The write chain below serialises them,
|
|
14
|
+
* so no update is lost to a read-modify-write race. Every read comes from the
|
|
15
|
+
* in-memory snapshot, so the on-disk file only needs to survive a restart: a
|
|
16
|
+
* malformed file reads as empty and is rewritten on the next save, never
|
|
17
|
+
* taking the plugin down with it.
|
|
18
|
+
*/
|
|
19
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
20
|
+
import { dirname } from 'node:path';
|
|
21
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
22
|
+
import { PROVIDER_IDS } from './auth/store.js';
|
|
23
|
+
import { parseJsonText } from './json-text.js';
|
|
24
|
+
/** Absolute path of the defaults file. */
|
|
25
|
+
export function modelDefaultsFilePath() {
|
|
26
|
+
return dshHomePath('plugins', 'subscriptions', 'model-defaults.json');
|
|
27
|
+
}
|
|
28
|
+
const EMPTY = Object.freeze({});
|
|
29
|
+
/** In-memory snapshot read by every consumer (adapters, RPC). */
|
|
30
|
+
let current = EMPTY;
|
|
31
|
+
/** One lazy load of the on-disk file (read once per process). */
|
|
32
|
+
let ready;
|
|
33
|
+
/** Last load failure, surfaced to callers that care; defaults stay empty. */
|
|
34
|
+
let loadError;
|
|
35
|
+
/**
|
|
36
|
+
* Serialises every write: the read-modify-write sequence must not interleave,
|
|
37
|
+
* or a fast second save would compute its snapshot from the stale `current`
|
|
38
|
+
* and silently drop the first update (the UI disables only the row being
|
|
39
|
+
* saved, so overlaps are reachable).
|
|
40
|
+
*/
|
|
41
|
+
let writeChain = Promise.resolve();
|
|
42
|
+
/**
|
|
43
|
+
* Validate one persisted provider section: a string→string map, or undefined.
|
|
44
|
+
* Malformed *entries* are skipped, not the whole section: one bad value (a
|
|
45
|
+
* hand edit losing its quotes) must not silently un-configure every model in
|
|
46
|
+
* that provider. What was dropped is reported so the caller can surface it
|
|
47
|
+
* instead of the loss disappearing.
|
|
48
|
+
*/
|
|
49
|
+
function sanitizeProvider(value, dropped) {
|
|
50
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
51
|
+
return undefined;
|
|
52
|
+
const entries = {};
|
|
53
|
+
for (const [model, effort] of Object.entries(value)) {
|
|
54
|
+
if (typeof effort !== 'string' || effort.length === 0) {
|
|
55
|
+
dropped.push(model);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
entries[model] = effort;
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(entries).length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return Object.freeze(entries);
|
|
63
|
+
}
|
|
64
|
+
/** Validate the raw document: only known providers, malformed sections dropped. */
|
|
65
|
+
function sanitizeDefaults(value) {
|
|
66
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
67
|
+
return { defaults: EMPTY, dropped: [] };
|
|
68
|
+
const record = value;
|
|
69
|
+
const result = {};
|
|
70
|
+
const dropped = [];
|
|
71
|
+
for (const provider of PROVIDER_IDS) {
|
|
72
|
+
const section = sanitizeProvider(record[provider], dropped);
|
|
73
|
+
if (section !== undefined)
|
|
74
|
+
result[provider] = section;
|
|
75
|
+
}
|
|
76
|
+
return { defaults: Object.freeze(result), dropped };
|
|
77
|
+
}
|
|
78
|
+
/** Read and validate the on-disk file; a missing file reads as empty. */
|
|
79
|
+
async function loadFile(path) {
|
|
80
|
+
let text;
|
|
81
|
+
try {
|
|
82
|
+
text = await readFile(path, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code === 'ENOENT')
|
|
86
|
+
return EMPTY;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { defaults, dropped } = sanitizeDefaults(parseJsonText(text));
|
|
91
|
+
if (dropped.length > 0)
|
|
92
|
+
loadError = new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? 'y' : 'ies'} skipped (${dropped.join(', ')}); fix or delete the file`);
|
|
93
|
+
return defaults;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Resolve the module state once from disk; failures leave the defaults empty. */
|
|
100
|
+
async function ensureReady() {
|
|
101
|
+
ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
|
|
102
|
+
current = loaded;
|
|
103
|
+
// loadFile itself sets loadError for skipped entries; do not clobber it.
|
|
104
|
+
}, (error) => {
|
|
105
|
+
loadError = error;
|
|
106
|
+
current = EMPTY;
|
|
107
|
+
});
|
|
108
|
+
return ready;
|
|
109
|
+
}
|
|
110
|
+
/** Persist a snapshot atomically with owner-only permissions. */
|
|
111
|
+
async function atomicPersist(defaults, path) {
|
|
112
|
+
await mkdir(dirname(path), { recursive: true });
|
|
113
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
114
|
+
try {
|
|
115
|
+
await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 0o600 });
|
|
116
|
+
await chmod(tmp, 0o600);
|
|
117
|
+
await rename(tmp, path);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
await rm(tmp, { force: true });
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let persistDefaults = atomicPersist;
|
|
125
|
+
/**
|
|
126
|
+
* Clone one provider section, or undefined when nothing is configured for it.
|
|
127
|
+
* The clone is prototype-less: model ids are provider-supplied catalog data
|
|
128
|
+
* used as object keys, and consumers index the section directly (the RPC
|
|
129
|
+
* catalog in index.ts does), so an id like `toString` would otherwise yield an
|
|
130
|
+
* inherited *function* where a string is declared.
|
|
131
|
+
*/
|
|
132
|
+
function sectionOf(defaults, provider) {
|
|
133
|
+
const section = defaults[provider];
|
|
134
|
+
if (section === undefined)
|
|
135
|
+
return undefined;
|
|
136
|
+
return Object.assign(Object.create(null), section);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Ready the defaults store.
|
|
140
|
+
* @internal Exported for tests; index.ts calls it at apply time so every
|
|
141
|
+
* later synchronous read sees the persisted state.
|
|
142
|
+
*/
|
|
143
|
+
export async function loadModelDefaults() {
|
|
144
|
+
await ensureReady();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The last load failure, or a warning about entries that were skipped while
|
|
148
|
+
* loading; consumers only use it for diagnostics.
|
|
149
|
+
*/
|
|
150
|
+
export function modelDefaultsLoadError() {
|
|
151
|
+
return loadError;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The configured default effort for one model, or undefined when none (the
|
|
155
|
+
* picker then follows the provider's own default).
|
|
156
|
+
* @internal Exported for the adapters' `defaultEffortOf` options.
|
|
157
|
+
*/
|
|
158
|
+
export function defaultEffortOf(provider, model) {
|
|
159
|
+
const section = current[provider];
|
|
160
|
+
if (section === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
// Own-property lookup: a model id is provider-supplied catalog data, and a
|
|
163
|
+
// plain index would inherit from Object.prototype for names like
|
|
164
|
+
// `toString`, handing a *function* to mergeReasoning (which then throws and
|
|
165
|
+
// breaks that model's resolution).
|
|
166
|
+
return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : undefined;
|
|
167
|
+
}
|
|
168
|
+
/** A detached snapshot for the RPC surface (render + diffing). */
|
|
169
|
+
export function modelDefaultsSnapshot() {
|
|
170
|
+
const result = {};
|
|
171
|
+
for (const provider of PROVIDER_IDS) {
|
|
172
|
+
const section = sectionOf(current, provider);
|
|
173
|
+
if (section !== undefined)
|
|
174
|
+
result[provider] = section;
|
|
175
|
+
}
|
|
176
|
+
return Object.freeze(result);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
180
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
181
|
+
* write never leaves the live state ahead of the file.
|
|
182
|
+
* @param provider - the subscription provider route.
|
|
183
|
+
* @param model - the wire model id.
|
|
184
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
185
|
+
*/
|
|
186
|
+
export function setDefaultEffort(provider, model, effort) {
|
|
187
|
+
// Chained behind every earlier write: the snapshot `current` is read inside
|
|
188
|
+
// the chain, so two overlapping saves cannot lose either update. The caller
|
|
189
|
+
// receives the promise of its own write (a rejection propagates), not the
|
|
190
|
+
// shared chain.
|
|
191
|
+
const run = writeChain.then(async () => {
|
|
192
|
+
await ensureReady();
|
|
193
|
+
const section = { ...sectionOf(current, provider) ?? {} };
|
|
194
|
+
if (effort === undefined) {
|
|
195
|
+
delete section[model];
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
section[model] = effort;
|
|
199
|
+
}
|
|
200
|
+
const next = { ...current };
|
|
201
|
+
if (Object.keys(section).length === 0) {
|
|
202
|
+
delete next[provider];
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
next[provider] = Object.freeze(section);
|
|
206
|
+
}
|
|
207
|
+
const frozen = Object.freeze(next);
|
|
208
|
+
await persistDefaults(frozen, modelDefaultsFilePath());
|
|
209
|
+
current = frozen;
|
|
210
|
+
});
|
|
211
|
+
// Keep the chain alive even when one write fails, or every later save would
|
|
212
|
+
// be stuck behind the rejected promise. The caller has already received the
|
|
213
|
+
// rejection through `run`.
|
|
214
|
+
writeChain = run.catch(() => undefined);
|
|
215
|
+
return run;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Drop the in-memory state and the cached load. Test-only: lets a suite
|
|
219
|
+
* unwind the lazy singleton before the next `loadModelDefaults`.
|
|
220
|
+
* @internal Exported for tests only; not part of the plugin's public surface.
|
|
221
|
+
*/
|
|
222
|
+
export async function resetModelDefaultsForTests() {
|
|
223
|
+
current = EMPTY;
|
|
224
|
+
ready = undefined;
|
|
225
|
+
loadError = undefined;
|
|
226
|
+
writeChain = Promise.resolve();
|
|
227
|
+
persistDefaults = atomicPersist;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Test-only seam: replace the atomic persistence so a failure happens on the
|
|
231
|
+
* real write path. Proves a failed write propagates to the caller and does
|
|
232
|
+
* not wedge the write chain (resetModelDefaultsForTests restores the real
|
|
233
|
+
* implementation).
|
|
234
|
+
* @internal Exported for tests only.
|
|
235
|
+
*/
|
|
236
|
+
export function overridePersistForTests(persist) {
|
|
237
|
+
persistDefaults = persist;
|
|
238
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SpeedTier } from './auth/rpc.js';
|
|
2
|
+
/** Per-model composer preferences; publish changes only after the atomic save. */
|
|
3
|
+
export declare class ModelSpeeds {
|
|
4
|
+
private readonly path;
|
|
5
|
+
private current;
|
|
6
|
+
private writes;
|
|
7
|
+
constructor(path?: string);
|
|
8
|
+
get(model: string): SpeedTier | undefined;
|
|
9
|
+
set(model: string, tier: SpeedTier): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Durable composer speed choices, separate from Settings-page subagent defaults. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
6
|
+
/** Per-model composer preferences; publish changes only after the atomic save. */
|
|
7
|
+
export class ModelSpeeds {
|
|
8
|
+
path;
|
|
9
|
+
current = new Map();
|
|
10
|
+
writes = Promise.resolve();
|
|
11
|
+
constructor(path = dshHomePath('plugins', 'subscriptions', 'model-speeds.json')) {
|
|
12
|
+
this.path = path;
|
|
13
|
+
try {
|
|
14
|
+
const raw = JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''));
|
|
15
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
16
|
+
throw new Error('expected model-to-speed map');
|
|
17
|
+
for (const [model, tier] of Object.entries(raw)) {
|
|
18
|
+
if (!model.trim() || (tier !== 'standard' && tier !== 'fast'))
|
|
19
|
+
throw new Error('invalid model speed');
|
|
20
|
+
this.current.set(model, tier);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== 'ENOENT')
|
|
25
|
+
throw new Error(`Cannot read model speeds at ${path}`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
get(model) {
|
|
29
|
+
return this.current.get(model);
|
|
30
|
+
}
|
|
31
|
+
set(model, tier) {
|
|
32
|
+
const run = this.writes.then(async () => {
|
|
33
|
+
const next = new Map(this.current).set(model, tier);
|
|
34
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
35
|
+
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(temporary, JSON.stringify(Object.fromEntries(next), null, 2), { mode: 0o600 });
|
|
38
|
+
await rename(temporary, this.path);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await rm(temporary, { force: true });
|
|
42
|
+
}
|
|
43
|
+
this.current = next;
|
|
44
|
+
});
|
|
45
|
+
this.writes = run.catch(() => undefined);
|
|
46
|
+
return run;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The configurable-provider entries that make each subscription a row on the
|
|
3
|
+
* DSH Models settings page. The Models page joins `llm.listConfigurableProviders`
|
|
4
|
+
* with live routes and renders our `settings.models.provider-card` slot in every
|
|
5
|
+
* row whose `settingsNs` is this plugin's entry id.
|
|
6
|
+
*/
|
|
7
|
+
import type { LlmConfigurableProvider } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import type { ProviderId } from './auth/store.js';
|
|
9
|
+
/** Row titles on the Models page. */
|
|
10
|
+
export declare const PROVIDER_DISPLAY_NAMES: Record<ProviderId, string>;
|
|
11
|
+
/**
|
|
12
|
+
* Directory entries for the enabled providers.
|
|
13
|
+
* @param providers - enabled provider ids in display order.
|
|
14
|
+
* @param settingsNs - this plugin's Loader entry id; the Models page dispatches the card slot by it.
|
|
15
|
+
* @returns one entry per provider, OAuth-authenticated with no API-key variables.
|
|
16
|
+
*/
|
|
17
|
+
export declare function directoryEntries(providers: readonly ProviderId[], settingsNs: string): LlmConfigurableProvider[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Row titles on the Models page. */
|
|
2
|
+
export const PROVIDER_DISPLAY_NAMES = {
|
|
3
|
+
codex: 'ChatGPT (Codex)',
|
|
4
|
+
claude: 'Claude',
|
|
5
|
+
grok: 'Grok',
|
|
6
|
+
copilot: 'GitHub Copilot',
|
|
7
|
+
antigravity: 'Google Antigravity',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Directory entries for the enabled providers.
|
|
11
|
+
* @param providers - enabled provider ids in display order.
|
|
12
|
+
* @param settingsNs - this plugin's Loader entry id; the Models page dispatches the card slot by it.
|
|
13
|
+
* @returns one entry per provider, OAuth-authenticated with no API-key variables.
|
|
14
|
+
*/
|
|
15
|
+
export function directoryEntries(providers, settingsNs) {
|
|
16
|
+
return providers.map(provider => ({
|
|
17
|
+
provider,
|
|
18
|
+
displayName: PROVIDER_DISPLAY_NAMES[provider],
|
|
19
|
+
settingsNs,
|
|
20
|
+
// The row's settings address. An empty path marks the row configured once
|
|
21
|
+
// the namespace is described, which our volatile `usage` field ensures.
|
|
22
|
+
settingsPath: [],
|
|
23
|
+
nativeAuth: { env: [], oauth: true },
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ProviderId } from './auth/store.js';
|
|
2
|
+
export declare const PROVIDER_TOOLS: {
|
|
3
|
+
readonly codex: readonly ["image_generate", "web_search"];
|
|
4
|
+
readonly claude: readonly [];
|
|
5
|
+
readonly grok: readonly ["image_generate", "video_generate", "x_search"];
|
|
6
|
+
readonly copilot: readonly [];
|
|
7
|
+
readonly antigravity: readonly [];
|
|
8
|
+
};
|
|
9
|
+
export type SubscriptionTool = 'image_generate' | 'video_generate' | 'x_search' | 'web_search';
|
|
10
|
+
/** Mirrors `PromptCacheTtl` in translate/anthropic.ts; kept here so the client bundle needs no translate import. */
|
|
11
|
+
export declare const PROMPT_CACHE_TTLS: readonly ["5m", "1h"];
|
|
12
|
+
export type PromptCacheTtlSetting = typeof PROMPT_CACHE_TTLS[number];
|
|
13
|
+
export type SubagentSpeedTier = 'standard' | 'fast';
|
|
14
|
+
export interface AccountPreferences {
|
|
15
|
+
alias?: string;
|
|
16
|
+
/** Absent means included. */
|
|
17
|
+
poolEnabled?: boolean;
|
|
18
|
+
/** Absent means no separate picker entries. */
|
|
19
|
+
independentEntry?: boolean;
|
|
20
|
+
/** Absent allows every model; [] allows none. */
|
|
21
|
+
poolModels?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface ProviderPreferences {
|
|
24
|
+
/** Presentation only; never used as a provider or model routing key. */
|
|
25
|
+
displayName?: string;
|
|
26
|
+
modelDisplayNames?: Record<string, string>;
|
|
27
|
+
accounts?: Record<string, AccountPreferences>;
|
|
28
|
+
/** Absent follows discovery; an explicit selection hides newly discovered models. */
|
|
29
|
+
visibleModels?: string[];
|
|
30
|
+
contextWindows?: Record<string, number>;
|
|
31
|
+
/** Codex only: per-model defaults for subagents, independent of main-session speed. */
|
|
32
|
+
subagentSpeeds?: Record<string, SubagentSpeedTier>;
|
|
33
|
+
tools?: Partial<Record<SubscriptionTool, boolean>>;
|
|
34
|
+
/** Claude only: overrides the plugin config's `claudePromptCacheTtl`; absent follows config. */
|
|
35
|
+
promptCacheTtl?: PromptCacheTtlSetting;
|
|
36
|
+
}
|
|
37
|
+
export declare function validatePreferences(provider: ProviderId, input: unknown): ProviderPreferences;
|
|
38
|
+
/** Durable user preferences, independent of expiring discovery caches. */
|
|
39
|
+
export declare class ProviderSettingsStore {
|
|
40
|
+
private current;
|
|
41
|
+
private writes;
|
|
42
|
+
readonly path: string;
|
|
43
|
+
constructor(path?: string);
|
|
44
|
+
get(provider: ProviderId): ProviderPreferences;
|
|
45
|
+
visible(provider: ProviderId, model: string): boolean;
|
|
46
|
+
modelName(provider: ProviderId, model: string, fallback: string): string;
|
|
47
|
+
contextWindow(model: string): number | undefined;
|
|
48
|
+
/** Read on every request so saved changes apply to the next subagent request. */
|
|
49
|
+
subagentSpeed(model: string): SubagentSpeedTier;
|
|
50
|
+
/** The Settings-page Claude cache TTL; undefined follows the plugin config. Read per request, so no clone. */
|
|
51
|
+
promptCacheTtl(): PromptCacheTtlSetting | undefined;
|
|
52
|
+
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
53
|
+
toolEnabled(provider: ProviderId, tool: SubscriptionTool, createdAt?: number): boolean;
|
|
54
|
+
set(provider: ProviderId, input: unknown): Promise<void>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
import { PROVIDER_IDS } from './auth/store.js';
|
|
6
|
+
import { parseJsonText } from './json-text.js';
|
|
7
|
+
export const PROVIDER_TOOLS = {
|
|
8
|
+
codex: ['image_generate', 'web_search'],
|
|
9
|
+
claude: [],
|
|
10
|
+
grok: ['image_generate', 'video_generate', 'x_search'],
|
|
11
|
+
copilot: [],
|
|
12
|
+
antigravity: [],
|
|
13
|
+
};
|
|
14
|
+
/** Mirrors `PromptCacheTtl` in translate/anthropic.ts; kept here so the client bundle needs no translate import. */
|
|
15
|
+
export const PROMPT_CACHE_TTLS = ['5m', '1h'];
|
|
16
|
+
export function validatePreferences(provider, input) {
|
|
17
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
18
|
+
throw new Error('settings must be an object');
|
|
19
|
+
const raw = input;
|
|
20
|
+
const result = {};
|
|
21
|
+
const nameOf = (value) => {
|
|
22
|
+
if (typeof value !== 'string' || value.trim().length > 80 || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
23
|
+
throw new Error('display name must be a single line of at most 80 characters');
|
|
24
|
+
}
|
|
25
|
+
return value.trim();
|
|
26
|
+
};
|
|
27
|
+
if (raw.displayName !== undefined) {
|
|
28
|
+
const name = nameOf(raw.displayName);
|
|
29
|
+
if (name)
|
|
30
|
+
result.displayName = name;
|
|
31
|
+
}
|
|
32
|
+
if (raw.modelDisplayNames !== undefined) {
|
|
33
|
+
if (!raw.modelDisplayNames || typeof raw.modelDisplayNames !== 'object' || Array.isArray(raw.modelDisplayNames)) {
|
|
34
|
+
throw new Error('modelDisplayNames must be a model-to-name map');
|
|
35
|
+
}
|
|
36
|
+
const names = Object.create(null);
|
|
37
|
+
for (const [id, value] of Object.entries(raw.modelDisplayNames)) {
|
|
38
|
+
if (!id.trim())
|
|
39
|
+
throw new Error('model display name requires a model id');
|
|
40
|
+
const name = nameOf(value);
|
|
41
|
+
if (name)
|
|
42
|
+
names[id] = name;
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(names).length)
|
|
45
|
+
result.modelDisplayNames = names;
|
|
46
|
+
}
|
|
47
|
+
if (raw.accounts !== undefined) {
|
|
48
|
+
if (!raw.accounts || typeof raw.accounts !== 'object' || Array.isArray(raw.accounts))
|
|
49
|
+
throw new Error('accounts must be an account-to-preferences map');
|
|
50
|
+
result.accounts = Object.create(null);
|
|
51
|
+
for (const [key, value] of Object.entries(raw.accounts)) {
|
|
52
|
+
if (!key.trim() || !value || typeof value !== 'object' || Array.isArray(value))
|
|
53
|
+
throw new Error('invalid account preferences');
|
|
54
|
+
const account = value;
|
|
55
|
+
const parsed = {};
|
|
56
|
+
if (account.alias !== undefined) {
|
|
57
|
+
if (typeof account.alias !== 'string')
|
|
58
|
+
throw new Error('account alias must be a string');
|
|
59
|
+
parsed.alias = account.alias.trim();
|
|
60
|
+
}
|
|
61
|
+
for (const field of ['poolEnabled', 'independentEntry']) {
|
|
62
|
+
if (account[field] !== undefined) {
|
|
63
|
+
if (typeof account[field] !== 'boolean')
|
|
64
|
+
throw new Error(`${field} must be a boolean`);
|
|
65
|
+
parsed[field] = account[field];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (account.poolModels !== undefined) {
|
|
69
|
+
if (!Array.isArray(account.poolModels) || account.poolModels.some(id => typeof id !== 'string' || !id.trim()))
|
|
70
|
+
throw new Error('poolModels must be an array of model ids');
|
|
71
|
+
parsed.poolModels = [...new Set(account.poolModels)];
|
|
72
|
+
}
|
|
73
|
+
result.accounts[key] = parsed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (raw.visibleModels !== undefined) {
|
|
77
|
+
if (!Array.isArray(raw.visibleModels) || raw.visibleModels.some(id => typeof id !== 'string' || !id.trim())) {
|
|
78
|
+
throw new Error('visibleModels must be an array of model ids');
|
|
79
|
+
}
|
|
80
|
+
result.visibleModels = [...new Set(raw.visibleModels)];
|
|
81
|
+
}
|
|
82
|
+
if (raw.contextWindows !== undefined) {
|
|
83
|
+
if (provider !== 'codex')
|
|
84
|
+
throw new Error('context window overrides are currently supported only for Codex');
|
|
85
|
+
if (!raw.contextWindows || typeof raw.contextWindows !== 'object' || Array.isArray(raw.contextWindows)) {
|
|
86
|
+
throw new Error('contextWindows must be a model-to-token map');
|
|
87
|
+
}
|
|
88
|
+
result.contextWindows = Object.create(null);
|
|
89
|
+
for (const [model, value] of Object.entries(raw.contextWindows)) {
|
|
90
|
+
if (!model.trim() || typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
91
|
+
throw new Error('context windows must be positive safe integers');
|
|
92
|
+
}
|
|
93
|
+
result.contextWindows[model] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (raw.promptCacheTtl !== undefined) {
|
|
97
|
+
if (provider !== 'claude')
|
|
98
|
+
throw new Error('promptCacheTtl is currently supported only for Claude');
|
|
99
|
+
if (!PROMPT_CACHE_TTLS.includes(raw.promptCacheTtl)) {
|
|
100
|
+
throw new Error(`promptCacheTtl must be one of ${PROMPT_CACHE_TTLS.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
result.promptCacheTtl = raw.promptCacheTtl;
|
|
103
|
+
}
|
|
104
|
+
if (raw.subagentSpeeds !== undefined) {
|
|
105
|
+
if (provider !== 'codex')
|
|
106
|
+
throw new Error('subagent speeds are currently supported only for Codex');
|
|
107
|
+
if (!raw.subagentSpeeds || typeof raw.subagentSpeeds !== 'object' || Array.isArray(raw.subagentSpeeds)) {
|
|
108
|
+
throw new Error('subagent speeds must be a model-to-speed map');
|
|
109
|
+
}
|
|
110
|
+
result.subagentSpeeds = Object.create(null);
|
|
111
|
+
for (const [model, tier] of Object.entries(raw.subagentSpeeds)) {
|
|
112
|
+
if (!model.trim() || (tier !== 'standard' && tier !== 'fast')) {
|
|
113
|
+
throw new Error('subagent speeds require a model id and either "standard" or "fast"');
|
|
114
|
+
}
|
|
115
|
+
result.subagentSpeeds[model] = tier;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (raw.tools !== undefined) {
|
|
119
|
+
if (!raw.tools || typeof raw.tools !== 'object' || Array.isArray(raw.tools))
|
|
120
|
+
throw new Error('tools must be an object');
|
|
121
|
+
result.tools = {};
|
|
122
|
+
for (const [tool, enabled] of Object.entries(raw.tools)) {
|
|
123
|
+
if (!PROVIDER_TOOLS[provider].includes(tool) || typeof enabled !== 'boolean') {
|
|
124
|
+
throw new Error(`unsupported tool setting: ${provider}/${tool}`);
|
|
125
|
+
}
|
|
126
|
+
result.tools[tool] = enabled;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/** Durable user preferences, independent of expiring discovery caches. */
|
|
132
|
+
export class ProviderSettingsStore {
|
|
133
|
+
current = { providers: {}, toolHistory: [] };
|
|
134
|
+
writes = Promise.resolve();
|
|
135
|
+
path;
|
|
136
|
+
constructor(path = dshHomePath('plugins', 'subscriptions', 'provider-settings.json')) {
|
|
137
|
+
this.path = path;
|
|
138
|
+
try {
|
|
139
|
+
const raw = parseJsonText(readFileSync(path, 'utf8'));
|
|
140
|
+
if (!raw.providers || !Array.isArray(raw.toolHistory))
|
|
141
|
+
throw new Error('invalid settings document');
|
|
142
|
+
for (const provider of PROVIDER_IDS) {
|
|
143
|
+
if (Object.hasOwn(raw.providers, provider))
|
|
144
|
+
this.current.providers[provider] = validatePreferences(provider, raw.providers[provider]);
|
|
145
|
+
}
|
|
146
|
+
this.current.toolHistory = raw.toolHistory.map(revision => {
|
|
147
|
+
if (!PROVIDER_IDS.includes(revision.provider) || !Number.isSafeInteger(revision.at) || revision.at < 0)
|
|
148
|
+
throw new Error('invalid tool history');
|
|
149
|
+
const tools = validatePreferences(revision.provider, { tools: revision.tools }).tools ?? {};
|
|
150
|
+
return { at: revision.at, provider: revision.provider, tools };
|
|
151
|
+
}).sort((a, b) => a.at - b.at);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== 'ENOENT')
|
|
155
|
+
throw new Error(`Cannot read provider settings at ${path}`, { cause: error });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
get(provider) {
|
|
159
|
+
return structuredClone(this.current.providers[provider] ?? {});
|
|
160
|
+
}
|
|
161
|
+
visible(provider, model) {
|
|
162
|
+
return this.current.providers[provider]?.visibleModels?.includes(model) ?? true;
|
|
163
|
+
}
|
|
164
|
+
modelName(provider, model, fallback) {
|
|
165
|
+
const names = this.current.providers[provider]?.modelDisplayNames;
|
|
166
|
+
return names && Object.hasOwn(names, model) ? names[model] : fallback;
|
|
167
|
+
}
|
|
168
|
+
contextWindow(model) {
|
|
169
|
+
const windows = this.current.providers.codex?.contextWindows;
|
|
170
|
+
return windows && Object.hasOwn(windows, model) ? windows[model] : undefined;
|
|
171
|
+
}
|
|
172
|
+
/** Read on every request so saved changes apply to the next subagent request. */
|
|
173
|
+
subagentSpeed(model) {
|
|
174
|
+
const speeds = this.current.providers.codex?.subagentSpeeds;
|
|
175
|
+
return speeds && Object.hasOwn(speeds, model) ? speeds[model] : 'standard';
|
|
176
|
+
}
|
|
177
|
+
/** The Settings-page Claude cache TTL; undefined follows the plugin config. Read per request, so no clone. */
|
|
178
|
+
promptCacheTtl() {
|
|
179
|
+
return this.current.providers.claude?.promptCacheTtl;
|
|
180
|
+
}
|
|
181
|
+
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
182
|
+
toolEnabled(provider, tool, createdAt = Date.now()) {
|
|
183
|
+
let enabled = true;
|
|
184
|
+
for (const revision of this.current.toolHistory) {
|
|
185
|
+
if (revision.at > createdAt)
|
|
186
|
+
break;
|
|
187
|
+
if (revision.provider === provider)
|
|
188
|
+
enabled = revision.tools[tool] !== false;
|
|
189
|
+
}
|
|
190
|
+
return enabled;
|
|
191
|
+
}
|
|
192
|
+
set(provider, input) {
|
|
193
|
+
const preferences = validatePreferences(provider, input);
|
|
194
|
+
const run = this.writes.then(async () => {
|
|
195
|
+
const next = structuredClone(this.current);
|
|
196
|
+
const before = next.providers[provider]?.tools ?? {};
|
|
197
|
+
next.providers[provider] = preferences;
|
|
198
|
+
if (PROVIDER_TOOLS[provider].some(tool => (before[tool] !== false) !== (preferences.tools?.[tool] !== false))) {
|
|
199
|
+
next.toolHistory.push({ at: Date.now(), provider, tools: preferences.tools ?? {} });
|
|
200
|
+
}
|
|
201
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
202
|
+
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
203
|
+
try {
|
|
204
|
+
await writeFile(temporary, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
205
|
+
await rename(temporary, this.path);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await rm(temporary, { force: true });
|
|
209
|
+
}
|
|
210
|
+
this.current = next;
|
|
211
|
+
});
|
|
212
|
+
this.writes = run.catch(() => undefined);
|
|
213
|
+
return run;
|
|
214
|
+
}
|
|
215
|
+
}
|