@ryuu-reinzz/ryuu-cli 0.0.0-stage → 0.3.1
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/CHANGELOG.md +560 -0
- package/LICENSE +21 -0
- package/README.md +1213 -2
- package/package.json +45 -4
- package/src/agent/agent.js +1194 -0
- package/src/agent/approvals.js +76 -0
- package/src/agent/context.js +106 -0
- package/src/agent/planner.js +237 -0
- package/src/agent/worklog.js +318 -0
- package/src/cli/app.js +463 -0
- package/src/cli/commands.js +1150 -0
- package/src/cli/input.js +287 -0
- package/src/cli/markdown.js +527 -0
- package/src/cli/prompts.js +255 -0
- package/src/cli/renderer.js +2155 -0
- package/src/cli/settings-editor.js +280 -0
- package/src/cli/settings-tui.js +669 -0
- package/src/cli/tui.js +1737 -0
- package/src/cli/width.js +48 -0
- package/src/config/load.js +132 -0
- package/src/config/scaffold.js +50 -0
- package/src/config/settings-registry.js +495 -0
- package/src/config/settings.js +304 -0
- package/src/config/validate.js +451 -0
- package/src/index.js +449 -0
- package/src/providers/catalog.js +282 -0
- package/src/providers/index.js +84 -0
- package/src/providers/nvidia.js +645 -0
- package/src/providers/registry.js +104 -0
- package/src/services/clipboard.js +38 -0
- package/src/services/diff.js +153 -0
- package/src/services/doctor.js +126 -0
- package/src/services/logger.js +190 -0
- package/src/services/preferences.js +249 -0
- package/src/services/processes.js +345 -0
- package/src/services/provider-settings.js +226 -0
- package/src/services/sessions.js +279 -0
- package/src/services/undo.js +133 -0
- package/src/services/ushell.js +146 -0
- package/src/services/websearch/manager.js +616 -0
- package/src/services/websearch/mcp.js +233 -0
- package/src/services/websearch/normalize.js +284 -0
- package/src/services/websearch/providers.js +178 -0
- package/src/services/websearch/serper.js +263 -0
- package/src/services/workspace.js +589 -0
- package/src/tools/filesystem.js +304 -0
- package/src/tools/git.js +114 -0
- package/src/tools/index.js +48 -0
- package/src/tools/project.js +160 -0
- package/src/tools/registry.js +118 -0
- package/src/tools/search.js +210 -0
- package/src/tools/shell.js +279 -0
- package/src/tools/url.js +542 -0
- package/src/tools/web.js +67 -0
- package/src/utils/errors.js +160 -0
- package/src/utils/network.js +129 -0
- package/src/utils/redaction.js +73 -0
- package/src/utils/validation.js +370 -0
- package/src/utils/version.js +24 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider catalog.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place that declares which AI providers and models Ryuu CLI
|
|
5
|
+
* knows about. The agent core never names a provider: it asks a descriptor for
|
|
6
|
+
* the base URL, credentials, model list and capabilities, and talks to every
|
|
7
|
+
* OpenAI-compatible endpoint through the same client.
|
|
8
|
+
*
|
|
9
|
+
* Adding a provider here (or with `extraProviders` in config.json) is enough;
|
|
10
|
+
* no agent code changes are needed. The model list is the set of ids the CLI
|
|
11
|
+
* offers, not a guarantee that the provider currently serves them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** ID used when nothing else selects a provider. */
|
|
15
|
+
export const DEFAULT_PROVIDER_ID = 'nvidia';
|
|
16
|
+
|
|
17
|
+
export const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
18
|
+
export const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
19
|
+
export const MAX_MODEL_ID_LENGTH = 200;
|
|
20
|
+
|
|
21
|
+
/** NVIDIA Integrate API catalog, in the order `/model` lists (and numbers) it. */
|
|
22
|
+
export const MODEL_CATALOG = [
|
|
23
|
+
'nvidia/nemotron-3.5-super-vl-120b-a12b',
|
|
24
|
+
'z-ai/glm-5.3-flash',
|
|
25
|
+
'google/diffusiongemma-26b-a4b-it',
|
|
26
|
+
'poolside/laguna-xs-2.1',
|
|
27
|
+
'stepfun-ai/step-5-preview',
|
|
28
|
+
'minimaxai/minimax-m3.1-flash', // default, listed 6th on purpose
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Model the provider starts with. Kept separate from the list order so the
|
|
33
|
+
* default can be any entry without renumbering `/model`.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_MODEL = 'minimaxai/minimax-m3.1-flash';
|
|
36
|
+
export const DEFAULT_BASE_URL = 'https://integrate.api.nvidia.com/v1';
|
|
37
|
+
|
|
38
|
+
export const TOKENHARBOR_BASE_URL = 'https://tokenharbor.ai/v1';
|
|
39
|
+
export const TOKENHARBOR_MODEL = 'deepseek-v4.1-flash:free';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Built-in providers, in fallback order for key auto-detection.
|
|
43
|
+
*
|
|
44
|
+
* Credentials and per-provider overrides live in the user's config.json, never
|
|
45
|
+
* in this catalog: `buildProviders` merges the stored values into a copy.
|
|
46
|
+
*
|
|
47
|
+
* `capabilities` is advisory: the agent only relies on it to decide whether to
|
|
48
|
+
* send tools and whether to request a stream. Reasoning is never assumed —
|
|
49
|
+
* reasoning fields are read from the response when the provider sends them.
|
|
50
|
+
*/
|
|
51
|
+
export const BUILTIN_PROVIDERS = [
|
|
52
|
+
{
|
|
53
|
+
id: 'nvidia',
|
|
54
|
+
name: 'NVIDIA Integrate API',
|
|
55
|
+
apiFormat: 'openai-chat-completions',
|
|
56
|
+
baseURL: DEFAULT_BASE_URL,
|
|
57
|
+
defaultModel: DEFAULT_MODEL,
|
|
58
|
+
models: MODEL_CATALOG.map((id) => ({ id, name: id, ...(id === DEFAULT_MODEL ? { default: true } : {}) })),
|
|
59
|
+
capabilities: { streaming: true, tools: true, reasoning: true, multimodal: true },
|
|
60
|
+
docsUrl: 'https://build.nvidia.com/',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'tokenharbor',
|
|
64
|
+
name: 'Token Harbor',
|
|
65
|
+
apiFormat: 'openai-chat-completions',
|
|
66
|
+
baseURL: TOKENHARBOR_BASE_URL,
|
|
67
|
+
defaultModel: TOKENHARBOR_MODEL,
|
|
68
|
+
models: [{ id: TOKENHARBOR_MODEL, name: 'DeepSeek V4.1 Flash Free', default: true }],
|
|
69
|
+
capabilities: { streaming: true, tools: true, reasoning: true, multimodal: false },
|
|
70
|
+
docsUrl: 'https://tokenharbor.ai/',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** config.json path that holds a provider's API key. */
|
|
75
|
+
export function apiKeyPathFor(providerId) {
|
|
76
|
+
return `providers.${providerId}.apiKey`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** config.json path that holds a provider's base URL override. */
|
|
80
|
+
export function baseUrlPathFor(providerId) {
|
|
81
|
+
return `providers.${providerId}.baseURL`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** config.json path that holds a provider's selected model. */
|
|
85
|
+
export function modelPathFor(providerId) {
|
|
86
|
+
return `providerModel.${providerId}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const MAX_EXTRA_PROVIDERS = 20;
|
|
90
|
+
export const MAX_MODELS_PER_PROVIDER = 100;
|
|
91
|
+
|
|
92
|
+
/** Trim and validate a model id; returns null when it cannot be used. */
|
|
93
|
+
export function normalizeModelId(value) {
|
|
94
|
+
if (typeof value !== 'string') return null;
|
|
95
|
+
const model = value.trim();
|
|
96
|
+
if (!model || model.length > MAX_MODEL_ID_LENGTH) return null;
|
|
97
|
+
return MODEL_ID_PATTERN.test(model) ? model : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Trim and validate a provider id; returns null when it cannot be used. */
|
|
101
|
+
export function normalizeProviderId(value) {
|
|
102
|
+
if (typeof value !== 'string') return null;
|
|
103
|
+
const id = value.trim().toLowerCase();
|
|
104
|
+
if (!id || id.length > 64) return null;
|
|
105
|
+
return PROVIDER_ID_PATTERN.test(id) ? id : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeModelEntries(raw, issues, label) {
|
|
109
|
+
const entries = [];
|
|
110
|
+
if (!Array.isArray(raw)) {
|
|
111
|
+
issues.push(`${label} must be an array of model ids or { id, name } objects`);
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
for (const item of raw.slice(0, MAX_MODELS_PER_PROVIDER)) {
|
|
115
|
+
const id = normalizeModelId(typeof item === 'string' ? item : item?.id);
|
|
116
|
+
if (!id) {
|
|
117
|
+
issues.push(`${label} contains an invalid model id: ${JSON.stringify(item?.id ?? item)}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (entries.some((entry) => entry.id === id)) continue;
|
|
121
|
+
const name = typeof item?.name === 'string' && item.name.trim() ? item.name.trim().slice(0, 120) : id;
|
|
122
|
+
entries.push({ id, name });
|
|
123
|
+
}
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeCapabilities(raw, issues, label) {
|
|
128
|
+
const defaults = { streaming: true, tools: true, reasoning: false, multimodal: false };
|
|
129
|
+
if (raw === undefined || raw === null) return defaults;
|
|
130
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
131
|
+
issues.push(`${label}.capabilities must be an object`);
|
|
132
|
+
return defaults;
|
|
133
|
+
}
|
|
134
|
+
const out = { ...defaults };
|
|
135
|
+
for (const key of ['streaming', 'tools', 'reasoning', 'multimodal']) {
|
|
136
|
+
if (raw[key] === undefined) continue;
|
|
137
|
+
if (typeof raw[key] !== 'boolean') {
|
|
138
|
+
issues.push(`${label}.capabilities.${key} must be true or false`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out[key] = raw[key];
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse the `extraProviders` list from config.json.
|
|
148
|
+
*
|
|
149
|
+
* Shape: `[{ "id": "myproxy", "name": "My Proxy", "baseURL": "https://…/v1",
|
|
150
|
+
* "models": ["model-a", { "id": "b", "name": "B" }],
|
|
151
|
+
* "defaultModel": "model-a", "capabilities": { "tools": false } }]`
|
|
152
|
+
*
|
|
153
|
+
* The key for an extra provider is stored like any other provider's:
|
|
154
|
+
* `providers.myproxy.apiKey`. Invalid entries are reported as config issues
|
|
155
|
+
* instead of being guessed.
|
|
156
|
+
*/
|
|
157
|
+
export function parseExtraProviders(value, issues = []) {
|
|
158
|
+
if (value === undefined || value === null || String(value).trim() === '') return [];
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
issues.push(`extraProviders is not valid JSON: ${error.message}`);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(parsed)) {
|
|
167
|
+
issues.push('extraProviders must be a JSON array of provider objects');
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const builtinIds = new Set(BUILTIN_PROVIDERS.map((provider) => provider.id));
|
|
171
|
+
const extras = [];
|
|
172
|
+
for (const [index, raw] of parsed.slice(0, MAX_EXTRA_PROVIDERS).entries()) {
|
|
173
|
+
const label = `extraProviders[${index}]`;
|
|
174
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
175
|
+
issues.push(`${label} must be an object`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const id = normalizeProviderId(raw.id);
|
|
179
|
+
if (!id) {
|
|
180
|
+
issues.push(`${label}.id must match ${PROVIDER_ID_PATTERN}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (builtinIds.has(id) || extras.some((provider) => provider.id === id)) {
|
|
184
|
+
issues.push(`${label}.id ${JSON.stringify(id)} is already registered`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const baseURL = typeof raw.baseURL === 'string' ? raw.baseURL.trim() : '';
|
|
188
|
+
if (!baseURL) {
|
|
189
|
+
issues.push(`${label}.baseURL is required`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// A provider added with `/provider add` starts with no models; `/addmodel`
|
|
193
|
+
// fills the list, so a missing default is allowed until then.
|
|
194
|
+
const models = raw.models === undefined ? [] : normalizeModelEntries(raw.models, issues, `${label}.models`);
|
|
195
|
+
const defaultModel = normalizeModelId(raw.defaultModel) || models[0]?.id || null;
|
|
196
|
+
if (defaultModel && !models.some((entry) => entry.id === defaultModel)) {
|
|
197
|
+
models.unshift({ id: defaultModel, name: defaultModel });
|
|
198
|
+
}
|
|
199
|
+
extras.push({
|
|
200
|
+
id,
|
|
201
|
+
name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim().slice(0, 80) : id,
|
|
202
|
+
apiFormat: 'openai-chat-completions',
|
|
203
|
+
baseURL,
|
|
204
|
+
defaultModel,
|
|
205
|
+
models,
|
|
206
|
+
capabilities: normalizeCapabilities(raw.capabilities, issues, label),
|
|
207
|
+
docsUrl: null,
|
|
208
|
+
source: 'settings',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (parsed.length > MAX_EXTRA_PROVIDERS) {
|
|
212
|
+
issues.push(`extraProviders lists more than ${MAX_EXTRA_PROVIDERS} providers; the rest were ignored`);
|
|
213
|
+
}
|
|
214
|
+
return extras;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build the effective provider descriptors: built-ins plus `extraProviders`,
|
|
219
|
+
* with the API key, base URL and model stored in config.json merged in.
|
|
220
|
+
*
|
|
221
|
+
* `credentials` uses the settings shape: `{ nvidia: { apiKey, baseURL, model } }`.
|
|
222
|
+
* Credentials stay on the descriptor so a later `/provider` switch needs no
|
|
223
|
+
* extra lookup, and so the logger can redact every configured key at once.
|
|
224
|
+
*/
|
|
225
|
+
export function buildProviders({ credentials = {}, extras = [] } = {}) {
|
|
226
|
+
const providers = [];
|
|
227
|
+
for (const base of [...BUILTIN_PROVIDERS.map((provider) => ({ ...provider, models: [...provider.models] })), ...extras]) {
|
|
228
|
+
const stored = credentials?.[base.id] && typeof credentials[base.id] === 'object' ? credentials[base.id] : {};
|
|
229
|
+
const baseURL = String(stored.baseURL || base.baseURL).trim();
|
|
230
|
+
const apiKey = String(stored.apiKey || '').trim();
|
|
231
|
+
const model = normalizeModelId(stored.model) || base.defaultModel;
|
|
232
|
+
const models = [...base.models];
|
|
233
|
+
if (model && !models.some((entry) => entry.id === model)) models.push({ id: model, name: model });
|
|
234
|
+
providers.push({
|
|
235
|
+
...base,
|
|
236
|
+
baseURL,
|
|
237
|
+
apiKey,
|
|
238
|
+
apiKeyPath: apiKeyPathFor(base.id),
|
|
239
|
+
baseUrlPath: baseUrlPathFor(base.id),
|
|
240
|
+
modelPath: modelPathFor(base.id),
|
|
241
|
+
defaultModel: model,
|
|
242
|
+
models,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return providers;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Find one provider descriptor by id (case-insensitive). */
|
|
249
|
+
export function providerById(providers = [], id) {
|
|
250
|
+
const wanted = normalizeProviderId(id);
|
|
251
|
+
if (!wanted) return null;
|
|
252
|
+
return providers.find((provider) => provider.id === wanted) || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Which provider is active?
|
|
257
|
+
*
|
|
258
|
+
* Precedence: explicit selection (`--provider` / `provider` in config.json / a persisted
|
|
259
|
+
* `/provider` choice) > NVIDIA when its key is present > the first provider
|
|
260
|
+
* with a key > `nvidia` (so the resulting error names the expected variable).
|
|
261
|
+
*/
|
|
262
|
+
export function resolveProviderId({ explicit = null, providers = [] } = {}) {
|
|
263
|
+
if (explicit) return normalizeProviderId(explicit) || String(explicit).trim().toLowerCase();
|
|
264
|
+
const nvidia = providerById(providers, DEFAULT_PROVIDER_ID);
|
|
265
|
+
if (nvidia?.apiKey) return nvidia.id;
|
|
266
|
+
const configured = providers.find((provider) => provider.apiKey);
|
|
267
|
+
return configured?.id || DEFAULT_PROVIDER_ID;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Catalog models for a provider, plus user-added ids, de-duplicated in order. */
|
|
271
|
+
export function modelsFor(provider, custom = []) {
|
|
272
|
+
const entries = (provider?.models || []).map((entry) => ({ ...entry }));
|
|
273
|
+
for (const id of custom) {
|
|
274
|
+
if (!entries.some((entry) => entry.id === id)) entries.push({ id, name: id, added: true });
|
|
275
|
+
}
|
|
276
|
+
return entries;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** True when the model id is in the provider's declared list. */
|
|
280
|
+
export function isCatalogModel(provider, model) {
|
|
281
|
+
return Boolean(provider?.models?.some((entry) => entry.id === model));
|
|
282
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ProviderError } from '../utils/errors.js';
|
|
2
|
+
import { ProviderRegistry } from './registry.js';
|
|
3
|
+
import { createProvider as createChatCompletionsProvider } from './nvidia.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PROVIDER_ID,
|
|
6
|
+
normalizeModelId,
|
|
7
|
+
normalizeProviderId,
|
|
8
|
+
providerById,
|
|
9
|
+
} from './catalog.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provider wiring.
|
|
13
|
+
*
|
|
14
|
+
* Every catalog entry speaks the OpenAI Chat Completions protocol, so they all
|
|
15
|
+
* share one client implementation. The registry exists so a new protocol can be
|
|
16
|
+
* added later without touching the agent: register a factory under its id and
|
|
17
|
+
* the selection code will use it.
|
|
18
|
+
*/
|
|
19
|
+
function factory(config, deps) {
|
|
20
|
+
return createChatCompletionsProvider(config, deps);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const providerRegistry = new ProviderRegistry({
|
|
24
|
+
nvidia: factory,
|
|
25
|
+
tokenharbor: factory,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create the provider instance for an already validated config.
|
|
30
|
+
*
|
|
31
|
+
* `extraProviders` entries are registered on first use, which is why a provider
|
|
32
|
+
* added in config.json needs no code change.
|
|
33
|
+
*/
|
|
34
|
+
export function createActiveProvider(config, deps = {}) {
|
|
35
|
+
const id = normalizeProviderId(config.providerId) || DEFAULT_PROVIDER_ID;
|
|
36
|
+
if (!providerRegistry.has(id)) providerRegistry.register(id, factory);
|
|
37
|
+
return providerRegistry.resolve(id, config, deps);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the descriptor for a provider/model selection.
|
|
42
|
+
*
|
|
43
|
+
* Validates the provider id, that its key is configured, and the model id
|
|
44
|
+
* before anything is sent. A model that is not in the declared list is allowed
|
|
45
|
+
* (it may be a newer model) but must still be a well-formed id.
|
|
46
|
+
*/
|
|
47
|
+
export function selectionFor(config, providerId, { model = null } = {}) {
|
|
48
|
+
const descriptor = providerById(config.providers, providerId);
|
|
49
|
+
if (!descriptor) {
|
|
50
|
+
const known = (config.providers || []).map((provider) => provider.id).join(', ') || '(none)';
|
|
51
|
+
throw new ProviderError(`Unknown provider: ${providerId}. Known providers: ${known}.`, {
|
|
52
|
+
code: 'UNKNOWN_PROVIDER',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!descriptor.apiKey) {
|
|
56
|
+
throw new ProviderError(
|
|
57
|
+
`Provider ${descriptor.id} has no API key. Add your key to config.json with \`ryuu --settings\` (${descriptor.apiKeyPath}) before switching.`,
|
|
58
|
+
{ code: 'AUTH_MISSING' },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const requested = model === null || model === undefined || model === '' ? null : normalizeModelId(model);
|
|
62
|
+
if (model !== null && model !== undefined && model !== '' && !requested) {
|
|
63
|
+
throw new ProviderError(`Invalid model id: ${JSON.stringify(model)}`, { code: 'INVALID_MODEL' });
|
|
64
|
+
}
|
|
65
|
+
const chosen = requested || normalizeModelId(descriptor.defaultModel) || descriptor.models?.[0]?.id || null;
|
|
66
|
+
if (!chosen) {
|
|
67
|
+
throw new ProviderError(
|
|
68
|
+
`Provider ${descriptor.id} has no usable model. Add one with \`/addmodel ${descriptor.id} <model-id>\` or set ${descriptor.modelPath} in config.json.`,
|
|
69
|
+
{ code: 'INVALID_MODEL' },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
providerId: descriptor.id,
|
|
74
|
+
providerName: descriptor.name,
|
|
75
|
+
apiKeyPath: descriptor.apiKeyPath,
|
|
76
|
+
docsUrl: descriptor.docsUrl || null,
|
|
77
|
+
baseURL: descriptor.baseURL,
|
|
78
|
+
apiKey: descriptor.apiKey,
|
|
79
|
+
model: chosen,
|
|
80
|
+
capabilities: { ...(descriptor.capabilities || {}) },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { providerById, normalizeModelId, normalizeProviderId };
|