Files
openclaw/src/agents/models-config.providers.ts
Josh Avant 36d2ae2a22 SecretRef: harden custom/provider secret persistence and reuse (#42554)
* Models: gate custom provider keys by usable secret semantics

* Config: project runtime writes onto source snapshot

* Models: prevent stale apiKey preservation for marker-managed providers

* Runner: strip SecretRef marker headers from resolved models

* Secrets: scan active agent models.json path in audit

* Config: guard runtime-source projection for unrelated configs

* Extensions: fix onboarding type errors in CI

* Tests: align setup helper account-enabled expectation

* Secrets audit: harden models.json file reads

* fix: harden SecretRef custom/provider secret persistence (#42554) (thanks @joshavant)
2026-03-10 23:55:10 +00:00

836 lines
26 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
import {
DEFAULT_COPILOT_API_BASE_URL,
resolveCopilotApiToken,
} from "../providers/github-copilot-token.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import {
buildCloudflareAiGatewayModelDefinition,
resolveCloudflareAiGatewayBaseUrl,
} from "./cloudflare-ai-gateway.js";
import {
buildHuggingfaceProvider,
buildKilocodeProviderWithDiscovery,
buildOllamaProvider,
buildVeniceProvider,
buildVercelAiGatewayProvider,
buildVllmProvider,
resolveOllamaApiBase,
} from "./models-config.providers.discovery.js";
import {
buildBytePlusCodingProvider,
buildBytePlusProvider,
buildDoubaoCodingProvider,
buildDoubaoProvider,
buildKimiCodingProvider,
buildKilocodeProvider,
buildMinimaxPortalProvider,
buildMinimaxProvider,
buildModelStudioProvider,
buildMoonshotProvider,
buildNvidiaProvider,
buildOpenAICodexProvider,
buildOpenrouterProvider,
buildQianfanProvider,
buildQwenPortalProvider,
buildSyntheticProvider,
buildTogetherProvider,
buildXiaomiProvider,
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_ID,
XIAOMI_DEFAULT_MODEL_ID,
} from "./models-config.providers.static.js";
export {
buildKimiCodingProvider,
buildKilocodeProvider,
buildNvidiaProvider,
buildModelStudioProvider,
buildQianfanProvider,
buildXiaomiProvider,
MODELSTUDIO_BASE_URL,
MODELSTUDIO_DEFAULT_MODEL_ID,
QIANFAN_BASE_URL,
QIANFAN_DEFAULT_MODEL_ID,
XIAOMI_DEFAULT_MODEL_ID,
} from "./models-config.providers.static.js";
import {
MINIMAX_OAUTH_MARKER,
OLLAMA_LOCAL_AUTH_MARKER,
QWEN_OAUTH_MARKER,
isNonSecretApiKeyMarker,
resolveNonEnvSecretRefApiKeyMarker,
resolveNonEnvSecretRefHeaderValueMarker,
resolveEnvSecretRefHeaderValueMarker,
} from "./model-auth-markers.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
export { resolveOllamaApiBase } from "./models-config.providers.discovery.js";
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
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 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:
| {
env?: string;
file?: string;
exec?: string;
}
| 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;
}
export function normalizeGoogleModelId(id: string): string {
if (id === "gemini-3-pro") {
return "gemini-3-pro-preview";
}
if (id === "gemini-3-flash") {
return "gemini-3-flash-preview";
}
if (id === "gemini-3.1-pro") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3.1-flash-lite") {
return "gemini-3.1-flash-lite-preview";
}
// Preserve compatibility with earlier OpenClaw docs/config that pointed at a
// non-existent Gemini Flash preview ID. Google's current Flash text model is
// `gemini-3-flash-preview`.
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
return "gemini-3-flash-preview";
}
return id;
}
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);
}
export function normalizeProviders(params: {
providers: ModelsConfig["providers"];
agentDir: string;
env?: NodeJS.ProcessEnv;
secretDefaults?: {
env?: string;
file?: string;
exec?: string;
};
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") {
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;
}
return mutated ? next : providers;
}
type ImplicitProviderParams = {
agentDir: string;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
explicitProviders?: Record<string, ProviderConfig> | null;
};
type ProviderApiKeyResolver = (provider: string) => {
apiKey: string | undefined;
discoveryApiKey?: string;
};
type ImplicitProviderContext = ImplicitProviderParams & {
authStore: ReturnType<typeof ensureAuthProfileStore>;
env: NodeJS.ProcessEnv;
resolveProviderApiKey: ProviderApiKeyResolver;
};
type ImplicitProviderLoader = (
ctx: ImplicitProviderContext,
) => Promise<Record<string, ProviderConfig> | undefined>;
function withApiKey(
providerKey: string,
build: (params: {
apiKey: string;
discoveryApiKey?: string;
}) => ProviderConfig | Promise<ProviderConfig>,
): ImplicitProviderLoader {
return async (ctx) => {
const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(providerKey);
if (!apiKey) {
return undefined;
}
return {
[providerKey]: await build({ apiKey, discoveryApiKey }),
};
};
}
function withProfilePresence(
providerKey: string,
build: () => ProviderConfig | Promise<ProviderConfig>,
): ImplicitProviderLoader {
return async (ctx) => {
if (listProfilesForProvider(ctx.authStore, providerKey).length === 0) {
return undefined;
}
return {
[providerKey]: await build(),
};
};
}
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;
}
}
const SIMPLE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
withApiKey("minimax", async ({ apiKey }) => ({ ...buildMinimaxProvider(), apiKey })),
withApiKey("moonshot", async ({ apiKey }) => ({ ...buildMoonshotProvider(), apiKey })),
withApiKey("kimi-coding", async ({ apiKey }) => ({ ...buildKimiCodingProvider(), apiKey })),
withApiKey("synthetic", async ({ apiKey }) => ({ ...buildSyntheticProvider(), apiKey })),
withApiKey("venice", async ({ apiKey }) => ({ ...(await buildVeniceProvider()), apiKey })),
withApiKey("xiaomi", async ({ apiKey }) => ({ ...buildXiaomiProvider(), apiKey })),
withApiKey("vercel-ai-gateway", async ({ apiKey }) => ({
...(await buildVercelAiGatewayProvider()),
apiKey,
})),
withApiKey("together", async ({ apiKey }) => ({ ...buildTogetherProvider(), apiKey })),
withApiKey("huggingface", async ({ apiKey, discoveryApiKey }) => ({
...(await buildHuggingfaceProvider(discoveryApiKey)),
apiKey,
})),
withApiKey("qianfan", async ({ apiKey }) => ({ ...buildQianfanProvider(), apiKey })),
withApiKey("modelstudio", async ({ apiKey }) => ({ ...buildModelStudioProvider(), apiKey })),
withApiKey("openrouter", async ({ apiKey }) => ({ ...buildOpenrouterProvider(), apiKey })),
withApiKey("nvidia", async ({ apiKey }) => ({ ...buildNvidiaProvider(), apiKey })),
withApiKey("kilocode", async ({ apiKey }) => ({
...(await buildKilocodeProviderWithDiscovery()),
apiKey,
})),
];
const PROFILE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
async (ctx) => {
const envKey = resolveEnvApiKeyVarName("minimax-portal", ctx.env);
const hasProfiles = listProfilesForProvider(ctx.authStore, "minimax-portal").length > 0;
if (!envKey && !hasProfiles) {
return undefined;
}
return {
"minimax-portal": {
...buildMinimaxPortalProvider(),
apiKey: MINIMAX_OAUTH_MARKER,
},
};
},
withProfilePresence("qwen-portal", async () => ({
...buildQwenPortalProvider(),
apiKey: QWEN_OAUTH_MARKER,
})),
withProfilePresence("openai-codex", async () => buildOpenAICodexProvider()),
];
const PAIRED_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [
async (ctx) => {
const volcengineKey = ctx.resolveProviderApiKey("volcengine").apiKey;
if (!volcengineKey) {
return undefined;
}
return {
volcengine: { ...buildDoubaoProvider(), apiKey: volcengineKey },
"volcengine-plan": {
...buildDoubaoCodingProvider(),
apiKey: volcengineKey,
},
};
},
async (ctx) => {
const byteplusKey = ctx.resolveProviderApiKey("byteplus").apiKey;
if (!byteplusKey) {
return undefined;
}
return {
byteplus: { ...buildBytePlusProvider(), apiKey: byteplusKey },
"byteplus-plan": {
...buildBytePlusCodingProvider(),
apiKey: byteplusKey,
},
};
},
];
async function resolveCloudflareAiGatewayImplicitProvider(
ctx: ImplicitProviderContext,
): Promise<Record<string, ProviderConfig> | undefined> {
const cloudflareProfiles = listProfilesForProvider(ctx.authStore, "cloudflare-ai-gateway");
for (const profileId of cloudflareProfiles) {
const cred = ctx.authStore.profiles[profileId];
if (cred?.type !== "api_key") {
continue;
}
const accountId = cred.metadata?.accountId?.trim();
const gatewayId = cred.metadata?.gatewayId?.trim();
if (!accountId || !gatewayId) {
continue;
}
const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId });
if (!baseUrl) {
continue;
}
const envVarApiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway", ctx.env);
const profileApiKey = resolveApiKeyFromCredential(cred, ctx.env)?.apiKey;
const apiKey = envVarApiKey ?? profileApiKey ?? "";
if (!apiKey) {
continue;
}
return {
"cloudflare-ai-gateway": {
baseUrl,
api: "anthropic-messages",
apiKey,
models: [buildCloudflareAiGatewayModelDefinition()],
},
};
}
return undefined;
}
async function resolveOllamaImplicitProvider(
ctx: ImplicitProviderContext,
): Promise<Record<string, ProviderConfig> | undefined> {
const ollamaKey = ctx.resolveProviderApiKey("ollama").apiKey;
const explicitOllama = ctx.explicitProviders?.ollama;
const hasExplicitModels =
Array.isArray(explicitOllama?.models) && explicitOllama.models.length > 0;
if (hasExplicitModels && explicitOllama) {
return {
ollama: {
...explicitOllama,
baseUrl: resolveOllamaApiBase(explicitOllama.baseUrl),
api: explicitOllama.api ?? "ollama",
apiKey: ollamaKey ?? explicitOllama.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER,
},
};
}
const ollamaBaseUrl = explicitOllama?.baseUrl;
const hasExplicitOllamaConfig = Boolean(explicitOllama);
const ollamaProvider = await buildOllamaProvider(ollamaBaseUrl, {
quiet: !ollamaKey && !hasExplicitOllamaConfig,
});
if (ollamaProvider.models.length === 0 && !ollamaKey && !explicitOllama?.apiKey) {
return undefined;
}
return {
ollama: {
...ollamaProvider,
apiKey: ollamaKey ?? explicitOllama?.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER,
},
};
}
async function resolveVllmImplicitProvider(
ctx: ImplicitProviderContext,
): Promise<Record<string, ProviderConfig> | undefined> {
if (ctx.explicitProviders?.vllm) {
return undefined;
}
const { apiKey: vllmKey, discoveryApiKey } = ctx.resolveProviderApiKey("vllm");
if (!vllmKey) {
return undefined;
}
return {
vllm: {
...(await buildVllmProvider({ apiKey: discoveryApiKey })),
apiKey: vllmKey,
},
};
}
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 context: ImplicitProviderContext = {
...params,
authStore,
env,
resolveProviderApiKey,
};
for (const loader of SIMPLE_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
for (const loader of PROFILE_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
for (const loader of PAIRED_IMPLICIT_PROVIDER_LOADERS) {
mergeImplicitProviderSet(providers, await loader(context));
}
mergeImplicitProviderSet(providers, await resolveCloudflareAiGatewayImplicitProvider(context));
mergeImplicitProviderSet(providers, await resolveOllamaImplicitProvider(context));
mergeImplicitProviderSet(providers, await resolveVllmImplicitProvider(context));
if (!providers["github-copilot"]) {
const implicitCopilot = await resolveImplicitCopilotProvider({
agentDir: params.agentDir,
env,
});
if (implicitCopilot) {
providers["github-copilot"] = implicitCopilot;
}
}
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 resolveImplicitCopilotProvider(params: {
agentDir: string;
env?: NodeJS.ProcessEnv;
}): Promise<ProviderConfig | null> {
const env = params.env ?? process.env;
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const hasProfile = listProfilesForProvider(authStore, "github-copilot").length > 0;
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
const githubToken = (envToken ?? "").trim();
if (!hasProfile && !githubToken) {
return null;
}
let selectedGithubToken = githubToken;
if (!selectedGithubToken && hasProfile) {
// Use the first available profile as a default for discovery (it will be
// re-resolved per-run by the embedded runner).
const profileId = listProfilesForProvider(authStore, "github-copilot")[0];
const profile = profileId ? authStore.profiles[profileId] : undefined;
if (profile && profile.type === "token") {
selectedGithubToken = profile.token?.trim() ?? "";
if (!selectedGithubToken) {
const tokenRef = coerceSecretRef(profile.tokenRef);
if (tokenRef?.source === "env" && tokenRef.id.trim()) {
selectedGithubToken = (env[tokenRef.id] ?? process.env[tokenRef.id] ?? "").trim();
}
}
}
}
let baseUrl = DEFAULT_COPILOT_API_BASE_URL;
if (selectedGithubToken) {
try {
const token = await resolveCopilotApiToken({
githubToken: selectedGithubToken,
env,
});
baseUrl = token.baseUrl;
} catch {
baseUrl = DEFAULT_COPILOT_API_BASE_URL;
}
}
// We deliberately do not write pi-coding-agent auth.json here.
// OpenClaw keeps auth in auth-profiles and resolves runtime availability from that store.
// We intentionally do NOT define custom models for Copilot in models.json.
// pi-coding-agent treats providers with models as replacements requiring apiKey.
// We only override baseUrl; the model list comes from pi-ai built-ins.
return {
baseUrl,
models: [],
} satisfies ProviderConfig;
}
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;
}