openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
370 lines (369 loc) • 17.9 kB
JavaScript
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-6kR-86XC.mjs";
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { n as captureAsyncWorkTracker, t as AsyncWorkScope } from "./async-work-scope-BYeX3gh6.mjs";
import { r as defaultRuntime } from "./runtime-BC29JSZp.mjs";
import { r as DEFAULT_PROVIDER } from "./defaults-BbU4k6fu.mjs";
import { n as THINKING_LEVELS_HELP, s as normalizeThinkLevel } from "./thinking.shared-Dc3B6FDH.mjs";
import { t as ADMIN_SCOPE } from "./operator-scopes-D-CL26h0.mjs";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-S_xwnr2o.mjs";
import { d as normalizeMimeType, n as detectMime } from "./mime-CGDYrRzL.mjs";
import { n as runCommandWithRuntime } from "./cli-utils-DtS5_tuC.mjs";
import { n as formatEnvelopeForText, r as providerSummaryText } from "./output-BaRjhBNO.mjs";
import { n as runCapabilityCommand, t as registerLocalProvidersCommand } from "./providers-command-Df3zMn81.mjs";
import { t as collectOption } from "./helpers-Bd2A8dod.mjs";
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, rawAgentId) {
const { resolveCapabilityProviderAgentId } = await import("./shared-DNArGz-H.mjs");
const { readPreparedModelCatalog } = await import("./prepared-model-catalog-BtACSAo5.mjs");
return (await readPreparedModelCatalog({
config: cfg,
agentId: rawAgentId === void 0 ? void 0 : resolveCapabilityProviderAgentId(cfg, rawAgentId),
readOnly: true
})).toSorted((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
}
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.`);
const isHeic = HEIC_MODEL_RUN_MIMES.has(mimeType);
const imageBuffer = isHeic ? await (await import("./media-services-B3jczFfr.mjs")).convertHeicToJpeg(buffer) : buffer;
return {
path: resolvedPath,
fileName: path.basename(resolvedPath),
mimeType: isHeic ? "image/jpeg" : mimeType,
data: imageBuffer.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: ${THINKING_LEVELS_HELP}.`);
return normalized;
}
async function runModelRun(params) {
const { requireProviderModelOverride, resolveCapabilityProviderAgentId, resolveLocalCapabilityRuntimeConfig } = await import("./shared-DNArGz-H.mjs");
const { getModelsCommandSecretTargetIds } = await import("./command-secret-targets-yt2w4Rmy.mjs");
const { getRuntimeConfig } = await import("./config/config.js");
const { canonicalizeCaseOnlyCatalogModelRef } = await import("./model-selection-Ck2tM34M.mjs");
const { readPreparedModelCatalog } = await import("./prepared-model-catalog-BtACSAo5.mjs");
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 canonicalizeCaseOnlyCatalogModelRef({
raw: params.model,
cfg,
defaultProvider: DEFAULT_PROVIDER,
loadCatalog: () => readPreparedModelCatalog({
config: cfg,
agentId,
readOnly: true
}),
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 { acquireSimpleCompletionModelForAgent, completeWithPreparedSimpleCompletionModel } = await import("./simple-completion-runtime-UVGS8QYt.mjs");
const callerResult = createDeferredCore();
captureAsyncWorkTracker()(async () => {
const { prepareLocalCapabilityAccountSecrets } = await import("./local-account-secrets-7kSt8jF4.mjs");
await prepareLocalCapabilityAccountSecrets({
cfg,
agentId
});
const prepared = await acquireSimpleCompletionModelForAgent({
cfg,
agentId,
modelRef,
allowMissingApiKeyModes: ["aws-sdk"],
...hasExplicitProviderModelOverride ? { allowBundledStaticCatalogFallback: true } : {},
skipAgentDiscovery: true
});
if ("error" in prepared) throw new Error(prepared.error);
const work = new AsyncWorkScope();
try {
callerResult.resolve(await work.track(async () => {
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
}]
};
}));
} catch (error) {
callerResult.reject(error);
} finally {
await work.drain();
await prepared[Symbol.asyncDispose]();
}
}).catch((error) => callerResult.reject(error));
return await callerResult.promise;
}
const { buildExplicitSessionIdSessionKey } = await import("./session-CecajqB3.mjs");
const { callGateway, randomIdempotencyKey } = await import("./call-CZyV4q-u.mjs");
const { provider, model } = requireProviderModelOverride(modelRef) ?? {};
const hasModelOverride = Boolean(provider || model);
const sessionId = `model-run-${randomUUID()}`;
const response = await callGateway({
method: "agent",
params: {
agentId,
sessionId,
sessionKey: buildExplicitSessionIdSessionKey({
agentId,
sessionId
}),
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(cfg, agentId) {
const { providerHasGenericConfig, resolveSelectedProviderFromModelRef } = await import("./shared-DNArGz-H.mjs");
const { resolveAgentEffectiveModelPrimary } = await import("./agent-scope-DiYWAtUA.mjs");
const { getProviderEnvVarsCore } = await import("./provider-env-vars-DZVSKcdb.mjs");
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: getProviderEnvVarsCore(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-CJG6Idr2.mjs");
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 { getRuntimeConfig } = await import("./config/config.js");
const { resolveAgentDir } = await import("./agent-scope-DiYWAtUA.mjs");
const { listProfilesForProvider, loadAuthProfileStoreForRuntime } = await import("./auth-profiles-CphQlG7L.mjs");
const { updateAuthProfileStoreWithLock } = await import("./store-runtime-DzeIDOuC.mjs");
const agentDir = resolveAgentDir(getRuntimeConfig(), agent);
const profileIds = listProfilesForProvider(loadAuthProfileStoreForRuntime(agentDir), 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((opts, command) => runCapabilityCommand(opts.json, formatEnvelopeForText, async () => {
const { resolveCapabilityAgentOption, resolveTransport } = await import("./shared-DNArGz-H.mjs");
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"
});
return runModelRun({
prompt,
agent: resolveCapabilityAgentOption(command, opts.agent),
files: opts.file,
model: opts.model,
thinking,
transport
});
}));
model.command("list").description("List known models").option("--json", "Output JSON", false).action((opts, command) => runCapabilityCommand(opts.json, providerSummaryText, async () => {
const { resolveCapabilityAgentOption } = await import("./shared-DNArGz-H.mjs");
const { getRuntimeConfig } = await import("./config/config.js");
return loadModelCatalogForInspection(getRuntimeConfig(), resolveCapabilityAgentOption(command, opts.agent));
}));
model.command("inspect").description("Inspect one model catalog entry").requiredOption("--model <provider/model>", "Model id").option("--json", "Output JSON", false).action((opts, command) => runCapabilityCommand(opts.json, void 0, async () => {
const { resolveCapabilityAgentOption } = await import("./shared-DNArGz-H.mjs");
const { getRuntimeConfig } = await import("./config/config.js");
const target = normalizeStringifiedOptionalString(opts.model) ?? "";
const catalog = await loadModelCatalogForInspection(getRuntimeConfig(), resolveCapabilityAgentOption(command, opts.agent));
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}`);
return entry;
}));
registerLocalProvidersCommand(model, "List model providers from the catalog", buildModelProviders, providerSummaryText);
const modelAuth = model.command("auth").description("Provider auth helpers").option("--agent <id>", "Agent id (default: configured default agent)");
const resolveModelAuthAgent = async (command, rawAgentId, surface) => {
const { resolveCapabilityProviderAgentId, resolveCapabilityAgentOption } = await import("./shared-DNArGz-H.mjs");
const { getRuntimeConfig } = await import("./config/config.js");
return 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 = await resolveModelAuthAgent(command, opts.agent, "infer model auth login");
const { modelsAuthLoginCommand } = await import("./auth-C6553obz.mjs");
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((opts, command) => runCapabilityCommand(opts.json, void 0, async () => {
return runModelAuthLogout(String(opts.provider), await resolveModelAuthAgent(command, opts.agent, "infer model auth logout"));
}));
modelAuth.command("status").description("Show configured auth state").option("--agent <id>", "Agent id (default: configured default agent)").option("--json", "Output JSON", false).action((opts, command) => runCapabilityCommand(opts.json, void 0, async () => {
return runModelAuthStatus(await resolveModelAuthAgent(command, opts.agent, "infer model auth status"));
}));
}
//#endregion
export { registerModelCapabilityCommands };