@clawscarf/cli 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +3 -0
- package/THIRD_PARTY_NOTICES.md +102 -0
- package/deploy/execution/browser/LICENSE.playwright +202 -0
- package/deploy/execution/browser/seccomp.json +640 -0
- package/deploy/execution/browser-node/configuration.js +85 -0
- package/deploy/execution/browser-node/operator.js +167 -0
- package/deploy/execution/network/node-ingress.cfg +35 -0
- package/deploy/models/catalog.json +275 -0
- package/deploy/openshell/policy.yaml +24 -0
- package/generated/http/LICENSE.md +21 -0
- package/generated/http/client/client.gen.d.ts +2 -0
- package/generated/http/client/client.gen.js +216 -0
- package/generated/http/client/index.d.ts +10 -0
- package/generated/http/client/index.js +6 -0
- package/generated/http/client/types.gen.d.ts +120 -0
- package/generated/http/client/types.gen.js +2 -0
- package/generated/http/client/utils.gen.d.ts +37 -0
- package/generated/http/client/utils.gen.js +228 -0
- package/generated/http/core/auth.gen.d.ts +25 -0
- package/generated/http/core/auth.gen.js +14 -0
- package/generated/http/core/bodySerializer.gen.d.ts +25 -0
- package/generated/http/core/bodySerializer.gen.js +57 -0
- package/generated/http/core/params.gen.d.ts +43 -0
- package/generated/http/core/params.gen.js +109 -0
- package/generated/http/core/pathSerializer.gen.d.ts +33 -0
- package/generated/http/core/pathSerializer.gen.js +106 -0
- package/generated/http/core/queryKeySerializer.gen.d.ts +18 -0
- package/generated/http/core/queryKeySerializer.gen.js +92 -0
- package/generated/http/core/serverSentEvents.gen.d.ts +71 -0
- package/generated/http/core/serverSentEvents.gen.js +132 -0
- package/generated/http/core/types.gen.d.ts +83 -0
- package/generated/http/core/types.gen.js +2 -0
- package/generated/http/core/utils.gen.d.ts +19 -0
- package/generated/http/core/utils.gen.js +87 -0
- package/package.json +37 -0
- package/packs/README.md +130 -0
- package/packs/research-team/pack.json +20 -0
- package/packs/research-team/researcher/CLAW.md +19 -0
- package/packs/research-team/researcher/package.json +8 -0
- package/packs/research-team/researcher/profiles/openclaw.yml +6 -0
- package/packs/research-team/reviewer/CLAW.md +19 -0
- package/packs/research-team/reviewer/package.json +8 -0
- package/packs/research-team/reviewer/profiles/openclaw.yml +6 -0
- package/pnpm-lock.yaml +7158 -0
- package/recipes/README.md +37 -0
- package/recipes/team-server/recipe.json +28 -0
- package/release/README.md +137 -0
- package/release/components.json +29 -0
- package/release/operator.md +63 -0
- package/runtime/configuration.js +135 -0
- package/runtime/connections-configuration.js +70 -0
- package/runtime/model-contract.js +92 -0
- package/runtime/releases/0.1.0-alpha.1.json +37 -0
- package/scripts/clawscarf.js +92 -0
- package/scripts/cloud/login.js +134 -0
- package/scripts/cloud/registration.js +232 -0
- package/scripts/cloud/url.js +9 -0
- package/scripts/connections.js +193 -0
- package/scripts/controller.js +143 -0
- package/scripts/deployment/browser-node-compose.js +81 -0
- package/scripts/deployment/browser-node-helper.js +9 -0
- package/scripts/deployment/browser-node-pairing.js +165 -0
- package/scripts/deployment/browser-node.js +154 -0
- package/scripts/deployment/browser.js +143 -0
- package/scripts/deployment/certificates.js +76 -0
- package/scripts/deployment/compose.js +191 -0
- package/scripts/deployment/configuration.js +275 -0
- package/scripts/deployment/connection-policy.js +80 -0
- package/scripts/deployment/connection-settings.js +60 -0
- package/scripts/deployment/connections-runtime.js +31 -0
- package/scripts/deployment/connections.js +213 -0
- package/scripts/deployment/controller-compose.js +59 -0
- package/scripts/deployment/database.js +199 -0
- package/scripts/deployment/entrypoint.js +7 -0
- package/scripts/deployment/images.js +17 -0
- package/scripts/deployment/initial-services.js +53 -0
- package/scripts/deployment/launch.js +177 -0
- package/scripts/deployment/logs.js +15 -0
- package/scripts/deployment/model-gateway-compose.js +74 -0
- package/scripts/deployment/model-gateway.js +125 -0
- package/scripts/deployment/models.js +82 -0
- package/scripts/deployment/networks.js +354 -0
- package/scripts/deployment/policy.js +56 -0
- package/scripts/deployment/preflight.js +124 -0
- package/scripts/deployment/prepare.js +205 -0
- package/scripts/deployment/process.js +70 -0
- package/scripts/deployment/relay.js +58 -0
- package/scripts/deployment/runtime-binding.js +100 -0
- package/scripts/deployment/runtime.js +232 -0
- package/scripts/deployment/state.js +154 -0
- package/scripts/deployment/team.js +76 -0
- package/scripts/deployment/upgrade-rpc.py +148 -0
- package/scripts/deployment/upgrade-state.js +47 -0
- package/scripts/deployment/upgrade.js +298 -0
- package/scripts/deployment/volumes.js +24 -0
- package/scripts/errors.js +9 -0
- package/scripts/installation/administrator.js +23 -0
- package/scripts/installation/command.js +176 -0
- package/scripts/installation/configuration.js +104 -0
- package/scripts/installation/configure.js +122 -0
- package/scripts/installation/delete.js +111 -0
- package/scripts/installation/doctor.js +8 -0
- package/scripts/installation/errors.js +8 -0
- package/scripts/installation/files.js +51 -0
- package/scripts/installation/installer/cloud.js +22 -0
- package/scripts/installation/installer/collect.js +245 -0
- package/scripts/installation/installer/inputs.js +59 -0
- package/scripts/installation/installer/menu.js +49 -0
- package/scripts/installation/installer/prompts.js +139 -0
- package/scripts/installation/installer/run.js +251 -0
- package/scripts/installation/installer/secrets.js +16 -0
- package/scripts/installation/installer/sections/access.js +69 -0
- package/scripts/installation/installer/sections/certificates.js +18 -0
- package/scripts/installation/installer/sections/connections.js +4 -0
- package/scripts/installation/installer/sections/external-models.js +35 -0
- package/scripts/installation/installer/sections/models.js +119 -0
- package/scripts/installation/installer/sections/packs.js +82 -0
- package/scripts/installation/installer/sections/resources.js +10 -0
- package/scripts/installation/installer/settings.js +192 -0
- package/scripts/installation/installer/summary.js +44 -0
- package/scripts/installation/lifecycle.js +127 -0
- package/scripts/installation/location.js +3 -0
- package/scripts/installation/models.js +42 -0
- package/scripts/installation/options.js +360 -0
- package/scripts/installation/packs.js +150 -0
- package/scripts/installation/plan.js +121 -0
- package/scripts/installation/prerequisites.js +170 -0
- package/scripts/installation/recipes/catalog.js +55 -0
- package/scripts/installation/recipes/definition.js +32 -0
- package/scripts/installation/reconfigure.js +255 -0
- package/scripts/installation/requirements.js +23 -0
- package/scripts/installation/resolve.js +217 -0
- package/scripts/installation/runtime.js +87 -0
- package/scripts/installation/save.js +95 -0
- package/scripts/installation/setup.js +91 -0
- package/scripts/models/catalog.js +9 -0
- package/scripts/models/configuration.js +184 -0
- package/scripts/models/credentials.js +132 -0
- package/scripts/models/runtime.js +87 -0
- package/scripts/output.js +34 -0
- package/scripts/packs/connections.js +95 -0
- package/scripts/packs/lifecycle.js +125 -0
- package/scripts/packs/model.js +66 -0
- package/scripts/packs/native.js +69 -0
- package/scripts/packs/openshell.js +148 -0
- package/scripts/packs/policy.js +189 -0
- package/scripts/packs/requirements.in +1 -0
- package/scripts/packs/requirements.js +11 -0
- package/scripts/packs/requirements.txt +116 -0
- package/scripts/packs/source.js +90 -0
- package/scripts/packs/transport.py +68 -0
- package/scripts/people.js +52 -0
- package/scripts/release/create.js +62 -0
- package/scripts/release/definition.js +51 -0
- package/scripts/session.js +27 -0
- package/services/access/generated/client.gen.d.ts +12 -0
- package/services/access/generated/client.gen.js +3 -0
- package/services/access/generated/fastify.gen.d.ts +49 -0
- package/services/access/generated/fastify.gen.js +2 -0
- package/services/access/generated/index.d.ts +2 -0
- package/services/access/generated/index.js +2 -0
- package/services/access/generated/sdk.gen.d.ts +28 -0
- package/services/access/generated/sdk.gen.js +107 -0
- package/services/access/generated/types.gen.d.ts +349 -0
- package/services/access/generated/types.gen.js +2 -0
- package/services/access/migrations/001_access.sql +39 -0
- package/services/access/migrations/002_administrator_setup.sql +16 -0
- package/services/access/migrations/003_local_login_completion.sql +5 -0
- package/services/access/migrations/004_invitations.sql +21 -0
- package/services/access/repo/postgres.js +341 -0
- package/services/access/repo/session-logout.js +28 -0
- package/services/access/types/credential.js +2 -0
- package/services/access/types/errors.js +7 -0
- package/services/access/types/ingress.js +1 -0
- package/services/access/types/model.js +1 -0
- package/services/access/types/native-errors.js +7 -0
- package/services/access/types/native.js +1 -0
- package/services/cloud/generated/client.gen.d.ts +12 -0
- package/services/cloud/generated/client.gen.js +3 -0
- package/services/cloud/generated/index.d.ts +2 -0
- package/services/cloud/generated/index.js +2 -0
- package/services/cloud/generated/sdk.gen.d.ts +145 -0
- package/services/cloud/generated/sdk.gen.js +488 -0
- package/services/cloud/generated/types.gen.d.ts +2034 -0
- package/services/cloud/generated/types.gen.js +2 -0
- package/services/connections/cloud/generated/client.gen.d.ts +12 -0
- package/services/connections/cloud/generated/client.gen.js +3 -0
- package/services/connections/cloud/generated/fastify.gen.d.ts +68 -0
- package/services/connections/cloud/generated/fastify.gen.js +2 -0
- package/services/connections/cloud/generated/index.d.ts +2 -0
- package/services/connections/cloud/generated/index.js +2 -0
- package/services/connections/cloud/generated/sdk.gen.d.ts +95 -0
- package/services/connections/cloud/generated/sdk.gen.js +210 -0
- package/services/connections/cloud/generated/types.gen.d.ts +1044 -0
- package/services/connections/cloud/generated/types.gen.js +2 -0
- package/services/connections/migrations/001_connections.sql +220 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { selectModel } from "../../models.js";
|
|
2
|
+
import { gatewayRoutesSchema, configurationSchema, } from "../../../models/configuration.js";
|
|
3
|
+
import { secretInput } from "../secrets.js";
|
|
4
|
+
import { inputFile } from "../inputs.js";
|
|
5
|
+
import { InstallationError } from "../../errors.js";
|
|
6
|
+
export async function modelRoutes(file, inputs) {
|
|
7
|
+
return gatewayRoutesSchema.parse(await inputs.readJson(file));
|
|
8
|
+
}
|
|
9
|
+
export function providerLabel(route) {
|
|
10
|
+
const name = route.model.split("/")[0] ?? "Provider";
|
|
11
|
+
return ({
|
|
12
|
+
openrouter: "OpenRouter",
|
|
13
|
+
openai: "OpenAI",
|
|
14
|
+
anthropic: "Anthropic",
|
|
15
|
+
gemini: "Google",
|
|
16
|
+
}[name] ?? name);
|
|
17
|
+
}
|
|
18
|
+
/** Settings only. Credentials are collected after the complete installation review. */
|
|
19
|
+
export async function collectModels(ui, offers, current, inputs, presetFile, retainModels = false) {
|
|
20
|
+
if (current?.mode === "external") {
|
|
21
|
+
const catalog = configurationSchema.parse(await inputs.readJson(current.configurationFile));
|
|
22
|
+
if (catalog.mode !== "external")
|
|
23
|
+
throw new InstallationError("invalid_configuration", "Import the existing gateway's model catalog under Advanced.");
|
|
24
|
+
const defaultModel = await ui.select("Default model", catalog.models
|
|
25
|
+
.filter((model) => model.enabled)
|
|
26
|
+
.map((model) => ({ value: model.id, label: model.name })), catalog.defaultModel ?? undefined);
|
|
27
|
+
const model = catalog.models.find((model) => model.id === defaultModel);
|
|
28
|
+
const thinkingDefault = model?.reasoning
|
|
29
|
+
? await ui.select("Reasoning", ["low", "medium", "high"].map((value) => ({ value, label: value })), catalog.thinkingDefault ?? "medium")
|
|
30
|
+
: undefined;
|
|
31
|
+
const { thinkingDefault: _previous, ...base } = catalog;
|
|
32
|
+
return {
|
|
33
|
+
...current,
|
|
34
|
+
configurationFile: inputs.set("external-models.json", JSON.stringify({
|
|
35
|
+
...base,
|
|
36
|
+
defaultModel,
|
|
37
|
+
...(thinkingDefault ? { thinkingDefault } : {}),
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const file = current?.configurationFile ?? presetFile;
|
|
42
|
+
const routes = file ? await modelRoutes(file, inputs) : undefined;
|
|
43
|
+
const choices = new Map([
|
|
44
|
+
...offers.map((offer) => [offer.model.id, offer.model.name]),
|
|
45
|
+
...(routes?.models.map((model) => [model.id, model.name]) ?? []),
|
|
46
|
+
]);
|
|
47
|
+
if (!choices.size)
|
|
48
|
+
throw new InstallationError("invalid_configuration", "No model choices are available. Import a catalog under Advanced.");
|
|
49
|
+
const id = await ui.select("Default model", [...choices].map(([value, label]) => ({ value, label })), routes?.defaultModel);
|
|
50
|
+
const existing = routes?.models.find((model) => model.id === id);
|
|
51
|
+
const matching = offers.filter((offer) => offer.model.id === id);
|
|
52
|
+
const providerOptions = new Map(matching.map((offer) => [offer.model.route.model, offer]));
|
|
53
|
+
if (existing?.route && !providerOptions.has(existing.route.model))
|
|
54
|
+
providerOptions.set(existing.route.model, {
|
|
55
|
+
provider: providerLabel(existing.route),
|
|
56
|
+
model: { ...existing, route: existing.route },
|
|
57
|
+
reasoningLevels: existing.reasoning ? ["low", "medium", "high"] : [],
|
|
58
|
+
});
|
|
59
|
+
if (!providerOptions.size)
|
|
60
|
+
throw new InstallationError("invalid_configuration", "This model has no configured provider. Choose another model or import a catalog under Advanced.");
|
|
61
|
+
const selectedRoute = await ui.select("Provider", [...providerOptions].map(([value, offer]) => ({
|
|
62
|
+
value,
|
|
63
|
+
label: offer.provider,
|
|
64
|
+
})), existing?.route?.model);
|
|
65
|
+
const offer = providerOptions.get(selectedRoute);
|
|
66
|
+
if (!offer?.model.route)
|
|
67
|
+
throw new InstallationError("invalid_configuration", "Select a supported provider route.");
|
|
68
|
+
const thinkingDefault = offer.reasoningLevels.length
|
|
69
|
+
? await ui.select("Reasoning", offer.reasoningLevels.map((value) => ({
|
|
70
|
+
value,
|
|
71
|
+
label: value.charAt(0).toUpperCase() + value.slice(1),
|
|
72
|
+
})), routes?.thinkingDefault &&
|
|
73
|
+
offer.reasoningLevels.includes(routes.thinkingDefault)
|
|
74
|
+
? routes.thinkingDefault
|
|
75
|
+
: offer.reasoningLevels.includes("medium")
|
|
76
|
+
? "medium"
|
|
77
|
+
: offer.reasoningLevels[0])
|
|
78
|
+
: undefined;
|
|
79
|
+
return selectModel(current, routes, offer, thinkingDefault, inputs, retainModels);
|
|
80
|
+
}
|
|
81
|
+
export async function collectModelCredentials(ui, current, inputs) {
|
|
82
|
+
if (current.mode === "external")
|
|
83
|
+
return current.credentialFile
|
|
84
|
+
? current
|
|
85
|
+
: {
|
|
86
|
+
...current,
|
|
87
|
+
credentialFile: await secretInput(ui, inputs, "Model gateway key", "model-key"),
|
|
88
|
+
};
|
|
89
|
+
if (current.upstreamEnvironmentFile)
|
|
90
|
+
return current;
|
|
91
|
+
const routes = await modelRoutes(current.configurationFile, inputs);
|
|
92
|
+
const selected = routes.models.find((model) => model.id === routes.defaultModel);
|
|
93
|
+
ui.note(`${selected?.name ?? routes.defaultModel}${routes.thinkingDefault ? " · " + routes.thinkingDefault : ""}\nProvider: ${selected?.route ? providerLabel(selected.route) : "Custom"}`, "LLM credentials");
|
|
94
|
+
const mode = await ui.select("LLM API keys", [
|
|
95
|
+
{ value: "paste", label: "Enter keys (hidden)" },
|
|
96
|
+
{ value: "file", label: "Import private credentials file" },
|
|
97
|
+
], "paste");
|
|
98
|
+
if (mode === "file")
|
|
99
|
+
return {
|
|
100
|
+
...current,
|
|
101
|
+
upstreamEnvironmentFile: await inputFile(ui, "Provider credentials file (.env)", true),
|
|
102
|
+
};
|
|
103
|
+
const values = new Map();
|
|
104
|
+
for (const model of routes.models.filter((item) => item.enabled)) {
|
|
105
|
+
const route = model.route;
|
|
106
|
+
if (!route)
|
|
107
|
+
throw new InstallationError("invalid_configuration", "An enabled model has no provider route.");
|
|
108
|
+
if (values.has(route.apiKeyEnv))
|
|
109
|
+
continue;
|
|
110
|
+
const key = await ui.password(`${providerLabel(route)} LLM API key`);
|
|
111
|
+
if (!key.trim() || /[\r\n'"`]/.test(key))
|
|
112
|
+
throw new InstallationError("invalid_configuration", "Use a single-line provider key without quote characters, or import its credentials file.");
|
|
113
|
+
values.set(route.apiKeyEnv, key);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...current,
|
|
117
|
+
upstreamEnvironmentFile: inputs.set("models.env", [...values].map(([name, key]) => `${name}='${key}'`).join("\n") + "\n"),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { absolute, inputFile } from "../inputs.js";
|
|
3
|
+
import { openPack } from "../../../packs/source.js";
|
|
4
|
+
export async function collectPacks(ui, models, connections, current, available = []) {
|
|
5
|
+
const packs = structuredClone(current.packs);
|
|
6
|
+
let keep = false;
|
|
7
|
+
if (packs.length) {
|
|
8
|
+
const action = await ui.select("Existing packs", [
|
|
9
|
+
{ value: "keep", label: "Keep selections" },
|
|
10
|
+
{ value: "add", label: "Add a pack" },
|
|
11
|
+
{ value: "remove", label: "Remove selected packs" },
|
|
12
|
+
]);
|
|
13
|
+
keep = action === "keep";
|
|
14
|
+
if (action === "remove") {
|
|
15
|
+
const removed = await ui.multiselect("Remove packs", packs.map((pack, index) => ({
|
|
16
|
+
value: String(index),
|
|
17
|
+
label: pack.members.join(", "),
|
|
18
|
+
})));
|
|
19
|
+
const retained = packs.filter((_pack, index) => !removed.includes(String(index)));
|
|
20
|
+
return retained.length
|
|
21
|
+
? {
|
|
22
|
+
packs: retained,
|
|
23
|
+
...(current.packOperator
|
|
24
|
+
? { packOperator: current.packOperator }
|
|
25
|
+
: {}),
|
|
26
|
+
}
|
|
27
|
+
: { packs: [] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const selected = new Set(packs.flatMap((pack) => pack.members));
|
|
31
|
+
while (!keep &&
|
|
32
|
+
packs.length < 32 &&
|
|
33
|
+
(await ui.confirm(packs.length
|
|
34
|
+
? "Add another pack?"
|
|
35
|
+
: "Install a pack? (experimental native Claws)"))) {
|
|
36
|
+
const directory = available.length
|
|
37
|
+
? await ui.select("Pack", available.map((pack) => ({ value: pack.directory, label: pack.id })))
|
|
38
|
+
: absolute(await ui.text("Pack directory"));
|
|
39
|
+
const pack = await openPack(directory);
|
|
40
|
+
const eligible = pack.manifest.members.filter((member) => !selected.has(member.id) &&
|
|
41
|
+
(member.requirements.model === "none" || Boolean(models)) &&
|
|
42
|
+
(!member.requirements.connections.length ||
|
|
43
|
+
connections.mode !== "disabled"));
|
|
44
|
+
if (!eligible.length) {
|
|
45
|
+
ui.note("No unselected members meet the model and Connections prerequisites. Choose another pack or configure its prerequisites first.", "Pack unavailable");
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const members = z
|
|
49
|
+
.array(z.enum(eligible.map((member) => member.id)))
|
|
50
|
+
.min(1)
|
|
51
|
+
.parse(await ui.multiselect("Pack agents", eligible.map((member) => ({ value: member.id, label: member.id }))));
|
|
52
|
+
const needsConnections = eligible.some((member) => members.includes(member.id) && member.requirements.connections.length);
|
|
53
|
+
const bindingsFile = needsConnections
|
|
54
|
+
? await inputFile(ui, "Connected-account bindings file", true)
|
|
55
|
+
: undefined;
|
|
56
|
+
packs.push({
|
|
57
|
+
directory,
|
|
58
|
+
members,
|
|
59
|
+
...(bindingsFile ? { bindingsFile } : {}),
|
|
60
|
+
});
|
|
61
|
+
members.forEach((member) => selected.add(member));
|
|
62
|
+
}
|
|
63
|
+
return collectPackInputs(ui, { ...current, packs });
|
|
64
|
+
}
|
|
65
|
+
export async function collectPackInputs(ui, current) {
|
|
66
|
+
const packs = structuredClone(current.packs);
|
|
67
|
+
if (!packs.length)
|
|
68
|
+
return { packs };
|
|
69
|
+
for (const selection of packs) {
|
|
70
|
+
const pack = await openPack(selection.directory);
|
|
71
|
+
if (!selection.bindingsFile &&
|
|
72
|
+
pack.manifest.members.some((member) => selection.members.includes(member.id) &&
|
|
73
|
+
member.requirements.connections.length))
|
|
74
|
+
selection.bindingsFile = await inputFile(ui, "Connected-account bindings file", true);
|
|
75
|
+
}
|
|
76
|
+
const pythonExecutable = current.packOperator?.pythonExecutable ??
|
|
77
|
+
absolute(await ui.text("Python executable with the pinned OpenShell SDK", current.packOperator?.pythonExecutable));
|
|
78
|
+
return {
|
|
79
|
+
packs,
|
|
80
|
+
packOperator: { pythonExecutable, experimentalClaws: true },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { localInput } from "../../../deployment/configuration.js";
|
|
2
|
+
import { field } from "../inputs.js";
|
|
3
|
+
export async function collectResources(ui, current) {
|
|
4
|
+
return {
|
|
5
|
+
runtime: {
|
|
6
|
+
cpu: await field(ui, "Runtime CPUs", localInput.shape.cpu, current.runtime.cpu),
|
|
7
|
+
memory: await field(ui, "Runtime memory", localInput.shape.memory, current.runtime.memory),
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { checkHost, checkInstallationPrerequisites } from "../prerequisites.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { selectedDraft } from "../options.js";
|
|
4
|
+
import { setupContext } from "../setup.js";
|
|
5
|
+
import { configurationChanges, resolveConfigurationInputs, validateSelections, } from "../configure.js";
|
|
6
|
+
import { registerWithBrowser, registerUnattended } from "./cloud.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { rm, readFile } from "node:fs/promises";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
11
|
+
import { collectInstallation } from "./collect.js";
|
|
12
|
+
import { progress } from "./prompts.js";
|
|
13
|
+
import { SetupInputs, saveConfiguration } from "../save.js";
|
|
14
|
+
import { readJson } from "../files.js";
|
|
15
|
+
import { installationSchema } from "../configuration.js";
|
|
16
|
+
import { planSettingsChange, reconfigureInstallation } from "../reconfigure.js";
|
|
17
|
+
import { controlInstallation, startInstallation } from "../lifecycle.js";
|
|
18
|
+
import { InstallationError } from "../errors.js";
|
|
19
|
+
import { withInstallationLock, writePrivate } from "../../deployment/state.js";
|
|
20
|
+
/** The menu only gathers answers; the same plan/apply operations serve unattended callers. */
|
|
21
|
+
export async function editInstallationSettings(directory, ui, options = {}, prerequisites = { host: checkHost, check: checkInstallationPrerequisites }) {
|
|
22
|
+
directory = resolve(directory);
|
|
23
|
+
if (options.recipe || options.cloudUrl)
|
|
24
|
+
throw new InstallationError("change_unsupported", "Existing installations keep their recipe, software release and login service. Change models, keys, Connections or packs instead.");
|
|
25
|
+
if (options.nonInteractive && !options.yes)
|
|
26
|
+
throw new InstallationError("invalid_configuration", "Changing an existing installation noninteractively requires --yes. The server may restart and deselected packs may be removed.");
|
|
27
|
+
const task = (message, work) => progress(message, work, options);
|
|
28
|
+
const settingsFile = join(directory, "settings.json");
|
|
29
|
+
const before = await readFile(settingsFile, "utf8");
|
|
30
|
+
const config = installationSchema.parse(JSON.parse(before));
|
|
31
|
+
const record = z
|
|
32
|
+
.object({
|
|
33
|
+
settingsCandidate: z.string().optional(),
|
|
34
|
+
settingsReapply: z.enum(["models", "connections"]).optional(),
|
|
35
|
+
})
|
|
36
|
+
.parse(await readJson(join(directory, "prepared.json")));
|
|
37
|
+
const pending = record.settingsCandidate;
|
|
38
|
+
if (record.settingsReapply &&
|
|
39
|
+
options.reapply &&
|
|
40
|
+
record.settingsReapply !== options.reapply)
|
|
41
|
+
throw new InstallationError("change_unsupported", "Resume the pending change before selecting another capability.");
|
|
42
|
+
const reapply = options.reapply ?? record.settingsReapply;
|
|
43
|
+
if (pending &&
|
|
44
|
+
(dirname(dirname(pending)) !== directory ||
|
|
45
|
+
!/^\.settings-[a-f0-9-]+$/u.test(dirname(pending).slice(directory.length + 1))))
|
|
46
|
+
throw new InstallationError("invalid_configuration", "The pending configuration does not belong to this installation.");
|
|
47
|
+
const staging = pending
|
|
48
|
+
? dirname(pending)
|
|
49
|
+
: join(directory, `.settings-${randomUUID()}`);
|
|
50
|
+
const inputs = new SetupInputs(staging);
|
|
51
|
+
let attempted = false;
|
|
52
|
+
let authorizing;
|
|
53
|
+
let retained = Boolean(pending);
|
|
54
|
+
try {
|
|
55
|
+
const context = await setupContext({}, config.releaseFile);
|
|
56
|
+
const selected = await selectedDraft(context, config.recipe?.id ?? "custom", options, inputs, config);
|
|
57
|
+
if (pending && JSON.stringify(selected) !== JSON.stringify(config))
|
|
58
|
+
throw new InstallationError("change_unsupported", "Resume the interrupted configuration without new selections first.");
|
|
59
|
+
const draft = pending
|
|
60
|
+
? undefined
|
|
61
|
+
: options.nonInteractive
|
|
62
|
+
? { config: await validateSelections(context, selected) }
|
|
63
|
+
: await collectInstallation(ui, { existing: true }, { directory: staging, config: selected, inputs });
|
|
64
|
+
if ((await readFile(settingsFile, "utf8")) !== before)
|
|
65
|
+
throw new InstallationError("stale_plan", "Settings changed while this menu was open. Reopen it before applying.");
|
|
66
|
+
const candidate = pending ??
|
|
67
|
+
(await saveConfiguration(staging, installationSchema.parse(draft?.config), inputs, true));
|
|
68
|
+
if (pending &&
|
|
69
|
+
!options.nonInteractive &&
|
|
70
|
+
!(await ui.confirm("Resume this interrupted change?"))) {
|
|
71
|
+
await withInstallationLock(directory, async () => {
|
|
72
|
+
const prepared = z
|
|
73
|
+
.object({
|
|
74
|
+
ownerId: z.string(),
|
|
75
|
+
settingsCandidate: z.string().optional(),
|
|
76
|
+
settingsReapply: z.enum(["models", "connections"]).optional(),
|
|
77
|
+
settingsPending: z.string().optional(),
|
|
78
|
+
})
|
|
79
|
+
.parse(await readJson(join(directory, "prepared.json")));
|
|
80
|
+
if (prepared.settingsCandidate !== pending)
|
|
81
|
+
throw new InstallationError("stale_plan", "The pending change has changed. Reopen configure.");
|
|
82
|
+
if (!prepared.settingsPending) {
|
|
83
|
+
await writePrivate(join(directory, "prepared.json"), JSON.stringify({ ownerId: prepared.ownerId }));
|
|
84
|
+
retained = false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return { state: "cancelled" };
|
|
88
|
+
}
|
|
89
|
+
if (!pending &&
|
|
90
|
+
!reapply &&
|
|
91
|
+
!Object.values(await configurationChanges(config, resolveConfigurationInputs(installationSchema.parse(await readJson(candidate)), dirname(candidate)))).some(Boolean))
|
|
92
|
+
return { state: "unchanged" };
|
|
93
|
+
await task("Checking this machine", () => prerequisites.host());
|
|
94
|
+
await task("Preparing required software", (signal, report) => prerequisites.check(candidate, {
|
|
95
|
+
acquire: true,
|
|
96
|
+
signal,
|
|
97
|
+
report,
|
|
98
|
+
}));
|
|
99
|
+
authorizing = candidate;
|
|
100
|
+
if (options.nonInteractive)
|
|
101
|
+
await registerUnattended(candidate, options.cloudCredentialFile);
|
|
102
|
+
else
|
|
103
|
+
await registerWithBrowser(candidate, ui, task);
|
|
104
|
+
authorizing = undefined;
|
|
105
|
+
const plan = await planSettingsChange(candidate, reapply);
|
|
106
|
+
ui.note([
|
|
107
|
+
...(plan.scopes.models && plan.changes.models
|
|
108
|
+
? [
|
|
109
|
+
`Model: ${plan.changes.models.default ?? "None"} · ${plan.changes.models.reasoning}`,
|
|
110
|
+
]
|
|
111
|
+
: []),
|
|
112
|
+
...(plan.scopes.connections
|
|
113
|
+
? [
|
|
114
|
+
`Connections: ${plan.changes.connections.to === "disabled" ? "Off" : "On"}`,
|
|
115
|
+
]
|
|
116
|
+
: []),
|
|
117
|
+
...(plan.scopes.packs
|
|
118
|
+
? [`Packs: ${plan.changes.packs.selected.join(", ") || "None"}`]
|
|
119
|
+
: []),
|
|
120
|
+
...(plan.changes.packs.removed.length
|
|
121
|
+
? [
|
|
122
|
+
`Remove pack agents and native-owned data: ${plan.changes.packs.removed.join(", ")}`,
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
125
|
+
"The server will stop to apply these changes.",
|
|
126
|
+
].join("\n"), "Review change");
|
|
127
|
+
if (!pending &&
|
|
128
|
+
!options.nonInteractive &&
|
|
129
|
+
!(await ui.confirm("Apply these settings?")))
|
|
130
|
+
return { state: "cancelled" };
|
|
131
|
+
attempted = true;
|
|
132
|
+
const current = await controlInstallation(directory, "status");
|
|
133
|
+
if (current.state !== "stopped") {
|
|
134
|
+
await task("Stopping ClawScarf", async (signal) => {
|
|
135
|
+
await controlInstallation(directory, "stop");
|
|
136
|
+
const deadline = Date.now() + 120_000;
|
|
137
|
+
while ((await controlInstallation(directory, "status")).state !== "stopped") {
|
|
138
|
+
if (Date.now() >= deadline)
|
|
139
|
+
throw new InstallationError("unavailable", "Shutdown is not yet confirmed. Check status before applying settings.");
|
|
140
|
+
await delay(500, undefined, { signal });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
await task("Applying settings", () => reconfigureInstallation(candidate, plan.fingerprint, reapply));
|
|
145
|
+
// Only our preceding accepted menu draft is retired. Never remove user-supplied inputs.
|
|
146
|
+
const previousInputs = dirname(config.models.configurationFile);
|
|
147
|
+
if (previousInputs !== staging &&
|
|
148
|
+
dirname(previousInputs) === directory &&
|
|
149
|
+
/^\.settings-[a-f0-9-]+$/u.test(previousInputs.slice(directory.length + 1)))
|
|
150
|
+
await rm(previousInputs, { recursive: true });
|
|
151
|
+
if (options.start ??
|
|
152
|
+
(options.nonInteractive
|
|
153
|
+
? true
|
|
154
|
+
: await ui.confirm("Start with these settings?", true))) {
|
|
155
|
+
return await task("Starting ClawScarf", (signal, report) => startInstallation(directory, report, signal));
|
|
156
|
+
}
|
|
157
|
+
ui.note(`clawscarf start --directory '${resolve(options.directory ?? dirname(directory)).replaceAll("'", "'\\''")}'`, "Start later");
|
|
158
|
+
return { state: "prepared" };
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (authorizing && !pending) {
|
|
162
|
+
const candidate = authorizing;
|
|
163
|
+
await withInstallationLock(directory, async () => {
|
|
164
|
+
if ((await readFile(settingsFile, "utf8")) !== before)
|
|
165
|
+
throw new InstallationError("stale_plan", "Settings changed during authorization. Reopen configure before applying.");
|
|
166
|
+
const prepared = z
|
|
167
|
+
.object({
|
|
168
|
+
ownerId: z.string(),
|
|
169
|
+
settingsCandidate: z.string().optional(),
|
|
170
|
+
settingsReapply: z.enum(["models", "connections"]).optional(),
|
|
171
|
+
settingsPending: z.string().optional(),
|
|
172
|
+
})
|
|
173
|
+
.parse(await readJson(join(directory, "prepared.json")));
|
|
174
|
+
if (prepared.settingsCandidate || prepared.settingsPending)
|
|
175
|
+
throw new InstallationError("stale_plan", "Another configuration is pending. Resume it before applying these settings.");
|
|
176
|
+
await writePrivate(join(directory, "prepared.json"), JSON.stringify({
|
|
177
|
+
...prepared,
|
|
178
|
+
settingsCandidate: candidate,
|
|
179
|
+
...(reapply ? { settingsReapply: reapply } : {}),
|
|
180
|
+
}));
|
|
181
|
+
retained = true;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (attempted)
|
|
185
|
+
ui.note("Check status, then run configure --directory again to review and resume the interrupted change.", "Retained settings");
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
if (!attempted && !retained)
|
|
190
|
+
await rm(staging, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { inputErrorMessage } from "./inputs.js";
|
|
2
|
+
import { providerLabel } from "./sections/models.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { gatewayRoutesSchema } from "../../models/configuration.js";
|
|
5
|
+
import { readJson } from "../files.js";
|
|
6
|
+
export async function modelSummary(config, inputs) {
|
|
7
|
+
let model = "Provider credentials needed";
|
|
8
|
+
if (config.models) {
|
|
9
|
+
const placement = config.models.mode === "litellm" ? "Provider" : "Existing model gateway";
|
|
10
|
+
try {
|
|
11
|
+
const value = inputs
|
|
12
|
+
? await inputs.readJson(config.models.configurationFile)
|
|
13
|
+
: await readJson(config.models.configurationFile);
|
|
14
|
+
const catalog = z
|
|
15
|
+
.object({
|
|
16
|
+
models: gatewayRoutesSchema.shape.models,
|
|
17
|
+
defaultModel: z.string().nullable(),
|
|
18
|
+
thinkingDefault: gatewayRoutesSchema.shape.thinkingDefault,
|
|
19
|
+
})
|
|
20
|
+
.parse(value);
|
|
21
|
+
const selected = catalog.models.find((entry) => entry.id === catalog.defaultModel);
|
|
22
|
+
model = `${selected?.route ? providerLabel(selected.route) : placement} · ${selected?.name ?? "No default"}${catalog.thinkingDefault ? ` · ${catalog.thinkingDefault}` : ""}`;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!inputErrorMessage(error))
|
|
26
|
+
throw error;
|
|
27
|
+
model = `${placement} · Check model catalog`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return model;
|
|
31
|
+
}
|
|
32
|
+
export async function installationSummary(config, inputs) {
|
|
33
|
+
const model = await modelSummary(config, inputs);
|
|
34
|
+
const origin = config.exposure.mode === "local"
|
|
35
|
+
? `http://127.0.0.1:${String(config.exposure.applicationPort)}`
|
|
36
|
+
: config.exposure.applicationOrigin;
|
|
37
|
+
return [
|
|
38
|
+
`${config.name} — ${origin}`,
|
|
39
|
+
`Administrator: ${config.access.administratorName} · ${config.access.mode === "hosted" ? "ClawScarf login" : "Custom OIDC"}`,
|
|
40
|
+
`Models: ${model}`,
|
|
41
|
+
`Connections: ${config.connections.mode === "disabled" ? "Off" : "On"}`,
|
|
42
|
+
`Browser: ${config.browser.enabled ? "Experimental" : "Off"} · Packs: ${config.packs.flatMap((pack) => pack.members).join(", ") || "None"}`,
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { checkHost, checkInstallationPrerequisites } from "./prerequisites.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { administratorSetup } from "./administrator.js";
|
|
6
|
+
import { launchLocal, stopLocal } from "../deployment/launch.js";
|
|
7
|
+
import { compose } from "../deployment/compose.js";
|
|
8
|
+
import { readState, resourceNames, withInstallationLock, } from "../deployment/state.js";
|
|
9
|
+
import { run } from "../deployment/process.js";
|
|
10
|
+
import { localLogNames } from "../deployment/logs.js";
|
|
11
|
+
import { InstallationError } from "./errors.js";
|
|
12
|
+
import { activatePacks, packOutcomeSchema } from "./packs.js";
|
|
13
|
+
const containerSchema = z.object({
|
|
14
|
+
Service: z.string(),
|
|
15
|
+
State: z.string(),
|
|
16
|
+
Health: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
/** No daemon or cached readiness: observe Docker and the live Access endpoint. */
|
|
19
|
+
async function installationStatus(directory) {
|
|
20
|
+
const state = await readState(directory);
|
|
21
|
+
const rows = (await compose(directory, ["ps", "--all", "--format", "json"]))
|
|
22
|
+
.trim()
|
|
23
|
+
.split("\n")
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((line) => containerSchema.parse(JSON.parse(line)));
|
|
26
|
+
const expected = (await compose(directory, ["config", "--services"]))
|
|
27
|
+
.trim()
|
|
28
|
+
.split("\n");
|
|
29
|
+
const containers = rows.map((row) => ({
|
|
30
|
+
service: row.Service,
|
|
31
|
+
state: row.State,
|
|
32
|
+
...(row.Health ? { health: row.Health } : {}),
|
|
33
|
+
}));
|
|
34
|
+
const sandboxRows = z
|
|
35
|
+
.array(z.object({ State: z.string() }))
|
|
36
|
+
.parse(JSON.parse(await run("docker", [
|
|
37
|
+
"container",
|
|
38
|
+
"ls",
|
|
39
|
+
"--all",
|
|
40
|
+
"--format",
|
|
41
|
+
"json",
|
|
42
|
+
"--filter",
|
|
43
|
+
`label=openshell.ai/sandbox-namespace=${resourceNames(state).sandbox}`,
|
|
44
|
+
]).then((output) => "[" + output.trim().split("\n").filter(Boolean).join(",") + "]")));
|
|
45
|
+
const running = rows.some((row) => ["running", "restarting", "paused"].includes(row.State)) ||
|
|
46
|
+
sandboxRows.some((row) => ["running", "restarting", "paused"].includes(row.State));
|
|
47
|
+
const containersReady = sandboxRows.length === 1 &&
|
|
48
|
+
sandboxRows.every((row) => row.State === "running") &&
|
|
49
|
+
expected.every((name) => rows.some((row) => row.Service === name &&
|
|
50
|
+
row.State === "running" &&
|
|
51
|
+
(!row.Health || row.Health === "healthy")));
|
|
52
|
+
const servicesReady = containersReady &&
|
|
53
|
+
(await fetch(`http://127.0.0.1:${String(state.input.ports.native)}/healthz`, { signal: AbortSignal.timeout(3000) }).then(async (response) => {
|
|
54
|
+
await response.body?.cancel();
|
|
55
|
+
return response.ok;
|
|
56
|
+
}, () => false));
|
|
57
|
+
const administrator = servicesReady
|
|
58
|
+
? await administratorSetup(directory).then((setup) => (setup.complete ? "ready" : "pending"), () => "unavailable")
|
|
59
|
+
: "unavailable";
|
|
60
|
+
let packs = [];
|
|
61
|
+
try {
|
|
62
|
+
packs = z
|
|
63
|
+
.array(packOutcomeSchema)
|
|
64
|
+
.parse(JSON.parse(await readFile(join(directory, "pack-status.json"), "utf8")));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
state: !running
|
|
72
|
+
? "stopped"
|
|
73
|
+
: servicesReady && administrator !== "unavailable"
|
|
74
|
+
? "running"
|
|
75
|
+
: "degraded",
|
|
76
|
+
ready: servicesReady &&
|
|
77
|
+
administrator === "ready" &&
|
|
78
|
+
packs.every((pack) => pack.state === "complete"),
|
|
79
|
+
administrator,
|
|
80
|
+
packs,
|
|
81
|
+
services: containers,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function startInstallation(directory, report, signal) {
|
|
85
|
+
directory = resolve(directory);
|
|
86
|
+
return withInstallationLock(directory, async () => {
|
|
87
|
+
const state = await readState(directory);
|
|
88
|
+
if (!state.input.modelGateway && !state.input.models)
|
|
89
|
+
throw new InstallationError("invalid_configuration", "Bundled or existing LiteLLM is required.");
|
|
90
|
+
await checkHost();
|
|
91
|
+
await checkInstallationPrerequisites(join(directory, "settings.json"), {
|
|
92
|
+
acquire: true,
|
|
93
|
+
report,
|
|
94
|
+
...(signal ? { signal } : {}),
|
|
95
|
+
});
|
|
96
|
+
await launchLocal(directory, report, {
|
|
97
|
+
activate: async () => {
|
|
98
|
+
await activatePacks(directory, report);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return installationStatus(directory);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function controlInstallation(directory, action) {
|
|
105
|
+
directory = resolve(directory);
|
|
106
|
+
await readState(directory);
|
|
107
|
+
if (action === "stop")
|
|
108
|
+
await withInstallationLock(directory, async () => {
|
|
109
|
+
if ((await installationStatus(directory)).state !== "stopped")
|
|
110
|
+
await stopLocal(directory);
|
|
111
|
+
});
|
|
112
|
+
return installationStatus(directory);
|
|
113
|
+
}
|
|
114
|
+
export async function installationLogs(directory, service) {
|
|
115
|
+
await readState(resolve(directory));
|
|
116
|
+
const name = z.enum(localLogNames).parse(service);
|
|
117
|
+
return compose(directory, ["logs", "--no-color", "--tail", "100", name]);
|
|
118
|
+
}
|
|
119
|
+
/** Administrator verification completes setup; it cannot make failed services or packs ready. */
|
|
120
|
+
export function withVerifiedAdministrator(status) {
|
|
121
|
+
return {
|
|
122
|
+
...status,
|
|
123
|
+
administrator: "ready",
|
|
124
|
+
ready: status.state === "running" &&
|
|
125
|
+
status.packs.every((pack) => pack.state === "complete"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { InstallationError } from "./errors.js";
|
|
2
|
+
import { gatewayRoutesSchema } from "../models/configuration.js";
|
|
3
|
+
/** One model update policy for menu selections and command-line overrides. */
|
|
4
|
+
export function selectModel(current, routes, offer, thinkingDefault, inputs, retainModels = false) {
|
|
5
|
+
const id = offer.model.id;
|
|
6
|
+
const selected = gatewayRoutesSchema.parse({
|
|
7
|
+
models: [
|
|
8
|
+
...(routes?.models.filter((model) => model.id !== id && (retainModels || model.id !== routes.defaultModel)) ?? []),
|
|
9
|
+
{ ...offer.model, enabled: true },
|
|
10
|
+
],
|
|
11
|
+
defaultModel: id,
|
|
12
|
+
...(thinkingDefault ? { thinkingDefault } : {}),
|
|
13
|
+
});
|
|
14
|
+
const sameCredentials = current?.mode === "litellm" &&
|
|
15
|
+
routes &&
|
|
16
|
+
selected.models.every((model) => routes.models.some((before) => before.route?.apiKeyEnv === model.route?.apiKeyEnv &&
|
|
17
|
+
before.route?.model.split("/")[0] ===
|
|
18
|
+
model.route?.model.split("/")[0]));
|
|
19
|
+
return {
|
|
20
|
+
mode: "litellm",
|
|
21
|
+
configurationFile: inputs.set("selected-models.json", JSON.stringify(selected, null, 2)),
|
|
22
|
+
upstreamEnvironmentFile: sameCredentials
|
|
23
|
+
? current.upstreamEnvironmentFile
|
|
24
|
+
: "",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Recipes select an offering; model catalog metadata has one owner. */
|
|
28
|
+
export function recipeModelRoutes(choice, catalog) {
|
|
29
|
+
const matches = catalog.filter((item) => item.model.enabled &&
|
|
30
|
+
item.model.id === choice.model &&
|
|
31
|
+
item.model.route.model.split("/")[0] === choice.provider);
|
|
32
|
+
const offer = matches[0];
|
|
33
|
+
if (!offer ||
|
|
34
|
+
matches.length !== 1 ||
|
|
35
|
+
(choice.reasoning && !offer.reasoningLevels.includes(choice.reasoning)))
|
|
36
|
+
throw new InstallationError("invalid_configuration", "Recipe model, provider and reasoning must select one enabled offering in the model catalog.");
|
|
37
|
+
return gatewayRoutesSchema.parse({
|
|
38
|
+
models: [offer.model],
|
|
39
|
+
defaultModel: choice.model,
|
|
40
|
+
...(choice.reasoning ? { thinkingDefault: choice.reasoning } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|