UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

565 lines (564 loc) 23 kB
import { o as coerceSecretRef } from "../../types.secrets-_0JOMGE5.js"; import { l as collectConfiguredModelRefValues } from "../../channel-env-var-names-B6Cczraa.js"; import { n as isWSL2Sync } from "../../wsl-Bb_g7JpO.js"; import { d as isNonSecretApiKeyMarker } from "../../model-auth-markers-Sq8HdQFX.js"; import { r as describeImagesWithModel, t as describeImageWithModel } from "../../image-runtime-CmS6zMcE.js"; import "../../media-understanding-1KBZ_bnJ.js"; import "../../runtime-env-D_2q8-VK.js"; import { a as buildOpenAICompatibleReplayPolicy } from "../../provider-replay-helpers-BqlBAD9R.js"; import { t as definePluginEntry } from "../../plugin-entry-C7DUzV0e.js"; import { r as OPENAI_COMPATIBLE_REPLAY_HOOKS } from "../../provider-model-shared-D-lyTLvb.js"; import { n as buildApiKeyCredential } from "../../provider-auth-helpers-BgLOn-Ws.js"; import { t as createProviderApiKeyAuthMethod } from "../../provider-api-key-auth-VMMQYUA9.js"; import "../../provider-auth-DAOC_qI9.js"; import { r as resolvePluginConfigObject } from "../../plugin-config-runtime-CpE6DYgJ.js"; import "../../provider-auth-api-key-nSdfYNnB.js"; import { i as OLLAMA_DEFAULT_BASE_URL, n as OLLAMA_CLOUD_DEFAULT_MODELS, r as OLLAMA_CLOUD_PROVIDER_ID, t as OLLAMA_CLOUD_BASE_URL } from "../../defaults-CPlCYE-I.js"; import { i as buildOllamaProvider, l as queryOllamaModelShowInfo, r as buildOllamaModelDefinition, t as readProviderBaseUrl } from "../../provider-base-url-BEd2ujfU.js"; import { i as promptAndConfigureOllama, n as configureOllamaNonInteractive, r as ensureOllamaModelPulled } from "../../setup-7F0SySva.js"; import { d as resolveConfiguredOllamaProviderConfig, o as createConfiguredOllamaCompatStreamWrapper, s as createConfiguredOllamaStreamFn } from "../../stream-6ngdagUd.js"; import "../../api-CMrPS6tR.js"; import { n as resolveThinkingProfile } from "../../provider-policy-api-BnGfXpYt.js"; import { a as shouldUseSyntheticOllamaAuth, i as resolveOllamaDiscoveryResult, n as OLLAMA_PROVIDER_ID, r as isLocalOllamaBaseUrl, t as OLLAMA_DEFAULT_API_KEY } from "../../discovery-shared-NJabet2N.js"; import { n as createOllamaEmbeddingProvider, t as DEFAULT_OLLAMA_EMBEDDING_MODEL } from "../../embedding-provider-qPw9Sdhf.js"; import { t as createOllamaWebSearchProvider } from "../../web-search-provider-D-Z60QxN.js"; import { access } from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; //#region extensions/ollama/src/media-understanding-provider.ts const ollamaMediaUnderstandingProvider = { id: OLLAMA_PROVIDER_ID, capabilities: ["image"], describeImage: describeImageWithModel, describeImages: describeImagesWithModel }; //#endregion //#region extensions/ollama/src/memory-embedding-adapter.ts const ollamaMemoryEmbeddingProviderAdapter = { id: "ollama", defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL, transport: "remote", authProviderId: "ollama", create: async (options) => { const { provider, client } = await createOllamaEmbeddingProvider({ ...options, provider: "ollama", fallback: "none" }); return { provider, runtime: { id: "ollama", inlineBatchTimeoutMs: 10 * 6e4, cacheKeyData: { provider: "ollama", model: client.model } } }; } }; //#endregion //#region extensions/ollama/src/wsl2-crash-loop-check.ts const execFileAsync = promisify(execFile); const SYSTEMCTL_TIMEOUT_MS = 5e3; const WSL_CUDA_MARKERS = [ "/dev/dxg", "/usr/lib/wsl/lib/nvidia-smi", "/usr/lib/wsl/lib/libcuda.so.1", "/usr/local/cuda" ]; function parseSystemctlShowProperties(stdout) { const properties = /* @__PURE__ */ new Map(); for (const line of stdout.split(/\r?\n/u)) { const separator = line.indexOf("="); if (separator <= 0) continue; properties.set(line.slice(0, separator), line.slice(separator + 1)); } return properties; } async function isOllamaEnabledWithRestartAlways() { try { const { stdout } = await execFileAsync("systemctl", [ "show", "ollama.service", "--property=UnitFileState,Restart", "--no-pager" ], { timeout: SYSTEMCTL_TIMEOUT_MS }); const properties = parseSystemctlShowProperties(stdout); return properties.get("UnitFileState") === "enabled" && properties.get("Restart") === "always"; } catch { return false; } } async function hasWslCuda() { for (const marker of WSL_CUDA_MARKERS) try { await access(marker); return true; } catch {} return false; } async function checkWsl2CrashLoopRisk(logger) { try { if (!isWSL2Sync()) return; if (!await isOllamaEnabledWithRestartAlways()) return; if (!await hasWslCuda()) return; logger.warn([ "[ollama] WSL2 crash-loop risk: ollama.service is enabled with Restart=always and CUDA is visible.", "On WSL2, GPU-backed Ollama can pin host memory while loading a model.", "Hyper-V memory reclaim cannot always reclaim those pinned pages, so Windows can terminate and restart the WSL2 VM.", "", "Common evidence: repeated WSL2 reboots, high CPU in app.slice at startup, and SIGTERM from systemd rather than the Linux OOM killer.", "See: https://github.com/ollama/ollama/issues/11317", "", "Mitigation:", " 1. Disable autostart: sudo systemctl disable ollama", " 2. Add [experimental] autoMemoryReclaim=disabled to %USERPROFILE%\\.wslconfig on Windows, then run wsl --shutdown", " 3. Set OLLAMA_KEEP_ALIVE=5m in the Ollama service environment or start ollama serve manually when needed" ].join("\n")); } catch {} } //#endregion //#region extensions/ollama/index.ts function buildNativeOllamaReplayPolicy() { return { ...buildOpenAICompatibleReplayPolicy("openai-completions", { sanitizeToolCallIds: false }), sanitizeToolCallIds: false }; } const dynamicModelCache = /* @__PURE__ */ new Map(); const OLLAMA_CLOUD_DEFAULT_MODEL_REF = `${OLLAMA_CLOUD_PROVIDER_ID}/${OLLAMA_CLOUD_DEFAULT_MODELS[0]}`; const OLLAMA_CONFIGURED_SHOW_CONCURRENCY = 4; const OLLAMA_CONFIGURED_SHOW_MAX_MODELS = 8; const OLLAMA_API_KEY_ENV_REF_RE = /^[A-Z_][A-Z0-9_]*$/u; function buildDynamicCacheKey(provider, baseUrl) { return `${provider}\0${baseUrl ?? ""}`; } function hasOllamaDiscoverySignal(providerConfig) { return Boolean(process.env.OLLAMA_API_KEY?.trim()) || shouldUseSyntheticOllamaAuth(providerConfig) || Boolean(providerConfig?.apiKey); } function toDynamicOllamaModel(params) { const input = (params.model.input ?? ["text"]).filter((value) => value === "text" || value === "image"); return { id: params.model.id, name: params.model.name ?? params.model.id, provider: params.provider, api: "ollama", baseUrl: readProviderBaseUrl(params.providerConfig) ?? "", reasoning: params.model.reasoning ?? false, input: input.length > 0 ? input : ["text"], cost: params.model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: params.model.contextWindow ?? 8192, maxTokens: params.model.maxTokens ?? 8192, ...params.model.compat ? { compat: params.model.compat } : {}, ...params.model.params ? { params: params.model.params } : {} }; } function stripTrailingAuthProfile(raw) { const trimmed = raw.trim(); const lastSlash = trimmed.lastIndexOf("/"); let delimiter = trimmed.indexOf("@", lastSlash + 1); if (delimiter <= 0) return trimmed; const suffix = () => trimmed.slice(delimiter + 1); if (/^\d{8}(?:@|$)/.test(suffix())) { const next = trimmed.indexOf("@", delimiter + 9); if (next < 0) return trimmed; delimiter = next; } if (/^(?:i?q\d+(?:_[a-z0-9]+)*|\d+bit)(?:@|$)/i.test(suffix())) { const next = trimmed.indexOf("@", delimiter + 1); if (next < 0) return trimmed; delimiter = next; } const model = trimmed.slice(0, delimiter).trim(); const profile = trimmed.slice(delimiter + 1).trim(); return model && profile ? model : trimmed; } function needsOllamaCatalogMetadata(entry) { return !(entry.contextWindow !== void 0 || entry.contextTokens !== void 0) || entry.reasoning === void 0 || entry.input === void 0 || entry.compat === void 0; } function readConfiguredOllamaApiKey(value) { if (typeof value === "string") return value.trim() || void 0; if (value && typeof value === "object" && "value" in value) { const resolved = value.value; if (typeof resolved === "string") return resolved.trim() || void 0; } } function readConcreteOllamaApiKey(value) { if (coerceSecretRef(value)) return; const apiKey = readConfiguredOllamaApiKey(value); return apiKey && !isNonSecretApiKeyMarker(apiKey) ? apiKey : void 0; } function readEnvBackedOllamaApiKey(value, env) { const ref = coerceSecretRef(value); if (ref?.source === "env") return readConcreteOllamaApiKey(env[ref.id.trim()]); const apiKey = readConfiguredOllamaApiKey(value); return apiKey && OLLAMA_API_KEY_ENV_REF_RE.test(apiKey) ? readConcreteOllamaApiKey(env[apiKey]) : void 0; } function isAmbientOllamaApiKeyMarker(value) { return value === "ollama-local" || value === "OLLAMA_API_KEY"; } function readUsableOllamaShowApiKey(params) { const explicitEnvApiKey = readEnvBackedOllamaApiKey(params.explicitApiKey, params.env); if (explicitEnvApiKey) return explicitEnvApiKey; const explicitApiKey = readConcreteOllamaApiKey(params.explicitApiKey); if (explicitApiKey) return explicitApiKey; const resolvedApiKey = readConfiguredOllamaApiKey(params.resolved?.apiKey); const canUseResolvedDiscovery = params.allowAmbientEnvFallback || !isAmbientOllamaApiKeyMarker(resolvedApiKey); const discoveryApiKey = readConcreteOllamaApiKey(params.resolved?.discoveryApiKey); if (discoveryApiKey && canUseResolvedDiscovery) return discoveryApiKey; const resolvedEnvApiKey = readEnvBackedOllamaApiKey(params.resolved?.apiKey, params.env); if (resolvedEnvApiKey && canUseResolvedDiscovery) return resolvedEnvApiKey; const apiKey = readConcreteOllamaApiKey(params.resolved?.apiKey); if (apiKey && !OLLAMA_API_KEY_ENV_REF_RE.test(apiKey)) return apiKey; return params.allowAmbientEnvFallback ? readConcreteOllamaApiKey(params.env.OLLAMA_API_KEY) : void 0; } function collectConfiguredOllamaModelIds(params) { const providerPrefix = `${params.provider.toLowerCase()}/`; const models = /* @__PURE__ */ new Map(); const addModelId = (modelId, api, name) => { const trimmed = modelId.trim(); if (!trimmed || trimmed === "*") return; const trimmedName = typeof name === "string" ? name.trim() : ""; const existing = models.get(trimmed); if (existing) { if (!existing.api && api || !existing.name && trimmedName) models.set(trimmed, { ...existing, ...api && !existing.api ? { api } : {}, ...trimmedName && !existing.name ? { name: trimmedName } : {} }); return; } models.set(trimmed, { id: trimmed, ...api ? { api } : {}, ...trimmedName ? { name: trimmedName } : {} }); }; const addRef = (raw) => { if (typeof raw !== "string") return; const trimmed = stripTrailingAuthProfile(raw); if (!trimmed.toLowerCase().startsWith(providerPrefix)) return; addModelId(trimmed.slice(providerPrefix.length).trim()); }; for (const ref of collectConfiguredModelRefValues(params.config)) addRef(ref); for (const entry of params.entries ?? []) if (entry.provider.toLowerCase() === params.provider.toLowerCase() && entry.id.trim() && needsOllamaCatalogMetadata(entry)) addModelId(entry.id.trim(), entry.api, entry.name); return [...models.values()]; } function buildStaticOllamaCloudProvider() { return { baseUrl: OLLAMA_CLOUD_BASE_URL, api: "ollama", models: OLLAMA_CLOUD_DEFAULT_MODELS.map((model) => buildOllamaModelDefinition(model)) }; } async function buildOllamaCloudProvider() { const discovered = await buildOllamaProvider(OLLAMA_CLOUD_BASE_URL, { quiet: true }); return discovered.models?.length ? discovered : buildStaticOllamaCloudProvider(); } async function resolveRequestedDynamicOllamaModel(params) { const showBaseUrl = readProviderBaseUrl(params.providerConfig) ?? "http://127.0.0.1:11434"; const showInfo = params.showApiKey ? await queryOllamaModelShowInfo(showBaseUrl, params.modelId, { apiKey: params.showApiKey }) : await queryOllamaModelShowInfo(showBaseUrl, params.modelId); if (typeof showInfo.contextWindow !== "number" && (showInfo.capabilities?.length ?? 0) === 0) return; return toDynamicOllamaModel({ provider: params.provider, providerConfig: params.providerConfig, model: buildOllamaModelDefinition(params.modelId, showInfo.contextWindow, showInfo.capabilities) }); } async function augmentConfiguredOllamaCatalogModels(params) { const models = collectConfiguredOllamaModelIds({ config: params.config, provider: params.provider, entries: params.entries }); if (models.length === 0) return []; const configuredProvider = resolveConfiguredOllamaProviderConfig({ config: params.config, providerId: params.provider }); const baseUrl = readProviderBaseUrl(configuredProvider) ?? params.defaultBaseUrl; const isLocalBaseUrl = isLocalOllamaBaseUrl(baseUrl); const showApiKey = readUsableOllamaShowApiKey({ env: params.env, allowAmbientEnvFallback: !isLocalBaseUrl, explicitApiKey: readConfiguredOllamaApiKey(configuredProvider?.apiKey), resolved: params.resolveProviderApiKey?.(params.provider) }); if (!isLocalBaseUrl && !showApiKey) return []; const providerConfig = { ...configuredProvider, models: configuredProvider?.models ?? [], baseUrl, api: configuredProvider?.api ?? "ollama" }; const entries = []; const modelsToProbe = models.slice(0, OLLAMA_CONFIGURED_SHOW_MAX_MODELS); for (let index = 0; index < modelsToProbe.length; index += OLLAMA_CONFIGURED_SHOW_CONCURRENCY) { const batch = modelsToProbe.slice(index, index + OLLAMA_CONFIGURED_SHOW_CONCURRENCY); const rows = await Promise.all(batch.map(async (model) => { const requested = await resolveRequestedDynamicOllamaModel({ provider: params.provider, providerConfig, modelId: model.id, showApiKey }); return requested ? { id: requested.id, name: model.name ?? requested.name, provider: requested.provider, api: model.api ?? providerConfig.api, reasoning: requested.reasoning, input: requested.input, contextWindow: requested.contextWindow, compat: requested.compat } : void 0; })); for (const row of rows) if (row) entries.push(row); } return entries; } var ollama_default = definePluginEntry({ id: "ollama", name: "Ollama Provider", description: "Bundled Ollama provider plugin", register(api) { if (api.registrationMode === "full") checkWsl2CrashLoopRisk(api.logger); api.registerMemoryEmbeddingProvider(ollamaMemoryEmbeddingProviderAdapter); api.registerMediaUnderstandingProvider(ollamaMediaUnderstandingProvider); const startupPluginConfig = api.pluginConfig ?? {}; const resolveCurrentPluginConfig = (config) => { const runtimePluginConfig = resolvePluginConfigObject(config, "ollama"); if (runtimePluginConfig) return runtimePluginConfig; return config ? {} : startupPluginConfig; }; api.registerWebSearchProvider(createOllamaWebSearchProvider()); api.registerProvider({ id: OLLAMA_CLOUD_PROVIDER_ID, label: "Ollama Cloud", docsPath: "/providers/ollama", envVars: ["OLLAMA_API_KEY"], auth: [createProviderApiKeyAuthMethod({ providerId: OLLAMA_CLOUD_PROVIDER_ID, methodId: "api-key", label: "Ollama Cloud API key", hint: "Hosted models via ollama.com", optionKey: "ollamaCloudApiKey", flagName: "--ollama-cloud-api-key", envVar: "OLLAMA_API_KEY", promptMessage: "Enter Ollama Cloud API key", defaultModel: OLLAMA_CLOUD_DEFAULT_MODEL_REF, noteTitle: "Ollama Cloud", noteMessage: "Manage API keys at https://ollama.com/settings/keys", wizard: { choiceId: "ollama-cloud", choiceLabel: "Ollama Cloud", choiceHint: "Hosted models via ollama.com", groupId: "ollama", groupLabel: "Ollama", groupHint: "Cloud and local open models" } })], catalog: { order: "simple", run: async (ctx) => { const apiKey = ctx.resolveProviderApiKey(OLLAMA_CLOUD_PROVIDER_ID).apiKey; if (!apiKey) return null; return { provider: { ...await buildOllamaCloudProvider(), apiKey } }; } }, staticCatalog: { order: "simple", run: async () => ({ provider: buildStaticOllamaCloudProvider() }) }, createStreamFn: ({ config, model, provider }) => { return createConfiguredOllamaStreamFn({ model, providerBaseUrl: readProviderBaseUrl(resolveConfiguredOllamaProviderConfig({ config, providerId: provider })) ?? "https://ollama.com" }); }, ...OPENAI_COMPATIBLE_REPLAY_HOOKS, buildReplayPolicy: (ctx) => ctx.modelApi === "ollama" ? buildNativeOllamaReplayPolicy() : buildOpenAICompatibleReplayPolicy(ctx.modelApi), resolveReasoningOutputMode: () => "native", resolveThinkingProfile, wrapStreamFn: createConfiguredOllamaCompatStreamWrapper, augmentModelCatalog: async (ctx) => await augmentConfiguredOllamaCatalogModels({ config: ctx.config, defaultBaseUrl: OLLAMA_CLOUD_BASE_URL, env: ctx.env, provider: OLLAMA_CLOUD_PROVIDER_ID, entries: ctx.entries, resolveProviderApiKey: ctx.resolveProviderApiKey }), matchesContextOverflowError: ({ errorMessage }) => /\bollama\b.*(?:context length|too many tokens|context window)/i.test(errorMessage) || /\btruncating input\b.*\btoo long\b/i.test(errorMessage), buildUnknownModelHint: () => "Ollama Cloud requires an API key. Set OLLAMA_API_KEY or run \"openclaw onboard --auth-choice ollama-cloud\". See: https://docs.openclaw.ai/providers/ollama" }); api.registerProvider({ id: OLLAMA_PROVIDER_ID, label: "Ollama", docsPath: "/providers/ollama", envVars: ["OLLAMA_API_KEY"], auth: [{ id: "local", label: "Ollama", hint: "Cloud and local open models", kind: "custom", run: async (ctx) => { const result = await promptAndConfigureOllama({ cfg: ctx.config, env: ctx.env, opts: ctx.opts, prompter: ctx.prompter, secretInputMode: ctx.secretInputMode, allowSecretRefPrompt: ctx.allowSecretRefPrompt }); return { profiles: [{ profileId: "ollama:default", credential: buildApiKeyCredential(OLLAMA_PROVIDER_ID, result.credential, void 0, result.credentialMode ? { secretInputMode: result.credentialMode, config: ctx.config } : void 0) }], configPatch: result.config }; }, runNonInteractive: async (ctx) => { return await configureOllamaNonInteractive({ nextConfig: ctx.config, opts: { customBaseUrl: ctx.opts.customBaseUrl, customModelId: ctx.opts.customModelId }, runtime: ctx.runtime, agentDir: ctx.agentDir }); } }], catalog: { order: "late", run: async (ctx) => await resolveOllamaDiscoveryResult({ ctx, pluginConfig: resolveCurrentPluginConfig(ctx.config), buildProvider: buildOllamaProvider }) }, wizard: { setup: { choiceId: "ollama", choiceLabel: "Ollama", choiceHint: "Cloud and local open models", groupId: "ollama", groupLabel: "Ollama", groupHint: "Cloud and local open models", methodId: "local", modelSelection: { promptWhenAuthChoiceProvided: true, allowKeepCurrent: false } }, modelPicker: { label: "Ollama (custom)", hint: "Detect models from a local or remote Ollama instance", methodId: "local" } }, onModelSelected: async ({ config, model, prompter }) => { if (!model.startsWith("ollama/")) return; await ensureOllamaModelPulled({ config, model, prompter }); }, createStreamFn: ({ config, model, provider }) => { return createConfiguredOllamaStreamFn({ model, providerBaseUrl: readProviderBaseUrl(resolveConfiguredOllamaProviderConfig({ config, providerId: provider })) }); }, ...OPENAI_COMPATIBLE_REPLAY_HOOKS, buildReplayPolicy: (ctx) => ctx.modelApi === "ollama" ? buildNativeOllamaReplayPolicy() : buildOpenAICompatibleReplayPolicy(ctx.modelApi), resolveReasoningOutputMode: () => "native", resolveThinkingProfile, wrapStreamFn: createConfiguredOllamaCompatStreamWrapper, augmentModelCatalog: async (ctx) => await augmentConfiguredOllamaCatalogModels({ config: ctx.config, defaultBaseUrl: OLLAMA_DEFAULT_BASE_URL, env: ctx.env, provider: OLLAMA_PROVIDER_ID, entries: ctx.entries, resolveProviderApiKey: ctx.resolveProviderApiKey }), createEmbeddingProvider: async ({ config, model, provider: embeddingProvider, remote }) => { const { provider, client } = await createOllamaEmbeddingProvider({ config, remote, model: model || "nomic-embed-text", provider: embeddingProvider || "ollama" }); return { ...provider, client }; }, matchesContextOverflowError: ({ errorMessage }) => /\bollama\b.*(?:context length|too many tokens|context window)/i.test(errorMessage) || /\btruncating input\b.*\btoo long\b/i.test(errorMessage), resolveSyntheticAuth: ({ provider, providerConfig }) => { if (!shouldUseSyntheticOllamaAuth(providerConfig)) return; return { apiKey: OLLAMA_DEFAULT_API_KEY, source: `models.providers.${provider ?? "ollama"} (synthetic local key)`, mode: "api-key" }; }, shouldDeferSyntheticProfileAuth: ({ resolvedApiKey }) => resolvedApiKey?.trim() === OLLAMA_DEFAULT_API_KEY, prepareDynamicModel: async (ctx) => { const providerConfig = resolveConfiguredOllamaProviderConfig({ config: ctx.config, providerId: ctx.provider }); if (!hasOllamaDiscoverySignal(providerConfig)) return; const baseUrl = readProviderBaseUrl(providerConfig); const provider = await buildOllamaProvider(baseUrl, { quiet: true }); const dynamicModels = (provider.models ?? []).map((model) => toDynamicOllamaModel({ provider: ctx.provider, providerConfig: provider, model })); if (!dynamicModels.some((model) => model.id === ctx.modelId)) { const requestedModel = await resolveRequestedDynamicOllamaModel({ provider: ctx.provider, providerConfig: provider, modelId: ctx.modelId }); if (requestedModel) dynamicModels.push(requestedModel); } dynamicModelCache.set(buildDynamicCacheKey(ctx.provider, baseUrl), dynamicModels); }, resolveDynamicModel: (ctx) => { const providerConfig = resolveConfiguredOllamaProviderConfig({ config: ctx.config, providerId: ctx.provider }); return dynamicModelCache.get(buildDynamicCacheKey(ctx.provider, readProviderBaseUrl(providerConfig)))?.find((model) => model.id === ctx.modelId); }, buildUnknownModelHint: () => "Ollama requires authentication to be registered as a provider. Set OLLAMA_API_KEY=\"ollama-local\" (any value works) or run \"openclaw configure\". See: https://docs.openclaw.ai/providers/ollama" }); } }); //#endregion export { ollama_default as default };