diff --git a/src/agents/models-config.providers.source-managed.ts b/src/agents/models-config.providers.source-managed.ts new file mode 100644 index 00000000000..48a0d1100de --- /dev/null +++ b/src/agents/models-config.providers.source-managed.ts @@ -0,0 +1,153 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { resolveSecretInputRef } from "../config/types.secrets.js"; +import { isRecord } from "../utils.js"; +import { + resolveNonEnvSecretRefApiKeyMarker, + resolveNonEnvSecretRefHeaderValueMarker, + resolveEnvSecretRefHeaderValueMarker, +} from "./model-auth-markers.js"; +import type { ProviderConfig, SecretDefaults } from "./models-config.providers.secrets.js"; + +type ModelsConfig = NonNullable; + +function normalizeSourceProviderLookup( + providers: ModelsConfig["providers"] | undefined, +): Record { + if (!providers) { + return {}; + } + const out: Record = {}; + 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 { + const sourceHeaders = isRecord(params.sourceProvider?.headers) + ? (params.sourceProvider.headers as Record) + : undefined; + if (!sourceHeaders) { + return {}; + } + const markers: Record = {}; + 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; +}): 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 | 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) + : undefined; + const nextHeaders = { + ...(currentHeaders as Record[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; +} diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 4b18fbfe2b5..8864654b1d8 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -1,5 +1,4 @@ import type { OpenClawConfig } from "../config/config.js"; -import { resolveSecretInputRef } from "../config/types.secrets.js"; import { resolveBedrockConfigApiKey } from "../plugin-sdk/amazon-bedrock.js"; import { resolveAnthropicVertexConfigApiKey } from "../plugin-sdk/anthropic-vertex.js"; import { @@ -8,14 +7,8 @@ import { } from "../plugin-sdk/google.js"; import { applyModelStudioNativeStreamingUsageCompat } from "../plugin-sdk/modelstudio.js"; import { applyMoonshotNativeStreamingUsageCompat } from "../plugin-sdk/moonshot.js"; -import { isRecord } from "../utils.js"; import { ensureAuthProfileStore } from "./auth-profiles.js"; export * from "./models-config.providers.static.js"; -import { - resolveNonEnvSecretRefApiKeyMarker, - resolveNonEnvSecretRefHeaderValueMarker, - resolveEnvSecretRefHeaderValueMarker, -} from "./model-auth-markers.js"; export { resolveImplicitProviders } from "./models-config.providers.implicit.js"; import type { ProviderConfig, SecretDefaults } from "./models-config.providers.secrets.js"; import { @@ -25,6 +18,7 @@ import { resolveApiKeyFromProfiles, resolveMissingProviderApiKey, } from "./models-config.providers.secrets.js"; +import { enforceSourceManagedProviderSecrets } from "./models-config.providers.source-managed.js"; export type { ProfileApiKeyResolution, ProviderApiKeyResolver, @@ -32,6 +26,7 @@ export type { ProviderConfig, SecretDefaults, } from "./models-config.providers.secrets.js"; +export { enforceSourceManagedProviderSecrets }; export { resolveOllamaApiBase } from "../plugin-sdk/ollama-surface.js"; export { normalizeGoogleModelId } from "../plugin-sdk/google.js"; export { normalizeXaiModelId } from "../plugin-sdk/xai.js"; @@ -76,148 +71,6 @@ function normalizeProviderSpecificConfig( return provider; } -function normalizeSourceProviderLookup( - providers: ModelsConfig["providers"] | undefined, -): Record { - if (!providers) { - return {}; - } - const out: Record = {}; - 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 { - const sourceHeaders = isRecord(params.sourceProvider?.headers) - ? (params.sourceProvider.headers as Record) - : undefined; - if (!sourceHeaders) { - return {}; - } - const markers: Record = {}; - 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; -}): 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 | 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) - : undefined; - const nextHeaders = { - ...(currentHeaders as Record[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;