Files
openclaw/src/agents/models-config.providers.implicit.ts
2026-03-29 23:05:58 +09:00

319 lines
9.4 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
resolvePluginDiscoveryProviders,
runProviderCatalog,
} from "../plugins/provider-discovery.js";
import { ensureAuthProfileStore } from "./auth-profiles/store.js";
import {
isNonSecretApiKeyMarker,
resolveNonEnvSecretRefApiKeyMarker,
} from "./model-auth-markers.js";
import type {
ProviderApiKeyResolver,
ProviderAuthResolver,
ProviderConfig,
} from "./models-config.providers.secrets.js";
import {
createProviderApiKeyResolver,
createProviderAuthResolver,
} from "./models-config.providers.secrets.js";
import { findNormalizedProviderValue } from "./provider-id.js";
const log = createSubsystemLogger("agents/model-providers");
const PROVIDER_IMPLICIT_MERGERS: Partial<
Record<
string,
(params: { existing: ProviderConfig | undefined; implicit: ProviderConfig }) => ProviderConfig
>
> = {
ollama: ({ implicit }) => implicit,
};
const PLUGIN_DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const;
type ImplicitProviderParams = {
agentDir: string;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
explicitProviders?: Record<string, ProviderConfig> | null;
};
type ImplicitProviderContext = ImplicitProviderParams & {
authStore: ReturnType<typeof ensureAuthProfileStore>;
env: NodeJS.ProcessEnv;
resolveProviderApiKey: ProviderApiKeyResolver;
resolveProviderAuth: ProviderAuthResolver;
};
function resolveLiveProviderCatalogTimeoutMs(env: NodeJS.ProcessEnv): number | null {
const live =
env.OPENCLAW_LIVE_TEST === "1" || env.OPENCLAW_LIVE_GATEWAY === "1" || env.LIVE === "1";
if (!live) {
return null;
}
const raw = env.OPENCLAW_LIVE_PROVIDER_DISCOVERY_TIMEOUT_MS?.trim();
if (!raw) {
return 15_000;
}
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 15_000;
}
function resolveLiveProviderDiscoveryFilter(env: NodeJS.ProcessEnv): string[] | undefined {
const live =
env.OPENCLAW_LIVE_TEST === "1" || env.OPENCLAW_LIVE_GATEWAY === "1" || env.LIVE === "1";
if (!live) {
return undefined;
}
const raw = env.OPENCLAW_LIVE_PROVIDERS?.trim();
if (!raw || raw === "all") {
return undefined;
}
const ids = raw
.split(",")
.map((value) => value.trim())
.filter(Boolean);
return ids.length > 0 ? [...new Set(ids)] : undefined;
}
function mergeImplicitProviderSet(
target: Record<string, ProviderConfig>,
additions: Record<string, ProviderConfig> | undefined,
): void {
if (!additions) {
return;
}
for (const [key, value] of Object.entries(additions)) {
target[key] = value;
}
}
function mergeImplicitProviderConfig(params: {
providerId: string;
existing: ProviderConfig | undefined;
implicit: ProviderConfig;
}): ProviderConfig {
const { providerId, existing, implicit } = params;
if (!existing) {
return implicit;
}
const merge = PROVIDER_IMPLICIT_MERGERS[providerId];
if (merge) {
return merge({ existing, implicit });
}
return {
...implicit,
...existing,
models:
Array.isArray(existing.models) && existing.models.length > 0
? existing.models
: implicit.models,
};
}
function resolveConfiguredImplicitProvider(params: {
configuredProviders?: Record<string, ProviderConfig> | null;
providerIds: readonly string[];
}): ProviderConfig | undefined {
for (const providerId of params.providerIds) {
const configured = findNormalizedProviderValue(
params.configuredProviders ?? undefined,
providerId,
);
if (configured) {
return configured;
}
}
return undefined;
}
function resolveExistingImplicitProviderFromContext(params: {
ctx: ImplicitProviderContext;
providerIds: readonly string[];
}): ProviderConfig | undefined {
return (
resolveConfiguredImplicitProvider({
configuredProviders: params.ctx.explicitProviders,
providerIds: params.providerIds,
}) ??
resolveConfiguredImplicitProvider({
configuredProviders: params.ctx.config?.models?.providers,
providerIds: params.providerIds,
})
);
}
async function resolvePluginImplicitProviders(
ctx: ImplicitProviderContext,
order: import("../plugins/types.js").ProviderDiscoveryOrder,
): Promise<Record<string, ProviderConfig> | undefined> {
const onlyPluginIds = resolveLiveProviderDiscoveryFilter(ctx.env);
const providers = await resolvePluginDiscoveryProviders({
config: ctx.config,
workspaceDir: ctx.workspaceDir,
env: ctx.env,
onlyPluginIds,
});
const byOrder = groupPluginDiscoveryProvidersByOrder(providers);
const discovered: Record<string, ProviderConfig> = {};
const catalogConfig = buildPluginCatalogConfig(ctx);
for (const provider of byOrder[order]) {
const resolveCatalogProviderApiKey = (providerId?: string) => {
const resolvedProviderId = providerId?.trim() || provider.id;
const resolved = ctx.resolveProviderApiKey(resolvedProviderId);
if (resolved.apiKey) {
return resolved;
}
if (
!findNormalizedProviderValue(
{
[provider.id]: true,
...Object.fromEntries((provider.aliases ?? []).map((alias) => [alias, true])),
...Object.fromEntries((provider.hookAliases ?? []).map((alias) => [alias, true])),
},
resolvedProviderId,
)
) {
return resolved;
}
const synthetic = provider.resolveSyntheticAuth?.({
config: catalogConfig,
provider: resolvedProviderId,
providerConfig: catalogConfig.models?.providers?.[resolvedProviderId],
});
const syntheticApiKey = synthetic?.apiKey?.trim();
if (!syntheticApiKey) {
return resolved;
}
return {
apiKey: isNonSecretApiKeyMarker(syntheticApiKey)
? syntheticApiKey
: resolveNonEnvSecretRefApiKeyMarker("file"),
discoveryApiKey: undefined,
};
};
const result = await runProviderCatalogWithTimeout({
provider,
config: catalogConfig,
agentDir: ctx.agentDir,
workspaceDir: ctx.workspaceDir,
env: ctx.env,
resolveProviderApiKey: resolveCatalogProviderApiKey,
resolveProviderAuth: (providerId, options) =>
ctx.resolveProviderAuth(providerId?.trim() || provider.id, options),
timeoutMs: resolveLiveProviderCatalogTimeoutMs(ctx.env),
});
if (!result) {
continue;
}
const normalizedResult = normalizePluginDiscoveryResult({
provider,
result,
});
for (const [providerId, implicitProvider] of Object.entries(normalizedResult)) {
discovered[providerId] = mergeImplicitProviderConfig({
providerId,
existing:
discovered[providerId] ??
resolveExistingImplicitProviderFromContext({
ctx,
providerIds: [
providerId,
provider.id,
...(provider.aliases ?? []),
...(provider.hookAliases ?? []),
],
}),
implicit: implicitProvider,
});
}
}
return Object.keys(discovered).length > 0 ? discovered : undefined;
}
function buildPluginCatalogConfig(ctx: ImplicitProviderContext): OpenClawConfig {
if (!ctx.explicitProviders || Object.keys(ctx.explicitProviders).length === 0) {
return ctx.config ?? {};
}
return {
...ctx.config,
models: {
...ctx.config?.models,
providers: {
...ctx.config?.models?.providers,
...ctx.explicitProviders,
},
},
};
}
async function runProviderCatalogWithTimeout(
params: Parameters<typeof runProviderCatalog>[0] & {
timeoutMs: number | null;
},
): Promise<Awaited<ReturnType<typeof runProviderCatalog>> | undefined> {
const catalogRun = runProviderCatalog(params);
const timeoutMs = params.timeoutMs ?? undefined;
if (!timeoutMs) {
return await catalogRun;
}
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
catalogRun,
new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(
new Error(`provider catalog timed out after ${timeoutMs}ms: ${params.provider.id}`),
);
}, timeoutMs);
timer.unref?.();
}),
]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("provider catalog timed out after")) {
log.warn(`${message}; skipping provider discovery`);
return undefined;
}
throw error;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
export async function resolveImplicitProviders(
params: ImplicitProviderParams,
): Promise<NonNullable<OpenClawConfig["models"]>["providers"]> {
const providers: Record<string, ProviderConfig> = {};
const env = params.env ?? process.env;
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const context: ImplicitProviderContext = {
...params,
authStore,
env,
resolveProviderApiKey: createProviderApiKeyResolver(env, authStore, params.config),
resolveProviderAuth: createProviderAuthResolver(env, authStore, params.config),
};
for (const order of PLUGIN_DISCOVERY_ORDERS) {
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, order));
}
return providers;
}