fix: support Bedrock auth + inline provider ids (#1286) (thanks @alauppe)

This commit is contained in:
Peter Steinberger 2026-01-20 07:02:52 +00:00
parent 9ca5cb2db9
commit 8df39f19c1
5 changed files with 124 additions and 4 deletions

View File

@ -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

View File

@ -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("<authenticated>");
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("<authenticated>");
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;
}
}
});
});

View File

@ -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: "<authenticated>", 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: "<authenticated>", 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: "<authenticated>",
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");
}

View File

@ -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 };

View File

@ -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<Api>);