@wavespeed/cli 0.4.18 → 0.4.20
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/dist/commands/setup.js +114 -13
- package/dist/lib/setup-agents.js +200 -3
- package/package.json +1 -1
- package/skills/SKILL.md +2 -1
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,19 +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, applyYamlScalars, claudeEdits, miniAgentValues, openclawEdits, opencodeEdits, qwenEdits, revertCodexToml, revertHermesYaml, revertJson, revertYamlScalars, 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 },
|
|
19
24
|
{ id: 'qwen', name: 'Qwen Code', file: path.join(home, '.qwen', 'settings.json'), command: 'qwen', kind: 'json', keyInFile: false },
|
|
20
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 },
|
|
21
32
|
// Cline's CLI keeps provider settings (including the key) in this file; we write it only through `cline auth`.
|
|
22
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`.
|
|
23
35
|
];
|
|
36
|
+
const sha256 = (data) => crypto.createHash('sha256').update(data).digest('hex');
|
|
24
37
|
const shown = (p) => p.replace(home, '~');
|
|
25
38
|
function onPath(cmd) {
|
|
26
39
|
const exts = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
@@ -67,10 +80,11 @@ function readJson(file, json5 = false) {
|
|
|
67
80
|
}
|
|
68
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`);
|
|
69
82
|
}
|
|
70
|
-
|
|
83
|
+
/** `keepOriginalBackup`: setup already ran for this agent, so an existing .bak is the true original. */
|
|
84
|
+
function writeFile(file, content, secret, keepOriginalBackup) {
|
|
71
85
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
72
86
|
const bak = `${file}.wavespeed.bak`;
|
|
73
|
-
if (fs.existsSync(file) && !fs.existsSync(bak))
|
|
87
|
+
if (fs.existsSync(file) && (!keepOriginalBackup || !fs.existsSync(bak)))
|
|
74
88
|
fs.copyFileSync(file, bak);
|
|
75
89
|
fs.writeFileSync(file, content, secret ? { mode: 0o600 } : undefined);
|
|
76
90
|
if (secret)
|
|
@@ -84,7 +98,7 @@ function envHint() {
|
|
|
84
98
|
export function registerSetup(program) {
|
|
85
99
|
program
|
|
86
100
|
.command('setup [agent]')
|
|
87
|
-
.description('Point a coding agent (claude, codex, opencode, openclaw, hermes, cline, qwen, miniagent) 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')
|
|
88
102
|
.option('--model <id>', `Model to use (default: ${DEFAULT_MODEL})`)
|
|
89
103
|
.option('--dry-run', 'Show what would change without writing anything')
|
|
90
104
|
.option('--undo', "Restore the agent's config to what it was before setup")
|
|
@@ -122,7 +136,29 @@ export function registerSetup(program) {
|
|
|
122
136
|
delete rest[agent.id];
|
|
123
137
|
userConfig.set('setup', rest);
|
|
124
138
|
};
|
|
125
|
-
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') {
|
|
126
162
|
if (!fs.existsSync(agent.file))
|
|
127
163
|
return fail(`${shown(agent.file)} does not exist; nothing to undo`);
|
|
128
164
|
const before = fs.readFileSync(agent.file, 'utf8');
|
|
@@ -154,6 +190,47 @@ export function registerSetup(program) {
|
|
|
154
190
|
forget();
|
|
155
191
|
}
|
|
156
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
|
+
}
|
|
157
234
|
else if (agent.kind === 'yaml') {
|
|
158
235
|
const after = revertHermesYaml(fs.readFileSync(state.file, 'utf8'), state.previous);
|
|
159
236
|
if (!opts.dryRun) {
|
|
@@ -201,10 +278,25 @@ export function registerSetup(program) {
|
|
|
201
278
|
previous = r.previous;
|
|
202
279
|
}
|
|
203
280
|
else if (agent.kind === 'yamlscalars') {
|
|
204
|
-
const
|
|
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);
|
|
205
283
|
next = r.text;
|
|
206
284
|
previous = r.previous;
|
|
207
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
|
+
}
|
|
208
300
|
else if (agent.kind === 'cline') {
|
|
209
301
|
previous = { existed: fs.existsSync(agent.file) };
|
|
210
302
|
if (!onPath('cline'))
|
|
@@ -212,7 +304,7 @@ export function registerSetup(program) {
|
|
|
212
304
|
}
|
|
213
305
|
else {
|
|
214
306
|
const base = readJson(agent.file, agent.kind === 'json5');
|
|
215
|
-
const edits = agent.id === 'claude' ? claudeEdits(model, apiKey) : agent.id === 'openclaw' ? openclawEdits(model, base) : agent.id === 'qwen' ? qwenEdits(model) : 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);
|
|
216
308
|
const r = applyJson(base, edits);
|
|
217
309
|
previous = r.previous;
|
|
218
310
|
next = JSON.stringify(r.doc, null, 2) + '\n';
|
|
@@ -225,7 +317,7 @@ export function registerSetup(program) {
|
|
|
225
317
|
if (opts.dryRun) {
|
|
226
318
|
if (opts.json)
|
|
227
319
|
return emitJson({ agent: agent.id, model, file: agent.file, dry_run: true, will_create: !exists });
|
|
228
|
-
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' : ''})`));
|
|
229
321
|
info(chalk.gray('Nothing written.'));
|
|
230
322
|
return;
|
|
231
323
|
}
|
|
@@ -247,12 +339,21 @@ export function registerSetup(program) {
|
|
|
247
339
|
}
|
|
248
340
|
else {
|
|
249
341
|
const hadComments = agent.kind === 'json5' && exists && /^\s*\/\/|\/\*/m.test(fs.readFileSync(agent.file, 'utf8'));
|
|
250
|
-
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
|
+
}
|
|
251
347
|
if (hadComments)
|
|
252
348
|
info(chalk.yellow(`Comments in ${shown(agent.file)} were not kept; the original is saved as ${shown(agent.file)}.wavespeed.bak`));
|
|
253
349
|
}
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
}
|
|
256
357
|
const check = opts.check ? await selfCheck(apiKey, model) : null;
|
|
257
358
|
if (opts.json) {
|
|
258
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 });
|
|
@@ -260,7 +361,7 @@ export function registerSetup(program) {
|
|
|
260
361
|
process.exit(1);
|
|
261
362
|
return;
|
|
262
363
|
}
|
|
263
|
-
info(chalk.green(
|
|
364
|
+
info(chalk.green(`${exists ? 'Updated' : 'Created'} ${shown(agent.file)}`) + chalk.gray(` (model ${model})`));
|
|
264
365
|
if (check)
|
|
265
366
|
info(check.ok ? chalk.green(`Verified: streamed reply from ${model} (${check.detail}).`) : chalk.red(`Check failed: ${check.detail}`));
|
|
266
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.
|
|
@@ -118,6 +124,69 @@ export function qwenEdits(model) {
|
|
|
118
124
|
{ path: ['model', 'name'], value: m },
|
|
119
125
|
];
|
|
120
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
|
+
}
|
|
121
190
|
// ---------- Mini-Agent (flat YAML scalars; the key lives in the file) ----------
|
|
122
191
|
export function miniAgentValues(model, apiKey) {
|
|
123
192
|
return { api_key: apiKey, api_base: LLM_BASE_URL, model: validateModel(model), provider: 'openai' };
|
|
@@ -319,6 +388,134 @@ export function revertHermesYaml(input, previous) {
|
|
|
319
388
|
}
|
|
320
389
|
return text;
|
|
321
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
|
+
}
|
|
322
519
|
// ---------- Codex (TOML, edited as text so the user's comments survive) ----------
|
|
323
520
|
const TOP_OPEN = '# >>> wavespeed (managed by `wavespeed setup`) >>>';
|
|
324
521
|
const TOP_CLOSE = '# <<< wavespeed <<<';
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
|
@@ -77,4 +77,5 @@ The CLI never modifies the user's prompt or inputs. The single exception is expl
|
|
|
77
77
|
- Don't invent model IDs. Always confirm via `wavespeed models` or `wavespeed schema <id>` before running.
|
|
78
78
|
- Use `--json` on every `run` so you can read `outputs[0]` programmatically.
|
|
79
79
|
- `wavespeed delete` requires `--yes` when run non-interactively (that includes you).
|
|
80
|
-
-
|
|
80
|
+
- Cost and spend: `wavespeed price <id> -i k=v --json` estimates a run without generating or charging; `wavespeed balance` shows remaining credit; `wavespeed usage` (totals, per-model) and `wavespeed billings` (per-charge records) show what was spent.
|
|
81
|
+
- A `run` that fails with a gateway or network error (502/503/504, connection reset, timeout) does NOT mean the task failed, and the submit request is never retried automatically, so re-running can create a duplicate paid task. Do not re-run immediately. If the error contains a task id (`task_id` / prediction id), keep checking it with `wavespeed show <id> --json` until its status is terminal (`completed`, `failed`, `cancelled`, `timeout`, `deleted`). If no id was returned, check recent predictions with `wavespeed history --json` (add `--model <id>`; default window is the last 24h) and re-run once only if none matches. A client-side wait timeout or an interrupted `run` does not mean the task failed: tasks can run for a long time, so do not give up after a fixed time.
|