openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
468 lines (467 loc) • 17.7 kB
JavaScript
import { a as asOptionalRecord, c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { p as normalizeTrimmedStringList } from "./string-normalization-DsCfAx8q.js";
import { n as findNormalizedProviderValue, r as normalizeProviderId } from "./provider-id-DMd-TDFp.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import "./http-body-D3IMwTJJ.js";
import { t as cancelUnreadResponseBody } from "./http-response-body-CwT_cCNz.js";
import { t as listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan-BaLPCL7D.js";
import { f as parseConfiguredModelVisibilityEntries } from "./model-selection-shared-BlLyx1r2.js";
import { t as runTasksWithConcurrency } from "./run-with-concurrency-Dtu208ef.js";
import { m as readProviderJsonResponse } from "./provider-http-errors-U-nhuk_f.js";
import { b as ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "./ssrf-0QyXWOVG.js";
import { i as fetchWithSsrFGuard } from "./fetch-guard-BMdGQhbb.js";
import { t as normalizeOptionalSecretInput } from "./normalize-secret-input-Df_qhWv_.js";
import { d as upsertAuthProfileWithLock } from "./profiles-DOTqXcYA.js";
import { c as isNonSecretApiKeyMarker } from "./model-auth-markers-jBKQn38x.js";
import { t as applyAuthProfileConfig } from "./provider-auth-helpers-CaYTVMoC.js";
import "./provider-model-shared-fX-Ywqb3.js";
//#region src/agents/self-hosted-provider-defaults.ts
/**
* Conservative defaults for self-hosted providers when the model catalog
* cannot supply pricing or token limits.
*/
/** Default context window used for self-hosted provider catalog entries. */
const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128e3;
/** Default output-token cap used for self-hosted provider catalog entries. */
const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
/** Zero-cost pricing used for self-hosted provider catalog entries. */
const SELF_HOSTED_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0
};
//#endregion
//#region src/plugins/provider-self-hosted-setup.ts
function applyProviderDefaultModel(cfg, modelRef) {
const existingModel = cfg.agents?.defaults?.model;
const fallbacks = existingModel && typeof existingModel === "object" && "fallbacks" in existingModel ? existingModel.fallbacks : void 0;
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
model: {
...fallbacks ? { fallbacks } : void 0,
primary: modelRef
}
}
}
};
}
function buildOpenAICompatibleSelfHostedProviderConfig(params) {
const modelRef = `${params.providerId}/${params.modelId}`;
const profileId = `${params.providerId}:default`;
return {
config: {
...params.cfg,
models: {
...params.cfg.models,
mode: params.cfg.models?.mode ?? "merge",
providers: {
...params.cfg.models?.providers,
[params.providerId]: {
baseUrl: params.baseUrl,
api: "openai-completions",
apiKey: params.providerApiKey,
models: [{
id: params.modelId,
name: params.modelId,
reasoning: params.reasoning ?? false,
input: params.input ?? ["text"],
cost: SELF_HOSTED_DEFAULT_COST,
contextWindow: params.contextWindow ?? 128e3,
maxTokens: params.maxTokens ?? 8192
}]
}
}
}
},
modelId: params.modelId,
modelRef,
profileId
};
}
async function promptAndConfigureOpenAICompatibleSelfHostedProvider(params) {
const baseUrlRaw = await params.prompter.text({
message: `${params.providerLabel} base URL`,
initialValue: params.defaultBaseUrl,
placeholder: params.defaultBaseUrl,
validate: (value) => value?.trim() ? void 0 : "Required"
});
const apiKeyRaw = await params.prompter.text({
message: `${params.providerLabel} API key`,
placeholder: "sk-... (or any non-empty string)",
validate: (value) => value?.trim() ? void 0 : "Required",
sensitive: true
});
const modelIdRaw = await params.prompter.text({
message: `${params.providerLabel} model`,
placeholder: params.modelPlaceholder,
validate: (value) => value?.trim() ? void 0 : "Required"
});
const baseUrl = (baseUrlRaw ?? "").trim().replace(/\/+$/, "");
const apiKey = normalizeStringifiedOptionalString(apiKeyRaw) ?? "";
const modelId = normalizeStringifiedOptionalString(modelIdRaw) ?? "";
const credential = {
type: "api_key",
provider: params.providerId,
key: apiKey
};
const configured = buildOpenAICompatibleSelfHostedProviderConfig({
cfg: params.cfg,
providerId: params.providerId,
baseUrl,
providerApiKey: params.defaultApiKeyEnvVar,
modelId,
input: params.input,
reasoning: params.reasoning,
contextWindow: params.contextWindow,
maxTokens: params.maxTokens
});
return {
config: configured.config,
credential,
modelId: configured.modelId,
modelRef: configured.modelRef,
profileId: configured.profileId
};
}
async function promptAndConfigureOpenAICompatibleSelfHostedProviderAuth(params) {
const result = await promptAndConfigureOpenAICompatibleSelfHostedProvider(params);
return {
profiles: [{
profileId: result.profileId,
credential: result.credential
}],
configPatch: result.config,
defaultModel: result.modelRef
};
}
async function discoverOpenAICompatibleSelfHostedProvider(params) {
const configuredProvider = findNormalizedProviderValue(params.ctx.config.models?.providers, params.providerId);
const configuredBaseUrl = configuredProvider ? normalizeOptionalString(configuredProvider.baseUrl) : void 0;
if (configuredProvider) {
if (!parseConfiguredModelVisibilityEntries({ cfg: params.ctx.config }).providerWildcards.has(normalizeProviderId(params.providerId))) return null;
}
const { apiKey, discoveryApiKey } = params.ctx.resolveProviderApiKey(params.providerId);
if (!apiKey) return null;
return { provider: {
...await params.buildProvider({
apiKey: discoveryApiKey,
...configuredBaseUrl ? { baseUrl: configuredBaseUrl } : {}
}),
apiKey
} };
}
function buildMissingNonInteractiveModelIdMessage(params) {
return [`Missing --custom-model-id for --auth-choice ${params.authChoice}.`, `Pass the ${params.providerLabel} model id to use, for example ${params.modelPlaceholder}.`].join("\n");
}
function isProviderOwnedSyntheticAuthMarker(providerId, resolved) {
if (resolved.source !== "flag" || !isNonSecretApiKeyMarker(resolved.key, { includeEnvVarName: false })) return false;
const normalizedProvider = normalizeProviderId(providerId);
const matchesProvider = (provider) => normalizeProviderId(provider) === normalizedProvider;
const normalizedValue = resolved.key.trim();
return listOpenClawPluginManifestMetadata().some(({ origin, manifest }) => origin === "bundled" && normalizeTrimmedStringList(manifest.providers).some(matchesProvider) && normalizeTrimmedStringList(manifest.syntheticAuthRefs).some(matchesProvider) && (normalizedValue === "custom-local" || normalizeTrimmedStringList(manifest.nonSecretAuthMarkers).includes(normalizedValue)));
}
async function configureOpenAICompatibleSelfHostedProviderNonInteractive(params) {
const baseUrl = (normalizeOptionalSecretInput(params.ctx.opts.customBaseUrl) ?? params.defaultBaseUrl).replace(/\/+$/, "");
const modelId = normalizeOptionalSecretInput(params.ctx.opts.customModelId);
if (!modelId) {
params.ctx.runtime.error(buildMissingNonInteractiveModelIdMessage({
authChoice: params.ctx.authChoice,
providerLabel: params.providerLabel,
modelPlaceholder: params.modelPlaceholder
}));
params.ctx.runtime.exit(1);
return null;
}
const resolved = await params.ctx.resolveApiKey({
provider: params.providerId,
flagValue: normalizeOptionalSecretInput(params.ctx.opts.customApiKey),
flagName: "--custom-api-key",
envVar: params.defaultApiKeyEnvVar,
envVarName: params.defaultApiKeyEnvVar
});
if (!resolved) return null;
const storesCredential = !isProviderOwnedSyntheticAuthMarker(params.providerId, resolved) && resolved.source !== "profile";
const configured = buildOpenAICompatibleSelfHostedProviderConfig({
cfg: params.ctx.config,
providerId: params.providerId,
baseUrl,
providerApiKey: params.defaultApiKeyEnvVar,
modelId,
input: params.input,
reasoning: params.reasoning,
contextWindow: params.contextWindow,
maxTokens: params.maxTokens
});
if (storesCredential) {
const credential = params.ctx.toApiKeyCredential({
provider: params.providerId,
resolved
});
if (!credential) return null;
await upsertAuthProfileWithLock({
profileId: configured.profileId,
credential,
agentDir: params.ctx.agentDir
});
}
const withProfile = storesCredential ? applyAuthProfileConfig(configured.config, {
profileId: configured.profileId,
provider: params.providerId,
mode: "api_key"
}) : configured.config;
params.ctx.runtime.log(`Default ${params.providerLabel} model: ${modelId}`);
return applyProviderDefaultModel(withProfile, configured.modelRef);
}
//#endregion
//#region src/plugins/provider-self-hosted-discovery.ts
const log = createSubsystemLogger("plugins/self-hosted-provider-discovery");
const SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES = 16777216;
const SELF_HOSTED_RUNTIME_CONTEXT_MAX_MODELS = 200;
const SELF_HOSTED_RUNTIME_CONTEXT_CONCURRENCY = 8;
function readPositiveInteger(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
return Math.trunc(value);
}
const OPENAI_COMPAT_CONTEXT_WINDOW_FIELDS = [
"context_length",
"context_window",
"context_size"
];
function readOpenAICompatibleContextWindow(model) {
for (const field of OPENAI_COMPAT_CONTEXT_WINDOW_FIELDS) {
const contextWindow = readPositiveInteger(model?.[field]);
if (contextWindow !== void 0) return contextWindow;
}
}
function buildSelfHostedDiscoveryHeaders(params) {
const headers = {
...params.acceptJson ? { Accept: "application/json" } : {},
...params.headers
};
const hasAuthorization = Object.keys(headers).some((name) => name.trim().toLowerCase() === "authorization");
const apiKey = normalizeOptionalString(params.apiKey);
if (apiKey && !isNonSecretApiKeyMarker(apiKey) && !hasAuthorization) headers.Authorization = `Bearer ${apiKey}`;
return Object.keys(headers).length > 0 ? headers : void 0;
}
async function fetchSelfHostedDiscoveryJson(params) {
let guarded;
try {
guarded = await fetchWithSsrFGuard({
url: params.url,
init: { headers: buildSelfHostedDiscoveryHeaders(params) },
policy: ssrfPolicyFromHttpBaseUrlAllowedOrigin(params.origin),
timeoutMs: params.timeoutMs,
signal: params.signal,
auditContext: "self-hosted-provider-discovery"
});
} catch (error) {
return {
kind: "unreachable",
error
};
}
try {
if (!params.readBody || !guarded.response.ok) return {
kind: "response",
ok: guarded.response.ok,
status: guarded.response.status
};
try {
return {
kind: "response",
ok: true,
status: guarded.response.status,
body: await readProviderJsonResponse(guarded.response, `${params.label} discovery`, { maxBytes: SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES })
};
} catch (error) {
return {
kind: "invalid-response",
error
};
}
} finally {
await cancelUnreadResponseBody(guarded.response);
await guarded.release();
}
}
function readDiscoveryRows(body) {
const bodyRecord = asOptionalRecord(body);
if (!Array.isArray(bodyRecord?.data)) throw new Error("model list must contain data[]");
return bodyRecord.data.filter(isRecord);
}
function shouldProbeRuntimeProps(model) {
const status = asOptionalRecord(model.status)?.value;
return status === void 0 || status === "loaded" || status === "sleeping";
}
function resolveRuntimePropsUrl(params) {
const url = new URL(`${params.serverBaseUrl.replace(/\/+$/, "")}/props`);
const modelId = normalizeOptionalString(params.modelId);
if (modelId) {
url.searchParams.set("model", modelId);
url.searchParams.set("autoload", "false");
}
return url.toString();
}
/** Guarded model-row discovery for OpenAI-compatible self-hosted servers. */
async function discoverOpenAICompatibleModelRows(params) {
const inferenceBaseUrl = params.baseUrl.trim().replace(/\/+$/, "");
const inferredServerBaseUrl = inferenceBaseUrl.replace(/\/v1$/u, "");
const serverBaseUrl = (params.serverBaseUrl ?? inferredServerBaseUrl).replace(/\/+$/, "");
const timeoutMs = params.timeoutMs ?? 5e3;
const request = {
...params,
origin: new URL(serverBaseUrl).origin,
timeoutMs,
acceptJson: params.modelsPathOrder === "server-first",
readBody: true
};
let health = "unknown";
if (params.healthPath) {
const path = params.healthPath;
const healthResult = await fetchSelfHostedDiscoveryJson({
...request,
url: `${serverBaseUrl}${path}`,
acceptJson: true,
readBody: false
});
if (healthResult.kind === "unreachable") return healthResult;
if (healthResult.kind === "invalid-response") return {
...healthResult,
path
};
health = healthResult.status === 200 ? "ready" : healthResult.status === 503 ? "loading" : "unknown";
if (![
200,
404,
503
].includes(healthResult.status)) return {
kind: "http-error",
path,
status: healthResult.status
};
}
const modelCandidates = params.modelsPathOrder === "server-first" ? [{
path: "/models",
url: `${serverBaseUrl}/models`
}, {
path: "/v1/models",
url: `${inferenceBaseUrl}/models`
}] : [{
path: "/v1/models",
url: `${inferenceBaseUrl}/models`
}];
let modelList;
for (const candidate of modelCandidates) {
const result = await fetchSelfHostedDiscoveryJson({
...request,
url: candidate.url
});
if (result.kind === "unreachable") return result;
if (result.kind === "invalid-response") modelList = {
...result,
path: candidate.path
};
else if (!result.ok) modelList = {
kind: "http-error",
path: candidate.path,
status: result.status
};
else try {
modelList = {
kind: "success",
models: readDiscoveryRows(result.body)
};
} catch (error) {
modelList = {
kind: "invalid-response",
path: candidate.path,
error
};
}
if (modelList.kind === "success" || modelList.kind === "http-error" && modelList.status !== 404) break;
}
if (!modelList || modelList.kind !== "success") return modelList ?? {
kind: "unreachable",
error: /* @__PURE__ */ new Error("missing model response")
};
const rows = modelList.models.map((model) => ({ model }));
if (params.discoverRuntimeContext !== false) {
const queryByModel = params.routerModelProps && rows.some(({ model }) => asOptionalRecord(model.status) !== void 0) || !params.routerModelProps && rows.length > 1;
const probeRows = rows.filter(({ model }) => shouldProbeRuntimeProps(model)).slice(0, SELF_HOSTED_RUNTIME_CONTEXT_MAX_MODELS);
const deadline = performance.now() + timeoutMs;
await runTasksWithConcurrency({
limit: SELF_HOSTED_RUNTIME_CONTEXT_CONCURRENCY,
errorMode: "stop",
throwOnError: true,
tasks: probeRows.map((row) => async () => {
const remainingMs = deadline - performance.now();
if (remainingMs <= 0) return;
const modelId = normalizeOptionalString(row.model.id);
if (!modelId) return;
const result = await fetchSelfHostedDiscoveryJson({
...request,
url: resolveRuntimePropsUrl({
serverBaseUrl,
modelId: queryByModel ? modelId : void 0
}),
timeoutMs: Math.min(params.propsTimeoutMs ?? timeoutMs, remainingMs),
label: `${params.label} /props`
});
const props = result.kind === "response" && result.ok ? asOptionalRecord(result.body) : void 0;
if (props) row.props = props;
})
});
}
return {
kind: "success",
health,
rows,
fetchedAt: Date.now()
};
}
async function discoverOpenAICompatibleLocalModels(params) {
const env = params.env ?? process.env;
if (!params.rawResult && (env.VITEST || env.NODE_ENV === "test")) return [];
const result = await discoverOpenAICompatibleModelRows({
...params,
discoverRuntimeContext: params.contextWindow === void 0 && params.discoverRuntimeContext !== false,
propsTimeoutMs: params.propsTimeoutMs ?? 2500
});
if (params.rawResult) return result;
if (result.kind !== "success") {
if (result.kind === "invalid-response") log.warn(`${params.label} discovery: malformed JSON response: ${String(result.error)}`);
else {
const detail = result.kind === "http-error" ? result.status : String(result.error);
log.warn(`Failed to discover ${params.label} models: ${detail}`);
}
return [];
}
if (result.rows.length === 0) {
log.warn(`No ${params.label} models found on local instance`);
return [];
}
return result.rows.flatMap(({ model, props }) => {
const modelId = normalizeOptionalString(model.id);
if (!modelId) return [];
const meta = asOptionalRecord(model.meta);
const runtimeContextTokens = readPositiveInteger(asOptionalRecord(props?.default_generation_settings)?.n_ctx) ?? readPositiveInteger(props?.n_ctx);
return [{
id: modelId,
name: modelId,
reasoning: /r1|reasoning|think|reason/i.test(modelId),
input: ["text"],
cost: SELF_HOSTED_DEFAULT_COST,
contextWindow: params.contextWindow ?? readPositiveInteger(meta?.n_ctx_train) ?? readOpenAICompatibleContextWindow(model) ?? 128e3,
maxTokens: params.maxTokens ?? 8192,
...runtimeContextTokens ? { contextTokens: runtimeContextTokens } : {}
}];
});
}
//#endregion
export { promptAndConfigureOpenAICompatibleSelfHostedProviderAuth as a, SELF_HOSTED_DEFAULT_MAX_TOKENS as c, discoverOpenAICompatibleSelfHostedProvider as i, applyProviderDefaultModel as n, SELF_HOSTED_DEFAULT_CONTEXT_WINDOW as o, configureOpenAICompatibleSelfHostedProviderNonInteractive as r, SELF_HOSTED_DEFAULT_COST as s, discoverOpenAICompatibleLocalModels as t };