openclaw/src/agents/models-config.providers.ts

850 lines
26 KiB
TypeScript

import {
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_ID,
} from "../../extensions/qianfan/provider-catalog.js";
import { XIAOMI_DEFAULT_MODEL_ID } from "../../extensions/xiaomi/provider-catalog.js";
import type { OpenClawConfig } from "../config/config.js";
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
import { isRecord } from "../utils.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import { normalizeGoogleModelId } from "./model-id-normalization.js";
import { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
export { buildKimiCodingProvider } from "../../extensions/kimi-coding/provider-catalog.js";
export { buildKilocodeProvider } from "../../extensions/kilocode/provider-catalog.js";
export {
MODELSTUDIO_BASE_URL,
MODELSTUDIO_DEFAULT_MODEL_ID,
buildModelStudioProvider,
} from "../../extensions/modelstudio/provider-catalog.js";
export { buildNvidiaProvider } from "../../extensions/nvidia/provider-catalog.js";
export {
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_ID,
buildQianfanProvider,
} from "../../extensions/qianfan/provider-catalog.js";
export {
XIAOMI_DEFAULT_MODEL_ID,
buildXiaomiProvider,
} from "../../extensions/xiaomi/provider-catalog.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
resolvePluginDiscoveryProviders,
runProviderCatalog,
} from "../plugins/provider-discovery.js";
import {
isNonSecretApiKeyMarker,
resolveNonEnvSecretRefApiKeyMarker,
resolveNonEnvSecretRefHeaderValueMarker,
resolveEnvSecretRefHeaderValueMarker,
} from "./model-auth-markers.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
export { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
export { normalizeGoogleModelId };
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
type SecretDefaults = {
env?: string;
file?: string;
exec?: string;
};
const MOONSHOT_NATIVE_BASE_URLS = new Set([
"https://api.moonshot.ai/v1",
"https://api.moonshot.cn/v1",
]);
const MODELSTUDIO_NATIVE_BASE_URLS = new Set([
"https://coding-intl.dashscope.aliyuncs.com/v1",
"https://coding.dashscope.aliyuncs.com/v1",
]);
const ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
function normalizeApiKeyConfig(value: string): string {
const trimmed = value.trim();
const match = /^\$\{([A-Z0-9_]+)\}$/.exec(trimmed);
return match?.[1] ?? trimmed;
}
function normalizeProviderBaseUrl(baseUrl: string | undefined): string {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return "";
}
try {
const url = new URL(trimmed);
url.hash = "";
url.search = "";
return url.toString().replace(/\/+$/, "").toLowerCase();
} catch {
return trimmed.replace(/\/+$/, "").toLowerCase();
}
}
function withStreamingUsageCompat(provider: ProviderConfig): ProviderConfig {
if (!Array.isArray(provider.models) || provider.models.length === 0) {
return provider;
}
let changed = false;
const models = provider.models.map((model) => {
if (model.compat?.supportsUsageInStreaming !== undefined) {
return model;
}
changed = true;
return {
...model,
compat: {
...model.compat,
supportsUsageInStreaming: true,
},
};
});
return changed ? { ...provider, models } : provider;
}
export function applyNativeStreamingUsageCompat(
providers: Record<string, ProviderConfig>,
): Record<string, ProviderConfig> {
let changed = false;
const nextProviders: Record<string, ProviderConfig> = {};
for (const [providerKey, provider] of Object.entries(providers)) {
const normalizedBaseUrl = normalizeProviderBaseUrl(provider.baseUrl);
const isNativeMoonshot =
providerKey === "moonshot" && MOONSHOT_NATIVE_BASE_URLS.has(normalizedBaseUrl);
const isNativeModelStudio =
providerKey === "modelstudio" && MODELSTUDIO_NATIVE_BASE_URLS.has(normalizedBaseUrl);
const nextProvider =
isNativeMoonshot || isNativeModelStudio ? withStreamingUsageCompat(provider) : provider;
nextProviders[providerKey] = nextProvider;
changed ||= nextProvider !== provider;
}
return changed ? nextProviders : providers;
}
function resolveEnvApiKeyVarName(
provider: string,
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
const resolved = resolveEnvApiKey(provider, env);
if (!resolved) {
return undefined;
}
const match = /^(?:env: |shell env: )([A-Z0-9_]+)$/.exec(resolved.source);
return match ? match[1] : undefined;
}
function resolveAwsSdkApiKeyVarName(env: NodeJS.ProcessEnv = process.env): string {
return resolveAwsSdkEnvVarName(env) ?? "AWS_PROFILE";
}
function normalizeHeaderValues(params: {
headers: ProviderConfig["headers"] | undefined;
secretDefaults: SecretDefaults | undefined;
}): { headers: ProviderConfig["headers"] | undefined; mutated: boolean } {
const { headers } = params;
if (!headers) {
return { headers, mutated: false };
}
let mutated = false;
const nextHeaders: Record<string, NonNullable<ProviderConfig["headers"]>[string]> = {};
for (const [headerName, headerValue] of Object.entries(headers)) {
const resolvedRef = resolveSecretInputRef({
value: headerValue,
defaults: params.secretDefaults,
}).ref;
if (!resolvedRef || !resolvedRef.id.trim()) {
nextHeaders[headerName] = headerValue;
continue;
}
mutated = true;
nextHeaders[headerName] =
resolvedRef.source === "env"
? resolveEnvSecretRefHeaderValueMarker(resolvedRef.id)
: resolveNonEnvSecretRefHeaderValueMarker(resolvedRef.source);
}
if (!mutated) {
return { headers, mutated: false };
}
return { headers: nextHeaders, mutated: true };
}
type ProfileApiKeyResolution = {
apiKey: string;
source: "plaintext" | "env-ref" | "non-env-ref";
/** Optional secret value that may be used for provider discovery only. */
discoveryApiKey?: string;
};
function toDiscoveryApiKey(value: string | undefined): string | undefined {
const trimmed = value?.trim();
if (!trimmed || isNonSecretApiKeyMarker(trimmed)) {
return undefined;
}
return trimmed;
}
function resolveApiKeyFromCredential(
cred: ReturnType<typeof ensureAuthProfileStore>["profiles"][string] | undefined,
env: NodeJS.ProcessEnv = process.env,
): ProfileApiKeyResolution | undefined {
if (!cred) {
return undefined;
}
if (cred.type === "api_key") {
const keyRef = coerceSecretRef(cred.keyRef);
if (keyRef && keyRef.id.trim()) {
if (keyRef.source === "env") {
const envVar = keyRef.id.trim();
return {
apiKey: envVar,
source: "env-ref",
discoveryApiKey: toDiscoveryApiKey(env[envVar]),
};
}
return {
apiKey: resolveNonEnvSecretRefApiKeyMarker(keyRef.source),
source: "non-env-ref",
};
}
if (cred.key?.trim()) {
return {
apiKey: cred.key,
source: "plaintext",
discoveryApiKey: toDiscoveryApiKey(cred.key),
};
}
return undefined;
}
if (cred.type === "token") {
const tokenRef = coerceSecretRef(cred.tokenRef);
if (tokenRef && tokenRef.id.trim()) {
if (tokenRef.source === "env") {
const envVar = tokenRef.id.trim();
return {
apiKey: envVar,
source: "env-ref",
discoveryApiKey: toDiscoveryApiKey(env[envVar]),
};
}
return {
apiKey: resolveNonEnvSecretRefApiKeyMarker(tokenRef.source),
source: "non-env-ref",
};
}
if (cred.token?.trim()) {
return {
apiKey: cred.token,
source: "plaintext",
discoveryApiKey: toDiscoveryApiKey(cred.token),
};
}
}
return undefined;
}
function resolveApiKeyFromProfiles(params: {
provider: string;
store: ReturnType<typeof ensureAuthProfileStore>;
env?: NodeJS.ProcessEnv;
}): ProfileApiKeyResolution | undefined {
const ids = listProfilesForProvider(params.store, params.provider);
for (const id of ids) {
const resolved = resolveApiKeyFromCredential(params.store.profiles[id], params.env);
if (resolved) {
return resolved;
}
}
return undefined;
}
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
export function normalizeAntigravityModelId(id: string): string {
if (ANTIGRAVITY_BARE_PRO_IDS.has(id)) {
return `${id}-low`;
}
return id;
}
function normalizeProviderModels(
provider: ProviderConfig,
normalizeId: (id: string) => string,
): ProviderConfig {
let mutated = false;
const models = provider.models.map((model) => {
const nextId = normalizeId(model.id);
if (nextId === model.id) {
return model;
}
mutated = true;
return { ...model, id: nextId };
});
return mutated ? { ...provider, models } : provider;
}
function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
return normalizeProviderModels(provider, normalizeGoogleModelId);
}
function normalizeAntigravityProvider(provider: ProviderConfig): ProviderConfig {
return normalizeProviderModels(provider, normalizeAntigravityModelId);
}
function normalizeSourceProviderLookup(
providers: ModelsConfig["providers"] | undefined,
): Record<string, ProviderConfig> {
if (!providers) {
return {};
}
const out: Record<string, ProviderConfig> = {};
for (const [key, provider] of Object.entries(providers)) {
const normalizedKey = key.trim();
if (!normalizedKey || !isRecord(provider)) {
continue;
}
out[normalizedKey] = provider;
}
return out;
}
function resolveSourceManagedApiKeyMarker(params: {
sourceProvider: ProviderConfig | undefined;
sourceSecretDefaults: SecretDefaults | undefined;
}): string | undefined {
const sourceApiKeyRef = resolveSecretInputRef({
value: params.sourceProvider?.apiKey,
defaults: params.sourceSecretDefaults,
}).ref;
if (!sourceApiKeyRef || !sourceApiKeyRef.id.trim()) {
return undefined;
}
return sourceApiKeyRef.source === "env"
? sourceApiKeyRef.id.trim()
: resolveNonEnvSecretRefApiKeyMarker(sourceApiKeyRef.source);
}
function resolveSourceManagedHeaderMarkers(params: {
sourceProvider: ProviderConfig | undefined;
sourceSecretDefaults: SecretDefaults | undefined;
}): Record<string, string> {
const sourceHeaders = isRecord(params.sourceProvider?.headers)
? (params.sourceProvider.headers as Record<string, unknown>)
: undefined;
if (!sourceHeaders) {
return {};
}
const markers: Record<string, string> = {};
for (const [headerName, headerValue] of Object.entries(sourceHeaders)) {
const sourceHeaderRef = resolveSecretInputRef({
value: headerValue,
defaults: params.sourceSecretDefaults,
}).ref;
if (!sourceHeaderRef || !sourceHeaderRef.id.trim()) {
continue;
}
markers[headerName] =
sourceHeaderRef.source === "env"
? resolveEnvSecretRefHeaderValueMarker(sourceHeaderRef.id)
: resolveNonEnvSecretRefHeaderValueMarker(sourceHeaderRef.source);
}
return markers;
}
export function enforceSourceManagedProviderSecrets(params: {
providers: ModelsConfig["providers"];
sourceProviders: ModelsConfig["providers"] | undefined;
sourceSecretDefaults?: SecretDefaults;
secretRefManagedProviders?: Set<string>;
}): ModelsConfig["providers"] {
const { providers } = params;
if (!providers) {
return providers;
}
const sourceProvidersByKey = normalizeSourceProviderLookup(params.sourceProviders);
if (Object.keys(sourceProvidersByKey).length === 0) {
return providers;
}
let nextProviders: Record<string, ProviderConfig> | null = null;
for (const [providerKey, provider] of Object.entries(providers)) {
if (!isRecord(provider)) {
continue;
}
const sourceProvider = sourceProvidersByKey[providerKey.trim()];
if (!sourceProvider) {
continue;
}
let nextProvider = provider;
let providerMutated = false;
const sourceApiKeyMarker = resolveSourceManagedApiKeyMarker({
sourceProvider,
sourceSecretDefaults: params.sourceSecretDefaults,
});
if (sourceApiKeyMarker) {
params.secretRefManagedProviders?.add(providerKey.trim());
if (nextProvider.apiKey !== sourceApiKeyMarker) {
providerMutated = true;
nextProvider = {
...nextProvider,
apiKey: sourceApiKeyMarker,
};
}
}
const sourceHeaderMarkers = resolveSourceManagedHeaderMarkers({
sourceProvider,
sourceSecretDefaults: params.sourceSecretDefaults,
});
if (Object.keys(sourceHeaderMarkers).length > 0) {
const currentHeaders = isRecord(nextProvider.headers)
? (nextProvider.headers as Record<string, unknown>)
: undefined;
const nextHeaders = {
...(currentHeaders as Record<string, NonNullable<ProviderConfig["headers"]>[string]>),
};
let headersMutated = !currentHeaders;
for (const [headerName, marker] of Object.entries(sourceHeaderMarkers)) {
if (nextHeaders[headerName] === marker) {
continue;
}
headersMutated = true;
nextHeaders[headerName] = marker;
}
if (headersMutated) {
providerMutated = true;
nextProvider = {
...nextProvider,
headers: nextHeaders,
};
}
}
if (!providerMutated) {
continue;
}
if (!nextProviders) {
nextProviders = { ...providers };
}
nextProviders[providerKey] = nextProvider;
}
return nextProviders ?? providers;
}
export function normalizeProviders(params: {
providers: ModelsConfig["providers"];
agentDir: string;
env?: NodeJS.ProcessEnv;
secretDefaults?: SecretDefaults;
sourceProviders?: ModelsConfig["providers"];
sourceSecretDefaults?: SecretDefaults;
secretRefManagedProviders?: Set<string>;
}): ModelsConfig["providers"] {
const { providers } = params;
if (!providers) {
return providers;
}
const env = params.env ?? process.env;
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
let mutated = false;
const next: Record<string, ProviderConfig> = {};
for (const [key, provider] of Object.entries(providers)) {
const normalizedKey = key.trim();
if (!normalizedKey) {
mutated = true;
continue;
}
if (normalizedKey !== key) {
mutated = true;
}
let normalizedProvider = provider;
const normalizedHeaders = normalizeHeaderValues({
headers: normalizedProvider.headers,
secretDefaults: params.secretDefaults,
});
if (normalizedHeaders.mutated) {
mutated = true;
normalizedProvider = { ...normalizedProvider, headers: normalizedHeaders.headers };
}
const configuredApiKey = normalizedProvider.apiKey;
const configuredApiKeyRef = resolveSecretInputRef({
value: configuredApiKey,
defaults: params.secretDefaults,
}).ref;
const profileApiKey = resolveApiKeyFromProfiles({
provider: normalizedKey,
store: authStore,
env,
});
if (configuredApiKeyRef && configuredApiKeyRef.id.trim()) {
const marker =
configuredApiKeyRef.source === "env"
? configuredApiKeyRef.id.trim()
: resolveNonEnvSecretRefApiKeyMarker(configuredApiKeyRef.source);
if (normalizedProvider.apiKey !== marker) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey: marker };
}
params.secretRefManagedProviders?.add(normalizedKey);
} else if (typeof configuredApiKey === "string") {
// Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR".
const normalizedConfiguredApiKey = normalizeApiKeyConfig(configuredApiKey);
if (normalizedConfiguredApiKey !== configuredApiKey) {
mutated = true;
normalizedProvider = {
...normalizedProvider,
apiKey: normalizedConfiguredApiKey,
};
}
if (isNonSecretApiKeyMarker(normalizedConfiguredApiKey)) {
params.secretRefManagedProviders?.add(normalizedKey);
}
if (
profileApiKey &&
profileApiKey.source !== "plaintext" &&
normalizedConfiguredApiKey === profileApiKey.apiKey
) {
params.secretRefManagedProviders?.add(normalizedKey);
}
}
// Reverse-lookup: if apiKey looks like a resolved secret value (not an env
// var name), check whether it matches the canonical env var for this provider.
// This prevents resolveConfigEnvVars()-resolved secrets from being persisted
// to models.json as plaintext. (Fixes #38757)
const currentApiKey = normalizedProvider.apiKey;
if (
typeof currentApiKey === "string" &&
currentApiKey.trim() &&
!ENV_VAR_NAME_RE.test(currentApiKey.trim())
) {
const envVarName = resolveEnvApiKeyVarName(normalizedKey, env);
if (envVarName && env[envVarName] === currentApiKey) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey: envVarName };
params.secretRefManagedProviders?.add(normalizedKey);
}
}
// If a provider defines models, pi's ModelRegistry requires apiKey to be set.
// Fill it from the environment or auth profiles when possible.
const hasModels =
Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0;
const normalizedApiKey = normalizeOptionalSecretInput(normalizedProvider.apiKey);
const hasConfiguredApiKey = Boolean(normalizedApiKey || normalizedProvider.apiKey);
if (hasModels && !hasConfiguredApiKey) {
const authMode =
normalizedProvider.auth ?? (normalizedKey === "amazon-bedrock" ? "aws-sdk" : undefined);
if (authMode === "aws-sdk") {
const apiKey = resolveAwsSdkApiKeyVarName(env);
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
} else {
const fromEnv = resolveEnvApiKeyVarName(normalizedKey, env);
const apiKey = fromEnv ?? profileApiKey?.apiKey;
if (apiKey?.trim()) {
if (profileApiKey && profileApiKey.source !== "plaintext") {
params.secretRefManagedProviders?.add(normalizedKey);
}
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
}
}
}
if (normalizedKey === "google" || normalizedKey === "google-vertex") {
const googleNormalized = normalizeGoogleProvider(normalizedProvider);
if (googleNormalized !== normalizedProvider) {
mutated = true;
}
normalizedProvider = googleNormalized;
}
if (normalizedKey === "google-antigravity") {
const antigravityNormalized = normalizeAntigravityProvider(normalizedProvider);
if (antigravityNormalized !== normalizedProvider) {
mutated = true;
}
normalizedProvider = antigravityNormalized;
}
const existing = next[normalizedKey];
if (existing) {
// Keep deterministic behavior if users accidentally define duplicate
// provider keys that only differ by surrounding whitespace.
mutated = true;
next[normalizedKey] = {
...existing,
...normalizedProvider,
models: normalizedProvider.models ?? existing.models,
};
continue;
}
next[normalizedKey] = normalizedProvider;
}
const normalizedProviders = mutated ? next : providers;
return enforceSourceManagedProviderSecrets({
providers: normalizedProviders,
sourceProviders: params.sourceProviders,
sourceSecretDefaults: params.sourceSecretDefaults,
secretRefManagedProviders: params.secretRefManagedProviders,
});
}
type ImplicitProviderParams = {
agentDir: string;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
explicitProviders?: Record<string, ProviderConfig> | null;
};
type ProviderApiKeyResolver = (provider: string) => {
apiKey: string | undefined;
discoveryApiKey?: string;
};
type ProviderAuthResolver = (
provider: string,
options?: { oauthMarker?: string },
) => {
apiKey: string | undefined;
discoveryApiKey?: string;
mode: "api_key" | "oauth" | "token" | "none";
source: "env" | "profile" | "none";
profileId?: string;
};
type ImplicitProviderContext = ImplicitProviderParams & {
authStore: ReturnType<typeof ensureAuthProfileStore>;
env: NodeJS.ProcessEnv;
resolveProviderApiKey: ProviderApiKeyResolver;
resolveProviderAuth: ProviderAuthResolver;
};
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;
}
}
async function resolvePluginImplicitProviders(
ctx: ImplicitProviderContext,
order: import("../plugins/types.js").ProviderDiscoveryOrder,
): Promise<Record<string, ProviderConfig> | undefined> {
const providers = resolvePluginDiscoveryProviders({
config: ctx.config,
workspaceDir: ctx.workspaceDir,
env: ctx.env,
});
const byOrder = groupPluginDiscoveryProvidersByOrder(providers);
const discovered: Record<string, ProviderConfig> = {};
const catalogConfig =
ctx.explicitProviders && Object.keys(ctx.explicitProviders).length > 0
? {
...ctx.config,
models: {
...ctx.config?.models,
providers: {
...ctx.config?.models?.providers,
...ctx.explicitProviders,
},
},
}
: (ctx.config ?? {});
for (const provider of byOrder[order]) {
const result = await runProviderCatalog({
provider,
config: catalogConfig,
agentDir: ctx.agentDir,
workspaceDir: ctx.workspaceDir,
env: ctx.env,
resolveProviderApiKey: (providerId) =>
ctx.resolveProviderApiKey(providerId?.trim() || provider.id),
resolveProviderAuth: (providerId, options) =>
ctx.resolveProviderAuth(providerId?.trim() || provider.id, options),
});
mergeImplicitProviderSet(
discovered,
normalizePluginDiscoveryResult({
provider,
result,
}),
);
}
return Object.keys(discovered).length > 0 ? discovered : undefined;
}
export async function resolveImplicitProviders(
params: ImplicitProviderParams,
): Promise<ModelsConfig["providers"]> {
const providers: Record<string, ProviderConfig> = {};
const env = params.env ?? process.env;
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const resolveProviderApiKey: ProviderApiKeyResolver = (
provider: string,
): { apiKey: string | undefined; discoveryApiKey?: string } => {
const envVar = resolveEnvApiKeyVarName(provider, env);
if (envVar) {
return {
apiKey: envVar,
discoveryApiKey: toDiscoveryApiKey(env[envVar]),
};
}
const fromProfiles = resolveApiKeyFromProfiles({ provider, store: authStore, env });
return {
apiKey: fromProfiles?.apiKey,
discoveryApiKey: fromProfiles?.discoveryApiKey,
};
};
const resolveProviderAuth: ProviderAuthResolver = (
provider: string,
options?: { oauthMarker?: string },
) => {
const envVar = resolveEnvApiKeyVarName(provider, env);
if (envVar) {
return {
apiKey: envVar,
discoveryApiKey: toDiscoveryApiKey(env[envVar]),
mode: "api_key",
source: "env",
};
}
const ids = listProfilesForProvider(authStore, provider);
let oauthCandidate:
| {
apiKey: string | undefined;
discoveryApiKey?: string;
mode: "oauth";
source: "profile";
profileId: string;
}
| undefined;
for (const id of ids) {
const cred = authStore.profiles[id];
if (!cred) {
continue;
}
if (cred.type === "oauth") {
oauthCandidate ??= {
apiKey: options?.oauthMarker,
discoveryApiKey: toDiscoveryApiKey(cred.access),
mode: "oauth",
source: "profile",
profileId: id,
};
continue;
}
const resolved = resolveApiKeyFromCredential(cred, env);
if (!resolved) {
continue;
}
return {
apiKey: resolved.apiKey,
discoveryApiKey: resolved.discoveryApiKey,
mode: cred.type,
source: "profile",
profileId: id,
};
}
if (oauthCandidate) {
return oauthCandidate;
}
return {
apiKey: undefined,
discoveryApiKey: undefined,
mode: "none",
source: "none",
};
};
const context: ImplicitProviderContext = {
...params,
authStore,
env,
resolveProviderApiKey,
resolveProviderAuth,
};
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "simple"));
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "profile"));
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "paired"));
mergeImplicitProviderSet(providers, await resolvePluginImplicitProviders(context, "late"));
const implicitBedrock = await resolveImplicitBedrockProvider({
agentDir: params.agentDir,
config: params.config,
env,
});
if (implicitBedrock) {
const existing = providers["amazon-bedrock"];
providers["amazon-bedrock"] = existing
? {
...implicitBedrock,
...existing,
models:
Array.isArray(existing.models) && existing.models.length > 0
? existing.models
: implicitBedrock.models,
}
: implicitBedrock;
}
return providers;
}
export async function resolveImplicitBedrockProvider(params: {
agentDir: string;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): Promise<ProviderConfig | null> {
const env = params.env ?? process.env;
const discoveryConfig = params.config?.models?.bedrockDiscovery;
const enabled = discoveryConfig?.enabled;
const hasAwsCreds = resolveAwsSdkEnvVarName(env) !== undefined;
if (enabled === false) {
return null;
}
if (enabled !== true && !hasAwsCreds) {
return null;
}
const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1";
const models = await discoverBedrockModels({
region,
config: discoveryConfig,
});
if (models.length === 0) {
return null;
}
return {
baseUrl: `https://bedrock-runtime.${region}.amazonaws.com`,
api: "bedrock-converse-stream",
auth: "aws-sdk",
models,
} satisfies ProviderConfig;
}