@rizom/ops 0.2.0-alpha.679 → 0.2.0-alpha.680
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/brains-ops.js +148 -129
- package/dist/deploy.js +13 -13
- package/dist/index.js +149 -130
- package/dist/load-registry.d.ts +1 -0
- package/dist/schema.d.ts +1 -0
- package/package.json +1 -1
- package/templates/rover-pilot/.github/workflows/deploy.yml +9 -0
- package/templates/rover-pilot/deploy/scripts/pull-local-models.ts +69 -0
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +15 -0
package/dist/load-registry.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface ResolvedUserIdentity {
|
|
|
87
87
|
profileKind?: string | undefined;
|
|
88
88
|
anchorKind?: AnchorKind | undefined;
|
|
89
89
|
embeddingEnabled?: boolean | undefined;
|
|
90
|
+
embeddingModel?: string | undefined;
|
|
90
91
|
plugins?: UserConfig["plugins"];
|
|
91
92
|
spaces?: string[] | undefined;
|
|
92
93
|
topicExtractionEnabled?: boolean | undefined;
|
package/dist/schema.d.ts
CHANGED
|
@@ -141,6 +141,7 @@ export declare const userSchema: z.ZodObject<{
|
|
|
141
141
|
plugins: z.ZodOptional<typeof userPluginsSchema>;
|
|
142
142
|
spaces: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
143
143
|
embeddingEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
144
|
+
embeddingModel: z.ZodOptional<z.ZodString>;
|
|
144
145
|
topicExtractionEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
145
146
|
skillDerivationEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
146
147
|
swotDerivationEnabled: z.ZodOptional<z.ZodBoolean>;
|
package/package.json
CHANGED
|
@@ -207,6 +207,14 @@ jobs:
|
|
|
207
207
|
SERVICE_NAME: rover
|
|
208
208
|
run: bun deploy/scripts/create-predeploy-backup.ts
|
|
209
209
|
|
|
210
|
+
- name: Pull local models
|
|
211
|
+
env:
|
|
212
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
213
|
+
LOCAL_MODELS: ${{ steps.user_config.outputs.local_models }}
|
|
214
|
+
run: |
|
|
215
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
216
|
+
bun deploy/scripts/pull-local-models.ts
|
|
217
|
+
|
|
210
218
|
- name: Release operator-confirmed stale deploy lock
|
|
211
219
|
if: ${{ inputs.release_stale_lock }}
|
|
212
220
|
env:
|
|
@@ -230,6 +238,7 @@ jobs:
|
|
|
230
238
|
WWW_DOMAIN: ${{ steps.user_config.outputs.www_domain }}
|
|
231
239
|
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
232
240
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
241
|
+
LOCAL_MODELS: ${{ steps.user_config.outputs.local_models }}
|
|
233
242
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
234
243
|
|
|
235
244
|
- name: Install container health watchdog
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { requireEnv } from "./helpers";
|
|
2
|
+
|
|
3
|
+
/** The model runtime the `models` accessory runs; kamal-deploy.yml pins the same image. */
|
|
4
|
+
export const MODEL_RUNTIME_IMAGE = "ollama/ollama:0.40.11";
|
|
5
|
+
const MODELS_VOLUME = "/opt/brain-models:/root/.ollama";
|
|
6
|
+
const MODEL_TAG = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The local models the deploy names (`LOCAL_MODELS`, the `local:` models of
|
|
10
|
+
* brain.yaml without their prefix), once each.
|
|
11
|
+
*/
|
|
12
|
+
export function parseLocalModels(value: string | undefined): string[] {
|
|
13
|
+
const models = (value ?? "").split(/[\s,]+/).filter(Boolean);
|
|
14
|
+
const invalid = models.find((model) => !MODEL_TAG.test(model));
|
|
15
|
+
if (invalid !== undefined) {
|
|
16
|
+
throw new Error(`Invalid local model "${invalid}"`);
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(models)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pulls each model into the models volume with a one-off runtime container,
|
|
23
|
+
* so the models are on disk before the brain starts, whether or not the
|
|
24
|
+
* accessory runs yet.
|
|
25
|
+
*/
|
|
26
|
+
export function buildLocalModelPullScript(models: readonly string[]): string {
|
|
27
|
+
const pulls = models.map((model) => `ollama pull ${model}`).join(" && ");
|
|
28
|
+
return `#!/usr/bin/env bash
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
SUDO=""
|
|
31
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
32
|
+
SUDO=sudo
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
$SUDO mkdir -p /opt/brain-models
|
|
36
|
+
$SUDO docker run --rm --entrypoint /bin/sh -v ${MODELS_VOLUME} ${MODEL_RUNTIME_IMAGE} -c '
|
|
37
|
+
ollama serve >/dev/null 2>&1 &
|
|
38
|
+
for attempt in $(seq 1 30); do
|
|
39
|
+
ollama list >/dev/null 2>&1 && break
|
|
40
|
+
sleep 1
|
|
41
|
+
done
|
|
42
|
+
${pulls}
|
|
43
|
+
'
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function pullLocalModels(): Promise<void> {
|
|
48
|
+
const models = parseLocalModels(process.env["LOCAL_MODELS"]);
|
|
49
|
+
if (models.length === 0) {
|
|
50
|
+
console.log("No local models in brain.yaml; nothing to pull.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const serverIp = requireEnv("SERVER_IP");
|
|
54
|
+
const sshUser = requireEnv("SSH_USER").trim();
|
|
55
|
+
if (!sshUser) throw new Error("Missing SSH_USER");
|
|
56
|
+
const child = Bun.spawn(["ssh", `${sshUser}@${serverIp}`, "bash -s"], {
|
|
57
|
+
stdin: new Blob([buildLocalModelPullScript(models)]),
|
|
58
|
+
stdout: "inherit",
|
|
59
|
+
stderr: "inherit",
|
|
60
|
+
});
|
|
61
|
+
const exitCode = await child.exited;
|
|
62
|
+
if (exitCode !== 0) {
|
|
63
|
+
throw new Error(`Pulling local models failed with exit ${exitCode}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (import.meta.main) {
|
|
68
|
+
await pullLocalModels();
|
|
69
|
+
}
|
|
@@ -25,6 +25,20 @@ if (!brainDomain) {
|
|
|
25
25
|
throw new Error(`Missing domain in ${brainYamlPath}`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// The models the runtime beside the brain serves: brain.yaml's local:
|
|
29
|
+
// models, unprefixed. None means the deploy runs no runtime.
|
|
30
|
+
const localModels = [
|
|
31
|
+
...new Set(
|
|
32
|
+
[
|
|
33
|
+
...brainYaml.matchAll(
|
|
34
|
+
/^(?:embeddingModel|answerModel|visionModel|model):\s*['"]?local:([^'"\s]+)/gm,
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
.map((match) => match[1] ?? "")
|
|
38
|
+
.filter(Boolean),
|
|
39
|
+
),
|
|
40
|
+
].join(",");
|
|
41
|
+
|
|
28
42
|
const registry = await loadPilotRegistry(process.cwd());
|
|
29
43
|
const user = registry.users.find((entry) => entry.handle === handle);
|
|
30
44
|
if (!user) {
|
|
@@ -69,6 +83,7 @@ const outputs: Record<string, string> = {
|
|
|
69
83
|
image_repository: `ghcr.io/${repository}`,
|
|
70
84
|
image_tag: imageTag,
|
|
71
85
|
registry_username: repositoryOwner,
|
|
86
|
+
local_models: localModels,
|
|
72
87
|
};
|
|
73
88
|
|
|
74
89
|
const required = ["brain_version", "registry_username"];
|