Compare commits
3 Commits
main
...
feat/bedro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df39f19c1 | ||
|
|
9ca5cb2db9 | ||
|
|
7f25523d89 |
@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Gateway: strip inbound envelope headers from chat history messages to keep clients clean.
|
- 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.
|
- 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
|
## 2026.1.19-2
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
}): Promise<{ apiKey: string; profileId?: string; source: string }> {
|
}): Promise<{ apiKey: string; profileId?: string; source: string }> {
|
||||||
const { provider, cfg, profileId, preferredProfile } = params;
|
const { provider, cfg, profileId, preferredProfile } = params;
|
||||||
const store = params.store ?? ensureAuthProfileStore(params.agentDir);
|
const store = params.store ?? ensureAuthProfileStore(params.agentDir);
|
||||||
|
const normalized = normalizeProviderId(provider);
|
||||||
|
|
||||||
if (profileId) {
|
if (profileId) {
|
||||||
const resolved = await resolveApiKeyForProfile({
|
const resolved = await resolveApiKeyForProfile({
|
||||||
@ -88,6 +89,10 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
return { apiKey: customKey, source: "models.json" };
|
return { apiKey: customKey, source: "models.json" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalized === "amazon-bedrock") {
|
||||||
|
return { apiKey: "<authenticated>", source: "aws-sdk" };
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === "openai") {
|
if (provider === "openai") {
|
||||||
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
|
const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0;
|
||||||
if (hasCodex) {
|
if (hasCodex) {
|
||||||
@ -120,6 +125,12 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
|
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
|
||||||
return { apiKey: value, source };
|
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") {
|
if (normalized === "github-copilot") {
|
||||||
return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN");
|
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" };
|
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") {
|
if (normalized === "opencode") {
|
||||||
return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY");
|
return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,7 +143,10 @@ export function normalizeProviders(params: {
|
|||||||
provider: normalizedKey,
|
provider: normalizedKey,
|
||||||
store: authStore,
|
store: authStore,
|
||||||
});
|
});
|
||||||
const apiKey = fromEnv ?? fromProfiles;
|
const apiKey =
|
||||||
|
fromEnv ??
|
||||||
|
fromProfiles ??
|
||||||
|
(normalizedKey === "amazon-bedrock" ? "AWS_PROFILE" : undefined);
|
||||||
if (apiKey?.trim()) {
|
if (apiKey?.trim()) {
|
||||||
mutated = true;
|
mutated = true;
|
||||||
normalizedProvider = { ...normalizedProvider, apiKey };
|
normalizedProvider = { ...normalizedProvider, apiKey };
|
||||||
|
|||||||
@ -39,10 +39,10 @@ export function resolveModel(
|
|||||||
if (!model) {
|
if (!model) {
|
||||||
const providers = cfg?.models?.providers ?? {};
|
const providers = cfg?.models?.providers ?? {};
|
||||||
const inlineModels =
|
const inlineModels =
|
||||||
providers[provider]?.models ??
|
providers[provider]?.models?.map((entry) => ({ ...entry, provider })) ??
|
||||||
Object.values(providers)
|
Object.entries(providers).flatMap(([providerId, entry]) =>
|
||||||
.flatMap((entry) => entry?.models ?? [])
|
(entry?.models ?? []).map((modelEntry) => ({ ...modelEntry, provider: providerId })),
|
||||||
.map((entry) => ({ ...entry, provider }));
|
);
|
||||||
const inlineMatch = inlineModels.find((entry) => entry.id === modelId);
|
const inlineMatch = inlineModels.find((entry) => entry.id === modelId);
|
||||||
if (inlineMatch) {
|
if (inlineMatch) {
|
||||||
const normalized = normalizeModelCompat(inlineMatch as Model<Api>);
|
const normalized = normalizeModelCompat(inlineMatch as Model<Api>);
|
||||||
|
|||||||
@ -3,7 +3,8 @@ export type ModelApi =
|
|||||||
| "openai-responses"
|
| "openai-responses"
|
||||||
| "anthropic-messages"
|
| "anthropic-messages"
|
||||||
| "google-generative-ai"
|
| "google-generative-ai"
|
||||||
| "github-copilot";
|
| "github-copilot"
|
||||||
|
| "bedrock-converse-stream";
|
||||||
|
|
||||||
export type ModelCompatConfig = {
|
export type ModelCompatConfig = {
|
||||||
supportsStore?: boolean;
|
supportsStore?: boolean;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const ModelApiSchema = z.union([
|
|||||||
z.literal("anthropic-messages"),
|
z.literal("anthropic-messages"),
|
||||||
z.literal("google-generative-ai"),
|
z.literal("google-generative-ai"),
|
||||||
z.literal("github-copilot"),
|
z.literal("github-copilot"),
|
||||||
|
z.literal("bedrock-converse-stream"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ModelCompatSchema = z
|
export const ModelCompatSchema = z
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user