dsh-subscriptions 0.1.5 → 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/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 -0
- package/lib/client/SubscriptionsSection.js +2 -1
- 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 +3191 -2133
- package/lib/provider-settings.d.ts +2 -1
- package/lib/provider-settings.js +6 -4
- package/lib/providers/claude.d.ts +14 -1
- package/lib/providers/claude.js +19 -7
- package/lib/providers/common.d.ts +14 -0
- package/lib/providers/common.js +48 -0
- 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 +92 -8
- 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/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { callSubscriptionsAuth } from './subscriptions-rpc.js';
|
|
4
|
+
import { contextLabel, effortOptions, moveModel, parseContextInput, renameModel, setModelContext, setModelEffort, tickModel, tickModels, untickModel, untickModels, } from './ollama-draft.js';
|
|
5
|
+
import { subscriptionCardStyles as styles } from './subscription-card-styles.js';
|
|
6
|
+
import { providerSettingsCss } from './provider-settings-styles.js';
|
|
7
|
+
const border = '0.5px solid var(--dsw-alias-border-l2)';
|
|
8
|
+
const stack = { display: 'grid', gap: 12, minWidth: 0 };
|
|
9
|
+
const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
|
|
10
|
+
function message(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
/** `vision · thinking · context 262K`, dropping whatever the model does not advertise. */
|
|
14
|
+
function capabilities(t, model) {
|
|
15
|
+
const context = contextLabel(model.contextWindow);
|
|
16
|
+
return [
|
|
17
|
+
model.vision === true ? t('ollamaCapVision') : false,
|
|
18
|
+
model.thinking === true ? t('ollamaCapThinking') : false,
|
|
19
|
+
context === undefined ? false : t('ollamaContext', { tokens: context }),
|
|
20
|
+
].filter(Boolean).join(' · ');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ollama Cloud settings dialog. It owns one draft (model list, web toggle,
|
|
24
|
+
* base URL and credential reference) that Save writes in a single
|
|
25
|
+
* `ollamaSetSettings` call, so a failure keeps every edit. Fetching the cloud
|
|
26
|
+
* catalog is a read-only side trip, and a key typed for the fetch never
|
|
27
|
+
* leaves this component. The model list follows the shared model editor: a
|
|
28
|
+
* filter, select all/none over the filtered rows, and one checkbox per
|
|
29
|
+
* model, where ticking edits the draft and enables Save. Ollama adds what the
|
|
30
|
+
* shared editor lacks: a display name, ordering, and a local context window.
|
|
31
|
+
* Unticking keeps the row's full settings in a dialog-local stash, so ticking
|
|
32
|
+
* it again restores them even when the cloud listing no longer returns it.
|
|
33
|
+
*/
|
|
34
|
+
export function OllamaSettingsManager({ rpc, t, onClose, onSaved }) {
|
|
35
|
+
const dialog = useRef(null);
|
|
36
|
+
const title = useId();
|
|
37
|
+
const description = useId();
|
|
38
|
+
const [view, setView] = useState();
|
|
39
|
+
const [draft, setDraft] = useState({});
|
|
40
|
+
const [dirty, setDirty] = useState(false);
|
|
41
|
+
const [loading, setLoading] = useState(true);
|
|
42
|
+
const [saving, setSaving] = useState(false);
|
|
43
|
+
const [error, setError] = useState('');
|
|
44
|
+
const [saveError, setSaveError] = useState('');
|
|
45
|
+
const [attempt, setAttempt] = useState(0);
|
|
46
|
+
const [apiKey, setApiKey] = useState('');
|
|
47
|
+
const [candidates, setCandidates] = useState();
|
|
48
|
+
const [fetching, setFetching] = useState(false);
|
|
49
|
+
const [fetchError, setFetchError] = useState('');
|
|
50
|
+
const [query, setQuery] = useState('');
|
|
51
|
+
const [expanded, setExpanded] = useState(new Set());
|
|
52
|
+
const [stash, setStash] = useState(new Map());
|
|
53
|
+
const [contextText, setContextText] = useState({});
|
|
54
|
+
const alive = useRef(true);
|
|
55
|
+
const saveLock = useRef(false);
|
|
56
|
+
const fetchLock = useRef(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
alive.current = true;
|
|
59
|
+
const element = dialog.current;
|
|
60
|
+
const previous = document.activeElement;
|
|
61
|
+
element.showModal();
|
|
62
|
+
return () => {
|
|
63
|
+
alive.current = false;
|
|
64
|
+
element.close();
|
|
65
|
+
if (previous instanceof HTMLElement && previous.isConnected)
|
|
66
|
+
previous.focus();
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let current = true;
|
|
71
|
+
setLoading(true);
|
|
72
|
+
setError('');
|
|
73
|
+
void callSubscriptionsAuth(rpc, 'ollamaSettings', {}).then(data => {
|
|
74
|
+
if (!current)
|
|
75
|
+
return;
|
|
76
|
+
setView(data);
|
|
77
|
+
// An unset store value inherits the deployed config: the checkbox
|
|
78
|
+
// shows the effective state, and ticking it writes the override.
|
|
79
|
+
setDraft({ ...data.settings });
|
|
80
|
+
setStash(new Map());
|
|
81
|
+
setContextText({});
|
|
82
|
+
setDirty(false);
|
|
83
|
+
}).catch(failure => {
|
|
84
|
+
if (current)
|
|
85
|
+
setError(t('ollamaModelsLoadFailed', { message: message(failure) }));
|
|
86
|
+
}).finally(() => { if (current)
|
|
87
|
+
setLoading(false); });
|
|
88
|
+
return () => { current = false; };
|
|
89
|
+
}, [rpc, attempt]);
|
|
90
|
+
function edit(next) {
|
|
91
|
+
setDraft(next);
|
|
92
|
+
setDirty(true);
|
|
93
|
+
}
|
|
94
|
+
/** Fetch the cloud catalog; a failure keeps the previous list and the draft. */
|
|
95
|
+
async function fetchCandidates() {
|
|
96
|
+
if (fetchLock.current)
|
|
97
|
+
return;
|
|
98
|
+
fetchLock.current = true;
|
|
99
|
+
setFetching(true);
|
|
100
|
+
setFetchError('');
|
|
101
|
+
try {
|
|
102
|
+
const value = await callSubscriptionsAuth(rpc, 'ollamaModels', { ...(apiKey ? { apiKey } : {}), ...(draft.baseURL ? { baseURL: draft.baseURL } : {}) });
|
|
103
|
+
if (!alive.current)
|
|
104
|
+
return;
|
|
105
|
+
setCandidates(value.models);
|
|
106
|
+
}
|
|
107
|
+
catch (failure) {
|
|
108
|
+
if (alive.current)
|
|
109
|
+
setFetchError(t('ollamaFetchFailed', { message: message(failure) }));
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
fetchLock.current = false;
|
|
113
|
+
if (alive.current)
|
|
114
|
+
setFetching(false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Save the whole draft; a failure keeps the dialog open with every edit. */
|
|
118
|
+
async function save() {
|
|
119
|
+
if (saveLock.current)
|
|
120
|
+
return;
|
|
121
|
+
saveLock.current = true;
|
|
122
|
+
setSaving(true);
|
|
123
|
+
setSaveError('');
|
|
124
|
+
try {
|
|
125
|
+
await callSubscriptionsAuth(rpc, 'ollamaSetSettings', { settings: draft });
|
|
126
|
+
if (alive.current) {
|
|
127
|
+
onSaved();
|
|
128
|
+
onClose();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (failure) {
|
|
132
|
+
if (alive.current)
|
|
133
|
+
setSaveError(t('ollamaModelsSaveFailed', { message: message(failure) }));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
saveLock.current = false;
|
|
137
|
+
if (alive.current)
|
|
138
|
+
setSaving(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Drop every stored override, then reload the draft from the deployment values. */
|
|
142
|
+
async function resetDefaults() {
|
|
143
|
+
if (saveLock.current)
|
|
144
|
+
return;
|
|
145
|
+
if (!window.confirm(t('ollamaResetConfirm')))
|
|
146
|
+
return;
|
|
147
|
+
saveLock.current = true;
|
|
148
|
+
setSaving(true);
|
|
149
|
+
setSaveError('');
|
|
150
|
+
try {
|
|
151
|
+
await callSubscriptionsAuth(rpc, 'ollamaSetSettings', { settings: {} });
|
|
152
|
+
if (alive.current) {
|
|
153
|
+
setAttempt(value => value + 1);
|
|
154
|
+
onSaved();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (failure) {
|
|
158
|
+
if (alive.current)
|
|
159
|
+
setSaveError(t('ollamaModelsSaveFailed', { message: message(failure) }));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
saveLock.current = false;
|
|
163
|
+
if (alive.current)
|
|
164
|
+
setSaving(false);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function editOverride(field, value) {
|
|
168
|
+
const next = { ...draft };
|
|
169
|
+
if (value.trim())
|
|
170
|
+
next[field] = value;
|
|
171
|
+
else
|
|
172
|
+
delete next[field];
|
|
173
|
+
edit(next);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* One list for both sources, like the shared editor: the ticked models in
|
|
177
|
+
* draft order first (so Move up/down is visible), then the unticked ones
|
|
178
|
+
* (stashed in this dialog, or fetched and not yet adopted). A kept model
|
|
179
|
+
* the fetch did not return stays listed, ticked or not.
|
|
180
|
+
*/
|
|
181
|
+
const fetchedById = new Map((candidates ?? []).map(candidate => [candidate.id, candidate]));
|
|
182
|
+
const present = new Set((draft.models ?? []).map(model => model.id));
|
|
183
|
+
const asRow = (model, ticked) => {
|
|
184
|
+
const fetched = fetchedById.get(model.id);
|
|
185
|
+
// The latest fetch outranks the stored snapshot, matching the runtime:
|
|
186
|
+
// discovery is the ceiling and the snapshot only stands in without it.
|
|
187
|
+
const contextWindow = fetched?.contextWindow ?? model.contextWindow;
|
|
188
|
+
return {
|
|
189
|
+
id: model.id, name: model.name ?? fetched?.name ?? model.id, ticked,
|
|
190
|
+
vision: model.vision === true || fetched?.vision === true,
|
|
191
|
+
thinking: model.thinking === true || fetched?.thinking === true,
|
|
192
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
193
|
+
efforts: model.efforts ?? fetched?.efforts ?? [],
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const rows = [
|
|
197
|
+
...(draft.models ?? []).map(model => asRow(model, true)),
|
|
198
|
+
...[...stash.values()].filter(model => !present.has(model.id)).map(model => asRow(model, false)),
|
|
199
|
+
...(candidates ?? [])
|
|
200
|
+
.filter(candidate => !present.has(candidate.id) && !stash.has(candidate.id))
|
|
201
|
+
.map(candidate => asRow({ id: candidate.id }, false)),
|
|
202
|
+
];
|
|
203
|
+
const shown = rows.filter(row => `${row.name} ${row.id}`.toLowerCase().includes(query.trim().toLowerCase()));
|
|
204
|
+
/** Rename and default-effort edits ride the tested draft helpers. */
|
|
205
|
+
function rename(id, name) {
|
|
206
|
+
edit({ ...draft, models: renameModel(draft.models ?? [], id, name) });
|
|
207
|
+
}
|
|
208
|
+
function setEffort(id, effort) {
|
|
209
|
+
edit({ ...draft, models: setModelEffort(draft.models ?? [], id, effort) });
|
|
210
|
+
}
|
|
211
|
+
/** Keep the typed text; only a valid number (or blank) reaches the draft. */
|
|
212
|
+
function setContext(id, text) {
|
|
213
|
+
setContextText(current => ({ ...current, [id]: text }));
|
|
214
|
+
const tokens = parseContextInput(text);
|
|
215
|
+
if (tokens !== null)
|
|
216
|
+
edit({ ...draft, models: setModelContext(draft.models ?? [], id, tokens) });
|
|
217
|
+
}
|
|
218
|
+
function applyTicks(next) {
|
|
219
|
+
setStash(next.stash);
|
|
220
|
+
edit({ ...draft, models: next.models });
|
|
221
|
+
}
|
|
222
|
+
const invalidContext = Object.entries(contextText)
|
|
223
|
+
.filter(([id, text]) => present.has(id) && parseContextInput(text) === null).map(([id]) => id);
|
|
224
|
+
return _jsxs("dialog", { className: "dsh-subscription-manager", ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: event => { if (saveLock.current)
|
|
225
|
+
event.preventDefault(); }, onClose: onClose, style: { width: 620, maxWidth: 'calc(100vw - 32px)', maxHeight: 'calc(100dvh - 32px)', overflow: 'auto' }, children: [_jsx("style", { children: providerSettingsCss }), _jsxs("div", { style: { ...stack, padding: 20 }, children: [_jsxs("header", { style: { ...actions, justifyContent: 'space-between' }, children: [_jsx("h2", { id: title, style: { margin: 0 }, children: t('ollamaManageTitle') }), _jsx("button", { type: "button", autoFocus: true, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, className: "dsh-subscription-hint", children: t('ollamaManageHint') }), view?.configOverridesModels === true && _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaCatalogOverride') }), error && _jsx("p", { role: "alert", children: error }), loading && _jsx("p", { role: "status", className: "dsh-subscription-hint", children: t('modelDefaultsLoading') }), !loading && !view && _jsx("button", { type: "button", onClick: () => setAttempt(value => value + 1), children: t('modelDefaultsRetry') }), view && _jsxs("fieldset", { disabled: saving, style: { ...stack, border: 0, margin: 0, padding: 0, minWidth: 0 }, children: [_jsxs("div", { style: { borderTop: border, paddingTop: 12, display: 'grid', gap: 12 }, children: [_jsx("h3", { style: { margin: 0 }, children: t('modelsEdit') }), _jsxs("div", { style: actions, children: [_jsx("input", { type: "password", autoComplete: "off", style: { flex: '1 1 180px' }, "aria-label": t('ollamaKeyPlaceholder'), placeholder: t('ollamaKeyPlaceholder'), value: apiKey, onChange: event => setApiKey(event.target.value) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: fetching, onClick: () => { void fetchCandidates(); }, children: fetching ? t('ollamaFetching') : t('ollamaFetch') }), candidates !== undefined && _jsx("span", { className: "dsh-subscription-hint", children: t('ollamaCandidatesCount', { count: candidates.length }) })] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaKeyHint') }), fetchError && _jsx("p", { role: "alert", children: fetchError }), candidates?.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaCandidatesEmpty') }), _jsxs("div", { style: actions, children: [_jsx("input", { style: { flex: '1 1 180px' }, value: query, onChange: event => setQuery(event.target.value), placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder') }), _jsx("button", { type: "button", onClick: () => applyTicks(tickModels(draft.models ?? [], stash, candidates ?? [], shown.map(row => row.id))), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => applyTicks(untickModels(draft.models ?? [], stash, shown.map(row => row.id))), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { maxHeight: 360, overflowY: 'auto', display: 'grid', gap: 8 }, children: [shown.map(row => {
|
|
226
|
+
const stored = (draft.models ?? []).find(entry => entry.id === row.id);
|
|
227
|
+
const index = (draft.models ?? []).findIndex(entry => entry.id === row.id);
|
|
228
|
+
const open = expanded.has(row.id);
|
|
229
|
+
const details = `${row.id}-details`;
|
|
230
|
+
const efforts = stored === undefined ? [] : effortOptions(stored, fetchedById.get(row.id)?.efforts, view.models.find(entry => entry.id === row.id)?.efforts);
|
|
231
|
+
const contextProblem = invalidContext.includes(row.id);
|
|
232
|
+
return _jsxs("div", { style: { borderBottom: border, padding: '8px 2px', display: 'grid', gap: 8 }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: row.ticked, onChange: event => {
|
|
233
|
+
applyTicks(event.target.checked
|
|
234
|
+
? tickModel(draft.models ?? [], stash, candidates ?? [], row.id)
|
|
235
|
+
: untickModel(draft.models ?? [], stash, row.id));
|
|
236
|
+
} }), " ", row.name, " ", _jsx("span", { style: styles.status, children: capabilities(t, row) })] }), stored !== undefined && _jsxs("div", { style: actions, children: [row.thinking && efforts.length > 0 && _jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { "aria-label": `${row.name} ${t('modelDefaultsTitle')}`, value: stored.defaultEffort ?? '', onChange: event => setEffort(row.id, event.target.value), children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), efforts.map(effort => _jsx("option", { value: effort.id, disabled: effort.unavailable === true, children: effort.unavailable === true ? `${effort.name} (${t('modelsUnavailable')})` : effort.name }, effort.id))] })] }), row.thinking && efforts.length === 0 && _jsx("small", { children: t('ollamaEffortsUnknown') }), _jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", "aria-invalid": contextProblem, value: contextText[row.id] ?? (stored.contextOverride === undefined ? '' : String(stored.contextOverride)), "aria-label": `${row.name} ${t('modelsContext')}`, placeholder: row.contextWindow === undefined ? '' : String(row.contextWindow), onChange: event => setContext(row.id, event.target.value) })] }), row.contextWindow !== undefined && _jsx("small", { children: t('modelsContextBounds', { default: row.contextWindow, max: row.contextWindow }) }), _jsxs("button", { type: "button", className: "dsh-subscription-quiet", "aria-expanded": open, "aria-controls": details, "aria-label": `${row.name} ${t('ollamaRowDetails')}`, onClick: () => {
|
|
237
|
+
const next = new Set(expanded);
|
|
238
|
+
if (open)
|
|
239
|
+
next.delete(row.id);
|
|
240
|
+
else
|
|
241
|
+
next.add(row.id);
|
|
242
|
+
setExpanded(next);
|
|
243
|
+
}, children: [t('ollamaRowDetails'), " ", open ? '\u25BE' : '\u25B8'] })] }), contextProblem && _jsx("p", { role: "alert", children: t('modelsContextInvalid', { model: row.name }) }), stored !== undefined && open && _jsxs("div", { id: details, style: actions, children: [_jsxs("label", { style: actions, children: [t('ollamaDisplayName'), _jsx("input", { value: stored.name ?? '', placeholder: row.id, "aria-label": `${row.name} ${t('ollamaDisplayName')}`, onChange: event => rename(row.id, event.target.value), onBlur: event => rename(row.id, event.target.value.trim()) })] }), _jsx("button", { type: "button", disabled: index === 0, "aria-label": `${row.name} ${t('ollamaMoveUp')}`, onClick: () => edit({ ...draft, models: moveModel(draft.models ?? [], row.id, -1) }), children: t('ollamaMoveUp') }), _jsx("button", { type: "button", disabled: index === (draft.models ?? []).length - 1, "aria-label": `${row.name} ${t('ollamaMoveDown')}`, onClick: () => edit({ ...draft, models: moveModel(draft.models ?? [], row.id, 1) }), children: t('ollamaMoveDown') })] })] }, row.id);
|
|
244
|
+
}), shown.length === 0 && _jsx("span", { className: "dsh-subscription-hint", children: query.trim() ? t('modelDefaultsFilterEmpty', { query }) : t('ollamaModelsEmpty') })] }), _jsx("small", { children: t('modelsContextHint') })] }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('ollamaBaseURL') }), _jsx("input", { value: draft.baseURL ?? '', placeholder: view.effective.baseURL, onChange: event => editOverride('baseURL', event.target.value) })] }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('ollamaApiKeyRef') }), _jsx("input", { value: draft.apiKeyRef ?? '', placeholder: view.effective.apiKeyRef, onChange: event => editOverride('apiKeyRef', event.target.value) })] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaConnectionHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: (draft.web ?? view.effective.web) === true, onChange: event => edit({ ...draft, web: event.target.checked }) }), " ", t('modelsWebSearch')] }), _jsx("p", { className: "dsh-subscription-hint", children: t('ollamaWebHint') })] }), _jsxs("footer", { style: { ...stack, gap: 8, borderTop: border, padding: '14px 0 8px',
|
|
245
|
+
position: 'sticky', bottom: 0, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))' }, children: [saveError && _jsx("p", { role: "alert", children: saveError }), _jsxs("div", { style: { ...actions, justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", style: { marginRight: 'auto' }, disabled: loading || saving, onClick: () => { void resetDefaults(); }, children: t('ollamaResetDefaults') }), _jsx("button", { type: "button", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: loading || saving || !dirty || invalidContext.length > 0, onClick: () => { void save(); }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] })] });
|
|
246
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
3
|
import { callSubscriptionsAuth } from './subscriptions-rpc.js';
|
|
4
|
+
/** Providers whose runtime honours a local context-window override. */
|
|
5
|
+
const supportsContext = (provider) => provider === 'codex' || provider === 'claude';
|
|
4
6
|
const border = '0.5px solid var(--dsw-alias-border-l2, #ddd)';
|
|
5
7
|
const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
|
|
6
8
|
/**
|
|
@@ -64,7 +66,7 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
|
|
|
64
66
|
windows[model] = value;
|
|
65
67
|
}
|
|
66
68
|
setError('');
|
|
67
|
-
const { accounts: _accounts, ...settings } = { ...draft, ...(provider
|
|
69
|
+
const { accounts: _accounts, ...settings } = { ...draft, ...(supportsContext(provider) ? { contextWindows: windows } : {}) };
|
|
68
70
|
return {
|
|
69
71
|
settings,
|
|
70
72
|
efforts: catalog.models.flatMap(model => {
|
|
@@ -97,7 +99,7 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
|
|
|
97
99
|
setEfforts({ ...efforts, [model.id]: event.target.value });
|
|
98
100
|
setDirty(true);
|
|
99
101
|
}, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured && !model.efforts?.some(effort => effort.id === model.configured) &&
|
|
100
|
-
_jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] }), model.efforts?.map(effort => _jsx("option", { value: effort.id, children: effort.name }, effort.id))] })] }), provider
|
|
102
|
+
_jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] }), model.efforts?.map(effort => _jsx("option", { value: effort.id, children: effort.name }, effort.id))] })] }), supportsContext(provider) && model.maxContextWindow !== undefined && _jsxs("div", { style: actions, children: [_jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", value: contexts[model.id] ?? '', "aria-label": `${model.name} ${t('modelsContext')}`, placeholder: String(model.defaultContextWindow), onChange: event => { setContexts({ ...contexts, [model.id]: event.target.value }); setDirty(true); } })] }), _jsx("small", { children: t('modelsContextBounds', { default: model.defaultContextWindow, max: model.maxContextWindow }) })] })] })] }, model.id)), models.length === 0 && _jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) })] }), supportsContext(provider) && _jsx("small", { children: t('modelsContextHint') }), catalog.tools.length > 0 && _jsxs("div", { style: { display: 'grid', gap: 8 }, children: [_jsx("strong", { children: t('modelsTools') }), _jsx("small", { children: t('modelsToolsHint') }), catalog.tools.map(tool => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.tools?.[tool] !== false, onChange: event => edit({
|
|
101
103
|
...draft, tools: { ...draft.tools, [tool]: event.target.checked },
|
|
102
104
|
}) }), " ", t(tool === 'image_generate' ? 'modelsImage' : tool === 'video_generate' ? 'modelsVideo' : tool === 'web_search' ? 'modelsWebSearch' : 'modelsSearch')] }, tool))] })] })] })] });
|
|
103
105
|
});
|
|
@@ -48,7 +48,7 @@ export interface ProviderUsageDisplay {
|
|
|
48
48
|
/** The account the collapsed pill reads: the default one, else the first listed. */
|
|
49
49
|
export declare function pillAccountOf(d: ProviderUsageDisplay): AccountUsageDisplay;
|
|
50
50
|
/** Brand display names (short form for the compact badge). */
|
|
51
|
-
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn';
|
|
51
|
+
export type BadgeProvider = SubscriptionProvider | 'cursor-subscription' | 'opencode-go' | 'kimi-coding' | 'minimax' | 'minimax-cn' | 'ollama-cloud';
|
|
52
52
|
export interface UsageRosterEntry {
|
|
53
53
|
provider: BadgeProvider;
|
|
54
54
|
account: AccountStatus;
|
|
@@ -64,6 +64,7 @@ const PROVIDER_NAMES = {
|
|
|
64
64
|
'kimi-coding': 'Kimi Code',
|
|
65
65
|
'minimax': 'MiniMax',
|
|
66
66
|
'minimax-cn': 'MiniMax CN',
|
|
67
|
+
'ollama-cloud': 'Ollama Cloud',
|
|
67
68
|
};
|
|
68
69
|
/** Key of one account's cached windows: `provider:accountKey`. */
|
|
69
70
|
function usageKeyOf(provider, account) {
|
|
@@ -127,7 +128,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
127
128
|
}
|
|
128
129
|
if (external.status === 'fulfilled') {
|
|
129
130
|
for (const [source, provider] of [
|
|
130
|
-
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'],
|
|
131
|
+
['opencode-go', 'opencode-go'], ['kimi-code', 'kimi-coding'], ['minimax', 'minimax'], ['minimax-cn', 'minimax-cn'], ['ollama-cloud', 'ollama-cloud'],
|
|
131
132
|
]) {
|
|
132
133
|
refreshed.add(provider);
|
|
133
134
|
if (external.value[source]?.configured)
|
|
@@ -141,7 +142,7 @@ export async function loadBadgeRoster(rpc) {
|
|
|
141
142
|
export async function usageOf(rpc, { provider, account }) {
|
|
142
143
|
if (provider === 'cursor-subscription')
|
|
143
144
|
return callSubscriptionsAuth(rpc, 'cursorUsage', {});
|
|
144
|
-
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn') {
|
|
145
|
+
if (provider === 'opencode-go' || provider === 'kimi-coding' || provider === 'minimax' || provider === 'minimax-cn' || provider === 'ollama-cloud') {
|
|
145
146
|
return callSubscriptionsAuth(rpc, 'externalUsage', { source: provider === 'kimi-coding' ? 'kimi-code' : provider });
|
|
146
147
|
}
|
|
147
148
|
return callSubscriptionsAuth(rpc, 'usage', { provider, account: account.key });
|
|
@@ -33,6 +33,8 @@ export interface UsageWindow {
|
|
|
33
33
|
startsAt?: number;
|
|
34
34
|
windowDurationMs?: number;
|
|
35
35
|
fixedWindow?: boolean;
|
|
36
|
+
/** Display-only assumed start; the marker rides it, never pace. */
|
|
37
|
+
displayStartsAt?: number;
|
|
36
38
|
}
|
|
37
39
|
/** `usage` endpoint value: the node half owns this shape. */
|
|
38
40
|
export interface ProviderUsage {
|
|
@@ -19,6 +19,7 @@ import { ProviderAccountManager } from './ProviderAccountManager.js';
|
|
|
19
19
|
import { providerSettingsCss } from './provider-settings-styles.js';
|
|
20
20
|
import { ExternalUsageCards } from './ExternalUsageCards.js';
|
|
21
21
|
import { CursorCard } from './CursorCard.js';
|
|
22
|
+
import { OllamaCard } from './OllamaCard.js';
|
|
22
23
|
import { UsageMeter } from './UsageMeter.js';
|
|
23
24
|
import { ResetCreditsDisclosure, ResetCreditsErrorLine, showsResetCredits } from './reset-credits-view.js';
|
|
24
25
|
import { displayUsedPercent } from './usage-pace.js';
|
|
@@ -562,5 +563,5 @@ export function SubscriptionsSection(props) {
|
|
|
562
563
|
void prepareReset(account.key); },
|
|
563
564
|
} } : {}, ...resetAccount === account.key && resetMessage !== '' ? { statusMessage: resetMessage } : {} }))] }))] }, account.key));
|
|
564
565
|
}), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
565
|
-
}), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && _jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) }), _jsx("style", { children: '.subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }' }), _jsx("dialog", { ref: resetDialogRef, className: "subscriptions-reset-dialog", "aria-labelledby": "subscriptions-reset-title", onCancel: () => setResetConfirmation(undefined), onClose: () => setResetConfirmation(undefined), style: { ...styles.modal, display: resetConfirmation ? 'flex' : 'none', margin: 'auto', color: 'var(--dsw-alias-label-primary)' }, children: resetConfirmation && _jsxs(_Fragment, { children: [_jsx("h3", { id: "subscriptions-reset-title", style: { ...styles.modalTitle, margin: 0 }, children: t('resetUseConfirmTitle') }), _jsx("p", { style: { ...styles.statusLine, overflowWrap: 'anywhere' }, children: statuses.codex?.accounts.find(a => a.key === resetConfirmation.account)?.account ?? resetConfirmation.account }), _jsxs("div", { style: { ...styles.usageMeta, gap: 12 }, children: [_jsx("span", { children: t('usageWeekly') }), _jsxs("strong", { children: [Math.round(resetConfirmation.weeklyUsedPercent), "%"] })] }), _jsx("div", { style: styles.usageMeta, children: _jsx("span", { children: resetConfirmation.creditExpiresAt === undefined ? t('resetCreditExpiryUnknown') : t('resetCreditExpires', { date: new Date(resetConfirmation.creditExpiresAt).toLocaleString() }) }) }), _jsx("p", { style: { ...styles.statusLine, margin: '4px 0' }, children: t('resetUseWarning') }), _jsxs("label", { style: { display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12 }, children: [_jsx("input", { type: "checkbox", checked: resetAcknowledged, onChange: e => setResetAcknowledged(e.target.checked) }), t('resetUseAcknowledge')] }), _jsxs("div", { className: "dsh-subscription-dialog-actions", style: { ...styles.actions, justifyContent: 'flex-end', marginTop: 8 }, children: [_jsx("button", { type: "button", autoFocus: true, onClick: () => setResetConfirmation(undefined), children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: !resetAcknowledged || resetBusy, onClick: () => { void consumeReset(); }, children: t('resetUseConfirm') })] })] }) })] }));
|
|
566
|
+
}), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(OllamaCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && _jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) }), _jsx("style", { children: '.subscriptions-reset-dialog::backdrop { background: var(--dsw-alias-bg-mask-1, #0006); backdrop-filter: var(--dsw-mask-blur, none); }' }), _jsx("dialog", { ref: resetDialogRef, className: "subscriptions-reset-dialog", "aria-labelledby": "subscriptions-reset-title", onCancel: () => setResetConfirmation(undefined), onClose: () => setResetConfirmation(undefined), style: { ...styles.modal, display: resetConfirmation ? 'flex' : 'none', margin: 'auto', color: 'var(--dsw-alias-label-primary)' }, children: resetConfirmation && _jsxs(_Fragment, { children: [_jsx("h3", { id: "subscriptions-reset-title", style: { ...styles.modalTitle, margin: 0 }, children: t('resetUseConfirmTitle') }), _jsx("p", { style: { ...styles.statusLine, overflowWrap: 'anywhere' }, children: statuses.codex?.accounts.find(a => a.key === resetConfirmation.account)?.account ?? resetConfirmation.account }), _jsxs("div", { style: { ...styles.usageMeta, gap: 12 }, children: [_jsx("span", { children: t('usageWeekly') }), _jsxs("strong", { children: [Math.round(resetConfirmation.weeklyUsedPercent), "%"] })] }), _jsx("div", { style: styles.usageMeta, children: _jsx("span", { children: resetConfirmation.creditExpiresAt === undefined ? t('resetCreditExpiryUnknown') : t('resetCreditExpires', { date: new Date(resetConfirmation.creditExpiresAt).toLocaleString() }) }) }), _jsx("p", { style: { ...styles.statusLine, margin: '4px 0' }, children: t('resetUseWarning') }), _jsxs("label", { style: { display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12 }, children: [_jsx("input", { type: "checkbox", checked: resetAcknowledged, onChange: e => setResetAcknowledged(e.target.checked) }), t('resetUseAcknowledge')] }), _jsxs("div", { className: "dsh-subscription-dialog-actions", style: { ...styles.actions, justifyContent: 'flex-end', marginTop: 8 }, children: [_jsx("button", { type: "button", autoFocus: true, onClick: () => setResetConfirmation(undefined), children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: !resetAcknowledged || resetBusy, onClick: () => { void consumeReset(); }, children: t('resetUseConfirm') })] })] }) })] }));
|
|
566
567
|
}
|
package/lib/client/UsageMeter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
import { elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
3
|
+
import { displayStartPercent, elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
4
4
|
import { useUsageColorPreset } from './usage-color-preferences.js';
|
|
5
5
|
/** Singular/plural locale keys per countdown unit (zh maps both to one string). */
|
|
6
6
|
const UNIT_KEYS = {
|
|
@@ -20,11 +20,13 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
20
20
|
const valid = validUsage(quota.usedPercent);
|
|
21
21
|
const used = valid ? quota.usedPercent : 0;
|
|
22
22
|
const fresh = isUsageFresh(quota, { observedAt, stale }, current);
|
|
23
|
-
// The cursor is where the window stands in time
|
|
24
|
-
//
|
|
23
|
+
// The cursor is where the window stands in time: a provider-verified start
|
|
24
|
+
// through elapsedPercent, else the display-only assumption.
|
|
25
25
|
const elapsed = elapsedPercent(quota, current);
|
|
26
|
+
const marker = elapsed ?? displayStartPercent(quota, current);
|
|
26
27
|
// Comparing usage with elapsed time is meaningful only while the percentage
|
|
27
|
-
// is current
|
|
28
|
+
// is current AND the start is verified: a display-estimated start must not
|
|
29
|
+
// color the bar — elapsed already excludes it, so pace below is safe.
|
|
28
30
|
const pace = fresh ? elapsed : undefined;
|
|
29
31
|
const state = usageColorState(quota.usedPercent, pace, preset, fresh);
|
|
30
32
|
const lead = pace === undefined ? 0 : Math.round(used - pace);
|
|
@@ -34,7 +36,9 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
34
36
|
!valid ? '' : !fresh ? t('usageMeterStale') : state === 'red' ? t('usageMeterNearLimit')
|
|
35
37
|
: pace === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
|
|
36
38
|
: state === 'yellow' ? t('usageMeterAhead') : t('usageMeterNoWarning'),
|
|
37
|
-
elapsed
|
|
39
|
+
// The elapsed-time line is a timing fact: it shows whenever the window's
|
|
40
|
+
// interval is known, even for a stale or invalid reading.
|
|
41
|
+
marker === undefined ? '' : t('usageMeterPace', { elapsed: Math.round(marker) }),
|
|
38
42
|
pace === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
|
|
39
43
|
: lead < 0 ? t('usageMeterPaceBehind', { points: -lead }) : t('usageMeterPaceEven'),
|
|
40
44
|
quota.resetsAt !== undefined && quota.resetsAt > current ? t('usageMeterReset', {
|
|
@@ -48,8 +52,8 @@ export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
|
48
52
|
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
49
53
|
...style, position: 'relative',
|
|
50
54
|
}, children: [_jsx("div", { style: { height: '100%', borderRadius: 'inherit', width: `${used}%`,
|
|
51
|
-
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }),
|
|
52
|
-
position: 'absolute', left: `clamp(0px, calc(${
|
|
55
|
+
background: usageBarColor(quota.usedPercent, pace, preset, fresh) } }), marker !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
|
|
56
|
+
position: 'absolute', left: `clamp(0px, calc(${marker}% - 1px), calc(100% - 2px))`,
|
|
53
57
|
top: 0, bottom: 0, width: 2, background: 'var(--dsw-alias-label-primary)',
|
|
54
58
|
boxShadow: '0 0 0 1px var(--dsw-alias-bg-layer-1)',
|
|
55
59
|
} })] });
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export declare const en: {
|
|
|
73
73
|
usageRefresh: string;
|
|
74
74
|
usageLoading: string;
|
|
75
75
|
usageEmpty: string;
|
|
76
|
+
usageUnsupported: string;
|
|
76
77
|
usageError: string;
|
|
77
78
|
usageSession: string;
|
|
78
79
|
usageWeekly: string;
|
|
@@ -199,6 +200,39 @@ export declare const en: {
|
|
|
199
200
|
speedFastDescription: string;
|
|
200
201
|
commandFast: string;
|
|
201
202
|
commandFastUnavailable: string;
|
|
203
|
+
ollamaTitle: string;
|
|
204
|
+
ollamaStatus: string;
|
|
205
|
+
ollamaStatusNone: string;
|
|
206
|
+
ollamaWebOn: string;
|
|
207
|
+
ollamaWebOff: string;
|
|
208
|
+
ollamaManageTitle: string;
|
|
209
|
+
ollamaManageHint: string;
|
|
210
|
+
ollamaFetch: string;
|
|
211
|
+
ollamaFetching: string;
|
|
212
|
+
ollamaKeyPlaceholder: string;
|
|
213
|
+
ollamaKeyHint: string;
|
|
214
|
+
ollamaFetchFailed: string;
|
|
215
|
+
ollamaCandidatesCount: string;
|
|
216
|
+
ollamaCandidatesEmpty: string;
|
|
217
|
+
ollamaModelsEmpty: string;
|
|
218
|
+
ollamaModelsLoadFailed: string;
|
|
219
|
+
ollamaModelsSaveFailed: string;
|
|
220
|
+
ollamaConfigureHint: string;
|
|
221
|
+
ollamaDisplayName: string;
|
|
222
|
+
ollamaRowDetails: string;
|
|
223
|
+
ollamaEffortsUnknown: string;
|
|
224
|
+
ollamaMoveUp: string;
|
|
225
|
+
ollamaMoveDown: string;
|
|
226
|
+
ollamaBaseURL: string;
|
|
227
|
+
ollamaApiKeyRef: string;
|
|
228
|
+
ollamaConnectionHint: string;
|
|
229
|
+
ollamaWebHint: string;
|
|
230
|
+
ollamaCatalogOverride: string;
|
|
231
|
+
ollamaResetDefaults: string;
|
|
232
|
+
ollamaResetConfirm: string;
|
|
233
|
+
ollamaCapVision: string;
|
|
234
|
+
ollamaCapThinking: string;
|
|
235
|
+
ollamaContext: string;
|
|
202
236
|
};
|
|
203
237
|
/** zh strings, one per {@link en} key. */
|
|
204
238
|
export declare const zh: {
|
|
@@ -274,6 +308,7 @@ export declare const zh: {
|
|
|
274
308
|
usageRefresh: string;
|
|
275
309
|
usageLoading: string;
|
|
276
310
|
usageEmpty: string;
|
|
311
|
+
usageUnsupported: string;
|
|
277
312
|
usageError: string;
|
|
278
313
|
usageSession: string;
|
|
279
314
|
usageWeekly: string;
|
|
@@ -400,6 +435,39 @@ export declare const zh: {
|
|
|
400
435
|
speedFastDescription: string;
|
|
401
436
|
commandFast: string;
|
|
402
437
|
commandFastUnavailable: string;
|
|
438
|
+
ollamaTitle: string;
|
|
439
|
+
ollamaStatus: string;
|
|
440
|
+
ollamaStatusNone: string;
|
|
441
|
+
ollamaWebOn: string;
|
|
442
|
+
ollamaWebOff: string;
|
|
443
|
+
ollamaManageTitle: string;
|
|
444
|
+
ollamaManageHint: string;
|
|
445
|
+
ollamaFetch: string;
|
|
446
|
+
ollamaFetching: string;
|
|
447
|
+
ollamaKeyPlaceholder: string;
|
|
448
|
+
ollamaKeyHint: string;
|
|
449
|
+
ollamaFetchFailed: string;
|
|
450
|
+
ollamaCandidatesCount: string;
|
|
451
|
+
ollamaCandidatesEmpty: string;
|
|
452
|
+
ollamaModelsEmpty: string;
|
|
453
|
+
ollamaModelsLoadFailed: string;
|
|
454
|
+
ollamaModelsSaveFailed: string;
|
|
455
|
+
ollamaConfigureHint: string;
|
|
456
|
+
ollamaDisplayName: string;
|
|
457
|
+
ollamaRowDetails: string;
|
|
458
|
+
ollamaEffortsUnknown: string;
|
|
459
|
+
ollamaMoveUp: string;
|
|
460
|
+
ollamaMoveDown: string;
|
|
461
|
+
ollamaBaseURL: string;
|
|
462
|
+
ollamaApiKeyRef: string;
|
|
463
|
+
ollamaConnectionHint: string;
|
|
464
|
+
ollamaWebHint: string;
|
|
465
|
+
ollamaCatalogOverride: string;
|
|
466
|
+
ollamaResetDefaults: string;
|
|
467
|
+
ollamaResetConfirm: string;
|
|
468
|
+
ollamaCapVision: string;
|
|
469
|
+
ollamaCapThinking: string;
|
|
470
|
+
ollamaContext: string;
|
|
403
471
|
};
|
|
404
472
|
/** The Subscriptions namespace key union (en is the key-set source of truth). */
|
|
405
473
|
export type SubscriptionsKey = keyof typeof en;
|
package/lib/client/locales.js
CHANGED
|
@@ -73,6 +73,7 @@ export const en = {
|
|
|
73
73
|
usageRefresh: 'Refresh',
|
|
74
74
|
usageLoading: 'Loading usage…',
|
|
75
75
|
usageEmpty: 'No usage windows reported.',
|
|
76
|
+
usageUnsupported: 'This endpoint has no usage surface.',
|
|
76
77
|
usageError: 'Usage lookup failed: {message}',
|
|
77
78
|
usageSession: '5-hour window',
|
|
78
79
|
usageWeekly: 'Weekly',
|
|
@@ -199,6 +200,47 @@ export const en = {
|
|
|
199
200
|
speedFastDescription: '1.5x speed, more usage',
|
|
200
201
|
commandFast: 'Switch the Codex speed tier (Standard/Fast)',
|
|
201
202
|
commandFastUnavailable: 'The current model has no fast tier; /fast only works on Codex models whose catalog advertises one',
|
|
203
|
+
ollamaTitle: 'Ollama Cloud',
|
|
204
|
+
ollamaStatus: '{count} model(s) in the picker',
|
|
205
|
+
ollamaStatusNone: 'No models configured',
|
|
206
|
+
ollamaWebOn: 'Web search/fetch enabled',
|
|
207
|
+
ollamaWebOff: 'Web search/fetch off',
|
|
208
|
+
ollamaManageTitle: 'Manage Ollama Cloud',
|
|
209
|
+
ollamaManageHint: 'Fetch the Cloud list, tick the models this route serves, then Save. The picker '
|
|
210
|
+
+ 'shows the ticked models in order; the API key itself stays in DSH credentials.',
|
|
211
|
+
ollamaFetch: 'Fetch cloud models',
|
|
212
|
+
ollamaFetching: 'Fetching…',
|
|
213
|
+
ollamaKeyPlaceholder: 'One-time API key (optional)',
|
|
214
|
+
ollamaKeyHint: 'A key typed here is used for this fetch only and is never saved. Leave it '
|
|
215
|
+
+ 'blank to use the stored credential.',
|
|
216
|
+
ollamaFetchFailed: 'Could not fetch cloud models: {message}',
|
|
217
|
+
ollamaCandidatesCount: '{count} returned',
|
|
218
|
+
ollamaCandidatesEmpty: 'No cloud models returned.',
|
|
219
|
+
ollamaModelsEmpty: 'No models configured. Fetch the cloud list and tick the ones to keep.',
|
|
220
|
+
ollamaModelsLoadFailed: 'Could not load Ollama settings: {message}',
|
|
221
|
+
ollamaModelsSaveFailed: 'Could not save Ollama settings: {message}',
|
|
222
|
+
ollamaConfigureHint: 'No API key found at {ref}. Add it to the credentials file '
|
|
223
|
+
+ '(~/.dsh/.credentials.yaml).',
|
|
224
|
+
ollamaDisplayName: 'Display name',
|
|
225
|
+
ollamaRowDetails: 'Details',
|
|
226
|
+
ollamaEffortsUnknown: 'This model reasons; its level set will come from live discovery.',
|
|
227
|
+
ollamaMoveUp: 'Move up',
|
|
228
|
+
ollamaMoveDown: 'Move down',
|
|
229
|
+
ollamaBaseURL: 'Base URL',
|
|
230
|
+
ollamaApiKeyRef: 'API key reference',
|
|
231
|
+
ollamaConnectionHint: "The base URL receives this route's requests and the credential lookup; the "
|
|
232
|
+
+ "reference names the DSH credential, not the key itself. Requests send "
|
|
233
|
+
+ "the resolved key to that address, so point it only at a host you trust.",
|
|
234
|
+
ollamaWebHint: 'Enables the Ollama Cloud web search and fetch providers. The host still '
|
|
235
|
+
+ 'has to select them before the web tools use them.',
|
|
236
|
+
ollamaCatalogOverride: "A profile pins this route's model list; the picker ignores store models "
|
|
237
|
+
+ "until that override is removed.",
|
|
238
|
+
ollamaResetDefaults: 'Restore deployment defaults',
|
|
239
|
+
ollamaResetConfirm: 'Clear every saved Ollama override? The profile values and built-in '
|
|
240
|
+
+ 'defaults apply again.',
|
|
241
|
+
ollamaCapVision: 'vision',
|
|
242
|
+
ollamaCapThinking: 'thinking',
|
|
243
|
+
ollamaContext: 'context {tokens}',
|
|
202
244
|
};
|
|
203
245
|
/** zh strings, one per {@link en} key. */
|
|
204
246
|
export const zh = {
|
|
@@ -274,6 +316,7 @@ export const zh = {
|
|
|
274
316
|
usageRefresh: '刷新',
|
|
275
317
|
usageLoading: '用量加载中…',
|
|
276
318
|
usageEmpty: '服务商未返回任何用量窗口。',
|
|
319
|
+
usageUnsupported: '该端点不提供用量查询。',
|
|
277
320
|
usageError: '用量查询失败:{message}',
|
|
278
321
|
usageSession: '5 小时窗口',
|
|
279
322
|
usageWeekly: '每周',
|
|
@@ -400,4 +443,38 @@ export const zh = {
|
|
|
400
443
|
speedFastDescription: '约 1.5 倍速度,消耗更多用量',
|
|
401
444
|
commandFast: '切换 Codex 速度档(标准/快速)',
|
|
402
445
|
commandFastUnavailable: '当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用',
|
|
446
|
+
ollamaTitle: 'Ollama Cloud',
|
|
447
|
+
ollamaStatus: '选择器中 {count} 个模型',
|
|
448
|
+
ollamaStatusNone: '尚未配置模型',
|
|
449
|
+
ollamaWebOn: '网页搜索/抓取已启用',
|
|
450
|
+
ollamaWebOff: '网页搜索/抓取已关闭',
|
|
451
|
+
ollamaManageTitle: '管理 Ollama Cloud',
|
|
452
|
+
ollamaManageHint: '选择此路由提供的云端模型后保存。模型选择器按此顺序显示这份列表;API 密钥本身仍保存在 DSH 凭据中。',
|
|
453
|
+
ollamaFetch: '拉取云端模型',
|
|
454
|
+
ollamaFetching: '拉取中…',
|
|
455
|
+
ollamaKeyPlaceholder: '一次性 API 密钥(可选)',
|
|
456
|
+
ollamaKeyHint: '在此输入的密钥仅用于本次拉取,不会保存。留空则使用已存凭据。',
|
|
457
|
+
ollamaFetchFailed: '拉取云端模型失败:{message}',
|
|
458
|
+
ollamaCandidatesCount: '返回 {count} 个',
|
|
459
|
+
ollamaCandidatesEmpty: '未返回任何云端模型。',
|
|
460
|
+
ollamaModelsEmpty: '尚未配置模型。请拉取云端列表并勾选需要保留的模型。',
|
|
461
|
+
ollamaModelsLoadFailed: 'Ollama 设置加载失败:{message}',
|
|
462
|
+
ollamaModelsSaveFailed: 'Ollama 设置保存失败:{message}',
|
|
463
|
+
ollamaConfigureHint: '未在 {ref} 找到 API 密钥。请在凭据文件(~/.dsh/.credentials.yaml)中添加。',
|
|
464
|
+
ollamaDisplayName: '显示名',
|
|
465
|
+
ollamaRowDetails: '详情',
|
|
466
|
+
ollamaEffortsUnknown: '该模型支持思考,推理档位将由实时发现补充。',
|
|
467
|
+
ollamaMoveUp: '上移',
|
|
468
|
+
ollamaMoveDown: '下移',
|
|
469
|
+
ollamaBaseURL: '基础 URL',
|
|
470
|
+
ollamaApiKeyRef: 'API 密钥引用',
|
|
471
|
+
ollamaConnectionHint: '基础 URL 用于此路由的请求与凭据查询;引用是 DSH 凭据的名称,不是密钥本身。'
|
|
472
|
+
+ '请求会把解析出的密钥发往该地址,请只填写可信主机。',
|
|
473
|
+
ollamaWebHint: '启用 Ollama Cloud 的网页搜索与抓取能力。宿主仍需选择它们,网页工具才会使用。',
|
|
474
|
+
ollamaCatalogOverride: '配置档案固定了此路由的模型列表;在移除该覆盖前,选择器会忽略存储中的模型。',
|
|
475
|
+
ollamaResetDefaults: '恢复部署默认值',
|
|
476
|
+
ollamaResetConfirm: '清除全部已保存的 Ollama 覆盖项?将重新使用配置档案值与内置默认值。',
|
|
477
|
+
ollamaCapVision: '视觉',
|
|
478
|
+
ollamaCapThinking: '思考',
|
|
479
|
+
ollamaContext: '上下文 {tokens}',
|
|
403
480
|
};
|