@boundless.network/setup 0.1.2 → 0.1.3
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/app.js +6 -8
- package/dist/harnesses/claude-code.js +1 -2
- package/dist/harnesses/codebuddy.js +48 -0
- package/dist/harnesses/codex.js +1 -2
- package/dist/harnesses/gui.js +50 -5
- package/dist/harnesses/hermes.js +1 -2
- package/dist/harnesses/index.js +24 -3
- package/dist/harnesses/omp.js +1 -2
- package/dist/harnesses/openclaw.js +47 -0
- package/dist/harnesses/opencode.js +69 -46
- package/dist/harnesses/openhands.js +36 -0
- package/dist/harnesses/qwen-code.js +40 -0
- package/dist/lib/catalog.js +3 -0
- package/dist/lib/files.js +30 -1
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
// Async screens run their task exactly once, on mount; the wizard object is mutable and shared.
|
|
5
5
|
import { HARNESSES } from "./harnesses/index.js";
|
|
6
6
|
import { openBrowser } from "./lib/browser.js";
|
|
7
|
-
import { keyWorks, listModels } from "./lib/catalog.js";
|
|
7
|
+
import { keyWorks, listModels, RECOMMENDED_BACKGROUND, RECOMMENDED_MODEL } from "./lib/catalog.js";
|
|
8
8
|
import { CONSOLE, KEY_ENV, KEYS_URL, discoverSetup, SetupError } from "./lib/config.js";
|
|
9
9
|
import { inspectEnvironment } from "./lib/env.js";
|
|
10
10
|
import { exchangeForKey } from "./lib/exchange.js";
|
|
@@ -76,7 +76,7 @@ export function App({ onFinish }) {
|
|
|
76
76
|
smokes: wizard.smokes,
|
|
77
77
|
tests: wizard.tests,
|
|
78
78
|
});
|
|
79
|
-
const hasBackgroundSlot = () => chosen().some((harness) => harness.
|
|
79
|
+
const hasBackgroundSlot = () => chosen().some((harness) => harness.backgroundSlot);
|
|
80
80
|
const chooseModel = (role, id) => {
|
|
81
81
|
if (role === "primary") {
|
|
82
82
|
wizard.primary = id;
|
|
@@ -172,7 +172,7 @@ export function App({ onFinish }) {
|
|
|
172
172
|
setScreen({ kind: "model", role: "primary" });
|
|
173
173
|
}, onError: (error) => fail(error, { kind: "catalog" }), wizard: wizard }));
|
|
174
174
|
case "model":
|
|
175
|
-
return (_jsx(ModelPicker, { catalog: wizard.catalog,
|
|
175
|
+
return (_jsx(ModelPicker, { catalog: wizard.catalog, onSelect: (id) => chooseModel(screen.role, id), role: screen.role }));
|
|
176
176
|
case "review":
|
|
177
177
|
return (_jsx(Review, { harnesses: chosen(), input: planInput(), onCancel: () => exit(), onPlans: (plans) => {
|
|
178
178
|
wizard.plans = plans;
|
|
@@ -309,10 +309,8 @@ function Catalog({ onDone, onError, wizard }) {
|
|
|
309
309
|
}, []);
|
|
310
310
|
return (_jsx(Frame, { hints: "", children: _jsx(Spinner, { label: "Reading the model catalog" }) }));
|
|
311
311
|
}
|
|
312
|
-
function ModelPicker({ catalog,
|
|
313
|
-
|
|
314
|
-
const recommended = role === "primary" ? lead?.defaultModel : lead?.backgroundDefault;
|
|
315
|
-
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog", lead ? `; the recommendation is ${lead.name}'s documented default.` : "."] }), _jsx(ModelSelect, { models: catalog, onSelect: onSelect, recommended: recommended ?? undefined }, role)] }));
|
|
312
|
+
function ModelPicker({ catalog, onSelect, role, }) {
|
|
313
|
+
return (_jsxs(Frame, { hints: NAV, children: [_jsx(Title, { children: role === "primary" ? "Which model should be the default?" : "Which model for background work?" }), _jsxs(Muted, { center: true, children: [role === "primary" ? "Every configured harness gets this model." : "Cheap, fast tasks (Claude Code haiku slot, OpenCode small_model, omp tiny).", " Prices are USD per 1M tokens from the live catalog."] }), _jsx(ModelSelect, { models: catalog, onSelect: onSelect, recommended: role === "primary" ? RECOMMENDED_MODEL : RECOMMENDED_BACKGROUND }, role)] }));
|
|
316
314
|
}
|
|
317
315
|
function Review({ harnesses, input, onCancel, onPlans, onProceed, wizard, }) {
|
|
318
316
|
const [plans, setPlans] = useState(null);
|
|
@@ -398,7 +396,7 @@ function Test({ harnesses, onDone, wizard }) {
|
|
|
398
396
|
const pairs = new Map();
|
|
399
397
|
for (const harness of harnesses) {
|
|
400
398
|
pairs.set(`${harness.wire}:${wizard.primary}`, { model: wizard.primary, wire: harness.wire });
|
|
401
|
-
if (harness.
|
|
399
|
+
if (harness.backgroundSlot) {
|
|
402
400
|
pairs.set(`${harness.wire}:${wizard.background}`, { model: wizard.background, wire: harness.wire });
|
|
403
401
|
}
|
|
404
402
|
}
|
|
@@ -6,8 +6,7 @@ function settingsPath(input) {
|
|
|
6
6
|
return input.scope === "user" ? join(claudeHome(), "settings.json") : join(input.env.cwd, ".claude", "settings.local.json");
|
|
7
7
|
}
|
|
8
8
|
export const claudeCode = {
|
|
9
|
-
|
|
10
|
-
defaultModel: "glm-5.2",
|
|
9
|
+
backgroundSlot: true,
|
|
11
10
|
detect: () => installed("claude"),
|
|
12
11
|
id: "claude-code",
|
|
13
12
|
name: "Claude Code",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
const VENDOR = "Boundless";
|
|
4
|
+
export const codebuddy = {
|
|
5
|
+
backgroundSlot: false,
|
|
6
|
+
detect: () => installed("codebuddy"),
|
|
7
|
+
id: "codebuddy",
|
|
8
|
+
name: "CodeBuddy",
|
|
9
|
+
plan: (input) => {
|
|
10
|
+
const path = home(".codebuddy", "models.json");
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = await readJson(path);
|
|
16
|
+
const others = (Array.isArray(current.models) ? current.models : []).filter((entry) => !(typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.vendor === VENDOR));
|
|
17
|
+
const ours = input.catalog.map((model) => ({
|
|
18
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
19
|
+
id: model.id,
|
|
20
|
+
maxInputTokens: model.contextTokens,
|
|
21
|
+
maxOutputTokens: 32_000,
|
|
22
|
+
name: model.id,
|
|
23
|
+
supportsReasoning: model.supports.includes("reasoning"),
|
|
24
|
+
supportsToolCall: model.supports.includes("tool-use"),
|
|
25
|
+
url: `${input.gatewayUrl}/v1/chat/completions`,
|
|
26
|
+
vendor: VENDOR,
|
|
27
|
+
}));
|
|
28
|
+
const available = Array.isArray(current.availableModels) ? current.availableModels : [];
|
|
29
|
+
const ids = new Set(input.catalog.map((model) => model.id));
|
|
30
|
+
const next = {
|
|
31
|
+
...current,
|
|
32
|
+
availableModels: [...available.filter((id) => typeof id !== "string" || !ids.has(id)), ...ids],
|
|
33
|
+
models: [...others, ...ours],
|
|
34
|
+
};
|
|
35
|
+
const saved = await backup(path);
|
|
36
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
37
|
+
return { backup: saved };
|
|
38
|
+
},
|
|
39
|
+
path,
|
|
40
|
+
summary: `${input.catalog.length} Boundless models at ${input.gatewayUrl}/v1/chat/completions, key read from ${KEY_ENV}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
manual: [],
|
|
44
|
+
notes: [`CodeBuddy has no default-model setting: start it with codebuddy --model "${input.primary}" or pick it in the IDE (tagged custom).`],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
wire: "openai",
|
|
48
|
+
};
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -5,8 +5,7 @@ import { codexHome } from "../lib/env.js";
|
|
|
5
5
|
import { installed } from "../lib/files.js";
|
|
6
6
|
import { backup, readText, tilde, writePrivate } from "../lib/files.js";
|
|
7
7
|
export const codex = {
|
|
8
|
-
|
|
9
|
-
defaultModel: "qwen3.6",
|
|
8
|
+
backgroundSlot: false,
|
|
10
9
|
detect: () => installed("codex"),
|
|
11
10
|
id: "codex",
|
|
12
11
|
name: "Codex CLI",
|
package/dist/harnesses/gui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { anyExists, home } from "../lib/files.js";
|
|
1
|
+
import { anyExists, home, installed } from "../lib/files.js";
|
|
2
2
|
import { readExpression } from "../lib/store.js";
|
|
3
3
|
function reveal(input) {
|
|
4
4
|
if (input.keyFile) {
|
|
@@ -7,8 +7,7 @@ function reveal(input) {
|
|
|
7
7
|
return input.store ? readExpression(input.store) : "echo $BOUNDLESS_API_KEY";
|
|
8
8
|
}
|
|
9
9
|
export const cursor = {
|
|
10
|
-
|
|
11
|
-
defaultModel: "glm-5.2",
|
|
10
|
+
backgroundSlot: false,
|
|
12
11
|
detect: () => anyExists(["/Applications/Cursor.app", home(".cursor")]),
|
|
13
12
|
id: "cursor",
|
|
14
13
|
name: "Cursor",
|
|
@@ -25,8 +24,7 @@ export const cursor = {
|
|
|
25
24
|
wire: "openai",
|
|
26
25
|
};
|
|
27
26
|
export const copilot = {
|
|
28
|
-
|
|
29
|
-
defaultModel: "glm-5.2",
|
|
27
|
+
backgroundSlot: false,
|
|
30
28
|
detect: () => anyExists([home(".vscode"), home(".copilot")]),
|
|
31
29
|
id: "copilot",
|
|
32
30
|
name: "GitHub Copilot",
|
|
@@ -41,3 +39,50 @@ export const copilot = {
|
|
|
41
39
|
}),
|
|
42
40
|
wire: "openai",
|
|
43
41
|
};
|
|
42
|
+
export const cline = {
|
|
43
|
+
backgroundSlot: false,
|
|
44
|
+
detect: () => anyExists([home(".vscode", "extensions"), home(".cursor", "extensions")]),
|
|
45
|
+
id: "cline",
|
|
46
|
+
name: "Cline",
|
|
47
|
+
plan: (input) => Promise.resolve({
|
|
48
|
+
changes: [],
|
|
49
|
+
manual: [
|
|
50
|
+
'In Cline choose "OpenAI Compatible" as the API provider.',
|
|
51
|
+
`Base URL ${input.gatewayUrl}/v1, model ID ${input.primary}, key revealed with: ${reveal(input)}`,
|
|
52
|
+
"In Model Configuration set the context window and max output tokens from the model lineup; Cline's defaults are conservative.",
|
|
53
|
+
],
|
|
54
|
+
notes: [],
|
|
55
|
+
}),
|
|
56
|
+
wire: "openai",
|
|
57
|
+
};
|
|
58
|
+
export const cherryStudio = {
|
|
59
|
+
backgroundSlot: false,
|
|
60
|
+
detect: () => anyExists(["/Applications/Cherry Studio.app", home(".config", "CherryStudio")]),
|
|
61
|
+
id: "cherry-studio",
|
|
62
|
+
name: "Cherry Studio",
|
|
63
|
+
plan: (input) => Promise.resolve({
|
|
64
|
+
changes: [],
|
|
65
|
+
manual: [
|
|
66
|
+
"Settings → Model Provider → Add Provider, endpoint type OpenAI.",
|
|
67
|
+
`API address ${input.gatewayUrl} (without /v1; Cherry appends it), key revealed with: ${reveal(input)}`,
|
|
68
|
+
`In the provider wizard select all models under "Choose models", then "Verify and enable"; start on ${input.primary}.`,
|
|
69
|
+
"Settings → Default Model: point Default Assistant Model, Quick Model and the translate model at this provider.",
|
|
70
|
+
],
|
|
71
|
+
notes: ["Cherry Studio stores the key in its own database; it cannot read an environment variable."],
|
|
72
|
+
}),
|
|
73
|
+
wire: "openai",
|
|
74
|
+
};
|
|
75
|
+
export const muse = {
|
|
76
|
+
backgroundSlot: false,
|
|
77
|
+
detect: () => installed("muse"),
|
|
78
|
+
id: "muse",
|
|
79
|
+
name: "Muse",
|
|
80
|
+
plan: (input) => Promise.resolve({
|
|
81
|
+
changes: [],
|
|
82
|
+
manual: [
|
|
83
|
+
`Muse has no provider setting; run it as: META_API_KEY="$BOUNDLESS_API_KEY" muse --provider meta --base-url "${input.gatewayUrl}/v1" --model "${input.primary}"`,
|
|
84
|
+
],
|
|
85
|
+
notes: ["Both Muse flags are undocumented and unverified against this gateway."],
|
|
86
|
+
}),
|
|
87
|
+
wire: "openai",
|
|
88
|
+
};
|
package/dist/harnesses/hermes.js
CHANGED
|
@@ -3,8 +3,7 @@ import { KEY_ENV } from "../lib/config.js";
|
|
|
3
3
|
import { installed } from "../lib/files.js";
|
|
4
4
|
import { backup, home, readText, writePrivate } from "../lib/files.js";
|
|
5
5
|
export const hermes = {
|
|
6
|
-
|
|
7
|
-
defaultModel: "qwen3.6",
|
|
6
|
+
backgroundSlot: false,
|
|
8
7
|
detect: () => installed("hermes"),
|
|
9
8
|
id: "hermes",
|
|
10
9
|
name: "Hermes",
|
package/dist/harnesses/index.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { claudeCode } from "./claude-code.js";
|
|
2
|
+
import { codebuddy } from "./codebuddy.js";
|
|
2
3
|
import { codex } from "./codex.js";
|
|
3
|
-
import { copilot, cursor } from "./gui.js";
|
|
4
|
+
import { cherryStudio, cline, copilot, cursor, muse } from "./gui.js";
|
|
4
5
|
import { hermes } from "./hermes.js";
|
|
5
6
|
import { omp } from "./omp.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import { openclaw } from "./openclaw.js";
|
|
8
|
+
import { kiloCode, mimoCode, opencode } from "./opencode.js";
|
|
9
|
+
import { openhands } from "./openhands.js";
|
|
10
|
+
import { qwenCode } from "./qwen-code.js";
|
|
11
|
+
export const HARNESSES = [
|
|
12
|
+
claudeCode,
|
|
13
|
+
codex,
|
|
14
|
+
opencode,
|
|
15
|
+
hermes,
|
|
16
|
+
omp,
|
|
17
|
+
cursor,
|
|
18
|
+
copilot,
|
|
19
|
+
openhands,
|
|
20
|
+
cline,
|
|
21
|
+
openclaw,
|
|
22
|
+
kiloCode,
|
|
23
|
+
cherryStudio,
|
|
24
|
+
codebuddy,
|
|
25
|
+
mimoCode,
|
|
26
|
+
qwenCode,
|
|
27
|
+
muse,
|
|
28
|
+
];
|
package/dist/harnesses/omp.js
CHANGED
|
@@ -3,8 +3,7 @@ import { KEY_ENV } from "../lib/config.js";
|
|
|
3
3
|
import { installed } from "../lib/files.js";
|
|
4
4
|
import { backup, home, readText, writePrivate } from "../lib/files.js";
|
|
5
5
|
export const omp = {
|
|
6
|
-
|
|
7
|
-
defaultModel: "glm-5.2",
|
|
6
|
+
backgroundSlot: true,
|
|
8
7
|
detect: () => installed("omp"),
|
|
9
8
|
id: "omp",
|
|
10
9
|
name: "omp (Oh My Pi)",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
export const openclaw = {
|
|
4
|
+
backgroundSlot: false,
|
|
5
|
+
detect: () => installed("openclaw"),
|
|
6
|
+
id: "openclaw",
|
|
7
|
+
name: "OpenClaw",
|
|
8
|
+
plan: (input) => {
|
|
9
|
+
const path = home(".openclaw", "openclaw.json");
|
|
10
|
+
return Promise.resolve({
|
|
11
|
+
changes: [
|
|
12
|
+
{
|
|
13
|
+
apply: async () => {
|
|
14
|
+
const current = await readJson(path);
|
|
15
|
+
const next = merge(current, {
|
|
16
|
+
agents: { defaults: { model: { primary: `boundless/${input.primary}` } } },
|
|
17
|
+
models: {
|
|
18
|
+
providers: {
|
|
19
|
+
boundless: {
|
|
20
|
+
api: "openai-completions",
|
|
21
|
+
apiKey: `\${${KEY_ENV}}`,
|
|
22
|
+
baseUrl: `${input.gatewayUrl}/v1`,
|
|
23
|
+
models: input.catalog.map((model) => ({
|
|
24
|
+
contextWindow: model.contextTokens,
|
|
25
|
+
id: model.id,
|
|
26
|
+
maxTokens: 32_000,
|
|
27
|
+
name: model.name,
|
|
28
|
+
reasoning: model.supports.includes("reasoning"),
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const saved = await backup(path);
|
|
35
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
36
|
+
return { backup: saved };
|
|
37
|
+
},
|
|
38
|
+
path,
|
|
39
|
+
summary: `provider boundless with ${input.catalog.length} models, primary boundless/${input.primary}, key read from ${KEY_ENV}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
manual: [],
|
|
43
|
+
notes: [`OpenClaw reads the key from ${KEY_ENV} in your shell environment.`],
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
wire: "openai",
|
|
47
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { KEY_ENV } from "../lib/config.js";
|
|
3
|
-
import { installed } from "../lib/files.js";
|
|
4
|
-
import { backup, home, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
5
4
|
function modelEntry(model) {
|
|
6
5
|
const inputs = ["text", ...["image", "audio", "video"].filter((kind) => model.supports.includes(`${kind} input`))];
|
|
7
6
|
return {
|
|
@@ -12,55 +11,79 @@ function modelEntry(model) {
|
|
|
12
11
|
...(inputs.length > 1 ? { modalities: { input: inputs } } : {}),
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/** OpenCode and its forks share one config format: a provider block plus model/small_model. */
|
|
15
|
+
function opencodeFamily(family) {
|
|
16
|
+
const configPath = (input) => input.scope === "project"
|
|
17
|
+
? join(input.env.cwd, family.file)
|
|
18
|
+
: join(process.env.XDG_CONFIG_HOME ?? home(".config"), family.dir, family.file);
|
|
19
|
+
return {
|
|
20
|
+
backgroundSlot: true,
|
|
21
|
+
detect: () => installed(family.binary),
|
|
22
|
+
id: family.id,
|
|
23
|
+
name: family.name,
|
|
24
|
+
plan: (input) => {
|
|
25
|
+
const path = configPath(input);
|
|
26
|
+
const apiKey = input.scope === "project" && input.keyFile ? `{file:${input.keyFile}}` : `{env:${KEY_ENV}}`;
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
changes: [
|
|
29
|
+
{
|
|
30
|
+
apply: async () => {
|
|
31
|
+
const current = await readJson(path);
|
|
32
|
+
const next = merge(current, {
|
|
33
|
+
$schema: family.schema,
|
|
34
|
+
model: `boundless/${input.primary}`,
|
|
35
|
+
provider: {
|
|
36
|
+
boundless: {
|
|
37
|
+
models: Object.fromEntries(input.catalog.map((model) => [model.id, modelEntry(model)])),
|
|
38
|
+
name: "Boundless",
|
|
39
|
+
npm: "@ai-sdk/openai-compatible",
|
|
40
|
+
options: { apiKey, baseURL: `${input.gatewayUrl}/v1` },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
small_model: `boundless/${input.background}`,
|
|
44
|
+
});
|
|
45
|
+
const saved = await backup(path);
|
|
46
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
47
|
+
return { backup: saved };
|
|
48
|
+
},
|
|
49
|
+
path,
|
|
50
|
+
summary: `provider boundless with ${input.catalog.length} models, model boundless/${input.primary}, small_model boundless/${input.background}, apiKey ${apiKey}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
manual: [],
|
|
54
|
+
notes: family.file.endsWith(".jsonc") ? [`Comments in an existing ${family.file} are not preserved; the backup keeps the original.`] : [],
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
smoke: family.smoke,
|
|
58
|
+
wire: "openai",
|
|
59
|
+
};
|
|
20
60
|
}
|
|
21
|
-
export const opencode = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
export const opencode = opencodeFamily({
|
|
62
|
+
binary: "opencode",
|
|
63
|
+
dir: "opencode",
|
|
64
|
+
file: "opencode.json",
|
|
25
65
|
id: "opencode",
|
|
26
66
|
name: "OpenCode",
|
|
27
|
-
|
|
28
|
-
const path = configPath(input);
|
|
29
|
-
const apiKey = input.scope === "project" && input.keyFile ? `{file:${input.keyFile}}` : `{env:${KEY_ENV}}`;
|
|
30
|
-
return Promise.resolve({
|
|
31
|
-
changes: [
|
|
32
|
-
{
|
|
33
|
-
apply: async () => {
|
|
34
|
-
const current = await readJson(path);
|
|
35
|
-
const next = merge(current, {
|
|
36
|
-
$schema: "https://opencode.ai/config.json",
|
|
37
|
-
model: `boundless/${input.primary}`,
|
|
38
|
-
provider: {
|
|
39
|
-
boundless: {
|
|
40
|
-
models: Object.fromEntries(input.catalog.map((model) => [model.id, modelEntry(model)])),
|
|
41
|
-
name: "Boundless",
|
|
42
|
-
npm: "@ai-sdk/openai-compatible",
|
|
43
|
-
options: { apiKey, baseURL: `${input.gatewayUrl}/v1` },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
small_model: `boundless/${input.background}`,
|
|
47
|
-
});
|
|
48
|
-
const saved = await backup(path);
|
|
49
|
-
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
50
|
-
return { backup: saved };
|
|
51
|
-
},
|
|
52
|
-
path,
|
|
53
|
-
summary: `provider boundless with ${input.catalog.length} models, model boundless/${input.primary}, small_model boundless/${input.background}, apiKey ${apiKey}`,
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
manual: [],
|
|
57
|
-
notes: [],
|
|
58
|
-
});
|
|
59
|
-
},
|
|
67
|
+
schema: "https://opencode.ai/config.json",
|
|
60
68
|
smoke: (input) => ({
|
|
61
69
|
args: ["run", "--model", `boundless/${input.primary}`, "Use the bash tool to run printf boundless, then report the output."],
|
|
62
70
|
command: "opencode",
|
|
63
71
|
expect: /boundless/i,
|
|
64
72
|
}),
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
});
|
|
74
|
+
export const kiloCode = opencodeFamily({
|
|
75
|
+
binary: "kilo",
|
|
76
|
+
dir: "kilo",
|
|
77
|
+
file: "kilo.jsonc",
|
|
78
|
+
id: "kilo-code",
|
|
79
|
+
name: "Kilo Code",
|
|
80
|
+
schema: "https://app.kilo.ai/config.json",
|
|
81
|
+
});
|
|
82
|
+
export const mimoCode = opencodeFamily({
|
|
83
|
+
binary: "mimo",
|
|
84
|
+
dir: "mimocode",
|
|
85
|
+
file: "mimocode.jsonc",
|
|
86
|
+
id: "mimo-code",
|
|
87
|
+
name: "MiMo Code",
|
|
88
|
+
schema: "https://mimo.xiaomi.com/mimocode/config.json",
|
|
89
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { parse, stringify } from "smol-toml";
|
|
3
|
+
import { backup, installed, readText, tilde, writePrivate } from "../lib/files.js";
|
|
4
|
+
export const openhands = {
|
|
5
|
+
backgroundSlot: false,
|
|
6
|
+
detect: () => installed("openhands"),
|
|
7
|
+
id: "openhands",
|
|
8
|
+
name: "OpenHands",
|
|
9
|
+
plan: (input) => {
|
|
10
|
+
const path = join(input.env.cwd, "config.toml");
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = parse((await readText(path)) ?? "");
|
|
16
|
+
const llm = (current.llm ?? {});
|
|
17
|
+
const next = {
|
|
18
|
+
...current,
|
|
19
|
+
llm: { ...llm, api_key: input.key, base_url: `${input.gatewayUrl}/v1`, model: `openai/${input.primary}` },
|
|
20
|
+
};
|
|
21
|
+
const saved = await backup(path);
|
|
22
|
+
await writePrivate(path, stringify(next));
|
|
23
|
+
return { backup: saved };
|
|
24
|
+
},
|
|
25
|
+
path,
|
|
26
|
+
summary: `[llm] model openai/${input.primary}, base_url ${input.gatewayUrl}/v1, api_key (key kept in this 0600 file)`,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
manual: [],
|
|
30
|
+
notes: [
|
|
31
|
+
`OpenHands reads config.toml from the working directory, so it is written to ${tilde(path)} and holds the key itself; keep it out of git.`,
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
wire: "openai",
|
|
36
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KEY_ENV } from "../lib/config.js";
|
|
2
|
+
import { backup, home, installed, merge, readJson, writePrivate } from "../lib/files.js";
|
|
3
|
+
export const qwenCode = {
|
|
4
|
+
backgroundSlot: false,
|
|
5
|
+
detect: () => installed("qwen"),
|
|
6
|
+
id: "qwen-code",
|
|
7
|
+
name: "Qwen Code",
|
|
8
|
+
plan: (input) => {
|
|
9
|
+
const path = home(".qwen", "settings.json");
|
|
10
|
+
const baseUrl = `${input.gatewayUrl}/v1`;
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
changes: [
|
|
13
|
+
{
|
|
14
|
+
apply: async () => {
|
|
15
|
+
const current = await readJson(path);
|
|
16
|
+
const providers = current.modelProviders;
|
|
17
|
+
const existing = typeof providers === "object" && providers !== null && !Array.isArray(providers) && Array.isArray(providers.openai)
|
|
18
|
+
? providers.openai
|
|
19
|
+
: [];
|
|
20
|
+
const others = existing.filter((entry) => !(typeof entry === "object" && entry !== null && !Array.isArray(entry) && entry.baseUrl === baseUrl));
|
|
21
|
+
const ours = input.catalog.map((model) => ({ baseUrl, envKey: KEY_ENV, id: model.id, name: model.id }));
|
|
22
|
+
const next = merge(current, {
|
|
23
|
+
model: { name: input.primary },
|
|
24
|
+
modelProviders: { openai: [...others, ...ours] },
|
|
25
|
+
security: { auth: { selectedType: "openai" } },
|
|
26
|
+
});
|
|
27
|
+
const saved = await backup(path);
|
|
28
|
+
await writePrivate(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
29
|
+
return { backup: saved };
|
|
30
|
+
},
|
|
31
|
+
path,
|
|
32
|
+
summary: `${input.catalog.length} models under modelProviders.openai, model ${input.primary}, key read from ${KEY_ENV}`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
manual: [],
|
|
36
|
+
notes: [`Qwen Code reads the key from ${KEY_ENV} in your shell environment (or ~/.qwen/.env).`],
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
wire: "openai",
|
|
40
|
+
};
|
package/dist/lib/catalog.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { CONSOLE, request, SetupError } from "./config.js";
|
|
2
|
+
/** One recommendation for every harness: the default model, and the model for cheap background slots. */
|
|
3
|
+
export const RECOMMENDED_MODEL = "deepseek-v4.1-flash";
|
|
4
|
+
export const RECOMMENDED_BACKGROUND = "dsv4";
|
|
2
5
|
export async function listModels(gatewayUrl, key) {
|
|
3
6
|
const [catalog, served] = await Promise.all([
|
|
4
7
|
request(`${CONSOLE}/api/catalog`),
|
package/dist/lib/files.js
CHANGED
|
@@ -85,12 +85,41 @@ export function merge(base, patch) {
|
|
|
85
85
|
}
|
|
86
86
|
return patch;
|
|
87
87
|
}
|
|
88
|
+
// Drops line and block comments outside strings so a .jsonc file parses. Comments are not written back.
|
|
89
|
+
export function stripComments(text) {
|
|
90
|
+
let out = "";
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < text.length) {
|
|
93
|
+
const ch = text[i];
|
|
94
|
+
if (ch === '"') {
|
|
95
|
+
let j = i + 1;
|
|
96
|
+
while (j < text.length && text[j] !== '"') {
|
|
97
|
+
j += text[j] === "\\" ? 2 : 1;
|
|
98
|
+
}
|
|
99
|
+
out += text.slice(i, j + 1);
|
|
100
|
+
i = j + 1;
|
|
101
|
+
}
|
|
102
|
+
else if (ch === "/" && text[i + 1] === "/") {
|
|
103
|
+
i = text.indexOf("\n", i);
|
|
104
|
+
i = i === -1 ? text.length : i;
|
|
105
|
+
}
|
|
106
|
+
else if (ch === "/" && text[i + 1] === "*") {
|
|
107
|
+
const end = text.indexOf("*/", i + 2);
|
|
108
|
+
i = end === -1 ? text.length : end + 2;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out += ch;
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
88
117
|
export async function readJson(path) {
|
|
89
118
|
const text = await readText(path);
|
|
90
119
|
if (text === null || text.trim() === "") {
|
|
91
120
|
return {};
|
|
92
121
|
}
|
|
93
|
-
const parsed = JSON.parse(text);
|
|
122
|
+
const parsed = JSON.parse(stripComments(text).replace(/,(\s*[}\]])/g, "$1"));
|
|
94
123
|
if (!isObject(parsed)) {
|
|
95
124
|
throw new Error(`${path} is not a JSON object`);
|
|
96
125
|
}
|
package/package.json
CHANGED