Skip to content

Commit bd00adc

Browse files
committed
fix(media-understanding): allow aws-sdk auth mode in image tool
The image tool unconditionally called requireApiKey which throws when no static API key is present. For amazon-bedrock using IAM role or instance profile credentials (auth.mode === 'aws-sdk'), the AWS SDK resolves credentials at call time and no static key exists. Add requireApiKeyAllowAwsSdk helper that returns an empty string for aws-sdk mode instead of throwing, and use it in resolveImageRuntime, resolveMinimaxVlmFallbackRuntime, and resolveProviderExecutionAuth. AI-assisted (Claude Code via Hermes orchestration). Fixes #72031
1 parent 3e2e265 commit bd00adc

6 files changed

Lines changed: 70 additions & 7 deletions

File tree

src/agents/model-auth-runtime-shared.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ export function requireApiKey(auth: ResolvedProviderAuth, provider: string): str
3232
}
3333
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`);
3434
}
35+
36+
export function requireApiKeyAllowAwsSdk(auth: ResolvedProviderAuth, provider: string): string {
37+
if (auth.mode === "aws-sdk") {
38+
return normalizeSecretInput(auth.apiKey) ?? "";
39+
}
40+
return requireApiKey(auth, provider);
41+
}

src/agents/model-auth.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ let applyAuthHeaderOverride: typeof import("./model-auth.js").applyAuthHeaderOve
8888
let applyLocalNoAuthHeaderOverride: typeof import("./model-auth.js").applyLocalNoAuthHeaderOverride;
8989
let hasUsableCustomProviderApiKey: typeof import("./model-auth.js").hasUsableCustomProviderApiKey;
9090
let requireApiKey: typeof import("./model-auth.js").requireApiKey;
91+
let requireApiKeyAllowAwsSdk: typeof import("./model-auth.js").requireApiKeyAllowAwsSdk;
9192
let resolveApiKeyForProvider: typeof import("./model-auth.js").resolveApiKeyForProvider;
9293
let resolveAwsSdkEnvVarName: typeof import("./model-auth.js").resolveAwsSdkEnvVarName;
9394
let resolveModelAuthMode: typeof import("./model-auth.js").resolveModelAuthMode;
@@ -105,6 +106,7 @@ beforeAll(async () => {
105106
applyLocalNoAuthHeaderOverride,
106107
hasUsableCustomProviderApiKey,
107108
requireApiKey,
109+
requireApiKeyAllowAwsSdk,
108110
resolveApiKeyForProvider,
109111
resolveAwsSdkEnvVarName,
110112
resolveModelAuthMode,

src/agents/model-auth.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ import {
4343
import { normalizeProviderId } from "./model-selection.js";
4444

4545
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
46-
export { requireApiKey, resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js";
46+
export {
47+
requireApiKey,
48+
requireApiKeyAllowAwsSdk,
49+
resolveAwsSdkEnvVarName,
50+
} from "./model-auth-runtime-shared.js";
4751
export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
4852
export type ProviderCredentialPrecedence = "profile-first" | "env-first";
4953

src/media-understanding/image.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const hoisted = vi.hoisted(() => ({
1414
mode: "oauth",
1515
})),
1616
requireApiKeyMock: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? ""),
17+
requireApiKeyAllowAwsSdkMock: vi.fn((auth: { apiKey?: string }) => auth.apiKey ?? ""),
1718
setRuntimeApiKeyMock: vi.fn(),
1819
discoverModelsMock: vi.fn(),
1920
fetchMock: vi.fn(),
@@ -27,6 +28,7 @@ const {
2728
getApiKeyForModelMock,
2829
resolveApiKeyForProviderMock,
2930
requireApiKeyMock,
31+
requireApiKeyAllowAwsSdkMock,
3032
setRuntimeApiKeyMock,
3133
discoverModelsMock,
3234
fetchMock,
@@ -60,6 +62,7 @@ vi.mock("../agents/model-auth.js", () => ({
6062
getApiKeyForModel: getApiKeyForModelMock,
6163
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
6264
requireApiKey: requireApiKeyMock,
65+
requireApiKeyAllowAwsSdk: requireApiKeyAllowAwsSdkMock,
6366
}));
6467

6568
vi.mock("../agents/provider-stream.js", () => ({
@@ -145,7 +148,7 @@ describe("describeImageWithModel", () => {
145148
expect(getApiKeyForModelMock).toHaveBeenCalledWith(
146149
expect.objectContaining({ store: authStore }),
147150
);
148-
expect(requireApiKeyMock).toHaveBeenCalled();
151+
expect(requireApiKeyAllowAwsSdkMock).toHaveBeenCalled();
149152
expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("minimax-portal", "oauth-test");
150153
expect(fetchMock).toHaveBeenCalledWith(
151154
"https://api.minimax.io/v1/coding_plan/vlm",
@@ -613,4 +616,51 @@ describe("describeImageWithModel", () => {
613616
);
614617
expect(setRuntimeApiKeyMock).toHaveBeenCalledWith("google", "oauth-test");
615618
});
619+
620+
it("does not throw for amazon-bedrock aws-sdk auth mode without an API key", async () => {
621+
getApiKeyForModelMock.mockResolvedValueOnce({
622+
apiKey: "",
623+
source: "aws-sdk",
624+
mode: "aws-sdk",
625+
});
626+
discoverModelsMock.mockReturnValue({
627+
find: vi.fn(() => ({
628+
provider: "amazon-bedrock",
629+
id: "anthropic.claude-sonnet-4-6",
630+
api: "anthropic-messages",
631+
input: ["text", "image"],
632+
})),
633+
});
634+
completeMock.mockResolvedValue({
635+
role: "assistant",
636+
api: "anthropic-messages",
637+
provider: "amazon-bedrock",
638+
model: "anthropic.claude-sonnet-4-6",
639+
stopReason: "stop",
640+
timestamp: Date.now(),
641+
content: [{ type: "text", text: "bedrock ok" }],
642+
});
643+
requireApiKeyAllowAwsSdkMock.mockReturnValueOnce("");
644+
645+
const result = await describeImageWithModel({
646+
cfg: {},
647+
agentDir: "/tmp/openclaw-agent",
648+
provider: "amazon-bedrock",
649+
model: "anthropic.claude-sonnet-4-6",
650+
buffer: Buffer.from("png-bytes"),
651+
fileName: "image.png",
652+
mime: "image/png",
653+
prompt: "Describe the image.",
654+
timeoutMs: 1000,
655+
});
656+
657+
expect(result).toEqual({
658+
text: "bedrock ok",
659+
model: "anthropic.claude-sonnet-4-6",
660+
});
661+
expect(requireApiKeyAllowAwsSdkMock).toHaveBeenCalledWith(
662+
expect.objectContaining({ mode: "aws-sdk" }),
663+
"amazon-bedrock",
664+
);
665+
});
616666
});

src/media-understanding/image.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { complete } from "@mariozechner/pi-ai";
33
import { isMinimaxVlmModel, minimaxUnderstandImage } from "../agents/minimax-vlm.js";
44
import {
55
getApiKeyForModel,
6-
requireApiKey,
6+
requireApiKeyAllowAwsSdk,
77
resolveApiKeyForProvider,
88
} from "../agents/model-auth.js";
99
import { findNormalizedProviderValue, normalizeModelRef } from "../agents/model-selection.js";
@@ -202,7 +202,7 @@ async function resolveImageRuntime(params: {
202202
preferredProfile: params.preferredProfile,
203203
store: params.authStore,
204204
});
205-
const apiKey = requireApiKey(apiKeyInfo, model.provider);
205+
const apiKey = requireApiKeyAllowAwsSdk(apiKeyInfo, model.provider);
206206
authStorage.setRuntimeApiKey(model.provider, apiKey);
207207
return { apiKey, model };
208208
}
@@ -304,7 +304,7 @@ async function resolveMinimaxVlmFallbackRuntime(params: {
304304
agentDir: params.agentDir,
305305
});
306306
return {
307-
apiKey: requireApiKey(auth, params.provider),
307+
apiKey: requireApiKeyAllowAwsSdk(auth, params.provider),
308308
modelBaseUrl: resolveConfiguredProviderBaseUrl(params.cfg, params.provider),
309309
};
310310
}

src/media-understanding/runner.entries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
collectProviderApiKeysForExecution,
55
executeWithApiKeyRotation,
66
} from "../agents/api-key-rotation.js";
7-
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
7+
import { requireApiKeyAllowAwsSdk, resolveApiKeyForProvider } from "../agents/model-auth.js";
88
import {
99
mergeModelProviderRequestOverrides,
1010
sanitizeConfiguredModelProviderRequest,
@@ -404,7 +404,7 @@ async function resolveProviderExecutionAuth(params: {
404404
return {
405405
apiKeys: collectProviderApiKeysForExecution({
406406
provider: params.providerId,
407-
primaryApiKey: requireApiKey(auth, params.providerId),
407+
primaryApiKey: requireApiKeyAllowAwsSdk(auth, params.providerId),
408408
}),
409409
providerConfig: params.cfg.models?.providers?.[params.providerId],
410410
};

0 commit comments

Comments
 (0)