openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
414 lines (413 loc) • 17.7 kB
JavaScript
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 };