@wavespeed/cli 0.4.17 → 0.4.19
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 +1 -1
- package/dist/commands/setup.js +130 -12
- package/dist/lib/setup-agents.js +260 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ wavespeed logout Clear stored API key
|
|
|
141
141
|
wavespeed status Show masked key, base URL, useful links
|
|
142
142
|
wavespeed config [--default-model …] View / update CLI defaults
|
|
143
143
|
wavespeed upgrade [--check] [--json] Upgrade to the latest version (--check only reports)
|
|
144
|
-
wavespeed setup [agent] Point claude, codex, opencode, openclaw, hermes or
|
|
144
|
+
wavespeed setup [agent] Point claude, codex, opencode, openclaw, hermes, cline, qwen or miniagent at the WaveSpeed LLM gateway (--dry-run, --undo, --model)
|
|
145
145
|
|
|
146
146
|
# generation
|
|
147
147
|
wavespeed run [model|alias] -p "…" Run any model or alias (uses defaultModel if omitted)
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -8,17 +9,31 @@ import JSON5 from 'json5';
|
|
|
8
9
|
import { getApiKey, userConfig } from '../lib/config.js';
|
|
9
10
|
import { clientAttributionHeaders } from '../lib/client-headers.js';
|
|
10
11
|
import { emitJson, emitJsonError, info, setJsonMode } from '../lib/log.js';
|
|
11
|
-
import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, applyCodexToml, applyHermesYaml, applyJson, claudeEdits, openclawEdits, opencodeEdits, revertCodexToml, revertHermesYaml, revertJson, validateModel, } from '../lib/setup-agents.js';
|
|
12
|
+
import { DEFAULT_MODEL, KEY_ENV, LLM_BASE_URL, aiderValues, applyCodexToml, applyContinueYaml, applyDshPatch, applyHermesYaml, applyJson, applyYamlScalars, claudeEdits, crushEdits, droidEdits, gooseProviderFile, goosePlainValues, miniAgentValues, openclawEdits, opencodeEdits, qwenEdits, revertCodexToml, revertContinueYaml, revertDshPatch, revertHermesYaml, revertJson, revertYamlScalars, validateModel, } from '../lib/setup-agents.js';
|
|
12
13
|
const home = os.homedir();
|
|
14
|
+
// Goose: ~/.config/goose on macOS and Linux, %APPDATA%\Block\goose\config on Windows (per its docs; Windows not tested here).
|
|
15
|
+
const GOOSE_DIR = process.platform === 'win32' ? path.join(process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'Block', 'goose', 'config') : path.join(home, '.config', 'goose');
|
|
16
|
+
const GOOSE_PROVIDER_FILE = path.join(GOOSE_DIR, 'custom_providers', 'wavespeed.json');
|
|
13
17
|
const AGENTS = [
|
|
14
|
-
{ id: 'claude', name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), command: 'claude', kind: 'json', keyInFile: true },
|
|
15
18
|
{ id: 'codex', name: 'Codex', file: path.join(home, '.codex', 'config.toml'), command: 'codex', kind: 'toml', keyInFile: false },
|
|
19
|
+
{ id: 'claude', name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), command: 'claude', kind: 'json', keyInFile: true },
|
|
16
20
|
{ id: 'opencode', name: 'OpenCode', file: path.join(home, '.config', 'opencode', 'opencode.json'), command: 'opencode', kind: 'json', keyInFile: false },
|
|
17
21
|
{ id: 'openclaw', name: 'OpenClaw', file: path.join(home, '.openclaw', 'openclaw.json'), command: 'openclaw', kind: 'json5', keyInFile: false },
|
|
18
22
|
{ id: 'hermes', name: 'Hermes', file: path.join(home, '.hermes', 'config.yaml'), command: 'hermes', kind: 'yaml', keyInFile: false },
|
|
23
|
+
{ id: 'kilo', name: 'Kilo Code', file: path.join(home, '.config', 'kilo', 'kilo.json'), command: 'kilo', kind: 'json5', keyInFile: false },
|
|
24
|
+
{ id: 'qwen', name: 'Qwen Code', file: path.join(home, '.qwen', 'settings.json'), command: 'qwen', kind: 'json', keyInFile: false },
|
|
25
|
+
{ id: 'miniagent', name: 'Mini-Agent', file: path.join(home, '.mini-agent', 'config', 'config.yaml'), command: 'mini-agent', kind: 'yamlscalars', keyInFile: true },
|
|
26
|
+
{ id: 'crush', name: 'Crush', file: path.join(home, '.config', 'crush', 'crush.json'), command: 'crush', kind: 'json', keyInFile: false },
|
|
27
|
+
{ id: 'droid', name: 'Factory Droid', file: path.join(home, '.factory', 'settings.json'), command: 'droid', kind: 'json', keyInFile: false },
|
|
28
|
+
{ id: 'aider', name: 'Aider', file: path.join(home, '.aider.conf.yml'), command: 'aider', kind: 'yamlscalars', keyInFile: true },
|
|
29
|
+
{ id: 'continue', name: 'Continue CLI', file: path.join(home, '.continue', 'config.yaml'), command: 'cn', kind: 'yamlseq', keyInFile: false },
|
|
30
|
+
{ id: 'goose', name: 'Goose', file: path.join(GOOSE_DIR, 'config.yaml'), command: 'goose', kind: 'goose', keyInFile: false },
|
|
31
|
+
{ id: 'dsh', name: 'DeepSeek Harness', file: path.join(process.env.DSH_HOME || path.join(home, '.dsh'), 'cordis.patch.yml'), command: 'dsh', kind: 'dsh', keyInFile: false },
|
|
19
32
|
// Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
|
|
20
33
|
{ id: 'cline', name: 'Cline', file: path.join(home, '.cline', 'data', 'settings', 'providers.json'), command: 'cline', kind: 'cline', keyInFile: true },
|
|
34
|
+
// Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
|
|
21
35
|
];
|
|
36
|
+
const sha256 = (data) => crypto.createHash('sha256').update(data).digest('hex');
|
|
22
37
|
const shown = (p) => p.replace(home, '~');
|
|
23
38
|
function onPath(cmd) {
|
|
24
39
|
const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
@@ -65,10 +80,11 @@ function readJson(file, json5 = false) {
|
|
|
65
80
|
}
|
|
66
81
|
throw new Error(`${shown(file)} is not plain JSON (comments or trailing commas?). Edit it by hand using the snippet at https://wavespeed.ai/llm/agents`);
|
|
67
82
|
}
|
|
68
|
-
|
|
83
|
+
/** `keepOriginalBackup`: setup already ran for this agent, so an existing .bak is the true original. */
|
|
84
|
+
function writeFile(file, content, secret, keepOriginalBackup) {
|
|
69
85
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
86
|
const bak = `${file}.wavespeed.bak`;
|
|
71
|
-
if (fs.existsSync(file) && !fs.existsSync(bak))
|
|
87
|
+
if (fs.existsSync(file) && (!keepOriginalBackup || !fs.existsSync(bak)))
|
|
72
88
|
fs.copyFileSync(file, bak);
|
|
73
89
|
fs.writeFileSync(file, content, secret ? { mode: 0o600 } : undefined);
|
|
74
90
|
if (secret)
|
|
@@ -82,7 +98,7 @@ function envHint() {
|
|
|
82
98
|
export function registerSetup(program) {
|
|
83
99
|
program
|
|
84
100
|
.command('setup [agent]')
|
|
85
|
-
.description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline) at the WaveSpeed LLM gateway')
|
|
101
|
+
.description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline, qwen, miniagent, kilo, droid, crush, aider, continue, goose, dsh) at the WaveSpeed LLM gateway')
|
|
86
102
|
.option('--model <id>', `Model to use (default: ${DEFAULT_MODEL})`)
|
|
87
103
|
.option('--dry-run', 'Show what would change without writing anything')
|
|
88
104
|
.option('--undo', "Restore the agent's config to what it was before setup")
|
|
@@ -120,7 +136,29 @@ export function registerSetup(program) {
|
|
|
120
136
|
delete rest[agent.id];
|
|
121
137
|
userConfig.set('setup', rest);
|
|
122
138
|
};
|
|
123
|
-
if (agent.kind
|
|
139
|
+
if (state && agent.kind !== 'cline' && !fs.existsSync(state.file)) {
|
|
140
|
+
if (!opts.dryRun)
|
|
141
|
+
forget();
|
|
142
|
+
info(chalk.yellow(`${shown(state.file)} is already gone; cleared the saved setup state.`));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Untouched since setup wrote it: put the original bytes back (or remove a file we created).
|
|
146
|
+
const exact = !!state?.hash && agent.kind !== 'cline' && agent.kind !== 'goose' &&
|
|
147
|
+
fs.existsSync(state.file) && sha256(fs.readFileSync(state.file)) === state.hash;
|
|
148
|
+
if (exact && state) {
|
|
149
|
+
const bak = `${state.file}.wavespeed.bak`;
|
|
150
|
+
if (!opts.dryRun) {
|
|
151
|
+
if (state.existed === false)
|
|
152
|
+
fs.rmSync(state.file, { force: true });
|
|
153
|
+
else if (fs.existsSync(bak)) {
|
|
154
|
+
fs.copyFileSync(bak, state.file);
|
|
155
|
+
fs.chmodSync(state.file, fs.statSync(bak).mode & 0o777);
|
|
156
|
+
}
|
|
157
|
+
fs.rmSync(bak, { force: true });
|
|
158
|
+
forget();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (agent.kind === 'toml') {
|
|
124
162
|
if (!fs.existsSync(agent.file))
|
|
125
163
|
return fail(`${shown(agent.file)} does not exist; nothing to undo`);
|
|
126
164
|
const before = fs.readFileSync(agent.file, 'utf8');
|
|
@@ -142,6 +180,57 @@ export function registerSetup(program) {
|
|
|
142
180
|
forget();
|
|
143
181
|
}
|
|
144
182
|
}
|
|
183
|
+
else if (agent.kind === 'yamlscalars') {
|
|
184
|
+
const after = revertYamlScalars(fs.readFileSync(state.file, 'utf8'), state.previous);
|
|
185
|
+
if (!opts.dryRun) {
|
|
186
|
+
if (after.trim() === '')
|
|
187
|
+
fs.rmSync(state.file, { force: true });
|
|
188
|
+
else
|
|
189
|
+
fs.writeFileSync(state.file, after);
|
|
190
|
+
forget();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (agent.kind === 'yamlseq') {
|
|
194
|
+
const after = revertContinueYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
|
|
195
|
+
if (!opts.dryRun) {
|
|
196
|
+
if (after.trim() === '')
|
|
197
|
+
fs.rmSync(state.file, { force: true });
|
|
198
|
+
else
|
|
199
|
+
fs.writeFileSync(state.file, after);
|
|
200
|
+
forget();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (agent.kind === 'dsh') {
|
|
204
|
+
const after = revertDshPatch(fs.readFileSync(state.file, 'utf8'));
|
|
205
|
+
if (!opts.dryRun) {
|
|
206
|
+
if (!state.previous.existed)
|
|
207
|
+
fs.rmSync(state.file, { force: true });
|
|
208
|
+
else
|
|
209
|
+
fs.writeFileSync(state.file, after);
|
|
210
|
+
forget();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (agent.kind === 'goose') {
|
|
214
|
+
const prev = state.previous;
|
|
215
|
+
const after = revertYamlScalars(fs.readFileSync(state.file, 'utf8'), prev.scalars);
|
|
216
|
+
if (!opts.dryRun) {
|
|
217
|
+
if (after.trim() === '')
|
|
218
|
+
fs.rmSync(state.file, { force: true });
|
|
219
|
+
else
|
|
220
|
+
fs.writeFileSync(state.file, after);
|
|
221
|
+
fs.rmSync(`${state.file}.wavespeed.bak`, { force: true });
|
|
222
|
+
if (prev.providerRaw !== null)
|
|
223
|
+
fs.writeFileSync(GOOSE_PROVIDER_FILE, prev.providerRaw);
|
|
224
|
+
else {
|
|
225
|
+
fs.rmSync(GOOSE_PROVIDER_FILE, { force: true });
|
|
226
|
+
try {
|
|
227
|
+
fs.rmdirSync(path.dirname(GOOSE_PROVIDER_FILE));
|
|
228
|
+
}
|
|
229
|
+
catch { /* not empty or already gone */ }
|
|
230
|
+
}
|
|
231
|
+
forget();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
145
234
|
else if (agent.kind === 'yaml') {
|
|
146
235
|
const after = revertHermesYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
|
|
147
236
|
if (!opts.dryRun) {
|
|
@@ -188,6 +277,26 @@ export function registerSetup(program) {
|
|
|
188
277
|
next = r.text;
|
|
189
278
|
previous = r.previous;
|
|
190
279
|
}
|
|
280
|
+
else if (agent.kind === 'yamlscalars') {
|
|
281
|
+
const values = agent.id === 'aider' ? aiderValues(model, apiKey) : miniAgentValues(model, apiKey);
|
|
282
|
+
const r = applyYamlScalars(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', values);
|
|
283
|
+
next = r.text;
|
|
284
|
+
previous = r.previous;
|
|
285
|
+
}
|
|
286
|
+
else if (agent.kind === 'yamlseq') {
|
|
287
|
+
const r = applyContinueYaml(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
|
|
288
|
+
next = r.text;
|
|
289
|
+
previous = r.previous;
|
|
290
|
+
}
|
|
291
|
+
else if (agent.kind === 'dsh') {
|
|
292
|
+
previous = { existed: fs.existsSync(agent.file) };
|
|
293
|
+
next = applyDshPatch(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', model);
|
|
294
|
+
}
|
|
295
|
+
else if (agent.kind === 'goose') {
|
|
296
|
+
const r = applyYamlScalars(fs.existsSync(agent.file) ? fs.readFileSync(agent.file, 'utf8') : '', goosePlainValues(model));
|
|
297
|
+
next = r.text;
|
|
298
|
+
previous = { scalars: r.previous, providerRaw: fs.existsSync(GOOSE_PROVIDER_FILE) ? fs.readFileSync(GOOSE_PROVIDER_FILE, 'utf8') : null };
|
|
299
|
+
}
|
|
191
300
|
else if (agent.kind === 'cline') {
|
|
192
301
|
previous = { existed: fs.existsSync(agent.file) };
|
|
193
302
|
if (!onPath('cline'))
|
|
@@ -195,7 +304,7 @@ export function registerSetup(program) {
|
|
|
195
304
|
}
|
|
196
305
|
else {
|
|
197
306
|
const base = readJson(agent.file, agent.kind === 'json5');
|
|
198
|
-
const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : opencodeEdits(model);
|
|
307
|
+
const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : agent.id === 'qwen' ? qwenEdits(model) : agent.id === 'crush' ? crushEdits(model) : agent.id === 'droid' ? droidEdits(model, base) : opencodeEdits(model);
|
|
199
308
|
const r = applyJson(base, edits);
|
|
200
309
|
previous = r.previous;
|
|
201
310
|
next = JSON.stringify(r.doc, null, 2) + '\n';
|
|
@@ -208,7 +317,7 @@ export function registerSetup(program) {
|
|
|
208
317
|
if (opts.dryRun) {
|
|
209
318
|
if (opts.json)
|
|
210
319
|
return emitJson({ agent: agent.id, model, file: agent.file, dry_run: true, will_create: !exists });
|
|
211
|
-
info(chalk.bold(`Would ${exists ? 'update' : 'create'} ${shown(agent.file)}`) + chalk.gray(` (model ${model}${agent.keyInFile ? '; your API key would be stored in this file' : ''})`));
|
|
320
|
+
info(chalk.bold(`Would ${exists ? 'update' : 'create'} ${shown(agent.file)}${agent.kind === 'goose' ? ` and ${shown(GOOSE_PROVIDER_FILE)}` : ''}`) + chalk.gray(` (model ${model}${agent.keyInFile ? '; your API key would be stored in this file' : ''})`));
|
|
212
321
|
info(chalk.gray('Nothing written.'));
|
|
213
322
|
return;
|
|
214
323
|
}
|
|
@@ -230,12 +339,21 @@ export function registerSetup(program) {
|
|
|
230
339
|
}
|
|
231
340
|
else {
|
|
232
341
|
const hadComments = agent.kind === 'json5' && exists && /^\s*\/\/|\/\*/m.test(fs.readFileSync(agent.file, 'utf8'));
|
|
233
|
-
writeFile(agent.file, next, agent.keyInFile);
|
|
342
|
+
writeFile(agent.file, next, agent.keyInFile, !!userConfig.get('setup')?.[agent.id]);
|
|
343
|
+
if (agent.kind === 'goose') {
|
|
344
|
+
fs.mkdirSync(path.dirname(GOOSE_PROVIDER_FILE), { recursive: true });
|
|
345
|
+
fs.writeFileSync(GOOSE_PROVIDER_FILE, gooseProviderFile(model));
|
|
346
|
+
}
|
|
234
347
|
if (hadComments)
|
|
235
348
|
info(chalk.yellow(`Comments in ${shown(agent.file)} were not kept; the original is saved as ${shown(agent.file)}.wavespeed.bak`));
|
|
236
349
|
}
|
|
237
|
-
|
|
238
|
-
|
|
350
|
+
{
|
|
351
|
+
const prior = userConfig.get('setup')?.[agent.id];
|
|
352
|
+
const record = { file: agent.file, previous, hash: agent.kind === 'cline' ? undefined : sha256(next), existed: prior ? prior.existed : exists };
|
|
353
|
+
// Re-running setup on an already configured agent keeps the first run's undo data.
|
|
354
|
+
const keep = prior && agent.kind !== 'toml' && agent.kind !== 'cline' ? { ...record, previous: prior.previous } : record;
|
|
355
|
+
userConfig.set('setup', { ...(userConfig.get('setup') ?? {}), [agent.id]: keep });
|
|
356
|
+
}
|
|
239
357
|
const check = opts.check ? await selfCheck(apiKey, model) : null;
|
|
240
358
|
if (opts.json) {
|
|
241
359
|
emitJson({ agent: agent.id, model, file: agent.file, written: true, check: check ? { ok: check.ok, detail: check.detail } : 'skipped', needs_env: agent.keyInFile ? null : KEY_ENV });
|
|
@@ -243,7 +361,7 @@ export function registerSetup(program) {
|
|
|
243
361
|
process.exit(1);
|
|
244
362
|
return;
|
|
245
363
|
}
|
|
246
|
-
info(chalk.green(
|
|
364
|
+
info(chalk.green(`${exists ? 'Updated' : 'Created'} ${shown(agent.file)}`) + chalk.gray(` (model ${model})`));
|
|
247
365
|
if (check)
|
|
248
366
|
info(check.ok ? chalk.green(`Verified: streamed reply from ${model} (${check.detail}).`) : chalk.red(`Check failed: ${check.detail}`));
|
|
249
367
|
if (!agent.keyInFile) {
|
package/dist/lib/setup-agents.js
CHANGED
|
@@ -16,13 +16,17 @@ import { parseDocument } from 'yaml';
|
|
|
16
16
|
function isObj(v) {
|
|
17
17
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
18
18
|
}
|
|
19
|
+
/** Objects and arrays can both hold a path segment (array elements are addressed by index). */
|
|
20
|
+
function isContainer(v) {
|
|
21
|
+
return typeof v === 'object' && v !== null;
|
|
22
|
+
}
|
|
19
23
|
/** Set each path, remembering what was there so `revertJson` can put it back. */
|
|
20
24
|
export function applyJson(doc, edits) {
|
|
21
25
|
const previous = [];
|
|
22
26
|
for (const { path, value } of edits) {
|
|
23
27
|
let cur = doc;
|
|
24
28
|
for (const key of path.slice(0, -1)) {
|
|
25
|
-
if (!
|
|
29
|
+
if (!isContainer(cur[key]))
|
|
26
30
|
cur[key] = {};
|
|
27
31
|
cur = cur[key];
|
|
28
32
|
}
|
|
@@ -38,8 +42,8 @@ export function revertJson(doc, previous) {
|
|
|
38
42
|
const parents = [doc];
|
|
39
43
|
let cur = doc;
|
|
40
44
|
for (const key of path.slice(0, -1)) {
|
|
41
|
-
cur =
|
|
42
|
-
if (!
|
|
45
|
+
cur = isContainer(cur) ? cur[key] : undefined;
|
|
46
|
+
if (!isContainer(cur))
|
|
43
47
|
break;
|
|
44
48
|
parents.push(cur);
|
|
45
49
|
}
|
|
@@ -49,6 +53,8 @@ export function revertJson(doc, previous) {
|
|
|
49
53
|
const last = path[path.length - 1];
|
|
50
54
|
if (existed)
|
|
51
55
|
holder[last] = value;
|
|
56
|
+
else if (Array.isArray(holder))
|
|
57
|
+
holder.splice(Number(last), 1);
|
|
52
58
|
else
|
|
53
59
|
delete holder[last];
|
|
54
60
|
// Drop containers we created and that are now empty.
|
|
@@ -109,6 +115,129 @@ export function openclawEdits(model, doc) {
|
|
|
109
115
|
edits.push({ path: ['agents', 'defaults', 'models', `wavespeed/${m}`], value: {} });
|
|
110
116
|
return edits;
|
|
111
117
|
}
|
|
118
|
+
export function qwenEdits(model) {
|
|
119
|
+
const m = validateModel(model);
|
|
120
|
+
return [
|
|
121
|
+
{ path: ['modelProviders', 'wavespeed'], value: [{ id: m, name: m, envKey: KEY_ENV, baseUrl: LLM_BASE_URL }] },
|
|
122
|
+
{ path: ['providerProtocol', 'wavespeed'], value: 'openai' },
|
|
123
|
+
{ path: ['security', 'auth', 'selectedType'], value: 'openai' },
|
|
124
|
+
{ path: ['model', 'name'], value: m },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
export function crushEdits(model) {
|
|
128
|
+
const m = validateModel(model);
|
|
129
|
+
const sel = { provider: 'wavespeed', model: m };
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
path: ['providers', 'wavespeed'],
|
|
133
|
+
value: {
|
|
134
|
+
name: 'WaveSpeed',
|
|
135
|
+
type: 'openai-compat',
|
|
136
|
+
base_url: LLM_BASE_URL,
|
|
137
|
+
api_key: `$${KEY_ENV}`,
|
|
138
|
+
models: [{ id: m, name: m, context_window: 128000, default_max_tokens: 16000 }],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{ path: ['models', 'large'], value: sel },
|
|
142
|
+
{ path: ['models', 'small'], value: { ...sel } },
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Factory Droid keeps custom models in one array that also holds the user's other
|
|
147
|
+
* providers (and their keys): touch only our own element so the saved undo state
|
|
148
|
+
* never copies anyone else's entry.
|
|
149
|
+
*/
|
|
150
|
+
export function droidEdits(model, doc) {
|
|
151
|
+
const m = validateModel(model);
|
|
152
|
+
const existing = Array.isArray(doc.customModels) ? doc.customModels : [];
|
|
153
|
+
const at = existing.findIndex((e) => isObj(e) && e.baseUrl === LLM_BASE_URL);
|
|
154
|
+
const ours = {
|
|
155
|
+
model: m,
|
|
156
|
+
displayName: `WaveSpeed ${m}`,
|
|
157
|
+
baseUrl: LLM_BASE_URL,
|
|
158
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
159
|
+
provider: 'generic-chat-completion-api',
|
|
160
|
+
maxOutputTokens: 16000,
|
|
161
|
+
};
|
|
162
|
+
return [
|
|
163
|
+
{ path: ['customModels', String(at >= 0 ? at : existing.length)], value: ours },
|
|
164
|
+
// Without a default, `droid exec` asks for a Factory login unless -m custom:<model> is given.
|
|
165
|
+
{ path: ['sessionDefaultSettings', 'model'], value: `custom:${m}` },
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
// ---------- Goose (custom provider file + two scalars in config.yaml) ----------
|
|
169
|
+
export function goosePlainValues(model) {
|
|
170
|
+
return { GOOSE_PROVIDER: 'wavespeed', GOOSE_MODEL: validateModel(model) };
|
|
171
|
+
}
|
|
172
|
+
export function gooseProviderFile(model) {
|
|
173
|
+
const m = validateModel(model);
|
|
174
|
+
return (JSON.stringify({
|
|
175
|
+
name: 'wavespeed',
|
|
176
|
+
engine: 'openai',
|
|
177
|
+
display_name: 'WaveSpeed',
|
|
178
|
+
description: 'WaveSpeed LLM gateway',
|
|
179
|
+
api_key_env: KEY_ENV,
|
|
180
|
+
base_url: `${LLM_BASE_URL}/chat/completions`,
|
|
181
|
+
models: [{ name: m, context_limit: 128000 }],
|
|
182
|
+
supports_streaming: true,
|
|
183
|
+
}, null, 2) + '\n');
|
|
184
|
+
}
|
|
185
|
+
// ---------- Aider (flat YAML scalars; the key lives in the file) ----------
|
|
186
|
+
export function aiderValues(model, apiKey) {
|
|
187
|
+
// litellm needs the openai/ prefix to treat the gateway as a generic OpenAI-compatible endpoint.
|
|
188
|
+
return { 'openai-api-base': LLM_BASE_URL, 'openai-api-key': apiKey, model: `openai/${validateModel(model)}` };
|
|
189
|
+
}
|
|
190
|
+
// ---------- Mini-Agent (flat YAML scalars; the key lives in the file) ----------
|
|
191
|
+
export function miniAgentValues(model, apiKey) {
|
|
192
|
+
return { api_key: apiKey, api_base: LLM_BASE_URL, model: validateModel(model), provider: 'openai' };
|
|
193
|
+
}
|
|
194
|
+
/** Set top-level `key: "value"` pairs in place. Returns the raw text each key held before (null = absent). */
|
|
195
|
+
export function applyYamlScalars(input, values) {
|
|
196
|
+
let text = input;
|
|
197
|
+
if (text !== '' && !text.endsWith('\n'))
|
|
198
|
+
text += '\n';
|
|
199
|
+
const doc = parseDocument(text);
|
|
200
|
+
if (doc.errors.length)
|
|
201
|
+
throw new Error(`config.yaml is not valid YAML: ${doc.errors[0].message}`);
|
|
202
|
+
const root = doc.contents;
|
|
203
|
+
// null (not undefined) marks "absent": the state is stored as JSON, which drops undefined values.
|
|
204
|
+
const previous = {};
|
|
205
|
+
const edits = [];
|
|
206
|
+
let tail = '';
|
|
207
|
+
for (const [key, value] of Object.entries(values)) {
|
|
208
|
+
const node = root?.get?.(key, true);
|
|
209
|
+
const quoted = JSON.stringify(value);
|
|
210
|
+
if (node?.range) {
|
|
211
|
+
previous[key] = text.slice(node.range[0], node.range[1]);
|
|
212
|
+
edits.push({ start: node.range[0], end: node.range[1], text: quoted });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
previous[key] = null;
|
|
216
|
+
tail += `${key}: ${quoted}\n`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { text: [...edits].sort((a, b) => b.start - a.start).reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text) + tail, previous };
|
|
220
|
+
}
|
|
221
|
+
export function revertYamlScalars(input, previous) {
|
|
222
|
+
let text = input;
|
|
223
|
+
const doc = parseDocument(text);
|
|
224
|
+
const root = doc.contents;
|
|
225
|
+
const edits = [];
|
|
226
|
+
for (const [key, raw] of Object.entries(previous)) {
|
|
227
|
+
const node = root?.get?.(key, true);
|
|
228
|
+
if (!node?.range)
|
|
229
|
+
continue;
|
|
230
|
+
if (raw !== null) {
|
|
231
|
+
edits.push({ start: node.range[0], end: node.range[1], text: raw });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const pair = root.items.find((p) => p.key?.value === key);
|
|
235
|
+
const ks = pair.key.range[0];
|
|
236
|
+
edits.push({ start: lineStart(text, ks), end: lineEnd(text, node.range[1]), text: '' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return [...edits].sort((a, b) => b.start - a.start).reduce((t, e) => t.slice(0, e.start) + e.text + t.slice(e.end), text);
|
|
240
|
+
}
|
|
112
241
|
const lineStart = (t, off) => t.lastIndexOf('\n', off - 1) + 1;
|
|
113
242
|
const lineEnd = (t, off) => {
|
|
114
243
|
const i = t.indexOf('\n', off);
|
|
@@ -259,6 +388,134 @@ export function revertHermesYaml(input, previous) {
|
|
|
259
388
|
}
|
|
260
389
|
return text;
|
|
261
390
|
}
|
|
391
|
+
export function continueEntry(model, pad) {
|
|
392
|
+
const m = validateModel(model);
|
|
393
|
+
return [
|
|
394
|
+
`${pad}- name: ${JSON.stringify(`WaveSpeed ${m}`)}`,
|
|
395
|
+
`${pad} provider: openai`,
|
|
396
|
+
`${pad} model: ${JSON.stringify(m)}`,
|
|
397
|
+
`${pad} apiBase: ${LLM_BASE_URL}`,
|
|
398
|
+
`${pad} apiKey: "\${{ secrets.${KEY_ENV} }}"`,
|
|
399
|
+
`${pad} roles:`,
|
|
400
|
+
`${pad} - chat`,
|
|
401
|
+
`${pad} defaultCompletionOptions:`,
|
|
402
|
+
`${pad} contextLength: 128000`,
|
|
403
|
+
`${pad} maxTokens: 16000`,
|
|
404
|
+
'',
|
|
405
|
+
].join('\n');
|
|
406
|
+
}
|
|
407
|
+
function isOurContinueEntry(it) {
|
|
408
|
+
return typeof it?.get === 'function' && it.get('apiBase') === LLM_BASE_URL && it.get('provider') === 'openai';
|
|
409
|
+
}
|
|
410
|
+
export function applyContinueYaml(input, model) {
|
|
411
|
+
validateModel(model);
|
|
412
|
+
const created = input.trim() === '';
|
|
413
|
+
let text = input;
|
|
414
|
+
if (created) {
|
|
415
|
+
text = 'name: WaveSpeed\nversion: 1.0.0\nschema: v1\n';
|
|
416
|
+
}
|
|
417
|
+
else if (!text.endsWith('\n')) {
|
|
418
|
+
text += '\n';
|
|
419
|
+
}
|
|
420
|
+
const doc = parseYaml(text);
|
|
421
|
+
const root = doc.contents;
|
|
422
|
+
const previous = { created, hadKey: false };
|
|
423
|
+
const list = root?.get?.('models', true);
|
|
424
|
+
if (list === undefined || list === null) {
|
|
425
|
+
return { text: `${text}models:\n${continueEntry(model, ' ')}`, previous };
|
|
426
|
+
}
|
|
427
|
+
if (!list.items || list.flow === true)
|
|
428
|
+
throw new Error('models: in config.yaml is not a block list; edit it by hand');
|
|
429
|
+
previous.hadKey = true;
|
|
430
|
+
const mine = list.items.find(isOurContinueEntry);
|
|
431
|
+
if (mine?.range) {
|
|
432
|
+
const { dash, pad } = dashPad(text, mine.range[0]);
|
|
433
|
+
const end = contentLineEnd(text, mine.range[1]);
|
|
434
|
+
previous.entryRaw = text.slice(lineStart(text, dash), end);
|
|
435
|
+
return { text: text.slice(0, lineStart(text, dash)) + continueEntry(model, pad) + text.slice(end), previous };
|
|
436
|
+
}
|
|
437
|
+
if (list.items.length === 0)
|
|
438
|
+
throw new Error('models: in config.yaml is empty; edit it by hand');
|
|
439
|
+
// First in the list: Continue's CLI has no default-model key and uses the first chat-role entry.
|
|
440
|
+
const { dash, pad } = dashPad(text, list.items[0].range[0]);
|
|
441
|
+
const at = lineStart(text, dash);
|
|
442
|
+
return { text: text.slice(0, at) + continueEntry(model, pad) + text.slice(at), previous };
|
|
443
|
+
}
|
|
444
|
+
export function revertContinueYaml(input, previous) {
|
|
445
|
+
let text = input;
|
|
446
|
+
const doc = parseYaml(text);
|
|
447
|
+
const list = doc.contents?.get?.('models', true);
|
|
448
|
+
const mine = list?.items?.find(isOurContinueEntry);
|
|
449
|
+
if (mine?.range) {
|
|
450
|
+
const { dash } = dashPad(text, mine.range[0]);
|
|
451
|
+
const start = lineStart(text, dash);
|
|
452
|
+
text = text.slice(0, start) + (previous.entryRaw ?? '') + text.slice(contentLineEnd(text, mine.range[1]));
|
|
453
|
+
}
|
|
454
|
+
if (!previous.hadKey) {
|
|
455
|
+
const again = parseYaml(text);
|
|
456
|
+
const l2 = again.contents?.get?.('models', true);
|
|
457
|
+
if (l2 && (l2.items === undefined || l2.items.length === 0))
|
|
458
|
+
text = text.replace(/^models:[ \t]*\n?/m, '');
|
|
459
|
+
}
|
|
460
|
+
if (previous.created && text.trim() === 'name: WaveSpeed\nversion: 1.0.0\nschema: v1')
|
|
461
|
+
return '';
|
|
462
|
+
return text;
|
|
463
|
+
}
|
|
464
|
+
// ---------- DeepSeek Harness (user patch layer; a managed block in cordis.patch.yml) ----------
|
|
465
|
+
const DSH_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
|
|
466
|
+
const DSH_CLOSE = '# <<< wavespeed <<<';
|
|
467
|
+
export function dshBlock(model) {
|
|
468
|
+
const m = validateModel(model);
|
|
469
|
+
return [
|
|
470
|
+
DSH_OPEN,
|
|
471
|
+
'- id: llm-pi-ai',
|
|
472
|
+
' config:',
|
|
473
|
+
' providers:',
|
|
474
|
+
' wavespeed:',
|
|
475
|
+
` apiKeyEnv: ${KEY_ENV}`,
|
|
476
|
+
' api: openai-completions',
|
|
477
|
+
` baseURL: ${LLM_BASE_URL}`,
|
|
478
|
+
' models:',
|
|
479
|
+
` - id: ${m}`,
|
|
480
|
+
'- id: agent-default-model',
|
|
481
|
+
' config:',
|
|
482
|
+
' provider: wavespeed',
|
|
483
|
+
` model: ${m}`,
|
|
484
|
+
DSH_CLOSE,
|
|
485
|
+
'',
|
|
486
|
+
].join('\n');
|
|
487
|
+
}
|
|
488
|
+
export function revertDshPatch(text) {
|
|
489
|
+
const out = [];
|
|
490
|
+
let skipping = false;
|
|
491
|
+
for (const line of text.split('\n')) {
|
|
492
|
+
if (line.trim() === DSH_OPEN) {
|
|
493
|
+
skipping = true;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (skipping) {
|
|
497
|
+
if (line.trim() === DSH_CLOSE)
|
|
498
|
+
skipping = false;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
out.push(line);
|
|
502
|
+
}
|
|
503
|
+
const rest = out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
504
|
+
if (/^\s*-\s+id:/m.test(rest))
|
|
505
|
+
return rest.replace(/\n+$/, '\n');
|
|
506
|
+
// No entries left: dsh's own file is an empty list (plus any comments).
|
|
507
|
+
const noEmpty = rest.replace(/^\s*\[\]\s*$/m, '').replace(/\n+$/, '');
|
|
508
|
+
return (noEmpty ? noEmpty + '\n' : '') + '[]\n';
|
|
509
|
+
}
|
|
510
|
+
export function applyDshPatch(input, model) {
|
|
511
|
+
const base = revertDshPatch(input);
|
|
512
|
+
if (/^\s*-\s+id:\s*(llm-pi-ai|agent-default-model)\b/m.test(base)) {
|
|
513
|
+
throw new Error('cordis.patch.yml already patches llm-pi-ai or agent-default-model; merge the WaveSpeed provider by hand');
|
|
514
|
+
}
|
|
515
|
+
const withoutEmpty = base.replace(/^\s*\[\]\s*$/m, '').replace(/\n{3,}/g, '\n\n');
|
|
516
|
+
const head = withoutEmpty.trim() === '' ? '' : withoutEmpty.replace(/\n*$/, '\n\n');
|
|
517
|
+
return head + dshBlock(model);
|
|
518
|
+
}
|
|
262
519
|
// ---------- Codex (TOML, edited as text so the user's comments survive) ----------
|
|
263
520
|
const TOP_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
|
|
264
521
|
const TOP_CLOSE = '# <<< wavespeed <<<';
|