openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
384 lines (383 loc) • 13.9 kB
JavaScript
import { c as normalizeOptionalString, f as normalizeStringifiedOptionalString } from "./string-coerce-mnp54Vah.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { i as normalizeProviderId, n as findNormalizedProviderValue } from "./provider-id-Dq06Bcx6.js";
import { m as updateAuthProfileStoreWithLock } from "./store-C8spD0DG.js";
import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js";
import { n as normalizeSecretInput, t as normalizeOptionalSecretInput } from "./normalize-secret-input-OwRUfByL.js";
import { f as parseConfiguredModelVisibilityEntries } from "./model-selection-shared-b32cUYts.js";
import { t as applyAuthProfileConfig } from "./provider-auth-helpers-BgLOn-Ws.js";
//#region src/agents/auth-profiles/upsert-with-lock.ts
/**
* Locked auth profile upsert helper.
* Normalizes literal secrets before persistence and routes all writes through
* the shared SQLite lock to avoid racing concurrent auth updates.
*/
function normalizeAuthProfileCredential(credential) {
if (credential.type === "api_key") {
if (typeof credential.key !== "string") return credential;
const { key: _key, ...rest } = credential;
const key = normalizeSecretInput(credential.key);
return {
...rest,
...key ? { key } : {}
};
}
if (credential.type === "token") {
if (typeof credential.token !== "string") return credential;
const { token: _token, ...rest } = credential;
const token = normalizeSecretInput(credential.token);
return {
...rest,
...token ? { token } : {}
};
}
return credential;
}
/** Upserts an auth profile under the store lock, returning null on write failure. */
async function upsertAuthProfileWithLock(params) {
try {
const credential = normalizeAuthProfileCredential(params.credential);
return await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
saveOptions: {
filterExternalAuthProfiles: false,
syncExternalCli: false
},
updater: (store) => {
store.profiles[params.profileId] = credential;
return true;
}
});
} catch {
return null;
}
}
//#endregion
//#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
const log = createSubsystemLogger("plugins/self-hosted-provider-setup");
function isReasoningModelHeuristic(modelId) {
return /r1|reasoning|think|reason/i.test(modelId);
}
const SELF_HOSTED_ALWAYS_BLOCKED_HOSTNAMES = new Set(["metadata.google.internal"]);
function buildSelfHostedBaseUrlSsrFPolicy(baseUrl) {
try {
const parsed = new URL(baseUrl.trim());
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
if (SELF_HOSTED_ALWAYS_BLOCKED_HOSTNAMES.has(parsed.hostname.toLowerCase())) return;
return {
hostnameAllowlist: [parsed.hostname],
allowPrivateNetwork: true
};
} catch {
return;
}
}
function readPositiveInteger(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
return Math.trunc(value);
}
function resolveLlamaCppPropsUrl(baseUrl, modelId) {
const parsed = new URL(baseUrl);
const pathname = parsed.pathname.replace(/\/+$/, "");
parsed.pathname = `${(pathname.endsWith("/v1") ? pathname.slice(0, -3) || "/" : pathname).replace(/\/+$/, "")}/props`;
parsed.search = "";
parsed.hash = "";
const normalizedModelId = normalizeOptionalString(modelId);
if (normalizedModelId) {
parsed.searchParams.set("model", normalizedModelId);
parsed.searchParams.set("autoload", "false");
}
return parsed.toString();
}
async function discoverLlamaCppRuntimeContextTokens(params) {
let url;
try {
url = resolveLlamaCppPropsUrl(params.baseUrl, params.modelId);
} catch {
return;
}
try {
const trimmedApiKey = normalizeOptionalString(params.apiKey);
const { response, release } = await fetchWithSsrFGuard({
url,
init: { headers: trimmedApiKey ? { Authorization: `Bearer ${trimmedApiKey}` } : void 0 },
policy: buildSelfHostedBaseUrlSsrFPolicy(params.baseUrl),
timeoutMs: 2500
});
try {
if (!response.ok) return;
const data = await response.json();
return readPositiveInteger(data.default_generation_settings?.n_ctx) ?? readPositiveInteger(data.n_ctx);
} finally {
await release();
}
} catch {
return;
}
}
async function discoverOpenAICompatibleLocalModels(params) {
const env = params.env ?? process.env;
if (env.VITEST || env.NODE_ENV === "test") return [];
const trimmedBaseUrl = params.baseUrl.trim().replace(/\/+$/, "");
const url = `${trimmedBaseUrl}/models`;
try {
const trimmedApiKey = normalizeOptionalString(params.apiKey);
const { response, release } = await fetchWithSsrFGuard({
url,
init: { headers: trimmedApiKey ? { Authorization: `Bearer ${trimmedApiKey}` } : void 0 },
policy: buildSelfHostedBaseUrlSsrFPolicy(trimmedBaseUrl),
timeoutMs: 5e3
});
try {
if (!response.ok) {
log.warn(`Failed to discover ${params.label} models: ${response.status}`);
return [];
}
const models = (await response.json()).data ?? [];
if (models.length === 0) {
log.warn(`No ${params.label} models found on local instance`);
return [];
}
const discoveredModels = models.flatMap((model) => {
const modelId = normalizeOptionalString(model.id);
if (!modelId) return [];
return [{
id: modelId,
meta: model.meta
}];
});
const runtimeContextTokensByModelId = /* @__PURE__ */ new Map();
if (params.contextWindow === void 0) {
const uniqueModelIds = uniqueStrings(discoveredModels.map((model) => model.id));
const runtimeContextTokenResults = await Promise.all(uniqueModelIds.map(async (modelId) => [modelId, await discoverLlamaCppRuntimeContextTokens({
baseUrl: trimmedBaseUrl,
apiKey: params.apiKey,
modelId: uniqueModelIds.length > 1 ? modelId : void 0
})]));
for (const [modelId, runtimeContextTokens] of runtimeContextTokenResults) if (runtimeContextTokens) runtimeContextTokensByModelId.set(modelId, runtimeContextTokens);
}
return discoveredModels.map((model) => {
const modelConfig = {
id: model.id,
name: model.id,
reasoning: isReasoningModelHeuristic(model.id),
input: ["text"],
cost: SELF_HOSTED_DEFAULT_COST,
contextWindow: params.contextWindow ?? readPositiveInteger(model.meta?.n_ctx_train) ?? 128e3,
maxTokens: params.maxTokens ?? 8192
};
const runtimeContextTokens = runtimeContextTokensByModelId.get(model.id);
if (runtimeContextTokens) modelConfig.contextTokens = runtimeContextTokens;
return modelConfig;
});
} finally {
await release();
}
} catch (error) {
log.warn(`Failed to discover ${params.label} models: ${String(error)}`);
return [];
}
}
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
};
}
function buildSelfHostedProviderAuthResult(result) {
return {
profiles: [{
profileId: result.profileId,
credential: result.credential
}],
configPatch: result.config,
defaultModel: result.modelRef
};
}
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) {
return buildSelfHostedProviderAuthResult(await promptAndConfigureOpenAICompatibleSelfHostedProvider(params));
}
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 buildSelfHostedProviderCredential(params) {
return params.ctx.toApiKeyCredential({
provider: params.providerId,
resolved: params.resolved
});
}
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 credential = buildSelfHostedProviderCredential({
ctx: params.ctx,
providerId: params.providerId,
resolved
});
if (!credential) return null;
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
});
await upsertAuthProfileWithLock({
profileId: configured.profileId,
credential,
agentDir: params.ctx.agentDir
});
const withProfile = applyAuthProfileConfig(configured.config, {
profileId: configured.profileId,
provider: params.providerId,
mode: "api_key"
});
params.ctx.runtime.log(`Default ${params.providerLabel} model: ${modelId}`);
return applyProviderDefaultModel(withProfile, configured.modelRef);
}
//#endregion
export { promptAndConfigureOpenAICompatibleSelfHostedProvider as a, SELF_HOSTED_DEFAULT_COST as c, discoverOpenAICompatibleSelfHostedProvider as i, SELF_HOSTED_DEFAULT_MAX_TOKENS as l, configureOpenAICompatibleSelfHostedProviderNonInteractive as n, promptAndConfigureOpenAICompatibleSelfHostedProviderAuth as o, discoverOpenAICompatibleLocalModels as r, SELF_HOSTED_DEFAULT_CONTEXT_WINDOW as s, applyProviderDefaultModel as t };