@toddzheng024/dscode-bundle 0.7.12 → 0.7.14
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -2
- package/package.json +7 -5
- package/plugins/code-review/index.mjs +5 -2
- package/plugins/compaction/threshold.mjs +12 -0
- package/plugins/credentials/index.mjs +13 -0
- package/plugins/grok/adapter.mjs +114 -0
- package/plugins/grok/auth.mjs +76 -0
- package/plugins/grok/billing.mjs +146 -0
- package/plugins/grok/index.mjs +118 -0
- package/plugins/grok/models.mjs +129 -0
- package/plugins/grok/status.mjs +28 -0
- package/plugins/grok/wire.mjs +268 -0
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/providers/catalog.mjs +2 -0
- package/plugins/session-metrics/view.mjs +34 -3
- package/vendor/compaction-basic/index.js +91 -1
- package/vendor/persistent/index.js +10 -9
- package/vendor/terminal/index.js +5 -0
- package/vendor/tui/lib/app.mjs +6711 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +84 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2200 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1864 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider settings adapter for the TUI `/model` provider-management panel:
|
|
3
|
+
* the same-process equivalent of the web host's Models page join
|
|
4
|
+
* (`packages/client/ui-settings-models`), reading the advisory `ctx.llm`
|
|
5
|
+
* registry, the redacted `ctx.settings` descriptors, and the value-free
|
|
6
|
+
* `ctx.credentials` facts directly. Secrets never cross this module: settings
|
|
7
|
+
* are read with `redactSecrets: true`, credentials are only ever described
|
|
8
|
+
* (never resolved), and every message is single-line without embedding key
|
|
9
|
+
* data.
|
|
10
|
+
*
|
|
11
|
+
* @module @deepseek-ai/dsh-tui/provider-settings
|
|
12
|
+
*/
|
|
13
|
+
import { normalizeApiKey } from '@deepseek-ai/dsh-llm';
|
|
14
|
+
/* ------------------------------------------------------------------ *
|
|
15
|
+
* Pure helpers.
|
|
16
|
+
* ------------------------------------------------------------------ */
|
|
17
|
+
/** Human text for a rejection value (mirrors the web page's `messageOf`). */
|
|
18
|
+
function messageOf(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
/** Collapse every whitespace/control run to one space so a notice stays one line. */
|
|
22
|
+
function singleLine(message) {
|
|
23
|
+
return message.replace(/[\u0000-\u001F\u007F]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
24
|
+
}
|
|
25
|
+
/** Keep a misbehaving credential provider from reflecting the submitted secret. */
|
|
26
|
+
function credentialWriteMessage(error, secret) {
|
|
27
|
+
const message = singleLine(messageOf(error));
|
|
28
|
+
return message.includes(secret) ? 'credentials service rejected the API key' : message;
|
|
29
|
+
}
|
|
30
|
+
/** Obvious shell-assignment paste; mirrors the official Web Models editor. */
|
|
31
|
+
export const ENV_ASSIGNMENT = /^[A-Z][A-Z0-9_]*=[^=]/;
|
|
32
|
+
/** Whether the whole draft is wrapped in one matching quote pair. */
|
|
33
|
+
export function hasWrappingQuotes(value) {
|
|
34
|
+
const first = value[0];
|
|
35
|
+
return (first === '"' || first === '\'' || first === '`') && value.length > 1 && value.endsWith(first);
|
|
36
|
+
}
|
|
37
|
+
/** Read the value at a path through plain objects; undefined when any segment misses. */
|
|
38
|
+
function getPath(value, path) {
|
|
39
|
+
let current = value;
|
|
40
|
+
for (const segment of path) {
|
|
41
|
+
if (typeof current !== 'object' || current === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
current = current[segment];
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
/** Whether a path resolves to a defined value (the empty path reads the root). */
|
|
48
|
+
function hasPath(value, path) {
|
|
49
|
+
return path.length === 0 ? value !== undefined : getPath(value, path) !== undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The credential reference a resolved profile names (its `apiKeyEnv` field). */
|
|
52
|
+
function profileRefOf(profile) {
|
|
53
|
+
if (typeof profile !== 'object' || profile === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
const ref = profile.apiKeyEnv;
|
|
56
|
+
return typeof ref === 'string' && ref.length > 0 ? ref : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Extract only fields the terminal can round-trip without touching provider-specific extras. */
|
|
59
|
+
function configurationOf(profile) {
|
|
60
|
+
if (typeof profile !== 'object' || profile === null)
|
|
61
|
+
return { models: [] };
|
|
62
|
+
const record = profile;
|
|
63
|
+
const source = Array.isArray(record.models) ? record.models : [];
|
|
64
|
+
const models = source.flatMap((value) => {
|
|
65
|
+
if (typeof value !== 'object' || value === null)
|
|
66
|
+
return [];
|
|
67
|
+
const entry = value;
|
|
68
|
+
if (typeof entry.id !== 'string' || entry.id.trim() === '')
|
|
69
|
+
return [];
|
|
70
|
+
// Fields the editor does not understand ride along untouched: dropping
|
|
71
|
+
// them here would let the next save strip hand-written reasoningEfforts
|
|
72
|
+
// or compat declarations out of the stored profile.
|
|
73
|
+
const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
|
|
74
|
+
return [{
|
|
75
|
+
id: entry.id,
|
|
76
|
+
...(typeof entry.name === 'string' && entry.name.trim() !== '' ? { name: entry.name } : {}),
|
|
77
|
+
...(typeof entry.contextWindow === 'number' && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {}),
|
|
78
|
+
...(typeof entry.maxTokens === 'number' && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {}),
|
|
79
|
+
...Object.keys(extras).length > 0 ? { extras } : {},
|
|
80
|
+
}];
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
...(typeof record.baseURL === 'string' && record.baseURL.trim() !== '' ? { baseURL: record.baseURL } : {}),
|
|
84
|
+
...(typeof record.api === 'string' && record.api.trim() !== '' ? { api: record.api } : {}),
|
|
85
|
+
models,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/* ------------------------------------------------------------------ *
|
|
89
|
+
* Exported contracts.
|
|
90
|
+
* ------------------------------------------------------------------ */
|
|
91
|
+
/**
|
|
92
|
+
* The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
|
|
93
|
+
* with the route uppercased and every non-alphanumeric run collapsed to one
|
|
94
|
+
* underscore — the exact derivation the official Models page uses
|
|
95
|
+
* (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
|
|
96
|
+
* @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
|
|
97
|
+
* @returns the derived reference name (e.g. `PI_AI_API_KEY`).
|
|
98
|
+
*/
|
|
99
|
+
export function deriveCredentialRef(provider) {
|
|
100
|
+
return `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The seven canonical reasoning levels a reasoningEfforts key may name -
|
|
104
|
+
* pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
|
|
105
|
+
* upstream's own drift gate; this mirror exists so the terminal editor can
|
|
106
|
+
* validate drafts without importing the pi-ai package.
|
|
107
|
+
*/
|
|
108
|
+
export const REASONING_EFFORT_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
109
|
+
/** Whether a raw extras value is a declared efforts dict (non-empty, non-false). */
|
|
110
|
+
export function isDeclaredReasoningEfforts(value) {
|
|
111
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse the setup page's compact efforts draft into a storable declaration.
|
|
115
|
+
* Grammar: empty = clear back to inherit; the single token "false" = disable
|
|
116
|
+
* the picker; otherwise space-separated level:wire pairs where level is one
|
|
117
|
+
* of REASONING_EFFORT_LEVELS and wire is any non-empty string or the literal
|
|
118
|
+
* "null" (send no parameter).
|
|
119
|
+
*/
|
|
120
|
+
export function parseReasoningEffortsDraft(draft) {
|
|
121
|
+
const text = draft.trim();
|
|
122
|
+
if (text === '')
|
|
123
|
+
return { ok: true, value: undefined };
|
|
124
|
+
if (text === 'false')
|
|
125
|
+
return { ok: true, value: false };
|
|
126
|
+
const value = {};
|
|
127
|
+
for (const token of text.split(/\s+/u)) {
|
|
128
|
+
const split = token.indexOf(':');
|
|
129
|
+
if (split <= 0 || split === token.length - 1) {
|
|
130
|
+
return { ok: false, error: 'each entry needs level:wire, got "' + token + '"' };
|
|
131
|
+
}
|
|
132
|
+
const level = token.slice(0, split);
|
|
133
|
+
const wire = token.slice(split + 1);
|
|
134
|
+
if (!REASONING_EFFORT_LEVELS.includes(level)) {
|
|
135
|
+
return { ok: false, error: '"' + level + '" is not a level; use one of ' + REASONING_EFFORT_LEVELS.join('/') };
|
|
136
|
+
}
|
|
137
|
+
if (level in value) {
|
|
138
|
+
return { ok: false, error: 'level "' + level + '" appears twice' };
|
|
139
|
+
}
|
|
140
|
+
value[level] = wire === 'null' ? null : wire;
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, value };
|
|
143
|
+
}
|
|
144
|
+
/** Serialize a stored declaration back to the compact draft form (stored key order preserved). */
|
|
145
|
+
export function serializeReasoningEfforts(value) {
|
|
146
|
+
if (value === false)
|
|
147
|
+
return 'false';
|
|
148
|
+
if (!isDeclaredReasoningEfforts(value))
|
|
149
|
+
return '';
|
|
150
|
+
return Object.entries(value)
|
|
151
|
+
.map(([level, wire]) => level + ':' + (wire === null ? 'null' : String(wire)))
|
|
152
|
+
.join(' ');
|
|
153
|
+
}
|
|
154
|
+
/** Events that invalidate the official Models provider/settings/credential join. */
|
|
155
|
+
const PROVIDER_SETTINGS_EVENTS = [
|
|
156
|
+
'credentials/reference-updated',
|
|
157
|
+
'settings/document-updated',
|
|
158
|
+
'llm/adapters-updated',
|
|
159
|
+
];
|
|
160
|
+
/** Subscribe to the same provider-directory invalidations as the official Web Models page. */
|
|
161
|
+
export function subscribeProviderSettings(ctx, listener) {
|
|
162
|
+
const events = ctx;
|
|
163
|
+
const disposers = PROVIDER_SETTINGS_EVENTS.map(event => events.on(event, () => listener()));
|
|
164
|
+
return () => {
|
|
165
|
+
for (const dispose of disposers)
|
|
166
|
+
dispose();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** A single-line, bounded error from the provider-management adapter. */
|
|
170
|
+
export class ProviderSettingsError extends Error {
|
|
171
|
+
constructor(message) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = 'ProviderSettingsError';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/* ------------------------------------------------------------------ *
|
|
177
|
+
* Load.
|
|
178
|
+
* ------------------------------------------------------------------ */
|
|
179
|
+
/**
|
|
180
|
+
* Join the configurable-provider directory, the redacted settings
|
|
181
|
+
* namespaces, and the referenced credentials into panel rows, web-parity:
|
|
182
|
+
* - directory entries merge with `listProviders()` to mark each live or
|
|
183
|
+
* dormant, and routes registered without a directory declaration appear as
|
|
184
|
+
* read-only/unmanaged rows (no settings address);
|
|
185
|
+
* - a whole-section entry is configured whenever its namespace resolves;
|
|
186
|
+
* a path-addressed one only when the profile resolves there;
|
|
187
|
+
* - a row is removable when the user layer alone carries its profile;
|
|
188
|
+
* - only refs named by resolved profiles are described, and a per-ref failure
|
|
189
|
+
* degrades to that row's bounded error instead of losing it.
|
|
190
|
+
* Absent `settings`/`credentials` services are tolerated the same way.
|
|
191
|
+
* @param ctx - context carrying the `llm` service (settings/credentials optional).
|
|
192
|
+
* @returns the resolved directory; empty rows when `llm` is unavailable.
|
|
193
|
+
*/
|
|
194
|
+
export async function loadProviderSettings(ctx) {
|
|
195
|
+
const llm = ctx.get('llm');
|
|
196
|
+
if (llm === undefined)
|
|
197
|
+
return { rows: [], writable: false, failures: [] };
|
|
198
|
+
// Call listProviders/listConfigurableProviders AS METHODS: destructured off
|
|
199
|
+
// the service they lose `this` and throw on the first read (same lesson as
|
|
200
|
+
// loadModelDirectory).
|
|
201
|
+
const registered = llm.listProviders();
|
|
202
|
+
const failures = [];
|
|
203
|
+
const directoryEntries = [];
|
|
204
|
+
if (llm.listConfigurableProviders !== undefined) {
|
|
205
|
+
try {
|
|
206
|
+
directoryEntries.push(...llm.listConfigurableProviders());
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
failures.push(`configurable-provider directory failed: ${singleLine(messageOf(error))}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const settings = ctx.get('settings');
|
|
213
|
+
let descriptors = [];
|
|
214
|
+
let writable = false;
|
|
215
|
+
if (settings !== undefined) {
|
|
216
|
+
try {
|
|
217
|
+
descriptors = settings.describe({ redactSecrets: true });
|
|
218
|
+
writable = settings.writable === true;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
failures.push(`settings describe failed: ${singleLine(messageOf(error))}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const namespaces = new Map(descriptors.map(descriptor => [descriptor.ns, descriptor]));
|
|
225
|
+
const active = new Set(registered.map(provider => provider.id));
|
|
226
|
+
const declared = new Set(directoryEntries.map(entry => entry.provider));
|
|
227
|
+
// Directory order first, then registered-but-undeclared routes (web parity:
|
|
228
|
+
// they exist and serve models, just with no settings address).
|
|
229
|
+
const bases = [
|
|
230
|
+
...directoryEntries.map(entry => ({
|
|
231
|
+
provider: entry.provider,
|
|
232
|
+
displayName: entry.displayName,
|
|
233
|
+
active: active.has(entry.provider),
|
|
234
|
+
settingsNs: entry.settingsNs,
|
|
235
|
+
settingsPath: entry.settingsPath,
|
|
236
|
+
...entry.declared === undefined ? {} : { declared: entry.declared },
|
|
237
|
+
...entry.error === undefined ? {} : { error: singleLine(entry.error) },
|
|
238
|
+
})),
|
|
239
|
+
...registered
|
|
240
|
+
.filter(provider => !declared.has(provider.id))
|
|
241
|
+
.map(provider => ({
|
|
242
|
+
provider: provider.id,
|
|
243
|
+
displayName: provider.name,
|
|
244
|
+
active: true,
|
|
245
|
+
settingsNs: '',
|
|
246
|
+
settingsPath: [],
|
|
247
|
+
})),
|
|
248
|
+
];
|
|
249
|
+
const rows = bases.map((base) => {
|
|
250
|
+
const namespace = base.settingsNs.length === 0 ? undefined : namespaces.get(base.settingsNs);
|
|
251
|
+
const profile = namespace === undefined
|
|
252
|
+
? undefined
|
|
253
|
+
: base.settingsPath.length === 0
|
|
254
|
+
? namespace.value
|
|
255
|
+
: getPath(namespace.value, base.settingsPath);
|
|
256
|
+
const configured = namespace !== undefined
|
|
257
|
+
&& (base.settingsPath.length === 0 || profile !== undefined);
|
|
258
|
+
const removable = namespace !== undefined
|
|
259
|
+
&& base.settingsPath.length > 0
|
|
260
|
+
&& hasPath(namespace.user, base.settingsPath)
|
|
261
|
+
&& !hasPath(namespace.base, base.settingsPath);
|
|
262
|
+
const credentialRef = profileRefOf(profile);
|
|
263
|
+
return {
|
|
264
|
+
provider: base.provider,
|
|
265
|
+
displayName: base.displayName,
|
|
266
|
+
active: base.active,
|
|
267
|
+
settingsNs: base.settingsNs,
|
|
268
|
+
settingsPath: base.settingsPath,
|
|
269
|
+
settingsRevision: namespace?.revision ?? 0,
|
|
270
|
+
configured,
|
|
271
|
+
removable,
|
|
272
|
+
configuration: configurationOf(profile),
|
|
273
|
+
...credentialRef === undefined ? {} : { credentialRef },
|
|
274
|
+
suggestedRef: deriveCredentialRef(base.provider),
|
|
275
|
+
...base.declared === undefined ? {} : { declared: base.declared },
|
|
276
|
+
...base.error === undefined ? {} : { diagnostic: base.error },
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
const refs = [...new Set(rows.flatMap(row => row.credentialRef === undefined ? [] : [row.credentialRef]))];
|
|
280
|
+
const credentialViews = new Map();
|
|
281
|
+
const credentials = ctx.get('credentials');
|
|
282
|
+
if (refs.length > 0) {
|
|
283
|
+
if (credentials === undefined) {
|
|
284
|
+
for (const ref of refs) {
|
|
285
|
+
credentialViews.set(ref, { kind: 'error', message: 'credentials service is unavailable' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
await Promise.all(refs.map(async (ref) => {
|
|
290
|
+
try {
|
|
291
|
+
const facts = await credentials.describe(ref);
|
|
292
|
+
credentialViews.set(ref, {
|
|
293
|
+
kind: 'facts',
|
|
294
|
+
configured: facts.configured,
|
|
295
|
+
writable: facts.writable,
|
|
296
|
+
...facts.source === undefined ? {} : { source: facts.source },
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
credentialViews.set(ref, { kind: 'error', message: singleLine(messageOf(error)) });
|
|
301
|
+
}
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
rows: rows.map(row => ({
|
|
307
|
+
...row,
|
|
308
|
+
credential: row.credentialRef === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: credentialViews.get(row.credentialRef) ?? { kind: 'error', message: 'credential describe returned no view' },
|
|
311
|
+
})),
|
|
312
|
+
writable,
|
|
313
|
+
failures,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/* ------------------------------------------------------------------ *
|
|
317
|
+
* Writes. Official web ordering is kept: settings.mutate first whenever a
|
|
318
|
+
* profile/path or apiKeyEnv must be materialized, then credentials.set; a
|
|
319
|
+
* removal unsets the managed credential before the profile. All operations
|
|
320
|
+
* are idempotent, so re-running one after a partial failure is safe.
|
|
321
|
+
* ------------------------------------------------------------------ */
|
|
322
|
+
/**
|
|
323
|
+
* Store a provider API key, web-parity: validate with `normalizeApiKey`
|
|
324
|
+
* (single-line, actionable errors that never echo the key), materialize the
|
|
325
|
+
* profile/`apiKeyEnv` through `settings.mutate` first when the resolved
|
|
326
|
+
* profile names no reference (dormant route or ref-less profile), then store
|
|
327
|
+
* under the trusted named ref or the derived conventional ref. An existing
|
|
328
|
+
* whole-section DeepSeek whose resolved profile already names
|
|
329
|
+
* `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
|
|
330
|
+
* are refused before any service call.
|
|
331
|
+
* @param ctx - context carrying `settings` (when materializing) and `credentials`.
|
|
332
|
+
* @param target - the joined row to write through.
|
|
333
|
+
* @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
|
|
334
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
335
|
+
*/
|
|
336
|
+
export async function saveProviderCredential(ctx, target, rawKey) {
|
|
337
|
+
const trimmed = rawKey.trim();
|
|
338
|
+
if (ENV_ASSIGNMENT.test(trimmed) || hasWrappingQuotes(trimmed)) {
|
|
339
|
+
throw new ProviderSettingsError('paste only the API key, without an environment-variable name or wrapping quotes');
|
|
340
|
+
}
|
|
341
|
+
const checked = normalizeApiKey(rawKey);
|
|
342
|
+
if (!checked.ok) {
|
|
343
|
+
throw new ProviderSettingsError(checked.reason === 'empty'
|
|
344
|
+
? 'the API key is empty after trimming surrounding whitespace'
|
|
345
|
+
: 'the API key contains characters an HTTP header cannot carry; type a plain printable-ASCII key');
|
|
346
|
+
}
|
|
347
|
+
if (target.settingsNs.length === 0) {
|
|
348
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
|
|
349
|
+
}
|
|
350
|
+
if (target.credential?.kind === 'facts' && target.credential.writable === false) {
|
|
351
|
+
throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead of overwriting it here`);
|
|
352
|
+
}
|
|
353
|
+
const credentials = ctx.get('credentials');
|
|
354
|
+
if (credentials === undefined) {
|
|
355
|
+
throw new ProviderSettingsError('credentials service is unavailable; cannot store the API key');
|
|
356
|
+
}
|
|
357
|
+
const ref = target.credentialRef ?? deriveCredentialRef(target.provider);
|
|
358
|
+
if (target.credentialRef === undefined) {
|
|
359
|
+
const settings = ctx.get('settings');
|
|
360
|
+
if (settings === undefined) {
|
|
361
|
+
throw new ProviderSettingsError('settings service is unavailable; cannot materialize the credential reference');
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await settings.mutate(target.settingsNs, [{ op: 'set', path: [...target.settingsPath, 'apiKeyEnv'], value: ref }]);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
await credentials.set(ref, checked.value);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
|
|
378
|
+
export async function saveProviderConfiguration(ctx, target, configuration) {
|
|
379
|
+
if (target.settingsNs.length === 0) {
|
|
380
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
|
|
381
|
+
}
|
|
382
|
+
const settings = ctx.get('settings');
|
|
383
|
+
if (settings === undefined || settings.writable !== true) {
|
|
384
|
+
throw new ProviderSettingsError('settings are read-only; provider configuration cannot be changed here');
|
|
385
|
+
}
|
|
386
|
+
const baseURL = configuration.baseURL?.trim();
|
|
387
|
+
if (baseURL !== undefined && baseURL !== '') {
|
|
388
|
+
try {
|
|
389
|
+
const parsed = new URL(baseURL);
|
|
390
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
391
|
+
throw new Error('unsupported protocol');
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw new ProviderSettingsError('base URL must be an absolute http or https URL');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const seen = new Set();
|
|
398
|
+
const models = configuration.models.map((model) => {
|
|
399
|
+
const id = model.id.trim();
|
|
400
|
+
if (id === '' || seen.has(id))
|
|
401
|
+
throw new ProviderSettingsError('each selected model must have a unique non-empty id');
|
|
402
|
+
seen.add(id);
|
|
403
|
+
for (const [label, value] of [['context window', model.contextWindow], ['output window', model.maxTokens]]) {
|
|
404
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
405
|
+
throw new ProviderSettingsError(`${label} must be a positive integer`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
id,
|
|
410
|
+
// Editor-invisible fields ride along after the id; the modelled keys
|
|
411
|
+
// spread last so a panel edit (or clear) always wins over a carried
|
|
412
|
+
// value. extras never contains those keys — see configurationOf.
|
|
413
|
+
...model.extras,
|
|
414
|
+
...(model.name === undefined || model.name.trim() === '' ? {} : { name: model.name.trim() }),
|
|
415
|
+
...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }),
|
|
416
|
+
...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
const root = target.settingsPath;
|
|
420
|
+
const ops = [
|
|
421
|
+
baseURL === undefined || baseURL === ''
|
|
422
|
+
? { op: 'unset', path: [...root, 'baseURL'] }
|
|
423
|
+
: { op: 'set', path: [...root, 'baseURL'], value: baseURL },
|
|
424
|
+
// An explicit empty list is deliberate: it prevents a provider's shipped
|
|
425
|
+
// catalog from silently becoming the active selection.
|
|
426
|
+
{ op: 'set', path: [...root, 'models'], value: models },
|
|
427
|
+
];
|
|
428
|
+
try {
|
|
429
|
+
await settings.mutate(target.settingsNs, ops, target.settingsRevision);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Interrogate a provider endpoint for the models it really serves, through
|
|
437
|
+
* the model-discovery capability the provider's settings namespace
|
|
438
|
+
* registered — the same pipe the official Web Models page uses. The request
|
|
439
|
+
* is a draft: a typed key forces direct endpoint interrogation (gateway
|
|
440
|
+
* truth), while an empty key lets the harness resolve the route's stored
|
|
441
|
+
* credential; with neither baseURL nor route the adapter answers from its
|
|
442
|
+
* own knowledge.
|
|
443
|
+
* @param ctx - context carrying the `llm` service (optional discovery).
|
|
444
|
+
* @param target - provider row whose settings namespace serves the draft.
|
|
445
|
+
* @param request - typed key and/or endpoint override for this one probe.
|
|
446
|
+
* @param signal - caller cancellation (panel navigation aborts the probe).
|
|
447
|
+
* @returns the advertised models in endpoint order, deduplicated.
|
|
448
|
+
*/
|
|
449
|
+
export async function discoverProviderModels(ctx, target, request, signal) {
|
|
450
|
+
if (target.settingsNs.length === 0) {
|
|
451
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; its models cannot be discovered here`);
|
|
452
|
+
}
|
|
453
|
+
const llm = ctx.get('llm');
|
|
454
|
+
if (llm?.discoverModels === undefined) {
|
|
455
|
+
throw new ProviderSettingsError('model discovery is unavailable in this profile; enter models by hand');
|
|
456
|
+
}
|
|
457
|
+
const typedKey = request.apiKey?.trim();
|
|
458
|
+
const baseURL = request.baseURL?.trim();
|
|
459
|
+
const hasUrl = baseURL !== undefined && baseURL !== '';
|
|
460
|
+
let oneShotKey = typedKey;
|
|
461
|
+
// A filled endpoint means the user wants THAT endpoint's real list: sending
|
|
462
|
+
// the route id alongside would make the adapter short-circuit to its
|
|
463
|
+
// installed catalog (official providers) and ignore the URL entirely. The
|
|
464
|
+
// probe therefore goes out as a draft — with the typed key, or with the
|
|
465
|
+
// stored credential resolved once for this request (never displayed,
|
|
466
|
+
// never persisted; exactly what provider-mode resolution does internally).
|
|
467
|
+
if (hasUrl && (oneShotKey === undefined || oneShotKey === '')) {
|
|
468
|
+
const credentialsService = ctx.get('credentials');
|
|
469
|
+
const ref = target.credentialRef ?? target.suggestedRef;
|
|
470
|
+
if (credentialsService?.resolve !== undefined && ref !== undefined) {
|
|
471
|
+
try {
|
|
472
|
+
// Call resolve AS A METHOD on the service: destructured off it the
|
|
473
|
+
// call loses `this` and throws on the provider's first field read
|
|
474
|
+
// (the same lesson loadProviderSettings documents for listProviders).
|
|
475
|
+
const resolved = await credentialsService.resolve(ref);
|
|
476
|
+
oneShotKey = resolved?.value;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// A failed resolution degrades to an unauthenticated probe; the
|
|
480
|
+
// endpoint's own 401 names the problem better than we can.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const draft = hasUrl
|
|
485
|
+
? {
|
|
486
|
+
...(oneShotKey !== undefined && oneShotKey !== '' ? { apiKey: oneShotKey } : {}),
|
|
487
|
+
baseURL: baseURL,
|
|
488
|
+
...target.configuration.api === undefined ? {} : { api: target.configuration.api },
|
|
489
|
+
}
|
|
490
|
+
: {
|
|
491
|
+
// No endpoint override: the route's own knowledge answers (the official
|
|
492
|
+
// catalog for builtin providers — richer than any listing).
|
|
493
|
+
provider: target.provider,
|
|
494
|
+
};
|
|
495
|
+
try {
|
|
496
|
+
const discovered = signal === undefined
|
|
497
|
+
? await llm.discoverModels(target.settingsNs, draft)
|
|
498
|
+
: await llm.discoverModels(target.settingsNs, draft, signal);
|
|
499
|
+
// Defensive dedupe in endpoint order (the service dedupes too; an older
|
|
500
|
+
// one must not leak duplicate rows into the checkable list).
|
|
501
|
+
const seen = new Set();
|
|
502
|
+
const rows = [];
|
|
503
|
+
for (const model of discovered) {
|
|
504
|
+
if (typeof model.id !== 'string' || model.id.trim() === '' || seen.has(model.id))
|
|
505
|
+
continue;
|
|
506
|
+
seen.add(model.id);
|
|
507
|
+
rows.push({
|
|
508
|
+
id: model.id,
|
|
509
|
+
...model.name === undefined ? {} : { name: model.name },
|
|
510
|
+
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
511
|
+
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return rows;
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (signal?.aborted === true)
|
|
518
|
+
throw error;
|
|
519
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Remove the currently named credential without touching the provider
|
|
524
|
+
* profile. Only the resolved profile's own reference is unset; a dormant or
|
|
525
|
+
* ref-less row (nothing to remove), an already-absent key, and an
|
|
526
|
+
* env-supplied read-only key are rejected safely before any service call.
|
|
527
|
+
* @param ctx - context carrying the `credentials` service.
|
|
528
|
+
* @param target - the joined row whose named credential to unset.
|
|
529
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
530
|
+
*/
|
|
531
|
+
export async function unsetProviderCredential(ctx, target) {
|
|
532
|
+
const ref = target.credentialRef;
|
|
533
|
+
if (ref === undefined) {
|
|
534
|
+
throw new ProviderSettingsError(`provider "${target.provider}" names no credential reference to remove`);
|
|
535
|
+
}
|
|
536
|
+
const facts = target.credential;
|
|
537
|
+
if (facts?.kind === 'facts' && facts.configured === false) {
|
|
538
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no configured credential to remove`);
|
|
539
|
+
}
|
|
540
|
+
if (facts?.kind === 'facts' && facts.writable === false) {
|
|
541
|
+
throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead`);
|
|
542
|
+
}
|
|
543
|
+
const credentials = ctx.get('credentials');
|
|
544
|
+
if (credentials === undefined) {
|
|
545
|
+
throw new ProviderSettingsError('credentials service is unavailable; cannot remove the API key');
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await credentials.unset(ref);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Remove a user-added provider profile, web-parity: only `removable` rows may
|
|
556
|
+
* be removed; a page-managed credential — the derived ref, configured and
|
|
557
|
+
* writable — is unset first (so a second-step failure leaves the row visible
|
|
558
|
+
* and the operation retryable), then `settings.mutate` unsets
|
|
559
|
+
* `target.settingsPath`. Both steps are idempotent. A hand-named credential
|
|
560
|
+
* ref may be shared elsewhere and is left alone.
|
|
561
|
+
* @param ctx - context carrying `credentials` and `settings`.
|
|
562
|
+
* @param target - the joined row to remove.
|
|
563
|
+
* @throws {@link ProviderSettingsError} with a single-line, key-free message.
|
|
564
|
+
*/
|
|
565
|
+
export async function removeProviderSettings(ctx, target) {
|
|
566
|
+
if (!target.removable) {
|
|
567
|
+
throw new ProviderSettingsError(`provider "${target.provider}" is not removable from the user settings layer`);
|
|
568
|
+
}
|
|
569
|
+
if (target.settingsNs.length === 0) {
|
|
570
|
+
throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings profile to remove`);
|
|
571
|
+
}
|
|
572
|
+
const managedRef = target.credentialRef === target.suggestedRef
|
|
573
|
+
&& target.credential?.kind === 'facts'
|
|
574
|
+
&& target.credential.configured === true
|
|
575
|
+
&& target.credential.writable === true
|
|
576
|
+
? target.credentialRef
|
|
577
|
+
: undefined;
|
|
578
|
+
if (managedRef !== undefined) {
|
|
579
|
+
const credentials = ctx.get('credentials');
|
|
580
|
+
if (credentials === undefined) {
|
|
581
|
+
throw new ProviderSettingsError('credentials service is unavailable; cannot remove the managed API key');
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await credentials.unset(managedRef);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const settings = ctx.get('settings');
|
|
591
|
+
if (settings === undefined) {
|
|
592
|
+
throw new ProviderSettingsError('settings service is unavailable; cannot remove the provider profile');
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
await settings.mutate(target.settingsNs, [{ op: 'unset', path: [...target.settingsPath] }]);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
throw new ProviderSettingsError(singleLine(messageOf(error)));
|
|
599
|
+
}
|
|
600
|
+
}
|