openmates 0.12.0-alpha.20 → 0.12.0-alpha.22
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/{chunk-OSH7SOFJ.js → chunk-PHFCP5AM.js} +413 -16
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -6108,7 +6108,7 @@ function printLogo() {
|
|
|
6108
6108
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
6109
6109
|
import { realpathSync, writeFileSync as writeFileSync5 } from "fs";
|
|
6110
6110
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6111
|
-
import { basename as basename3, dirname as
|
|
6111
|
+
import { basename as basename3, dirname as dirname3 } from "path";
|
|
6112
6112
|
import WebSocket2 from "ws";
|
|
6113
6113
|
|
|
6114
6114
|
// ../secret-scanner/src/registry.ts
|
|
@@ -8577,8 +8577,9 @@ import { execSync, spawn as nodeSpawn } from "child_process";
|
|
|
8577
8577
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
8578
8578
|
import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8579
8579
|
import { createInterface as createInterface2 } from "readline";
|
|
8580
|
+
import { createInterface as createPromptInterface } from "readline/promises";
|
|
8580
8581
|
import { homedir as homedir5 } from "os";
|
|
8581
|
-
import { join as join3, resolve as resolve3 } from "path";
|
|
8582
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
8582
8583
|
var SOURCE_COMPOSE_FILE = join3("backend", "core", "docker-compose.yml");
|
|
8583
8584
|
var IMAGE_COMPOSE_FILE = join3("backend", "core", "docker-compose.selfhost.yml");
|
|
8584
8585
|
var COMPOSE_OVERRIDE = join3("backend", "core", "docker-compose.override.yml");
|
|
@@ -8596,12 +8597,42 @@ var IMAGE_CHANNEL_TAGS = {
|
|
|
8596
8597
|
dev: DEV_BRANCH
|
|
8597
8598
|
};
|
|
8598
8599
|
var BACKEND_CONFIG_FILE = join3("backend", "config", "backend_config.yml");
|
|
8600
|
+
var IMAGE_RUNTIME_CONFIG_FILE = join3("config", "backend_config.yml");
|
|
8601
|
+
var LOCAL_AI_MODELS_FILE = "local-ai-models.yml";
|
|
8599
8602
|
var OFF_BY_DEFAULT_FEATURES = /* @__PURE__ */ new Map([
|
|
8600
8603
|
["embed:code:application", "Application previews are still unstable"],
|
|
8601
8604
|
["platform:projects", "Projects workspace is not ready by default"],
|
|
8602
8605
|
["platform:workflows", "Workflows workspace is not implemented yet"],
|
|
8603
8606
|
["platform:tasks", "Tasks workspace is not implemented yet"]
|
|
8604
8607
|
]);
|
|
8608
|
+
var LOCAL_MODEL_RUNTIME_DEFAULTS = {
|
|
8609
|
+
ollama: {
|
|
8610
|
+
label: "Ollama",
|
|
8611
|
+
serverId: "ollama",
|
|
8612
|
+
baseUrl: "http://host.docker.internal:11434/v1",
|
|
8613
|
+
apiKey: "ollama"
|
|
8614
|
+
},
|
|
8615
|
+
"lm-studio": {
|
|
8616
|
+
label: "LM Studio",
|
|
8617
|
+
serverId: "lm_studio",
|
|
8618
|
+
baseUrl: "http://host.docker.internal:1234/v1",
|
|
8619
|
+
apiKey: "lm-studio"
|
|
8620
|
+
},
|
|
8621
|
+
custom: {
|
|
8622
|
+
label: "Custom OpenAI-compatible API",
|
|
8623
|
+
serverId: "custom_openai_compatible",
|
|
8624
|
+
baseUrl: "",
|
|
8625
|
+
apiKey: "local"
|
|
8626
|
+
}
|
|
8627
|
+
};
|
|
8628
|
+
var MODEL_CREATOR_OPTIONS = [
|
|
8629
|
+
{ id: "alibaba", name: "Alibaba / Qwen", match: /(^|[-_:/])qwen/i },
|
|
8630
|
+
{ id: "google", name: "Google / Gemma", match: /(^|[-_:/])gemma/i },
|
|
8631
|
+
{ id: "mistral", name: "Mistral", match: /(^|[-_:/])(mistral|mixtral|ministral)/i },
|
|
8632
|
+
{ id: "openai", name: "OpenAI", match: /(^|[-_:/])gpt-oss/i },
|
|
8633
|
+
{ id: "deepseek", name: "DeepSeek", match: /(^|[-_:/])deepseek/i },
|
|
8634
|
+
{ id: "custom", name: "Custom", match: null }
|
|
8635
|
+
];
|
|
8605
8636
|
var MINIMAL_ENV_TEMPLATE = `# OpenMates self-host image-mode environment
|
|
8606
8637
|
SECRET__MISTRAL_AI__API_KEY=
|
|
8607
8638
|
SECRET__CEREBRAS__API_KEY=
|
|
@@ -8898,8 +8929,10 @@ async function writeImageModeRuntimeFiles(installPath, imageTag) {
|
|
|
8898
8929
|
const coreDir = join3(installPath, "backend", "core");
|
|
8899
8930
|
const vaultConfigDir = join3(coreDir, "vault", "config");
|
|
8900
8931
|
mkdirSync3(vaultConfigDir, { recursive: true });
|
|
8932
|
+
mkdirSync3(join3(installPath, "config", "providers"), { recursive: true });
|
|
8901
8933
|
writeFileSync3(join3(coreDir, "docker-compose.selfhost.yml"), await loadSelfHostComposeTemplate(templateRefForImageTag(imageTag, getPackageVersion())));
|
|
8902
8934
|
writeFileSync3(join3(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
|
|
8935
|
+
ensureImageRuntimeConfig(installPath);
|
|
8903
8936
|
const envPath = join3(installPath, ".env");
|
|
8904
8937
|
let envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : MINIMAL_ENV_TEMPLATE;
|
|
8905
8938
|
envContent = setEnvIfEmpty(envContent, "DATABASE_ADMIN_PASSWORD", randomHex(12));
|
|
@@ -8963,6 +8996,7 @@ function defaultCloneBranchForVersion(version) {
|
|
|
8963
8996
|
}
|
|
8964
8997
|
function hasLlmCredentials(envPath) {
|
|
8965
8998
|
if (!existsSync5(envPath)) return false;
|
|
8999
|
+
if (hasLocalAiModels(dirname(envPath))) return true;
|
|
8966
9000
|
const content = readFileSync5(envPath, "utf-8");
|
|
8967
9001
|
for (const line of content.split("\n")) {
|
|
8968
9002
|
const trimmed = line.trim();
|
|
@@ -8987,7 +9021,7 @@ function warnIfMissingLlmCredentials(installPath) {
|
|
|
8987
9021
|
}
|
|
8988
9022
|
if (!hasLlmCredentials(envPath)) {
|
|
8989
9023
|
console.error(
|
|
8990
|
-
"No LLM provider API key found in .env.\nOpenMates will start, but AI chat/model processing will stay unavailable until you add one.\n\
|
|
9024
|
+
"No LLM provider API key found in .env.\nOpenMates will start, but AI chat/model processing will stay unavailable until you add one.\n\nRun 'openmates server ai models add' to add a local Ollama/LM Studio model, or add at least one of these to your .env file:\n SECRET__OPENAI__API_KEY=sk-...\n SECRET__ANTHROPIC__API_KEY=sk-ant-...\n SECRET__GOOGLE_AI_STUDIO__API_KEY=...\n\nAfter updating .env, run 'openmates server restart'."
|
|
8991
9025
|
);
|
|
8992
9026
|
}
|
|
8993
9027
|
}
|
|
@@ -9003,6 +9037,164 @@ async function confirmDestructive(phrase) {
|
|
|
9003
9037
|
function printJson(data) {
|
|
9004
9038
|
console.log(JSON.stringify(data, null, 2));
|
|
9005
9039
|
}
|
|
9040
|
+
function localModelsOverlayPath(installPath, installMode = getInstallMode(installPath)) {
|
|
9041
|
+
if (installMode === "source") return join3(installPath, "backend", "providers", LOCAL_AI_MODELS_FILE);
|
|
9042
|
+
return join3(installPath, "config", "providers", LOCAL_AI_MODELS_FILE);
|
|
9043
|
+
}
|
|
9044
|
+
function imageBackendConfigPath(installPath) {
|
|
9045
|
+
return join3(installPath, IMAGE_RUNTIME_CONFIG_FILE);
|
|
9046
|
+
}
|
|
9047
|
+
function ensureImageRuntimeConfig(installPath) {
|
|
9048
|
+
const configPath = imageBackendConfigPath(installPath);
|
|
9049
|
+
if (existsSync5(configPath)) return;
|
|
9050
|
+
mkdirSync3(dirname(configPath), { recursive: true });
|
|
9051
|
+
writeFileSync3(configPath, renderFeatureOverrides({ enabled: [], disabled: [] }));
|
|
9052
|
+
}
|
|
9053
|
+
function readLocalModelOverlay(path) {
|
|
9054
|
+
if (!existsSync5(path)) return { providers: [] };
|
|
9055
|
+
try {
|
|
9056
|
+
const parsed = JSON.parse(readFileSync5(path, "utf-8"));
|
|
9057
|
+
return { providers: Array.isArray(parsed.providers) ? parsed.providers : [] };
|
|
9058
|
+
} catch (error) {
|
|
9059
|
+
throw new Error(
|
|
9060
|
+
`Could not parse ${path}. This file is managed by the OpenMates CLI and must remain JSON-compatible YAML. ${error instanceof Error ? error.message : String(error)}`
|
|
9061
|
+
);
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
function writeLocalModelOverlay(path, overlay) {
|
|
9065
|
+
mkdirSync3(dirname(path), { recursive: true });
|
|
9066
|
+
writeFileSync3(path, `${JSON.stringify(overlay, null, 2)}
|
|
9067
|
+
`);
|
|
9068
|
+
}
|
|
9069
|
+
function hasLocalAiModels(installPath) {
|
|
9070
|
+
const imagePath = join3(installPath, "config", "providers", LOCAL_AI_MODELS_FILE);
|
|
9071
|
+
const sourcePath = join3(installPath, "backend", "providers", LOCAL_AI_MODELS_FILE);
|
|
9072
|
+
return [imagePath, sourcePath].some((path) => {
|
|
9073
|
+
if (!existsSync5(path)) return false;
|
|
9074
|
+
try {
|
|
9075
|
+
return readLocalModelOverlay(path).providers.some((provider) => provider.models.length > 0);
|
|
9076
|
+
} catch {
|
|
9077
|
+
return false;
|
|
9078
|
+
}
|
|
9079
|
+
});
|
|
9080
|
+
}
|
|
9081
|
+
function sanitizeModelId(raw) {
|
|
9082
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "local-model";
|
|
9083
|
+
}
|
|
9084
|
+
function inferCreatorId(modelId) {
|
|
9085
|
+
return MODEL_CREATOR_OPTIONS.find((option) => option.match?.test(modelId))?.id ?? "custom";
|
|
9086
|
+
}
|
|
9087
|
+
function creatorDisplayName(creatorId) {
|
|
9088
|
+
return MODEL_CREATOR_OPTIONS.find((option) => option.id === creatorId)?.name ?? creatorId;
|
|
9089
|
+
}
|
|
9090
|
+
function normalizeCreatorId(value) {
|
|
9091
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
9092
|
+
return normalized || "custom_local";
|
|
9093
|
+
}
|
|
9094
|
+
function normalizeRuntimeKey(value) {
|
|
9095
|
+
const normalized = value.trim().toLowerCase().replace(/_/g, "-");
|
|
9096
|
+
if (normalized === "lmstudio" || normalized === "lm-studio") return "lm-studio";
|
|
9097
|
+
if (normalized === "ollama") return "ollama";
|
|
9098
|
+
return "custom";
|
|
9099
|
+
}
|
|
9100
|
+
function boolFromFlag(value, defaultValue = false) {
|
|
9101
|
+
if (value === void 0) return defaultValue;
|
|
9102
|
+
if (value === true) return true;
|
|
9103
|
+
if (value === false) return false;
|
|
9104
|
+
const normalized = value.toLowerCase();
|
|
9105
|
+
return ["1", "true", "yes", "y", "on"].includes(normalized);
|
|
9106
|
+
}
|
|
9107
|
+
async function promptText(question, defaultValue = "") {
|
|
9108
|
+
const rl = createPromptInterface({ input: process.stdin, output: process.stderr });
|
|
9109
|
+
try {
|
|
9110
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
9111
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
9112
|
+
return answer.trim() || defaultValue;
|
|
9113
|
+
} finally {
|
|
9114
|
+
rl.close();
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
async function promptChoice(question, choices, defaultValue) {
|
|
9118
|
+
console.error(question);
|
|
9119
|
+
choices.forEach((choice, index) => {
|
|
9120
|
+
const marker = choice.value === defaultValue ? " [default]" : "";
|
|
9121
|
+
console.error(` ${index + 1}. ${choice.label}${marker}`);
|
|
9122
|
+
});
|
|
9123
|
+
const answer = await promptText("Choose number or value", defaultValue);
|
|
9124
|
+
const numeric = Number.parseInt(answer, 10);
|
|
9125
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
|
|
9126
|
+
return choices[numeric - 1].value;
|
|
9127
|
+
}
|
|
9128
|
+
const direct = choices.find((choice) => choice.value === answer || choice.label.toLowerCase() === answer.toLowerCase());
|
|
9129
|
+
return direct?.value ?? answer;
|
|
9130
|
+
}
|
|
9131
|
+
async function fetchLocalModelIds(baseUrl, apiKey) {
|
|
9132
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
|
|
9133
|
+
headers: { Authorization: `Bearer ${apiKey || "local"}` }
|
|
9134
|
+
});
|
|
9135
|
+
if (!response.ok) throw new Error(`Failed to fetch local models: HTTP ${response.status}`);
|
|
9136
|
+
const body = await response.json();
|
|
9137
|
+
return (body.data ?? []).map((model) => model.id).filter((id) => Boolean(id));
|
|
9138
|
+
}
|
|
9139
|
+
async function testLocalModel(baseUrl, apiKey, modelId) {
|
|
9140
|
+
const controller = new AbortController();
|
|
9141
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
9142
|
+
try {
|
|
9143
|
+
const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
9144
|
+
method: "POST",
|
|
9145
|
+
signal: controller.signal,
|
|
9146
|
+
headers: {
|
|
9147
|
+
Authorization: `Bearer ${apiKey || "local"}`,
|
|
9148
|
+
"Content-Type": "application/json"
|
|
9149
|
+
},
|
|
9150
|
+
body: JSON.stringify({
|
|
9151
|
+
model: modelId,
|
|
9152
|
+
messages: [
|
|
9153
|
+
{ role: "system", content: "Answer with only the number." },
|
|
9154
|
+
{ role: "user", content: "1+2?" }
|
|
9155
|
+
],
|
|
9156
|
+
stream: false,
|
|
9157
|
+
temperature: 0
|
|
9158
|
+
})
|
|
9159
|
+
});
|
|
9160
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
9161
|
+
const body = await response.json();
|
|
9162
|
+
return body.choices?.[0]?.message?.content?.trim() ?? "";
|
|
9163
|
+
} finally {
|
|
9164
|
+
clearTimeout(timeout);
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
function upsertLocalModel(path, providerId, providerName, model) {
|
|
9168
|
+
const overlay = readLocalModelOverlay(path);
|
|
9169
|
+
let provider = overlay.providers.find((item) => item.provider_id === providerId);
|
|
9170
|
+
if (!provider) {
|
|
9171
|
+
provider = {
|
|
9172
|
+
provider_id: providerId,
|
|
9173
|
+
name: providerName,
|
|
9174
|
+
description: `Local self-hosted models for ${providerName}.`,
|
|
9175
|
+
models: []
|
|
9176
|
+
};
|
|
9177
|
+
overlay.providers.push(provider);
|
|
9178
|
+
}
|
|
9179
|
+
const modelId = model.id;
|
|
9180
|
+
provider.models = provider.models.filter((existing) => existing.id !== modelId);
|
|
9181
|
+
provider.models.push(model);
|
|
9182
|
+
writeLocalModelOverlay(path, overlay);
|
|
9183
|
+
}
|
|
9184
|
+
function removeLocalModel(path, fullModelId) {
|
|
9185
|
+
const [providerId, modelId] = fullModelId.split("/", 2);
|
|
9186
|
+
if (!providerId || !modelId) throw new Error("Use provider/model-id format.");
|
|
9187
|
+
const overlay = readLocalModelOverlay(path);
|
|
9188
|
+
const provider = overlay.providers.find((item) => item.provider_id === providerId);
|
|
9189
|
+
if (!provider) return false;
|
|
9190
|
+
const before = provider.models.length;
|
|
9191
|
+
provider.models = provider.models.filter((model) => model.id !== modelId);
|
|
9192
|
+
writeLocalModelOverlay(path, overlay);
|
|
9193
|
+
return provider.models.length !== before;
|
|
9194
|
+
}
|
|
9195
|
+
function localModelsFromOverlay(overlay) {
|
|
9196
|
+
return overlay.providers.flatMap((provider) => provider.models.map((model) => ({ providerId: provider.provider_id, model })));
|
|
9197
|
+
}
|
|
9006
9198
|
async function serverStatus(flags) {
|
|
9007
9199
|
requireDocker();
|
|
9008
9200
|
const installPath = resolveServerPath(flags);
|
|
@@ -9461,7 +9653,9 @@ async function serverFeatures(rest, flags) {
|
|
|
9461
9653
|
const action = rest[0] ?? "list";
|
|
9462
9654
|
const featureId = rest[1];
|
|
9463
9655
|
const installPath = resolveServerPath(flags);
|
|
9464
|
-
const
|
|
9656
|
+
const installMode = getInstallMode(installPath);
|
|
9657
|
+
if (installMode === "image") ensureImageRuntimeConfig(installPath);
|
|
9658
|
+
const configPath = installMode === "image" ? imageBackendConfigPath(installPath) : join3(installPath, BACKEND_CONFIG_FILE);
|
|
9465
9659
|
if (!existsSync5(configPath)) {
|
|
9466
9660
|
throw new Error(`Backend config not found at ${configPath}. Run 'openmates server install' first or pass --path <dir>.`);
|
|
9467
9661
|
}
|
|
@@ -9521,6 +9715,179 @@ async function serverFeatures(rest, flags) {
|
|
|
9521
9715
|
}
|
|
9522
9716
|
throw new Error(`Unknown server features command '${action}'. Use list, enable, disable, reset, or explain.`);
|
|
9523
9717
|
}
|
|
9718
|
+
async function serverAiModelsAdd(flags) {
|
|
9719
|
+
const installPath = resolveServerPath(flags);
|
|
9720
|
+
const installMode = getInstallMode(installPath);
|
|
9721
|
+
const overlayPath = localModelsOverlayPath(installPath, installMode);
|
|
9722
|
+
const runtimeInput = typeof flags.runtime === "string" ? flags.runtime : await promptChoice(
|
|
9723
|
+
"Which local runtime should OpenMates use?",
|
|
9724
|
+
[
|
|
9725
|
+
{ value: "ollama", label: "Ollama" },
|
|
9726
|
+
{ value: "lm-studio", label: "LM Studio" },
|
|
9727
|
+
{ value: "custom", label: "Custom OpenAI-compatible API" }
|
|
9728
|
+
],
|
|
9729
|
+
"ollama"
|
|
9730
|
+
);
|
|
9731
|
+
const runtimeKey = normalizeRuntimeKey(runtimeInput);
|
|
9732
|
+
const runtime = LOCAL_MODEL_RUNTIME_DEFAULTS[runtimeKey];
|
|
9733
|
+
const baseUrl = typeof flags["base-url"] === "string" ? flags["base-url"] : await promptText("OpenAI-compatible base URL", runtime.baseUrl);
|
|
9734
|
+
const apiKey = typeof flags["api-key"] === "string" ? flags["api-key"] : runtimeKey === "custom" ? await promptText("API key", runtime.apiKey) : runtime.apiKey;
|
|
9735
|
+
let availableModels = [];
|
|
9736
|
+
try {
|
|
9737
|
+
availableModels = await fetchLocalModelIds(baseUrl, apiKey);
|
|
9738
|
+
} catch (error) {
|
|
9739
|
+
console.error(`Could not fetch /v1/models: ${error instanceof Error ? error.message : String(error)}`);
|
|
9740
|
+
}
|
|
9741
|
+
const rawModelId = typeof flags.model === "string" ? flags.model : availableModels.length ? await promptChoice(
|
|
9742
|
+
"Which installed model should OpenMates add?",
|
|
9743
|
+
[...availableModels.map((id) => ({ value: id, label: id })), { value: "manual", label: "Enter manually" }],
|
|
9744
|
+
availableModels[0]
|
|
9745
|
+
) : await promptText("Local model ID");
|
|
9746
|
+
const serverModelId = rawModelId === "manual" ? await promptText("Local model ID") : rawModelId;
|
|
9747
|
+
if (!serverModelId) throw new Error("Model ID is required.");
|
|
9748
|
+
const inferredCreator = inferCreatorId(serverModelId);
|
|
9749
|
+
const creatorInput = typeof flags.creator === "string" ? flags.creator : await promptChoice(
|
|
9750
|
+
"Who created the model?",
|
|
9751
|
+
MODEL_CREATOR_OPTIONS.map((option) => ({ value: option.id, label: option.name })),
|
|
9752
|
+
inferredCreator
|
|
9753
|
+
);
|
|
9754
|
+
const providerId = creatorInput === "custom" ? normalizeCreatorId(await promptText("Custom creator/provider ID", "custom_local")) : normalizeCreatorId(creatorInput);
|
|
9755
|
+
const providerName = providerId === creatorInput ? creatorDisplayName(providerId) : providerId;
|
|
9756
|
+
const internalModelId = typeof flags.id === "string" ? sanitizeModelId(flags.id) : `${sanitizeModelId(serverModelId)}-local`;
|
|
9757
|
+
const displayName = typeof flags.name === "string" ? flags.name : await promptText("Display name", serverModelId);
|
|
9758
|
+
const supportsImages = boolFromFlag(flags.images ?? flags.image ?? flags.vision, false);
|
|
9759
|
+
const supportsTools = boolFromFlag(flags.tools ?? flags["tool-use"], false);
|
|
9760
|
+
const contextWindowInput = typeof flags["context-window"] === "string" ? flags["context-window"] : await promptText("Context window tokens", "32768");
|
|
9761
|
+
const contextWindow = Number.parseInt(contextWindowInput, 10) || 32768;
|
|
9762
|
+
if (flags["skip-test"] !== true) {
|
|
9763
|
+
console.error(`Testing ${runtime.label} model '${serverModelId}'...`);
|
|
9764
|
+
const testOutput = await testLocalModel(baseUrl, apiKey, serverModelId);
|
|
9765
|
+
if (!testOutput) throw new Error("Local model test returned no content. Not saving model.");
|
|
9766
|
+
console.error(`Test response: ${testOutput}`);
|
|
9767
|
+
}
|
|
9768
|
+
const model = {
|
|
9769
|
+
id: internalModelId,
|
|
9770
|
+
name: displayName,
|
|
9771
|
+
description: `${displayName} served locally through ${runtime.label}.`,
|
|
9772
|
+
country_origin: "local",
|
|
9773
|
+
for_app_skill: "ai.ask",
|
|
9774
|
+
allow_auto_select: false,
|
|
9775
|
+
local: true,
|
|
9776
|
+
self_hosted: true,
|
|
9777
|
+
input_types: supportsImages ? ["text", "image"] : ["text"],
|
|
9778
|
+
output_types: ["text"],
|
|
9779
|
+
default_server: runtime.serverId,
|
|
9780
|
+
servers: [
|
|
9781
|
+
{
|
|
9782
|
+
id: runtime.serverId,
|
|
9783
|
+
name: runtime.label,
|
|
9784
|
+
model_id: serverModelId,
|
|
9785
|
+
region: "local",
|
|
9786
|
+
base_url: baseUrl.replace(/\/+$/, ""),
|
|
9787
|
+
api_key: apiKey,
|
|
9788
|
+
supports_tools: supportsTools
|
|
9789
|
+
}
|
|
9790
|
+
],
|
|
9791
|
+
pricing: { fixed: { credits: 0 } },
|
|
9792
|
+
costs: {
|
|
9793
|
+
input_per_million_token: { price: 0, currency: "USD", max_context: contextWindow },
|
|
9794
|
+
output_per_million_token: { price: 0, currency: "USD", max_context: contextWindow }
|
|
9795
|
+
},
|
|
9796
|
+
features: {
|
|
9797
|
+
streaming: true,
|
|
9798
|
+
tool_use: supportsTools,
|
|
9799
|
+
max_context: contextWindow
|
|
9800
|
+
}
|
|
9801
|
+
};
|
|
9802
|
+
upsertLocalModel(overlayPath, providerId, providerName, model);
|
|
9803
|
+
if (installMode === "image") ensureImageRuntimeConfig(installPath);
|
|
9804
|
+
if (flags.json === true) {
|
|
9805
|
+
printJson({ command: "server ai models add", status: "success", model: `${providerId}/${internalModelId}`, overlayPath });
|
|
9806
|
+
} else {
|
|
9807
|
+
console.log(`Added local model: ${providerId}/${internalModelId}`);
|
|
9808
|
+
console.log(`Updated ${overlayPath}`);
|
|
9809
|
+
console.log("Restart the server for model changes to take effect: openmates server restart");
|
|
9810
|
+
console.log("Self-hosted local models charge 0 credits; token usage may still be recorded in usage history.");
|
|
9811
|
+
}
|
|
9812
|
+
}
|
|
9813
|
+
async function serverAiModelsList(flags) {
|
|
9814
|
+
const installPath = resolveServerPath(flags);
|
|
9815
|
+
const overlayPath = localModelsOverlayPath(installPath);
|
|
9816
|
+
const overlay = readLocalModelOverlay(overlayPath);
|
|
9817
|
+
const models = localModelsFromOverlay(overlay).map(({ providerId, model }) => ({
|
|
9818
|
+
id: `${providerId}/${String(model.id ?? "")}`,
|
|
9819
|
+
name: model.name ?? model.id,
|
|
9820
|
+
server: Array.isArray(model.servers) ? model.servers[0]?.id : void 0,
|
|
9821
|
+
serverModelId: Array.isArray(model.servers) ? model.servers[0]?.model_id : void 0
|
|
9822
|
+
}));
|
|
9823
|
+
if (flags.json === true) {
|
|
9824
|
+
printJson({ overlayPath, models });
|
|
9825
|
+
return;
|
|
9826
|
+
}
|
|
9827
|
+
if (!models.length) {
|
|
9828
|
+
console.log("No local AI models configured. Add one with: openmates server ai models add");
|
|
9829
|
+
return;
|
|
9830
|
+
}
|
|
9831
|
+
console.log("Local AI models:");
|
|
9832
|
+
for (const model of models) {
|
|
9833
|
+
console.log(` ${model.id} (${model.server}: ${model.serverModelId})`);
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
async function serverAiModelsTest(rest, flags) {
|
|
9837
|
+
const installPath = resolveServerPath(flags);
|
|
9838
|
+
const overlayPath = localModelsOverlayPath(installPath);
|
|
9839
|
+
const fullModelId = rest[0];
|
|
9840
|
+
if (!fullModelId || !fullModelId.includes("/")) throw new Error("Usage: openmates server ai models test <provider/model-id>");
|
|
9841
|
+
const [providerId, modelId] = fullModelId.split("/", 2);
|
|
9842
|
+
const overlay = readLocalModelOverlay(overlayPath);
|
|
9843
|
+
const provider = overlay.providers.find((item) => item.provider_id === providerId);
|
|
9844
|
+
const model = provider?.models.find((item) => item.id === modelId);
|
|
9845
|
+
const server = Array.isArray(model?.servers) ? model.servers[0] : void 0;
|
|
9846
|
+
if (!server) throw new Error(`Local model not found in ${overlayPath}: ${fullModelId}`);
|
|
9847
|
+
const baseUrl = String(server.base_url ?? "");
|
|
9848
|
+
const apiKey = String(server.api_key ?? "local");
|
|
9849
|
+
const serverModelId = String(server.model_id ?? "");
|
|
9850
|
+
const output = await testLocalModel(baseUrl, apiKey, serverModelId);
|
|
9851
|
+
if (flags.json === true) {
|
|
9852
|
+
printJson({ model: fullModelId, status: "success", output });
|
|
9853
|
+
} else {
|
|
9854
|
+
console.log(`Model test succeeded: ${fullModelId}`);
|
|
9855
|
+
console.log(`Response: ${output}`);
|
|
9856
|
+
}
|
|
9857
|
+
}
|
|
9858
|
+
async function serverAiModelsRemove(rest, flags) {
|
|
9859
|
+
const installPath = resolveServerPath(flags);
|
|
9860
|
+
const overlayPath = localModelsOverlayPath(installPath);
|
|
9861
|
+
const fullModelId = rest[0];
|
|
9862
|
+
if (!fullModelId) throw new Error("Usage: openmates server ai models remove <provider/model-id>");
|
|
9863
|
+
const removed = removeLocalModel(overlayPath, fullModelId);
|
|
9864
|
+
if (flags.json === true) {
|
|
9865
|
+
printJson({ command: "server ai models remove", status: removed ? "success" : "not_found", model: fullModelId });
|
|
9866
|
+
} else if (removed) {
|
|
9867
|
+
console.log(`Removed local model: ${fullModelId}`);
|
|
9868
|
+
console.log("Restart the server for model changes to take effect: openmates server restart");
|
|
9869
|
+
} else {
|
|
9870
|
+
console.log(`Local model not found: ${fullModelId}`);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
async function serverAi(rest, flags) {
|
|
9874
|
+
const area = rest[0];
|
|
9875
|
+
const action = rest[1] ?? "list";
|
|
9876
|
+
const args = rest.slice(2);
|
|
9877
|
+
if (area !== "models") throw new Error("Usage: openmates server ai models <add|list|test|remove>");
|
|
9878
|
+
switch (action) {
|
|
9879
|
+
case "add":
|
|
9880
|
+
return serverAiModelsAdd(flags);
|
|
9881
|
+
case "list":
|
|
9882
|
+
return serverAiModelsList(flags);
|
|
9883
|
+
case "test":
|
|
9884
|
+
return serverAiModelsTest(args, flags);
|
|
9885
|
+
case "remove":
|
|
9886
|
+
return serverAiModelsRemove(args, flags);
|
|
9887
|
+
default:
|
|
9888
|
+
throw new Error(`Unknown server ai models command '${action}'. Use add, list, test, or remove.`);
|
|
9889
|
+
}
|
|
9890
|
+
}
|
|
9524
9891
|
async function serverUninstall(flags) {
|
|
9525
9892
|
requireDocker();
|
|
9526
9893
|
const installPath = resolveServerPath(flags);
|
|
@@ -9588,6 +9955,7 @@ Commands:
|
|
|
9588
9955
|
logs Display server logs
|
|
9589
9956
|
update Update to latest version (pull images, or git pull + rebuild for source installs)
|
|
9590
9957
|
make-admin Grant admin privileges to a user
|
|
9958
|
+
ai Manage self-hosted local AI models
|
|
9591
9959
|
reset Reset server data (requires confirmation)
|
|
9592
9960
|
uninstall Completely remove OpenMates (requires confirmation)
|
|
9593
9961
|
|
|
@@ -9639,12 +10007,19 @@ Command Options:
|
|
|
9639
10007
|
openmates server features reset <feature-id>
|
|
9640
10008
|
openmates server features explain <feature-id>
|
|
9641
10009
|
|
|
10010
|
+
ai models:
|
|
10011
|
+
openmates server ai models add
|
|
10012
|
+
openmates server ai models list
|
|
10013
|
+
openmates server ai models test <provider/model-id>
|
|
10014
|
+
openmates server ai models remove <provider/model-id>
|
|
10015
|
+
|
|
9642
10016
|
Examples:
|
|
9643
10017
|
openmates server install
|
|
9644
10018
|
openmates server start --with-overrides
|
|
9645
10019
|
openmates server logs --container api --follow
|
|
9646
10020
|
openmates server make-admin user@example.com
|
|
9647
10021
|
openmates server features disable app:videos
|
|
10022
|
+
openmates server ai models add
|
|
9648
10023
|
openmates server features enable embed:code:application
|
|
9649
10024
|
openmates server update
|
|
9650
10025
|
openmates server update --dry-run
|
|
@@ -9677,6 +10052,8 @@ async function handleServer(subcommand, rest, flags) {
|
|
|
9677
10052
|
return serverReset(flags);
|
|
9678
10053
|
case "make-admin":
|
|
9679
10054
|
return serverMakeAdmin(rest, flags);
|
|
10055
|
+
case "ai":
|
|
10056
|
+
return serverAi(rest, flags);
|
|
9680
10057
|
case "features":
|
|
9681
10058
|
return serverFeatures(rest, flags);
|
|
9682
10059
|
case "uninstall":
|
|
@@ -41926,7 +42303,7 @@ function buildAssistantFeedbackDecision(rating) {
|
|
|
41926
42303
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
41927
42304
|
import { existsSync as existsSync6, mkdtempSync, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync4 } from "fs";
|
|
41928
42305
|
import { tmpdir } from "os";
|
|
41929
|
-
import { dirname, join as join4, resolve as resolve5 } from "path";
|
|
42306
|
+
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
41930
42307
|
import { fileURLToPath } from "url";
|
|
41931
42308
|
var DEFAULT_JUDGE_MODEL = "google/gemini-3-flash-preview";
|
|
41932
42309
|
var DEFAULT_EXTENSIVE_SIZE = 10;
|
|
@@ -42778,13 +43155,13 @@ function normalizeModelKey(model) {
|
|
|
42778
43155
|
}
|
|
42779
43156
|
function findProvidersDir() {
|
|
42780
43157
|
const currentFile = fileURLToPath(import.meta.url);
|
|
42781
|
-
let current =
|
|
43158
|
+
let current = dirname2(currentFile);
|
|
42782
43159
|
for (let index = 0; index < 8; index += 1) {
|
|
42783
43160
|
const candidate = join4(current, "backend", "providers");
|
|
42784
43161
|
if (existsSync6(candidate)) return candidate;
|
|
42785
43162
|
const parentCandidate = join4(current, "..", "..", "backend", "providers");
|
|
42786
43163
|
if (existsSync6(parentCandidate)) return resolve5(parentCandidate);
|
|
42787
|
-
const next =
|
|
43164
|
+
const next = dirname2(current);
|
|
42788
43165
|
if (next === current) break;
|
|
42789
43166
|
current = next;
|
|
42790
43167
|
}
|
|
@@ -42905,7 +43282,7 @@ function round2(value) {
|
|
|
42905
43282
|
return Math.round(value * 100) / 100;
|
|
42906
43283
|
}
|
|
42907
43284
|
function defaultImageFixturePath() {
|
|
42908
|
-
const fixtureDir = join4(
|
|
43285
|
+
const fixtureDir = join4(dirname2(fileURLToPath(import.meta.url)), "..", "fixtures");
|
|
42909
43286
|
const fixturePath = join4(fixtureDir, "brandenburger-tor.png");
|
|
42910
43287
|
if (existsSync6(fixturePath)) return fixturePath;
|
|
42911
43288
|
const tempDir = mkdtempSync(join4(tmpdir(), "openmates-benchmark-"));
|
|
@@ -44528,6 +44905,25 @@ async function printSettingsMutationResult(resultPromise, flags) {
|
|
|
44528
44905
|
process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
|
|
44529
44906
|
if (result && typeof result === "object") printGenericObject(result);
|
|
44530
44907
|
}
|
|
44908
|
+
function printReportIssueCreateResult(result, flags) {
|
|
44909
|
+
if (flags.json === true) {
|
|
44910
|
+
printJson2(result);
|
|
44911
|
+
return;
|
|
44912
|
+
}
|
|
44913
|
+
process.stdout.write("\x1B[32m\u2713\x1B[0m Issue reported\n");
|
|
44914
|
+
const obj = result && typeof result === "object" ? result : {};
|
|
44915
|
+
const issueId = typeof obj.issue_id === "string" ? obj.issue_id : "";
|
|
44916
|
+
const shortIssueId = typeof obj.short_issue_id === "string" ? obj.short_issue_id : "";
|
|
44917
|
+
if (shortIssueId || issueId) {
|
|
44918
|
+
console.log(`Issue reference: ${shortIssueId || issueId}`);
|
|
44919
|
+
}
|
|
44920
|
+
if (issueId && shortIssueId && issueId !== shortIssueId) {
|
|
44921
|
+
console.log(`Internal issue ID: ${issueId}`);
|
|
44922
|
+
}
|
|
44923
|
+
if (typeof obj.message === "string") {
|
|
44924
|
+
console.log(obj.message);
|
|
44925
|
+
}
|
|
44926
|
+
}
|
|
44531
44927
|
function addQueryParam(params, key, value) {
|
|
44532
44928
|
if (typeof value === "string" && value.length > 0) params.set(key, value);
|
|
44533
44929
|
}
|
|
@@ -44671,11 +45067,11 @@ function parseYamlScalar(value) {
|
|
|
44671
45067
|
}
|
|
44672
45068
|
async function saveDownloadedDocument(document, output) {
|
|
44673
45069
|
const { mkdir, writeFile } = await import("fs/promises");
|
|
44674
|
-
const { join: join5, basename: basename4, dirname:
|
|
45070
|
+
const { join: join5, basename: basename4, dirname: dirname4 } = await import("path");
|
|
44675
45071
|
const target = typeof output === "string" ? output : ".";
|
|
44676
45072
|
const filename = basename4(document.filename || "document.pdf");
|
|
44677
45073
|
const filePath = target.endsWith(".pdf") ? target : join5(target, filename);
|
|
44678
|
-
await mkdir(
|
|
45074
|
+
await mkdir(dirname4(filePath), { recursive: true });
|
|
44679
45075
|
await writeFile(filePath, document.data);
|
|
44680
45076
|
return filePath;
|
|
44681
45077
|
}
|
|
@@ -44754,7 +45150,7 @@ async function promptSecret(question) {
|
|
|
44754
45150
|
}
|
|
44755
45151
|
async function writeSecretFile(filePath, content, force = false) {
|
|
44756
45152
|
const { mkdir, writeFile, stat: stat2 } = await import("fs/promises");
|
|
44757
|
-
const { dirname:
|
|
45153
|
+
const { dirname: dirname4 } = await import("path");
|
|
44758
45154
|
try {
|
|
44759
45155
|
await stat2(filePath);
|
|
44760
45156
|
if (!force) throw new Error(`${filePath} already exists. Use --force to overwrite.`);
|
|
@@ -44764,7 +45160,7 @@ async function writeSecretFile(filePath, content, force = false) {
|
|
|
44764
45160
|
}
|
|
44765
45161
|
if (error instanceof Error && !("code" in error)) throw error;
|
|
44766
45162
|
}
|
|
44767
|
-
await mkdir(
|
|
45163
|
+
await mkdir(dirname4(filePath), { recursive: true });
|
|
44768
45164
|
await writeFile(filePath, content, { mode: 384 });
|
|
44769
45165
|
return filePath;
|
|
44770
45166
|
}
|
|
@@ -45391,7 +45787,8 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
45391
45787
|
const title = typeof flags.title === "string" ? flags.title : void 0;
|
|
45392
45788
|
const body = typeof flags.body === "string" ? flags.body : void 0;
|
|
45393
45789
|
if (!title || !body) throw new Error("Provide --title and --body.");
|
|
45394
|
-
await
|
|
45790
|
+
const result = await client.settingsPost("issues", { title, description: body });
|
|
45791
|
+
printReportIssueCreateResult(result, flags);
|
|
45395
45792
|
return;
|
|
45396
45793
|
}
|
|
45397
45794
|
if (matches(tokens, ["report-issue", "status"])) {
|
|
@@ -47829,7 +48226,7 @@ async function handleDocs(client, subcommand, rest, flags) {
|
|
|
47829
48226
|
}
|
|
47830
48227
|
if (subcommand === "download") {
|
|
47831
48228
|
const { writeFile, mkdir } = await import("fs/promises");
|
|
47832
|
-
const { join: join5, dirname:
|
|
48229
|
+
const { join: join5, dirname: dirname4 } = await import("path");
|
|
47833
48230
|
if (flags.all === true) {
|
|
47834
48231
|
const outputDir = typeof flags.output === "string" ? flags.output : "./openmates-docs";
|
|
47835
48232
|
const tree = await client.listDocs();
|
|
@@ -47839,7 +48236,7 @@ async function handleDocs(client, subcommand, rest, flags) {
|
|
|
47839
48236
|
for (const slug2 of slugs) {
|
|
47840
48237
|
const content2 = await client.getDoc(slug2);
|
|
47841
48238
|
const filePath = join5(outputDir, `${slug2}.md`);
|
|
47842
|
-
await mkdir(
|
|
48239
|
+
await mkdir(dirname4(filePath), { recursive: true });
|
|
47843
48240
|
await writeFile(filePath, content2, "utf-8");
|
|
47844
48241
|
count++;
|
|
47845
48242
|
process.stderr.write(`\r Downloaded ${count}/${slugs.length}`);
|
|
@@ -47912,7 +48309,7 @@ function isCliEntrypoint() {
|
|
|
47912
48309
|
try {
|
|
47913
48310
|
const invokedPath = realpathSync(entrypoint);
|
|
47914
48311
|
const modulePath = realpathSync(fileURLToPath2(import.meta.url));
|
|
47915
|
-
return invokedPath === modulePath || basename3(invokedPath) === "cli.js" &&
|
|
48312
|
+
return invokedPath === modulePath || basename3(invokedPath) === "cli.js" && dirname3(invokedPath) === dirname3(modulePath);
|
|
47916
48313
|
} catch {
|
|
47917
48314
|
return false;
|
|
47918
48315
|
}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED