@rerouter/helper 0.0.0-stage → 0.1.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/README.md +119 -2
- package/package.json +36 -4
- package/src/cli.js +266 -0
- package/src/lib/api.js +35 -0
- package/src/lib/bridge-server.js +217 -0
- package/src/lib/env.js +32 -0
- package/src/lib/files.js +85 -0
- package/src/lib/responses-bridge.js +639 -0
- package/src/lib/targets.js +390 -0
- package/src/responses-bridge.test.js +580 -0
- package/src/targets.test.js +215 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
2
|
+
import { DEFAULT_BRIDGE_PORT } from "./bridge-server.js";
|
|
3
|
+
import { DEFAULT_BASE_URL } from "./env.js";
|
|
4
|
+
import { backupFile, expandHome, parseJsonc, readTextIfExists, writeFileEnsured } from "./files.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration target registry. Each target returns a "plan":
|
|
8
|
+
* { name, description, changes: [{file, summary, content}], notes: [] }
|
|
9
|
+
* Plan is printed first (preview); writing only happens via `applyPlan` after
|
|
10
|
+
* confirmation/--yes, and is ALWAYS preceded by a timestamped backup.
|
|
11
|
+
*
|
|
12
|
+
* The key is NEVER written to any file — only env/placeholder references.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const AUTOMATIC_TARGETS = ["claude", "codex", "opencode", "omp", "continue"];
|
|
16
|
+
export const SNIPPET_TARGETS = ["openai", "anthropic"];
|
|
17
|
+
export const GUIDE_TARGETS = ["cursor", "vscode", "cline", "roo"];
|
|
18
|
+
export const ALL_TARGETS = [...AUTOMATIC_TARGETS, ...SNIPPET_TARGETS, ...GUIDE_TARGETS];
|
|
19
|
+
|
|
20
|
+
function keyNeverWrittenNote() {
|
|
21
|
+
return "API key is NOT written to any file. Set env REROUTER_API_KEY (or the env named in the notes) in your shell.";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Small/cheap model heuristic for ANTHROPIC_SMALL_FAST_MODEL. */
|
|
25
|
+
export function pickSmallModel(models, mainModel) {
|
|
26
|
+
const ids = models.map((m) => String(m?.id ?? "")).filter(Boolean);
|
|
27
|
+
const cheap = ids.find((id) => /haiku|mini|flash|lite|small|nano/i.test(id));
|
|
28
|
+
return cheap ?? mainModel;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------- claude
|
|
32
|
+
function planClaude({ baseUrl, model, smallModel }) {
|
|
33
|
+
const file = expandHome("~/.claude/settings.json");
|
|
34
|
+
return {
|
|
35
|
+
name: "claude",
|
|
36
|
+
description: "[CC] — merge env in ~/.claude/settings.json",
|
|
37
|
+
file,
|
|
38
|
+
async build() {
|
|
39
|
+
const existing = await readTextIfExists(file);
|
|
40
|
+
let settings = {};
|
|
41
|
+
if (existing !== null) {
|
|
42
|
+
try {
|
|
43
|
+
settings = parseJsonc(existing);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${file} could not be parsed (ambiguous/broken JSONC).\n` +
|
|
47
|
+
`Manual merge — add:\n` +
|
|
48
|
+
` "env": { "ANTHROPIC_BASE_URL": "${baseUrl}", "ANTHROPIC_MODEL": "${model}", "ANTHROPIC_SMALL_FAST_MODEL": "${smallModel}" }`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
52
|
+
throw new Error(`${file} is not a JSON object — merge manually.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const env = { ...(settings.env ?? {}) };
|
|
56
|
+
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
57
|
+
env.ANTHROPIC_MODEL = model;
|
|
58
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = smallModel;
|
|
59
|
+
// Key not written: [CC] reads ANTHROPIC_AUTH_TOKEN from shell env.
|
|
60
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
61
|
+
delete env.ANTHROPIC_API_KEY;
|
|
62
|
+
const next = { ...settings, env };
|
|
63
|
+
return JSON.stringify(next, null, 2) + "\n";
|
|
64
|
+
},
|
|
65
|
+
notes: [
|
|
66
|
+
keyNeverWrittenNote(),
|
|
67
|
+
"Run [CC] with: ANTHROPIC_AUTH_TOKEN filled from REROUTER_API_KEY.",
|
|
68
|
+
" POSIX: export ANTHROPIC_AUTH_TOKEN=\"$REROUTER_API_KEY\"",
|
|
69
|
+
" PowerShell: $env:ANTHROPIC_AUTH_TOKEN = $env:REROUTER_API_KEY",
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------- opencode
|
|
75
|
+
function planOpencode({ baseUrl, model }) {
|
|
76
|
+
const file = expandHome("~/.config/opencode/opencode.json");
|
|
77
|
+
return {
|
|
78
|
+
name: "opencode",
|
|
79
|
+
description: "opencode — custom provider in ~/.config/opencode/opencode.json",
|
|
80
|
+
file,
|
|
81
|
+
async build() {
|
|
82
|
+
const existing = await readTextIfExists(file);
|
|
83
|
+
let config = {};
|
|
84
|
+
if (existing !== null) {
|
|
85
|
+
try {
|
|
86
|
+
config = parseJsonc(existing);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`${file} could not be parsed — merge manually (see Rerouter docs).`);
|
|
89
|
+
}
|
|
90
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
91
|
+
throw new Error(`${file} is not a JSON object — merge manually.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const providers = { ...(config.provider ?? {}) };
|
|
95
|
+
const prev = providers.rerouter ?? {};
|
|
96
|
+
providers.rerouter = {
|
|
97
|
+
npm: "@ai-sdk/openai-compatible",
|
|
98
|
+
name: "Rerouter",
|
|
99
|
+
...prev,
|
|
100
|
+
options: {
|
|
101
|
+
...(prev.options ?? {}),
|
|
102
|
+
baseURL: `${baseUrl}/v1`,
|
|
103
|
+
apiKey: "{env:REROUTER_API_KEY}",
|
|
104
|
+
},
|
|
105
|
+
models: {
|
|
106
|
+
...(prev.models ?? {}),
|
|
107
|
+
[model]: { name: model },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const next = {
|
|
111
|
+
$schema: "https://opencode.ai/config.json",
|
|
112
|
+
...config,
|
|
113
|
+
provider: providers,
|
|
114
|
+
model: config.model ?? `rerouter/${model}`,
|
|
115
|
+
};
|
|
116
|
+
return JSON.stringify(next, null, 2) + "\n";
|
|
117
|
+
},
|
|
118
|
+
notes: [keyNeverWrittenNote(), "opencode reads apiKey from env REROUTER_API_KEY ({env:...} reference)."],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------- omp
|
|
123
|
+
function planOmp({ baseUrl, model }) {
|
|
124
|
+
const file = expandHome("~/.omp/agent/models.yml");
|
|
125
|
+
return {
|
|
126
|
+
name: "omp",
|
|
127
|
+
description: "OMP — provider entry in ~/.omp/agent/models.yml",
|
|
128
|
+
file,
|
|
129
|
+
async build() {
|
|
130
|
+
const existing = await readTextIfExists(file);
|
|
131
|
+
let doc = {};
|
|
132
|
+
if (existing !== null) {
|
|
133
|
+
try {
|
|
134
|
+
doc = parseYaml(existing) ?? {};
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`${file} YAML broken — merge manually (see Rerouter docs).`);
|
|
137
|
+
}
|
|
138
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
139
|
+
throw new Error(`${file} is not a YAML mapping — merge manually.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const providers = { ...(doc.providers ?? {}) };
|
|
143
|
+
const prev = providers.rerouter ?? {};
|
|
144
|
+
const prevModels = Array.isArray(prev.models) ? prev.models : [];
|
|
145
|
+
const others = prevModels.filter((m) => m?.id !== model);
|
|
146
|
+
providers.rerouter = {
|
|
147
|
+
type: "openai-compatible",
|
|
148
|
+
base_url: `${baseUrl}/v1`,
|
|
149
|
+
api_key_env: "REROUTER_API_KEY",
|
|
150
|
+
...prev,
|
|
151
|
+
models: [{ id: model, default: true }, ...others.map((m) => ({ ...m, default: false }))],
|
|
152
|
+
};
|
|
153
|
+
return stringifyYaml({ ...doc, providers });
|
|
154
|
+
},
|
|
155
|
+
notes: [keyNeverWrittenNote(), "OMP reads key from env named by api_key_env."],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------- codex
|
|
160
|
+
const TOML_PROVIDER_BLOCK = (port) =>
|
|
161
|
+
`\n[model_providers.rerouter]\nname = "Rerouter (local bridge)"\nbase_url = "http://127.0.0.1:${port}/v1"\nenv_key = "REROUTER_API_KEY"\nwire_api = "responses"\n`;
|
|
162
|
+
|
|
163
|
+
function planCodex({ model, port = DEFAULT_BRIDGE_PORT }) {
|
|
164
|
+
const file = expandHome("~/.codex/config.toml");
|
|
165
|
+
return {
|
|
166
|
+
name: "codex",
|
|
167
|
+
description: `Codex CLI — local bridge provider in ~/.codex/config.toml (port ${port})`,
|
|
168
|
+
file,
|
|
169
|
+
async build() {
|
|
170
|
+
const existing = (await readTextIfExists(file)) ?? "";
|
|
171
|
+
|
|
172
|
+
// Separate top-scope (key=value before first section) from sections.
|
|
173
|
+
const firstSection = existing.search(/^\[/m);
|
|
174
|
+
const topRaw = firstSection >= 0 ? existing.slice(0, firstSection) : existing;
|
|
175
|
+
const sections = firstSection >= 0 ? existing.slice(firstSection) : "";
|
|
176
|
+
|
|
177
|
+
// Validate top-scope: only key=value, comments, or empty lines.
|
|
178
|
+
// Broken/ambiguous TOML → merge manually (file left intact).
|
|
179
|
+
for (const line of topRaw.split("\n")) {
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
182
|
+
if (!/^[A-Za-z0-9_.-]+\s*=/.test(trimmed)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`${file} contains an unrecognized top-level line ("${trimmed}") — merge manually.\n` +
|
|
185
|
+
`Add it yourself:\n model_provider = "rerouter"\n model = "${model}"\n${TOML_PROVIDER_BLOCK(port)}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Set/replace top-level keys.
|
|
191
|
+
let top = topRaw;
|
|
192
|
+
const upsert = (key, value) => {
|
|
193
|
+
const re = new RegExp(`^${key}\\s*=.*$`, "m");
|
|
194
|
+
top = re.test(top) ? top.replace(re, `${key} = ${value}`) : `${top.replace(/\s*$/, "")}${top.trim() ? "\n" : ""}${key} = ${value}\n`;
|
|
195
|
+
};
|
|
196
|
+
upsert("model_provider", '"rerouter"');
|
|
197
|
+
upsert("model", JSON.stringify(model));
|
|
198
|
+
|
|
199
|
+
// Provider block: append if not present (idempotent — old block left intact).
|
|
200
|
+
const hasProvider = /^\[model_providers\.rerouter\]\s*$/m.test(sections);
|
|
201
|
+
const body = hasProvider ? sections : `${sections.replace(/\s*$/, "")}${sections.trim() ? "\n" : ""}${TOML_PROVIDER_BLOCK(port)}`;
|
|
202
|
+
return `${top.replace(/\s*$/, "")}\n\n${body.replace(/^\n+/, "")}`;
|
|
203
|
+
},
|
|
204
|
+
notes: [
|
|
205
|
+
keyNeverWrittenNote(),
|
|
206
|
+
"Codex reads key from env REROUTER_API_KEY (env_key in config).",
|
|
207
|
+
`Codex speaks Responses API — run the local bridge BEFORE codex:`,
|
|
208
|
+
` npx @rerouter/helper bridge${port === DEFAULT_BRIDGE_PORT ? "" : ` --port ${port}`}`,
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------- continue
|
|
214
|
+
function planContinue({ baseUrl, model }) {
|
|
215
|
+
const configFile = expandHome("~/.continue/config.yaml");
|
|
216
|
+
const envFile = expandHome("~/.continue/.env");
|
|
217
|
+
return {
|
|
218
|
+
name: "continue",
|
|
219
|
+
description: "Continue — model entry in ~/.continue/config.yaml (+ .env placeholder)",
|
|
220
|
+
file: configFile,
|
|
221
|
+
extraFiles: [envFile],
|
|
222
|
+
async build() {
|
|
223
|
+
const existing = await readTextIfExists(configFile);
|
|
224
|
+
let doc = {};
|
|
225
|
+
if (existing !== null) {
|
|
226
|
+
try {
|
|
227
|
+
doc = parseYaml(existing) ?? {};
|
|
228
|
+
} catch {
|
|
229
|
+
throw new Error(`${configFile} YAML broken — merge manually (see Rerouter docs).`);
|
|
230
|
+
}
|
|
231
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
232
|
+
throw new Error(`${configFile} is not a YAML mapping — merge manually.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const models = Array.isArray(doc.models) ? [...doc.models] : [];
|
|
236
|
+
const entry = {
|
|
237
|
+
name: `Rerouter ${model}`,
|
|
238
|
+
provider: "openai",
|
|
239
|
+
model,
|
|
240
|
+
apiBase: `${baseUrl}/v1`,
|
|
241
|
+
apiKey: "${{ secrets.REROUTER_API_KEY }}",
|
|
242
|
+
roles: ["chat", "edit"],
|
|
243
|
+
default: true,
|
|
244
|
+
};
|
|
245
|
+
const idx = models.findIndex((m) => m?.name === entry.name || m?.apiBase === entry.apiBase);
|
|
246
|
+
const nextModels =
|
|
247
|
+
idx >= 0
|
|
248
|
+
? models.map((m, i) => (i === idx ? { ...entry, default: true } : { ...m, default: false }))
|
|
249
|
+
: [...models.map((m) => ({ ...m, default: false })), entry];
|
|
250
|
+
const next = {
|
|
251
|
+
name: doc.name ?? "Local Assistant",
|
|
252
|
+
version: doc.version ?? "1.0.0",
|
|
253
|
+
schema: doc.schema ?? "v1",
|
|
254
|
+
...doc,
|
|
255
|
+
models: nextModels,
|
|
256
|
+
};
|
|
257
|
+
return stringifyYaml(next);
|
|
258
|
+
},
|
|
259
|
+
async buildExtra() {
|
|
260
|
+
// Placeholder secret — NOT the real key. The user fills it in.
|
|
261
|
+
const existing = await readTextIfExists(envFile);
|
|
262
|
+
if (existing !== null && /^\s*REROUTER_API_KEY\s*=/m.test(existing)) return null;
|
|
263
|
+
const line =
|
|
264
|
+
"# Fill with your Rerouter key (sk-rr-...). Helper never writes the real key.\nREROUTER_API_KEY=\n";
|
|
265
|
+
return existing === null ? line : `${existing.replace(/\s*$/, "\n")}\n${line}`;
|
|
266
|
+
},
|
|
267
|
+
notes: [
|
|
268
|
+
keyNeverWrittenNote(),
|
|
269
|
+
`Fill your key in ${envFile} (line REROUTER_API_KEY=) — Continue reads it as a secret.`,
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function planTarget(target, { baseUrl = DEFAULT_BASE_URL, model, smallModel, port } = {}) {
|
|
275
|
+
if (!AUTOMATIC_TARGETS.includes(target)) {
|
|
276
|
+
throw new Error(`Target "${target}" is not an automatic target. Options: ${AUTOMATIC_TARGETS.join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
if (!model) throw new Error(`--model is required for target "${target}".`);
|
|
279
|
+
const ctx = { baseUrl, model, smallModel: smallModel ?? model, port };
|
|
280
|
+
switch (target) {
|
|
281
|
+
case "claude":
|
|
282
|
+
return planClaude(ctx);
|
|
283
|
+
case "codex":
|
|
284
|
+
return planCodex(ctx);
|
|
285
|
+
case "opencode":
|
|
286
|
+
return planOpencode(ctx);
|
|
287
|
+
case "omp":
|
|
288
|
+
return planOmp(ctx);
|
|
289
|
+
case "continue":
|
|
290
|
+
return planContinue(ctx);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Apply plan: timestamped backup first, then write. Returns a list of
|
|
296
|
+
* {file, backupPath} to report to the user.
|
|
297
|
+
*/
|
|
298
|
+
export async function applyPlan(plan, content) {
|
|
299
|
+
const results = [];
|
|
300
|
+
const backupPath = await backupFile(plan.file);
|
|
301
|
+
await writeFileEnsured(plan.file, content);
|
|
302
|
+
results.push({ file: plan.file, backupPath });
|
|
303
|
+
|
|
304
|
+
if (typeof plan.buildExtra === "function" && Array.isArray(plan.extraFiles)) {
|
|
305
|
+
const extra = await plan.buildExtra();
|
|
306
|
+
if (extra !== null && extra !== undefined) {
|
|
307
|
+
const extraFile = plan.extraFiles[0];
|
|
308
|
+
const extraBackup = await backupFile(extraFile);
|
|
309
|
+
await writeFileEnsured(extraFile, extra);
|
|
310
|
+
results.push({ file: extraFile, backupPath: extraBackup });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------- snippets & guides
|
|
317
|
+
export function snippet(target, { baseUrl = DEFAULT_BASE_URL, model } = {}) {
|
|
318
|
+
const m = model ?? "<MODEL_ID>";
|
|
319
|
+
if (target === "openai") {
|
|
320
|
+
return [
|
|
321
|
+
"# POSIX (bash/zsh)",
|
|
322
|
+
`export OPENAI_BASE_URL="${baseUrl}/v1"`,
|
|
323
|
+
`export OPENAI_API_KEY="$REROUTER_API_KEY"`,
|
|
324
|
+
"",
|
|
325
|
+
"# PowerShell",
|
|
326
|
+
`$env:OPENAI_BASE_URL = "${baseUrl}/v1"`,
|
|
327
|
+
`$env:OPENAI_API_KEY = $env:REROUTER_API_KEY`,
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
330
|
+
if (target === "anthropic") {
|
|
331
|
+
return [
|
|
332
|
+
"# POSIX (bash/zsh)",
|
|
333
|
+
`export ANTHROPIC_BASE_URL="${baseUrl}"`,
|
|
334
|
+
`export ANTHROPIC_AUTH_TOKEN="$REROUTER_API_KEY"`,
|
|
335
|
+
`export ANTHROPIC_MODEL="${m}"`,
|
|
336
|
+
`export ANTHROPIC_SMALL_FAST_MODEL="${m}"`,
|
|
337
|
+
"",
|
|
338
|
+
"# PowerShell",
|
|
339
|
+
`$env:ANTHROPIC_BASE_URL = "${baseUrl}"`,
|
|
340
|
+
`$env:ANTHROPIC_AUTH_TOKEN = $env:REROUTER_API_KEY`,
|
|
341
|
+
`$env:ANTHROPIC_MODEL = "${m}"`,
|
|
342
|
+
`$env:ANTHROPIC_SMALL_FAST_MODEL = "${m}"`,
|
|
343
|
+
].join("\n");
|
|
344
|
+
}
|
|
345
|
+
throw new Error(`No snippet for target "${target}".`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function guide(target, { baseUrl = DEFAULT_BASE_URL, model } = {}) {
|
|
349
|
+
const m = model ?? "<MODEL_ID>";
|
|
350
|
+
const oaiBase = `${baseUrl}/v1`;
|
|
351
|
+
switch (target) {
|
|
352
|
+
case "cursor":
|
|
353
|
+
return [
|
|
354
|
+
"Cursor — manual setup (no stable config file):",
|
|
355
|
+
" 1. Open Settings → Models.",
|
|
356
|
+
` 2. Fill OpenAI-compatible API Key with your Rerouter key (sk-rr-...).`,
|
|
357
|
+
` 3. Enable "Override OpenAI-compatible Base URL" → ${oaiBase}`,
|
|
358
|
+
` 4. Add custom model: ${m}`,
|
|
359
|
+
" Note: Cursor verifies the key with a small request — that's normal.",
|
|
360
|
+
].join("\n");
|
|
361
|
+
case "vscode":
|
|
362
|
+
return [
|
|
363
|
+
"VS Code (Copilot BYOK):",
|
|
364
|
+
" 1. Command Palette → \"Manage Language Models\" (or gear icon in model picker).",
|
|
365
|
+
" 2. Select custom/OpenAI-compatible provider → Custom Endpoint.",
|
|
366
|
+
` 3. Base URL: ${oaiBase} — API Key: your Rerouter key.`,
|
|
367
|
+
` 4. Model: ${m}`,
|
|
368
|
+
].join("\n");
|
|
369
|
+
case "cline":
|
|
370
|
+
return [
|
|
371
|
+
"Cline (VS Code extension):",
|
|
372
|
+
" 1. Open Cline → Settings (gear icon).",
|
|
373
|
+
" 2. API Provider: \"OpenAI-compatible\".",
|
|
374
|
+
` 3. Base URL: ${oaiBase}`,
|
|
375
|
+
" 4. API Key: your Rerouter key (sk-rr-...).",
|
|
376
|
+
` 5. Model ID: ${m}`,
|
|
377
|
+
].join("\n");
|
|
378
|
+
case "roo":
|
|
379
|
+
return [
|
|
380
|
+
"Roo Code:",
|
|
381
|
+
" 1. Open Roo Code → Settings → Providers.",
|
|
382
|
+
" 2. API Provider: \"OpenAI-compatible\".",
|
|
383
|
+
` 3. Base URL: ${oaiBase}`,
|
|
384
|
+
" 4. API Key: your Rerouter key (sk-rr-...).",
|
|
385
|
+
` 5. Model: ${m}`,
|
|
386
|
+
].join("\n");
|
|
387
|
+
default:
|
|
388
|
+
throw new Error(`No guide for target "${target}".`);
|
|
389
|
+
}
|
|
390
|
+
}
|