mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 03:10:42 +00:00
255 lines
8.1 KiB
TypeScript
255 lines
8.1 KiB
TypeScript
import { resolveConfiguredSecretInputString } from "openclaw/plugin-sdk/config-runtime";
|
|
import {
|
|
CUSTOM_LOCAL_AUTH_MARKER,
|
|
isKnownEnvApiKeyMarker,
|
|
isNonSecretApiKeyMarker,
|
|
normalizeApiKeyConfig,
|
|
normalizeOptionalSecretInput,
|
|
type OpenClawConfig,
|
|
} from "openclaw/plugin-sdk/provider-auth";
|
|
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
import {
|
|
LMSTUDIO_DEFAULT_API_KEY_ENV_VAR,
|
|
LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
|
|
LMSTUDIO_PROVIDER_ID,
|
|
} from "./defaults.js";
|
|
import { hasLmstudioAuthorizationHeader } from "./provider-auth.js";
|
|
|
|
type LmstudioAuthHeadersParams = {
|
|
apiKey?: string;
|
|
json?: boolean;
|
|
headers?: Record<string, string>;
|
|
};
|
|
|
|
export function buildLmstudioAuthHeaders(
|
|
params: LmstudioAuthHeadersParams,
|
|
): Record<string, string> | undefined {
|
|
const headers: Record<string, string> = { ...params.headers };
|
|
// Runtime auth resolution is strict, but guard known non-secret markers here.
|
|
const apiKey = params.apiKey?.trim();
|
|
const isSyntheticLocalKey = apiKey === LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER;
|
|
if (apiKey && !isSyntheticLocalKey && !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 : undefined;
|
|
}
|
|
|
|
function sanitizeStringHeaders(headers: unknown): Record<string, string> | undefined {
|
|
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
return undefined;
|
|
}
|
|
const next: Record<string, string> = {};
|
|
for (const [headerName, headerValue] of Object.entries(headers)) {
|
|
if (typeof headerValue !== "string") {
|
|
continue;
|
|
}
|
|
const normalized = headerValue.trim();
|
|
if (!normalized) {
|
|
continue;
|
|
}
|
|
next[headerName] = normalized;
|
|
}
|
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
}
|
|
|
|
function shouldSuppressResolvedRuntimeApiKeyForHeaderAuth(
|
|
source: string | undefined,
|
|
hasAuthorizationHeader: boolean,
|
|
): boolean {
|
|
if (!hasAuthorizationHeader || !source) {
|
|
return false;
|
|
}
|
|
return /^profile:|^(?:shell )?env(?::|$)/.test(source);
|
|
}
|
|
|
|
export async function resolveLmstudioConfiguredApiKey(params: {
|
|
config?: OpenClawConfig;
|
|
env?: NodeJS.ProcessEnv;
|
|
path?: string;
|
|
}): Promise<string | undefined> {
|
|
const providerConfig = params.config?.models?.providers?.[LMSTUDIO_PROVIDER_ID];
|
|
const apiKeyInput = providerConfig?.apiKey;
|
|
if (apiKeyInput === undefined || apiKeyInput === null) {
|
|
return undefined;
|
|
}
|
|
|
|
const directApiKey = normalizeOptionalSecretInput(apiKeyInput);
|
|
if (directApiKey !== undefined) {
|
|
const trimmed = normalizeApiKeyConfig(directApiKey).trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
if (isKnownEnvApiKeyMarker(trimmed)) {
|
|
const envValue = normalizeOptionalSecretInput((params.env ?? process.env)[trimmed]);
|
|
return envValue;
|
|
}
|
|
return isNonSecretApiKeyMarker(trimmed) ? undefined : trimmed;
|
|
}
|
|
|
|
if (!params.config) {
|
|
return undefined;
|
|
}
|
|
const path = params.path ?? "models.providers.lmstudio.apiKey";
|
|
const resolved = await resolveConfiguredSecretInputString({
|
|
config: params.config,
|
|
env: params.env ?? process.env,
|
|
value: apiKeyInput,
|
|
path,
|
|
unresolvedReasonStyle: "detailed",
|
|
});
|
|
if (resolved.unresolvedRefReason) {
|
|
throw new Error(`${path}: ${resolved.unresolvedRefReason}`);
|
|
}
|
|
const resolvedValue = normalizeOptionalSecretInput(resolved.value);
|
|
const trimmedResolvedValue = resolvedValue ? normalizeApiKeyConfig(resolvedValue).trim() : "";
|
|
if (!trimmedResolvedValue) {
|
|
return undefined;
|
|
}
|
|
if (isNonSecretApiKeyMarker(trimmedResolvedValue)) {
|
|
return undefined;
|
|
}
|
|
return trimmedResolvedValue;
|
|
}
|
|
|
|
export async function resolveLmstudioProviderHeaders(params: {
|
|
config?: OpenClawConfig;
|
|
env?: NodeJS.ProcessEnv;
|
|
headers?: unknown;
|
|
path?: string;
|
|
}): Promise<Record<string, string> | undefined> {
|
|
const headerInputs = params.headers;
|
|
if (!headerInputs || typeof headerInputs !== "object" || Array.isArray(headerInputs)) {
|
|
return undefined;
|
|
}
|
|
|
|
if (!params.config) {
|
|
return sanitizeStringHeaders(headerInputs);
|
|
}
|
|
|
|
const pathPrefix = params.path ?? "models.providers.lmstudio.headers";
|
|
const resolved: Record<string, string> = {};
|
|
for (const [headerName, headerValue] of Object.entries(headerInputs)) {
|
|
const resolvedHeader = await resolveConfiguredSecretInputString({
|
|
config: params.config,
|
|
env: params.env ?? process.env,
|
|
value: headerValue,
|
|
path: `${pathPrefix}.${headerName}`,
|
|
unresolvedReasonStyle: "detailed",
|
|
});
|
|
if (resolvedHeader.unresolvedRefReason) {
|
|
throw new Error(`${pathPrefix}.${headerName}: ${resolvedHeader.unresolvedRefReason}`);
|
|
}
|
|
const resolvedValue = resolvedHeader.value;
|
|
if (!resolvedValue) {
|
|
continue;
|
|
}
|
|
resolved[headerName] = resolvedValue;
|
|
}
|
|
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
}
|
|
|
|
/**
|
|
* Resolves LM Studio API key and provider headers in parallel.
|
|
* Use this as the standard auth setup step before discovery or model load calls.
|
|
*/
|
|
export async function resolveLmstudioRequestContext(params: {
|
|
config?: OpenClawConfig;
|
|
agentDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
providerHeaders?: unknown;
|
|
}): Promise<{ apiKey: string | undefined; headers: Record<string, string> | undefined }> {
|
|
const providerHeaders =
|
|
params.providerHeaders ?? params.config?.models?.providers?.[LMSTUDIO_PROVIDER_ID]?.headers;
|
|
const [apiKey, headers] = await Promise.all([
|
|
resolveLmstudioRuntimeApiKey({
|
|
config: params.config,
|
|
agentDir: params.agentDir,
|
|
env: params.env,
|
|
headers: providerHeaders,
|
|
}),
|
|
resolveLmstudioProviderHeaders({
|
|
config: params.config,
|
|
env: params.env,
|
|
headers: providerHeaders,
|
|
}),
|
|
]);
|
|
return { apiKey, headers };
|
|
}
|
|
|
|
/**
|
|
* Resolves LM Studio runtime API key from config.
|
|
*/
|
|
export async function resolveLmstudioRuntimeApiKey(params: {
|
|
config?: OpenClawConfig;
|
|
agentDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
headers?: unknown;
|
|
}): Promise<string | undefined> {
|
|
const config = params.config;
|
|
if (!config) {
|
|
return undefined;
|
|
}
|
|
const providerHeaders =
|
|
params.headers ?? config.models?.providers?.[LMSTUDIO_PROVIDER_ID]?.headers;
|
|
const hasAuthorizationHeader = hasLmstudioAuthorizationHeader(providerHeaders);
|
|
let configuredApiKeyPromise: Promise<string | undefined> | undefined;
|
|
const getConfiguredApiKey = async () => {
|
|
configuredApiKeyPromise ??= resolveLmstudioConfiguredApiKey({
|
|
config,
|
|
env: params.env,
|
|
});
|
|
return await configuredApiKeyPromise;
|
|
};
|
|
const resolveConfiguredApiKeyOrThrow = async () => {
|
|
const configuredApiKey = await getConfiguredApiKey();
|
|
if (configuredApiKey) {
|
|
return configuredApiKey;
|
|
}
|
|
if (hasAuthorizationHeader) {
|
|
return undefined;
|
|
}
|
|
const envMarker = `\${${LMSTUDIO_DEFAULT_API_KEY_ENV_VAR}}`;
|
|
throw new Error(
|
|
[
|
|
"LM Studio API key is required.",
|
|
`Set models.providers.lmstudio.apiKey (for example "${envMarker}")`,
|
|
'or run "openclaw models auth lmstudio".',
|
|
].join(" "),
|
|
);
|
|
};
|
|
let resolved: Awaited<ReturnType<typeof resolveApiKeyForProvider>>;
|
|
try {
|
|
resolved = await resolveApiKeyForProvider({
|
|
provider: LMSTUDIO_PROVIDER_ID,
|
|
cfg: config,
|
|
agentDir: params.agentDir,
|
|
});
|
|
} catch {
|
|
return await resolveConfiguredApiKeyOrThrow();
|
|
}
|
|
// Normalize empty/whitespace keys to undefined for callers.
|
|
const resolvedApiKey = resolved.apiKey?.trim();
|
|
if (!resolvedApiKey || resolvedApiKey.length === 0) {
|
|
return await resolveConfiguredApiKeyOrThrow();
|
|
}
|
|
if (shouldSuppressResolvedRuntimeApiKeyForHeaderAuth(resolved.source, hasAuthorizationHeader)) {
|
|
return await resolveConfiguredApiKeyOrThrow();
|
|
}
|
|
if (
|
|
isNonSecretApiKeyMarker(resolvedApiKey) &&
|
|
resolvedApiKey !== CUSTOM_LOCAL_AUTH_MARKER &&
|
|
resolvedApiKey !== LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER
|
|
) {
|
|
return await resolveConfiguredApiKeyOrThrow();
|
|
}
|
|
return resolvedApiKey;
|
|
}
|