openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
390 lines (389 loc) • 17.9 kB
JavaScript
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import { r as DEFAULT_PROVIDER } from "./defaults-CdX9UGcX.js";
import { o as resolveAgentEffectiveModelPrimary } from "./agent-scope-DbtJyKUL.js";
import { c as loadManifestMetadataSnapshot } from "./manifest-contract-eligibility-BbV7X6pV.js";
import { s as normalizeThinkLevel } from "./thinking.shared-bHYuuc1L.js";
import { t as getProviderEnvVars } from "./provider-env-vars-B8XgJCSL.js";
import { t as planEffectiveModelCatalogRows } from "./model-catalog-BiOJ2EJd.js";
import { t as ADMIN_SCOPE } from "./operator-scopes-Dw7Gu2cA.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js";
import "./config-Cs0XXL3x.js";
import { g as randomIdempotencyKey, o as callGateway } from "./call-BWR5pPgw.js";
import { d as normalizeMimeType, n as detectMime } from "./mime-CVpcq9ju.js";
import { S as updateAuthProfileStoreWithLock, f as loadAuthProfileStoreForRuntime } from "./store-F1B2duCT.js";
import { n as listProfilesForProvider } from "./profile-list-DyfWX-d2.js";
import { i as convertHeicToJpeg } from "./image-ops-DU1SIRgh.js";
import "./thinking-DterdXhr.js";
import { a as loadPreparedModelCatalog } from "./prepared-model-catalog-Bt4dYhnY.js";
import "./media-services-By-i0Drw.js";
import "./auth-profiles-BdUEhE7u.js";
import { t as canonicalizeCaseOnlyCatalogModelRef } from "./model-selection-di2kjKCB.js";
import { t as buildExplicitSessionIdSessionKey } from "./session-Ck6yDW4J.js";
import { u as getModelsCommandSecretTargetIds } from "./command-secret-targets-D0VrT_k_.js";
import { r as prepareSimpleCompletionModelForAgent, t as completeWithPreparedSimpleCompletionModel } from "./simple-completion-runtime-Dgdi_92D.js";
import { n as runCommandWithRuntime } from "./cli-utils-D1DAWB8d.js";
import { c as providerSummaryText, d as resolveCapabilityProviderAgentId, f as resolveLocalCapabilityRuntimeConfig, h as resolveTransport, l as requireProviderModelOverride, m as resolveSelectedProviderFromModelRef, n as formatEnvelopeForText, s as providerHasGenericConfig, t as emitJsonOrText, u as resolveCapabilityAgentOption } from "./shared-LJFJI8g1.js";
import { t as collectOption } from "./helpers-B-LqXQ3Z.js";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/cli/capability-cli/model.ts
const LOCAL_MODEL_RUN_SYSTEM_PROMPT = "You are a personal assistant running inside OpenClaw.";
const HEIC_MODEL_RUN_MIMES = /* @__PURE__ */ new Set([
"image/heic",
"image/heic-sequence",
"image/heif",
"image/heif-sequence"
]);
async function loadModelCatalogForInspection(cfg, agentId) {
const prepared = await loadPreparedModelCatalog({
config: cfg,
agentId,
readOnly: true
});
const metadataSnapshot = loadManifestMetadataSnapshot({
config: cfg,
env: process.env
});
const manifest = planEffectiveModelCatalogRows({
registry: metadataSnapshot.manifestRegistry,
config: cfg
}).rows;
const entries = /* @__PURE__ */ new Map();
for (const entry of manifest) entries.set(`${entry.provider}\0${entry.id}`, entry);
for (const entry of prepared) entries.set(`${entry.provider}\0${entry.id}`, entry);
return [...entries.values()].toSorted((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
}
async function canonicalizeModelRunRef(params) {
return await canonicalizeCaseOnlyCatalogModelRef({
cfg: params.cfg,
raw: params.raw,
defaultProvider: DEFAULT_PROVIDER,
loadCatalog: () => loadPreparedModelCatalog({
config: params.cfg,
readOnly: true
}),
preserveAuthProfile: params.preserveAuthProfile
});
}
function collectModelRunText(content) {
return content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").trim();
}
function requireModelRunPrompt(value) {
if (typeof value !== "string" || normalizeOptionalString(value) === void 0) throw new Error("--prompt cannot be empty or whitespace-only.");
return value;
}
async function readModelRunImageFiles(files) {
if (!files || files.length === 0) return [];
return await Promise.all(files.map(async (filePath) => {
const resolvedPath = path.resolve(filePath);
const buffer = await fs.readFile(resolvedPath);
const mimeType = normalizeMimeType(await detectMime({
buffer,
filePath: resolvedPath
}));
if (!mimeType?.startsWith("image/")) throw new Error(`Unsupported --file for model run: ${resolvedPath}. Only image files are supported; use infer audio transcribe for audio files.`);
if (HEIC_MODEL_RUN_MIMES.has(mimeType)) {
const converted = await convertHeicToJpeg(buffer);
return {
path: resolvedPath,
fileName: path.basename(resolvedPath),
mimeType: "image/jpeg",
data: converted.toString("base64")
};
}
return {
path: resolvedPath,
fileName: path.basename(resolvedPath),
mimeType,
data: buffer.toString("base64")
};
}));
}
function normalizeModelRunThinking(value) {
if (value === void 0) return;
if (typeof value !== "string") throw new Error("--thinking must be a string.");
const normalized = normalizeThinkLevel(value);
if (!normalized) throw new Error("Invalid thinking level. Use one of: off, minimal, low, medium, high, adaptive, xhigh, max.");
return normalized;
}
async function runModelRun(params) {
const explicitModelOverride = requireProviderModelOverride(params.model);
const cfg = params.transport === "local" ? await resolveLocalCapabilityRuntimeConfig({
commandName: "infer model run",
targetIds: getModelsCommandSecretTargetIds()
}) : getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, params.agent, "infer model run");
const modelRef = await canonicalizeModelRunRef({
raw: params.model,
cfg,
preserveAuthProfile: params.transport === "local"
});
const hasExplicitProviderModelOverride = Boolean(explicitModelOverride);
const imageFiles = await readModelRunImageFiles(params.files);
const messageContent = imageFiles.length > 0 ? [{
type: "text",
text: params.prompt
}, ...imageFiles.map((image) => ({
type: "image",
data: image.data,
mimeType: image.mimeType
}))] : params.prompt;
if (params.transport === "local") {
const prepared = await prepareSimpleCompletionModelForAgent({
cfg,
agentId,
modelRef,
allowMissingApiKeyModes: ["aws-sdk"],
...hasExplicitProviderModelOverride ? { allowBundledStaticCatalogFallback: true } : {},
skipAgentDiscovery: true
});
if ("error" in prepared) throw new Error(prepared.error);
if (prepared.selection.provider === "codex") throw new Error("The codex provider is served by the Codex app-server agent runtime, not the local simple-completion transport. Use an openai/<model> ref with provider/model agentRuntime.id: \"codex\", run through the gateway, or use /codex commands.");
const localModelRunSystemPrompt = prepared.model.api === "openai-chatgpt-responses" ? LOCAL_MODEL_RUN_SYSTEM_PROMPT : void 0;
const result = await completeWithPreparedSimpleCompletionModel({
model: prepared.model,
auth: prepared.auth,
cfg,
context: {
...localModelRunSystemPrompt ? { systemPrompt: localModelRunSystemPrompt } : {},
messages: [{
role: "user",
content: messageContent,
timestamp: Date.now()
}]
},
options: {
maxTokens: typeof prepared.model.maxTokens === "number" && Number.isFinite(prepared.model.maxTokens) ? prepared.model.maxTokens : void 0,
...params.thinking ? { reasoning: params.thinking } : {}
}
});
const text = collectModelRunText(result.content);
if (!text) {
const providerErrorMessage = result.errorMessage;
const detail = typeof providerErrorMessage === "string" && providerErrorMessage.trim() ? `: ${providerErrorMessage.trim()}` : "";
throw new Error(`No text output returned for provider "${prepared.selection.provider}" model "${prepared.selection.modelId}"${detail}.`);
}
return {
ok: true,
capability: "model.run",
transport: "local",
provider: prepared.selection.provider,
model: prepared.selection.modelId,
attempts: [],
...imageFiles.length > 0 ? { inputs: imageFiles.map((image) => ({
path: image.path,
mimeType: image.mimeType
})) } : {},
outputs: [{
text,
mediaUrl: null
}]
};
}
const { provider, model } = requireProviderModelOverride(modelRef) ?? {};
const hasModelOverride = Boolean(provider || model);
const sessionId = `model-run-${randomUUID()}`;
const sessionKey = buildExplicitSessionIdSessionKey({
agentId,
sessionId
});
const response = await callGateway({
method: "agent",
params: {
agentId,
sessionId,
sessionKey,
message: params.prompt,
attachments: imageFiles.length > 0 ? imageFiles.map((image) => ({
type: "image",
fileName: image.fileName,
mimeType: image.mimeType,
content: image.data
})) : void 0,
provider,
model,
...params.thinking ? { thinking: params.thinking } : {},
modelRun: true,
promptMode: "none",
cleanupBundleMcpOnRunEnd: true,
idempotencyKey: randomIdempotencyKey()
},
expectFinal: true,
timeoutMs: 12e4,
clientName: hasModelOverride ? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT : GATEWAY_CLIENT_NAMES.CLI,
mode: hasModelOverride ? GATEWAY_CLIENT_MODES.BACKEND : GATEWAY_CLIENT_MODES.CLI,
...hasModelOverride ? { scopes: [ADMIN_SCOPE] } : {}
});
return {
ok: true,
capability: "model.run",
transport: "gateway",
provider: response?.result?.meta?.agentMeta?.provider,
model: response?.result?.meta?.agentMeta?.model,
attempts: response?.result?.meta?.agentMeta?.fallbackAttempts ?? [],
outputs: (response?.result?.payloads ?? []).map((payload) => ({
text: payload.text,
mediaUrl: payload.mediaUrl,
mediaUrls: payload.mediaUrls
})),
...imageFiles.length > 0 ? { inputs: imageFiles.map((image) => ({
path: image.path,
mimeType: image.mimeType
})) } : {}
};
}
async function buildModelProviders(rawAgentId) {
const cfg = getRuntimeConfig();
const agentId = resolveCapabilityProviderAgentId(cfg, rawAgentId);
const catalog = await loadModelCatalogForInspection(cfg, agentId);
const selectedProvider = resolveSelectedProviderFromModelRef(resolveAgentEffectiveModelPrimary(cfg, agentId));
const grouped = /* @__PURE__ */ new Map();
for (const entry of catalog) {
const current = grouped.get(entry.provider) ?? {
provider: entry.provider,
count: 0,
defaults: [],
available: true,
configured: providerHasGenericConfig({
cfg,
providerId: entry.provider,
agentId,
envVars: getProviderEnvVars(entry.provider)
}),
selected: selectedProvider === entry.provider
};
current.count += 1;
if (current.defaults.length < 3) current.defaults.push(entry.id);
grouped.set(entry.provider, current);
}
return [...grouped.values()].toSorted((a, b) => a.provider.localeCompare(b.provider));
}
async function runModelAuthStatus(agent) {
const captured = [];
const { modelsStatusCommand } = await import("./list.status-command-lvamGNG3.js");
await modelsStatusCommand({
json: true,
agent
}, {
log: (...args) => captured.push(args.join(" ")),
error: (message) => {
throw message instanceof Error ? message : new Error(String(message));
},
exit: (code) => {
throw new Error(`exit ${code}`);
}
});
const raw = captured.find((line) => line.trim().startsWith("{"));
return raw ? JSON.parse(raw) : {};
}
async function runModelAuthLogout(provider, agent) {
const cfg = getRuntimeConfig();
const agentDir = resolveAgentDir(cfg, agent);
const store = loadAuthProfileStoreForRuntime(agentDir);
const profileIds = listProfilesForProvider(store, provider);
if (!await updateAuthProfileStoreWithLock({
agentDir,
updater: (nextStore) => {
let changed = false;
for (const profileId of profileIds) {
if (nextStore.profiles[profileId]) {
delete nextStore.profiles[profileId];
changed = true;
}
if (nextStore.usageStats?.[profileId]) {
delete nextStore.usageStats[profileId];
changed = true;
}
}
if (nextStore.order?.[provider]) {
delete nextStore.order[provider];
changed = true;
}
if (nextStore.lastGood?.[provider]) {
delete nextStore.lastGood[provider];
changed = true;
}
return changed;
}
})) throw new Error(`Failed to remove saved auth profiles for provider ${provider}.`);
return {
provider,
removedProfiles: profileIds
};
}
function registerModelCapabilityCommands(capability) {
const model = capability.command("model").description("Text inference and model catalog commands").option("--agent <id>", "Agent whose model and auth state should be used");
model.command("run").description("Run a one-shot model turn").requiredOption("--prompt <text>", "Prompt text").option("--file <path>", "Image file", collectOption, []).option("--model <provider/model>", "Model override").option("--thinking <level>", "Thinking level override").option("--local", "Force local execution", false).option("--gateway", "Force gateway execution", false).option("--agent <id>", "Agent whose model and credentials own the run (default: agents.defaults.systemAgent.agentId, then the sole agent)").option("--json", "Output JSON", false).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const prompt = requireModelRunPrompt(opts.prompt);
const thinking = normalizeModelRunThinking(opts.thinking);
const transport = resolveTransport({
local: Boolean(opts.local),
gateway: Boolean(opts.gateway),
supported: ["local", "gateway"],
defaultTransport: "local"
});
const result = await runModelRun({
prompt,
agent: resolveCapabilityAgentOption(command, opts.agent),
files: opts.file,
model: opts.model,
thinking,
transport
});
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, formatEnvelopeForText);
});
});
model.command("list").description("List known models").option("--json", "Output JSON", false).action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await loadModelCatalogForInspection(getRuntimeConfig());
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, providerSummaryText);
});
});
model.command("inspect").description("Inspect one model catalog entry").requiredOption("--model <provider/model>", "Model id").option("--json", "Output JSON", false).action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const target = normalizeStringifiedOptionalString(opts.model) ?? "";
const catalog = await loadModelCatalogForInspection(getRuntimeConfig());
const entry = catalog.find((candidate) => `${candidate.provider}/${candidate.id}` === target) ?? catalog.find((candidate) => candidate.id === target);
if (!entry) throw new Error(`Model not found: ${target}`);
emitJsonOrText(defaultRuntime, Boolean(opts.json), entry, (value) => JSON.stringify(value, null, 2));
});
});
model.command("providers").description("List model providers from the catalog").option("--agent <id>", "Agent whose provider state should be inspected").option("--json", "Output JSON", false).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await buildModelProviders(resolveCapabilityAgentOption(command, opts.agent));
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, providerSummaryText);
});
});
const modelAuth = model.command("auth").description("Provider auth helpers").option("--agent <id>", "Agent id (default: configured default agent)");
const resolveModelAuthAgent = (command, rawAgentId, surface) => resolveCapabilityProviderAgentId(getRuntimeConfig(), resolveCapabilityAgentOption(command, rawAgentId), surface);
modelAuth.command("login").description("Run provider auth login").requiredOption("--provider <id>", "Provider id").option("--method <id>", "Provider auth method id").option("--agent <id>", "Agent id (default: configured default agent)").action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const agent = resolveModelAuthAgent(command, opts.agent, "infer model auth login");
const { modelsAuthLoginCommand } = await import("./auth-TFrZM2mA.js");
await modelsAuthLoginCommand({
provider: String(opts.provider),
method: opts.method ? String(opts.method) : void 0,
agent
}, defaultRuntime);
});
});
modelAuth.command("logout").description("Remove saved auth profiles for one provider").requiredOption("--provider <id>", "Provider id").option("--agent <id>", "Agent id (default: agents.defaults.systemAgent.agentId, then the sole agent)").option("--json", "Output JSON", false).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runModelAuthLogout(String(opts.provider), resolveModelAuthAgent(command, opts.agent, "infer model auth logout"));
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => JSON.stringify(value, null, 2));
});
});
modelAuth.command("status").description("Show configured auth state").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Output JSON", false).action(async (opts, command) => {
await runCommandWithRuntime(defaultRuntime, async () => {
const result = await runModelAuthStatus(resolveModelAuthAgent(command, opts.agent, "infer model auth status"));
emitJsonOrText(defaultRuntime, Boolean(opts.json), result, (value) => JSON.stringify(value, null, 2));
});
});
}
//#endregion
export { registerModelCapabilityCommands };