UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

414 lines (413 loc) • 17.7 kB
import { a as LMSTUDIO_DEFAULT_BASE_URL, c as LMSTUDIO_PROVIDER_ID, i as LMSTUDIO_DEFAULT_API_KEY_ENV_VAR, n as resolveLmstudioProviderAuthMode, r as shouldUseLmstudioApiKeyPlaceholder, s as LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER, t as hasLmstudioAuthorizationHeader } from "./provider-auth-Ci3Zo3Cs.mjs"; import { isKnownEnvApiKeyMarker, isNonSecretApiKeyMarker, normalizeApiKeyConfig, normalizeOptionalSecretInput } from "openclaw/plugin-sdk/provider-auth"; import { asPositiveSafeInteger, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeTrimmedStringList, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; import "openclaw/plugin-sdk/provider-auth-runtime"; import { LiveModelCatalogHttpError, runLiveProviderCatalog } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import "openclaw/plugin-sdk/provider-model-shared"; import "openclaw/plugin-sdk/provider-onboard"; import { SELF_HOSTED_DEFAULT_CONTEXT_WINDOW, SELF_HOSTED_DEFAULT_COST, SELF_HOSTED_DEFAULT_MAX_TOKENS } from "openclaw/plugin-sdk/provider-setup"; import "openclaw/plugin-sdk/runtime-env"; import "openclaw/plugin-sdk/setup"; import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core"; import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http"; import "openclaw/plugin-sdk/response-limit-runtime"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime"; import "openclaw/plugin-sdk/setup-tools"; //#region extensions/lmstudio/src/model-reasoning.ts const LMSTUDIO_OPENAI_COMPAT_ENABLED_REASONING_EFFORTS = [ "minimal", "low", "medium", "high", "xhigh" ]; const LMSTUDIO_OPENAI_COMPAT_REASONING_EFFORTS = ["none", ...LMSTUDIO_OPENAI_COMPAT_ENABLED_REASONING_EFFORTS]; function buildLmstudioReasoningEffortMap(supportedReasoningEfforts) { const disabled = supportedReasoningEfforts.includes("none") ? "none" : void 0; const max = LMSTUDIO_OPENAI_COMPAT_ENABLED_REASONING_EFFORTS.findLast((option) => supportedReasoningEfforts.includes(option)) ?? supportedReasoningEfforts.find((option) => option !== "none"); const map = { ...disabled ? { off: disabled, none: disabled } : {}, ...max ? { adaptive: max, max } : {} }; return Object.keys(map).length > 0 ? map : void 0; } //#endregion //#region extensions/lmstudio/src/models.ts function normalizeReasoningOptions(value) { return uniqueStrings(normalizeTrimmedStringList(value).map((option) => option.toLowerCase())); } function isLmstudioBinaryReasoningOptions(allowedOptions) { return allowedOptions.some((option) => option === "on") && allowedOptions.every((option) => option === "on" || option === "off"); } function resolveLmstudioTransportReasoningEfforts(allowedOptions) { if (isLmstudioBinaryReasoningOptions(allowedOptions)) return allowedOptions.includes("off") ? [...LMSTUDIO_OPENAI_COMPAT_REASONING_EFFORTS] : [...LMSTUDIO_OPENAI_COMPAT_ENABLED_REASONING_EFFORTS]; return uniqueStrings(allowedOptions.map((option) => option === "off" ? "none" : option).filter((option) => option !== "on")); } function resolveLmstudioReasoningCompat(entry) { const supportedReasoningEfforts = resolveLmstudioTransportReasoningEfforts(normalizeReasoningOptions(entry.capabilities?.reasoning?.allowed_options)); if (!supportedReasoningEfforts.some((option) => option !== "none")) return; return { supportsReasoningEffort: true, supportedReasoningEfforts, reasoningEffortMap: buildLmstudioReasoningEffortMap(supportedReasoningEfforts) }; } function resolveLmstudioReasoningCapability(entry) { const reasoning = entry.capabilities?.reasoning; if (reasoning === void 0 || reasoning === null) return false; const allowedOptions = normalizeReasoningOptions(reasoning.allowed_options); if (allowedOptions.length > 0) return allowedOptions.some((option) => option !== "off"); const defaultOption = normalizeOptionalLowercaseString(reasoning.default); return defaultOption !== void 0 && defaultOption !== "off"; } function resolveLoadedContextWindow(entry) { const loadedInstances = Array.isArray(entry.loaded_instances) ? entry.loaded_instances : []; let contextWindow = null; for (const instance of loadedInstances) { const normalized = asPositiveSafeInteger(instance?.config?.context_length); if (normalized === void 0) continue; contextWindow = contextWindow === null ? normalized : Math.max(contextWindow, normalized); } return contextWindow; } function normalizeUrlPath(pathname) { return pathname.replace(/\/+$/, "").replace(/\/api\/v1$/i, "").replace(/\/v1$/i, ""); } function isLikelyHostBaseUrl(value) { return /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3}|[a-z0-9.-]+\.[a-z]{2,}|[^/\s?#]+:\d+)(?:[/?#].*)?$/i.test(value) && !value.startsWith("/"); } function toFetchableLmstudioBaseUrl(value) { if (/^https?:\/\//i.test(value) || !isLikelyHostBaseUrl(value)) return value; return `http://${value}`; } /** Resolves LM Studio server base URL (without /v1 or /api/v1). */ function resolveLmstudioServerBase(configuredBaseUrl) { const resolved = configuredBaseUrl?.trim() || "http://localhost:1234"; const fetchableBaseUrl = toFetchableLmstudioBaseUrl(resolved); const parsed = URL.parse(fetchableBaseUrl); if (parsed && (parsed.protocol === "http:" || parsed.protocol === "https:")) { const pathname = normalizeUrlPath(parsed.pathname); parsed.pathname = pathname.length > 0 ? pathname : "/"; parsed.search = ""; parsed.hash = ""; return parsed.toString().replace(/\/$/, ""); } const normalized = normalizeUrlPath(resolved.replace(/\/+$/, "")); return normalized.length > 0 ? normalized : LMSTUDIO_DEFAULT_BASE_URL; } function resolveLmstudioInferenceBase(configuredBaseUrl) { return `${resolveLmstudioServerBase(configuredBaseUrl)}/v1`; } function buildLmstudioModelName(model) { const tags = []; if (model.format === "mlx") tags.push("MLX"); else if (model.format === "gguf") tags.push("GGUF"); if (model.vision) tags.push("vision"); if (model.trainedForToolUse) tags.push("tool-use"); if (model.loaded) tags.push("loaded"); if (tags.length === 0) return model.displayName; return `${model.displayName} (${tags.join(", ")})`; } function mapLmstudioWireEntry(entry) { if (entry.type !== "llm") return null; const id = entry.key?.trim() ?? ""; if (!id) return null; const loadedContextWindow = resolveLoadedContextWindow(entry); const contextWindow = asPositiveSafeInteger(entry.max_context_length) ?? null ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW; const effectiveContextWindow = loadedContextWindow ?? contextWindow; const contextTokens = loadedContextWindow ?? Math.min(contextWindow, 64e3); const rawDisplayName = entry.display_name?.trim(); const reasoningCompat = resolveLmstudioReasoningCompat(entry); const trainedForToolUse = entry.capabilities?.trained_for_tool_use; const compat = trainedForToolUse === true ? { ...reasoningCompat, supportsTools: trainedForToolUse } : reasoningCompat; return { id, displayName: rawDisplayName || id, format: entry.format ?? null, vision: entry.capabilities?.vision === true, trainedForToolUse: entry.capabilities?.trained_for_tool_use === true, loaded: loadedContextWindow !== null, reasoning: resolveLmstudioReasoningCapability(entry), input: entry.capabilities?.vision ? ["text", "image"] : ["text"], cost: SELF_HOSTED_DEFAULT_COST, compat, contextWindow, contextTokens, maxTokens: Math.max(1, Math.min(effectiveContextWindow, SELF_HOSTED_DEFAULT_MAX_TOKENS)) }; } /** Config persistence keeps plain names; runtime discovery adds state tags and usage compat. */ function mapLmstudioWireModels(models, mode) { return models.map((entry) => { const base = mapLmstudioWireEntry(entry); if (!base) return null; return { id: base.id, name: mode === "runtime" ? buildLmstudioModelName(base) : base.displayName, reasoning: base.reasoning, input: base.input, cost: base.cost, ...mode === "runtime" ? { compat: { ...base.compat, supportsUsageInStreaming: true } } : base.compat ? { compat: base.compat } : {}, contextWindow: base.contextWindow, contextTokens: base.contextTokens, maxTokens: base.maxTokens }; }).filter((entry) => entry !== null); } //#endregion //#region extensions/lmstudio/src/runtime.ts var LmstudioConfigResolutionError = class extends Error {}; function buildLmstudioAuthHeaders(params) { const headers = { ...params.headers }; const apiKey = params.apiKey?.trim(); if (apiKey && !(apiKey === "lmstudio-local") && !isNonSecretApiKeyMarker(apiKey)) { for (const headerName of Object.keys(headers)) if (headerName.toLowerCase() === "authorization") delete headers[headerName]; headers.Authorization = `Bearer ${apiKey}`; } if (params.json) headers["Content-Type"] = "application/json"; return Object.keys(headers).length > 0 ? headers : void 0; } function sanitizeLmstudioStringHeaders(headers) { if (!headers || typeof headers !== "object" || Array.isArray(headers)) return; const next = {}; for (const [headerName, headerValue] of Object.entries(headers)) { const normalized = normalizeOptionalString(headerValue); if (normalized) next[headerName] = normalized; } return Object.keys(next).length > 0 ? next : void 0; } async function resolveLmstudioConfiguredApiKeyForProvider(params) { const config = params.config; const apiKeyInput = (config?.models?.providers?.[params.providerId])?.apiKey; if (apiKeyInput === void 0 || apiKeyInput === null || !config) return; const path = params.path ?? `models.providers[${JSON.stringify(params.providerId)}].apiKey`; const env = params.env ?? process.env; const directApiKey = normalizeOptionalSecretInput(apiKeyInput); const resolved = await resolveConfiguredSecretInputString({ config, env, value: directApiKey ?? apiKeyInput, path, unresolvedReasonStyle: "detailed" }); if (resolved.unresolvedRefReason) { if (params.allowUnresolved) return; throw new LmstudioConfigResolutionError(`${path}: ${resolved.unresolvedRefReason}`); } const resolvedValue = normalizeOptionalSecretInput(resolved.value); const trimmed = resolvedValue ? normalizeApiKeyConfig(resolvedValue).trim() : ""; if (!trimmed) return; if (directApiKey !== void 0 && isKnownEnvApiKeyMarker(trimmed)) return normalizeOptionalSecretInput(env[trimmed]); return isNonSecretApiKeyMarker(trimmed) ? void 0 : trimmed; } async function resolveLmstudioConfiguredApiKey(params) { return await resolveLmstudioConfiguredApiKeyForProvider({ ...params, providerId: LMSTUDIO_PROVIDER_ID }); } async function resolveLmstudioProviderHeaders(params) { const headerInputs = params.headers; if (!headerInputs || typeof headerInputs !== "object" || Array.isArray(headerInputs)) return; if (!params.config) return sanitizeLmstudioStringHeaders(headerInputs); const pathPrefix = params.path ?? "models.providers.lmstudio.headers"; const resolved = {}; for (const [headerName, headerValue] of Object.entries(headerInputs)) { const resolvedHeader = await resolveConfiguredSecretInputString({ config: params.config, env: params.env ?? process.env, value: headerValue, path: `${pathPrefix}[${JSON.stringify(headerName)}]`, unresolvedReasonStyle: "detailed" }); if (resolvedHeader.unresolvedRefReason) throw new LmstudioConfigResolutionError(`${pathPrefix}.${headerName}: ${resolvedHeader.unresolvedRefReason}`); const resolvedValue = resolvedHeader.value; if (!resolvedValue) continue; resolved[headerName] = resolvedValue; } return Object.keys(resolved).length > 0 ? resolved : void 0; } //#endregion //#region extensions/lmstudio/src/models.fetch.ts const log = createSubsystemLogger("extensions/lmstudio/models"); async function fetchLmstudioEndpoint(params) { params.signal?.throwIfAborted(); const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 1); let response; let release; if (params.ssrfPolicy) { const guarded = await fetchWithSsrFGuard({ url: params.url, init: params.init, timeoutMs, signal: params.signal, fetchImpl: params.fetchImpl, policy: params.ssrfPolicy, auditContext: params.auditContext }); response = guarded.response; release = guarded.release; } else { response = await (params.fetchImpl ?? fetch)(params.url, { ...params.init, signal: params.signal ? AbortSignal.any([params.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs) }); release = async () => void 0; } return { response, release: async () => { if (!response.bodyUsed) response.body?.cancel().catch(() => void 0); await release(); } }; } /** Fetches /api/v1/models and reports transport reachability separately from HTTP status. */ async function fetchLmstudioModels(params) { const baseUrl = resolveLmstudioServerBase(params.baseUrl); const timeoutMs = params.timeoutMs ?? 5e3; try { const { response, release } = await fetchLmstudioEndpoint({ url: `${baseUrl}/api/v1/models`, init: { headers: buildLmstudioAuthHeaders({ apiKey: params.apiKey, headers: params.headers }) }, timeoutMs, signal: params.signal, fetchImpl: params.fetchImpl, ssrfPolicy: params.ssrfPolicy, auditContext: "lmstudio-model-discovery" }); try { if (!response.ok) return { reachable: true, status: response.status, models: [] }; const models = await readProviderJsonArrayFieldResponse(response, "LM Studio model list", "models"); const validModels = models.filter(isRecord); if (models.length > 0 && validModels.length === 0) throw new Error("LM Studio model list: malformed JSON response"); return { reachable: true, status: response.status, models: validModels }; } finally { await release(); } } catch (error) { return { reachable: false, models: [], error }; } } async function discoverLmstudioModels(params) { const fetched = await fetchLmstudioModels({ baseUrl: params.baseUrl, apiKey: params.apiKey, headers: params.headers, fetchImpl: params.fetchImpl }); const quiet = params.quiet; if (!fetched.reachable || fetched.status !== void 0 && fetched.status >= 400) { const error = fetched.status === void 0 ? fetched.error : new LiveModelCatalogHttpError("lmstudio", fetched.status); if (params.discoveryMode === "strict") throw error; if (!quiet) log.debug(`Failed to discover LM Studio models: ${String(error)}`); return []; } return mapLmstudioWireModels(fetched.models, "runtime"); } //#endregion //#region extensions/lmstudio/src/setup.ts function resolvePersistedLmstudioApiKey(params) { if (params.explicitAuth === "api-key") { if (params.preferFallbackApiKey && params.fallbackApiKey !== void 0) return params.fallbackApiKey; if (resolveLmstudioProviderAuthMode(params.currentApiKey)) return params.currentApiKey; return params.fallbackApiKey; } return shouldUseLmstudioApiKeyPlaceholder({ hasModels: params.hasModels, resolvedApiKey: params.currentApiKey, hasAuthorizationHeader: params.hasAuthorizationHeader }) ? LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER : void 0; } async function discoverLmstudioProvider(ctx, options) { if (ctx.providerIds && !ctx.providerIds.includes("lmstudio")) return null; const explicit = ctx.config.models?.providers?.[LMSTUDIO_PROVIDER_ID]; const { headers: _headers, auth: explicitAuth, apiKey: _apiKey, ...explicitWithoutHeaders } = explicit ?? {}; const { apiKey, discoveryApiKey, profileId } = ctx.resolveProviderApiKey(LMSTUDIO_PROVIDER_ID); let resolvedHeaders; let hasAuthorizationHeader; let configuredDiscoveryApiKey; try { resolvedHeaders = await resolveLmstudioProviderHeaders({ config: ctx.config, env: ctx.env, headers: explicit?.headers }); hasAuthorizationHeader = hasLmstudioAuthorizationHeader(resolvedHeaders); configuredDiscoveryApiKey = await resolveLmstudioConfiguredApiKey({ config: ctx.config, env: ctx.env, allowUnresolved: hasAuthorizationHeader || Boolean(discoveryApiKey) }); } catch (error) { if (error instanceof LmstudioConfigResolutionError) return null; throw error; } const resolvedDiscoveryApiKey = hasAuthorizationHeader ? void 0 : discoveryApiKey ?? configuredDiscoveryApiKey; const resolvedApiKey = apiKey ?? explicit?.apiKey; const baseUrl = resolveLmstudioInferenceBase(explicit?.baseUrl); const quiet = !apiKey && !explicit && !resolvedDiscoveryApiKey; const run = async () => { const models = !options && explicit?.models?.length ? explicit.models : await discoverLmstudioModels({ baseUrl, apiKey: resolvedDiscoveryApiKey ?? "", headers: resolvedHeaders, quiet, ...!quiet && options ? options : {} }); if (models.length === 0 && (options ? quiet : !apiKey && !explicit?.apiKey)) return null; const persistedApiKey = resolvePersistedLmstudioApiKey({ currentApiKey: resolvedApiKey, explicitAuth, fallbackApiKey: LMSTUDIO_DEFAULT_API_KEY_ENV_VAR, hasModels: models.length > 0, hasAuthorizationHeader }); const persistedAuth = resolveLmstudioProviderAuthMode(persistedApiKey); return { provider: { ...explicitWithoutHeaders, baseUrl, api: explicit?.api ?? "openai-completions", ...resolvedHeaders ? { headers: resolvedHeaders } : {}, ...persistedApiKey ? { apiKey: persistedApiKey } : {}, ...persistedAuth ? { auth: persistedAuth } : {}, models } }; }; return options ? await runLiveProviderCatalog({ providerId: LMSTUDIO_PROVIDER_ID, profileId: !hasAuthorizationHeader && discoveryApiKey && !isNonSecretApiKeyMarker(discoveryApiKey) ? profileId : void 0, run }) : await run(); } //#endregion export { discoverLmstudioProvider };