From 8df39f19c1bd0fb5139d1b2e0d38ac7afa05c128 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 20 Jan 2026 07:02:52 +0000 Subject: [PATCH] fix: support Bedrock auth + inline provider ids (#1286) (thanks @alauppe) --- CHANGELOG.md | 1 + src/agents/model-auth.test.ts | 90 ++++++++++++++++++++++++++ src/agents/model-auth.ts | 26 ++++++++ src/agents/models-config.providers.ts | 5 +- src/agents/pi-embedded-runner/model.ts | 6 +- 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6b90e84..06ae0db29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot ### Fixes - Gateway: strip inbound envelope headers from chat history messages to keep clients clean. - UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) — thanks @bradleypriest. +- Models: allow Bedrock custom providers without API keys and preserve inline provider IDs. (#1286) — thanks @alauppe. ## 2026.1.19-2 diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index ad1bccc90..8840f3869 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -280,4 +280,94 @@ describe("getApiKeyForModel", () => { } } }); + + it("accepts AWS profile auth for amazon-bedrock", async () => { + const previousProfile = process.env.AWS_PROFILE; + const previousAccess = process.env.AWS_ACCESS_KEY_ID; + const previousSecret = process.env.AWS_SECRET_ACCESS_KEY; + const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK; + + try { + process.env.AWS_PROFILE = "bedrock-test"; + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_BEARER_TOKEN_BEDROCK; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "amazon-bedrock", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe(""); + expect(resolved.source).toContain("AWS_PROFILE"); + } finally { + if (previousProfile === undefined) { + delete process.env.AWS_PROFILE; + } else { + process.env.AWS_PROFILE = previousProfile; + } + if (previousAccess === undefined) { + delete process.env.AWS_ACCESS_KEY_ID; + } else { + process.env.AWS_ACCESS_KEY_ID = previousAccess; + } + if (previousSecret === undefined) { + delete process.env.AWS_SECRET_ACCESS_KEY; + } else { + process.env.AWS_SECRET_ACCESS_KEY = previousSecret; + } + if (previousBearer === undefined) { + delete process.env.AWS_BEARER_TOKEN_BEDROCK; + } else { + process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer; + } + } + }); + + it("allows amazon-bedrock without an API key", async () => { + const previousProfile = process.env.AWS_PROFILE; + const previousAccess = process.env.AWS_ACCESS_KEY_ID; + const previousSecret = process.env.AWS_SECRET_ACCESS_KEY; + const previousBearer = process.env.AWS_BEARER_TOKEN_BEDROCK; + + try { + delete process.env.AWS_PROFILE; + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_BEARER_TOKEN_BEDROCK; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "amazon-bedrock", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe(""); + expect(resolved.source).toBe("aws-sdk"); + } finally { + if (previousProfile === undefined) { + delete process.env.AWS_PROFILE; + } else { + process.env.AWS_PROFILE = previousProfile; + } + if (previousAccess === undefined) { + delete process.env.AWS_ACCESS_KEY_ID; + } else { + process.env.AWS_ACCESS_KEY_ID = previousAccess; + } + if (previousSecret === undefined) { + delete process.env.AWS_SECRET_ACCESS_KEY; + } else { + process.env.AWS_SECRET_ACCESS_KEY = previousSecret; + } + if (previousBearer === undefined) { + delete process.env.AWS_BEARER_TOKEN_BEDROCK; + } else { + process.env.AWS_BEARER_TOKEN_BEDROCK = previousBearer; + } + } + }); }); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 8e53bd21c..d22479441 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -36,6 +36,7 @@ export async function resolveApiKeyForProvider(params: { }): Promise<{ apiKey: string; profileId?: string; source: string }> { const { provider, cfg, profileId, preferredProfile } = params; const store = params.store ?? ensureAuthProfileStore(params.agentDir); + const normalized = normalizeProviderId(provider); if (profileId) { const resolved = await resolveApiKeyForProfile({ @@ -88,6 +89,10 @@ export async function resolveApiKeyForProvider(params: { return { apiKey: customKey, source: "models.json" }; } + if (normalized === "amazon-bedrock") { + return { apiKey: "", source: "aws-sdk" }; + } + if (provider === "openai") { const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0; if (hasCodex) { @@ -120,6 +125,12 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`; return { apiKey: value, source }; }; + const pickPresent = (envVar: string): EnvApiKeyResult | null => { + const value = process.env[envVar]?.trim(); + if (!value) return null; + const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`; + return { apiKey: "", source }; + }; if (normalized === "github-copilot") { return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN"); @@ -143,6 +154,21 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { return { apiKey: envKey, source: "gcloud adc" }; } + if (normalized === "amazon-bedrock") { + return ( + pickPresent("AWS_BEARER_TOKEN_BEDROCK") ?? + pickPresent("AWS_PROFILE") ?? + (process.env.AWS_ACCESS_KEY_ID?.trim() && process.env.AWS_SECRET_ACCESS_KEY?.trim() + ? { + apiKey: "", + source: applied.has("AWS_ACCESS_KEY_ID") + ? "shell env: AWS_ACCESS_KEY_ID" + : "env: AWS_ACCESS_KEY_ID", + } + : null) + ); + } + if (normalized === "opencode") { return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY"); } diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 0acb6aaaf..3a515ff3d 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -143,7 +143,10 @@ export function normalizeProviders(params: { provider: normalizedKey, store: authStore, }); - const apiKey = fromEnv ?? fromProfiles; + const apiKey = + fromEnv ?? + fromProfiles ?? + (normalizedKey === "amazon-bedrock" ? "AWS_PROFILE" : undefined); if (apiKey?.trim()) { mutated = true; normalizedProvider = { ...normalizedProvider, apiKey }; diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 2d146f80b..e2ece13bd 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -40,9 +40,9 @@ export function resolveModel( const providers = cfg?.models?.providers ?? {}; const inlineModels = providers[provider]?.models?.map((entry) => ({ ...entry, provider })) ?? - Object.values(providers) - .flatMap((entry) => entry?.models ?? []) - .map((entry) => ({ ...entry, provider })); + Object.entries(providers).flatMap(([providerId, entry]) => + (entry?.models ?? []).map((modelEntry) => ({ ...modelEntry, provider: providerId })), + ); const inlineMatch = inlineModels.find((entry) => entry.id === modelId); if (inlineMatch) { const normalized = normalizeModelCompat(inlineMatch as Model);