Files
adolf/extensions/amazon-bedrock/discovery.test.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

773 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Amazon Bedrock tests cover discovery plugin behavior.
import type { BedrockClient } from "@aws-sdk/client-bedrock";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
discoverBedrockModels,
mergeImplicitBedrockProvider,
resetBedrockDiscoveryCacheForTest,
resolveBedrockConfigApiKey,
resolveImplicitBedrockProvider,
} from "./api.js";
const sendMock = vi.fn();
const clientFactory = () => ({ send: sendMock }) as unknown as BedrockClient;
const baseActiveAnthropicSummary = {
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
modelName: "Claude 3.7 Sonnet",
providerName: "anthropic",
inputModalities: ["TEXT"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
};
function mockSingleActiveSummary(overrides: Partial<typeof baseActiveAnthropicSummary> = {}): void {
sendMock
.mockResolvedValueOnce({
modelSummaries: [{ ...baseActiveAnthropicSummary, ...overrides }],
})
// ListInferenceProfiles response (empty — no inference profiles in basic tests).
.mockResolvedValueOnce({ inferenceProfileSummaries: [] });
}
function expectModelFields(model: unknown, expected: Record<string, unknown>): void {
if (!model || typeof model !== "object") {
throw new Error("Expected model record");
}
const actual = model as Record<string, unknown>;
for (const [key, value] of Object.entries(expected)) {
expect(actual[key]).toEqual(value);
}
}
describe("bedrock discovery", () => {
beforeEach(() => {
sendMock.mockClear();
resetBedrockDiscoveryCacheForTest();
});
afterEach(() => {
resetBedrockDiscoveryCacheForTest();
});
it("filters to active streaming text models and maps modalities", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
modelName: "Claude 3.7 Sonnet",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
{
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
modelName: "Claude 3 Haiku",
providerName: "anthropic",
inputModalities: ["TEXT"],
outputModalities: ["TEXT"],
responseStreamingSupported: false,
modelLifecycle: { status: "ACTIVE" },
},
{
modelId: "meta.llama3-8b-instruct-v1:0",
modelName: "Llama 3 8B",
providerName: "meta",
inputModalities: ["TEXT"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "INACTIVE" },
},
{
modelId: "amazon.titan-embed-text-v1",
modelName: "Titan Embed",
providerName: "amazon",
inputModalities: ["TEXT"],
outputModalities: ["EMBEDDING"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({ inferenceProfileSummaries: [] });
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: "anthropic.claude-3-7-sonnet-20250219-v1:0",
name: "Claude 3.7 Sonnet",
reasoning: false,
input: ["text", "image"],
contextWindow: 200000,
maxTokens: 4096,
});
});
it("applies provider filter", async () => {
mockSingleActiveSummary();
const models = await discoverBedrockModels({
region: "us-east-1",
config: { providerFilter: ["amazon"] },
clientFactory,
});
expect(models).toHaveLength(0);
});
it("uses configured defaults for context and max tokens", async () => {
mockSingleActiveSummary({
modelId: "example.unknown-text-v1:0",
modelName: "Example Unknown Text",
providerName: "example",
});
const models = await discoverBedrockModels({
region: "us-east-1",
config: { defaultContextWindow: 64000, defaultMaxTokens: 8192 },
clientFactory,
});
expectModelFields(models[0], { contextWindow: 64000, maxTokens: 8192 });
});
it("keeps the conservative fallback for unknown inference profiles", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "jp.example.unknown-text-v1:0",
inferenceProfileName: "JP Example Unknown Text",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn:
"arn:aws:bedrock:ap-northeast-1::foundation-model/example.unknown-text-v1:0",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "ap-northeast-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: "jp.example.unknown-text-v1:0",
contextWindow: 32000,
maxTokens: 4096,
input: ["text"],
});
});
it("marks known Fable inference profile fallbacks as reasoning capable", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-fable-5",
inferenceProfileName: "US Claude Fable 5",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toHaveLength(1);
expectModelFields(models[0], {
id: "us.anthropic.claude-fable-5",
reasoning: true,
contextWindow: 1_000_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
});
});
it("skips Mythos Preview inference profiles because Mantle owns that route", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-mythos-preview",
inferenceProfileName: "US Claude Mythos Preview",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn:
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-mythos-preview",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expect(models).toEqual([]);
});
it("normalizes region-prefixed versioned model ids when resolving context windows", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "jp.anthropic.claude-sonnet-4-6-v1:0",
inferenceProfileName: "JP Claude Sonnet 4.6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn:
"arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-sonnet-4-6-v1:0",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "ap-northeast-1", clientFactory });
expectModelFields(models[0], {
id: "jp.anthropic.claude-sonnet-4-6-v1:0",
contextWindow: 1_000_000,
});
});
it("uses 1M context window for dotted Claude Opus 4.8 Bedrock refs", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-opus-4.8-v1:0",
modelName: "Claude Opus 4.8",
providerName: "anthropic",
inputModalities: ["TEXT"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-opus-4.8-v1:0",
inferenceProfileName: "US Claude Opus 4.8",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn:
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4.8-v1:0",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expectModelFields(
models.find((model) => model.id === "anthropic.claude-opus-4.8-v1:0"),
{
contextWindow: 1_000_000,
reasoning: true,
thinkingLevelMap: { xhigh: "xhigh", max: "max" },
},
);
expectModelFields(
models.find((model) => model.id === "us.anthropic.claude-opus-4.8-v1:0"),
{
contextWindow: 1_000_000,
reasoning: true,
thinkingLevelMap: { xhigh: "xhigh", max: "max" },
},
);
});
it("applies Fable limits and reasoning metadata to foundation and profile models", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-fable-5",
modelName: "Claude Fable 5",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "company-fable",
inferenceProfileName: "Company Fable",
status: "ACTIVE",
type: "APPLICATION",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-fable-5",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
const expected = {
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 128_000,
thinkingLevelMap: { off: "low", minimal: "low", xhigh: "xhigh", max: "max" },
};
expectModelFields(
models.find((model) => model.id === "anthropic.claude-fable-5"),
expected,
);
expectModelFields(
models.find((model) => model.id === "company-fable"),
{
...expected,
params: { canonicalModelId: "claude-fable-5" },
},
);
});
it("caches results when refreshInterval is enabled", async () => {
mockSingleActiveSummary();
await discoverBedrockModels({ region: "us-east-1", clientFactory });
await discoverBedrockModels({ region: "us-east-1", clientFactory });
// 2 calls on first discovery (ListFoundationModels + ListInferenceProfiles), 0 on cached second.
expect(sendMock).toHaveBeenCalledTimes(2);
});
it("skips cache when refreshInterval expiry overflows", async () => {
sendMock
.mockResolvedValueOnce({ modelSummaries: [baseActiveAnthropicSummary] })
.mockResolvedValueOnce({ inferenceProfileSummaries: [] })
.mockResolvedValueOnce({ modelSummaries: [baseActiveAnthropicSummary] })
.mockResolvedValueOnce({ inferenceProfileSummaries: [] });
await discoverBedrockModels({
region: "us-east-1",
config: { refreshInterval: 1 },
now: () => 8_640_000_000_000_000,
clientFactory,
});
await discoverBedrockModels({
region: "us-east-1",
config: { refreshInterval: 1 },
now: () => 8_640_000_000_000_000,
clientFactory,
});
expect(sendMock).toHaveBeenCalledTimes(4);
});
it("skips cache when refreshInterval is 0", async () => {
sendMock
.mockResolvedValueOnce({ modelSummaries: [baseActiveAnthropicSummary] })
.mockResolvedValueOnce({ inferenceProfileSummaries: [] })
.mockResolvedValueOnce({ modelSummaries: [baseActiveAnthropicSummary] })
.mockResolvedValueOnce({ inferenceProfileSummaries: [] });
await discoverBedrockModels({
region: "us-east-1",
config: { refreshInterval: 0 },
clientFactory,
});
await discoverBedrockModels({
region: "us-east-1",
config: { refreshInterval: 0 },
clientFactory,
});
// 2 calls per discovery (ListFoundationModels + ListInferenceProfiles) × 2 runs.
expect(sendMock).toHaveBeenCalledTimes(4);
});
it("resolves the Bedrock config apiKey from AWS auth env vars", () => {
expect(
resolveBedrockConfigApiKey({
AWS_BEARER_TOKEN_BEDROCK: "bearer", // pragma: allowlist secret
AWS_PROFILE: "default",
}),
).toBe("AWS_BEARER_TOKEN_BEDROCK");
// When no AWS env vars are present (e.g. instance role), no marker should be injected.
// The aws-sdk credential chain handles auth at request time. (#49891)
expect(resolveBedrockConfigApiKey({} as NodeJS.ProcessEnv)).toBeUndefined();
// When AWS_PROFILE is explicitly set, it should return the marker.
expect(resolveBedrockConfigApiKey({ AWS_PROFILE: "default" } as NodeJS.ProcessEnv)).toBe(
"AWS_PROFILE",
);
});
it("discovers inference profiles and inherits foundation model capabilities", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-sonnet-4-6",
modelName: "Claude Sonnet 4.6",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.anthropic.claude-sonnet-4-6",
inferenceProfileName: "US Anthropic Claude Sonnet 4.6",
inferenceProfileArn:
"arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-sonnet-4-6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-6",
},
{
modelArn: "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-sonnet-4-6",
},
],
},
{
inferenceProfileId: "eu.anthropic.claude-sonnet-4-6",
inferenceProfileName: "EU Anthropic Claude Sonnet 4.6",
inferenceProfileArn:
"arn:aws:bedrock:eu-west-1::inference-profile/eu.anthropic.claude-sonnet-4-6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-sonnet-4-6",
},
],
},
{
inferenceProfileId: "global.anthropic.claude-sonnet-4-6",
inferenceProfileName: "Global Anthropic Claude Sonnet 4.6",
inferenceProfileArn:
"arn:aws:bedrock:us-east-1::inference-profile/global.anthropic.claude-sonnet-4-6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-6",
},
],
},
// Inactive profile should be filtered out.
{
inferenceProfileId: "ap.anthropic.claude-sonnet-4-6",
inferenceProfileName: "AP Claude Sonnet 4.6",
status: "LEGACY",
type: "SYSTEM_DEFINED",
models: [],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
// Foundation model + 3 active inference profiles = 4 models.
expect(models).toHaveLength(4);
// Global profiles should be sorted first (recommended for most users).
expect(models[0]?.id).toBe("global.anthropic.claude-sonnet-4-6");
const foundationModel = models.find((m) => m.id === "anthropic.claude-sonnet-4-6");
const usProfile = models.find((m) => m.id === "us.anthropic.claude-sonnet-4-6");
const euProfile = models.find((m) => m.id === "eu.anthropic.claude-sonnet-4-6");
const globalProfile = models.find((m) => m.id === "global.anthropic.claude-sonnet-4-6");
// Foundation model has image input.
expectModelFields(foundationModel, { input: ["text", "image"] });
// Inference profiles inherit image input from the foundation model.
expectModelFields(usProfile, {
name: "US Anthropic Claude Sonnet 4.6",
input: ["text", "image"],
contextWindow: 1000000,
maxTokens: 4096,
params: { canonicalModelId: "claude-sonnet-4-6" },
});
expect(usProfile?.thinkingLevelMap).toBeUndefined();
expectModelFields(euProfile, { input: ["text", "image"] });
expectModelFields(globalProfile, { input: ["text", "image"] });
// Inactive profile should not be present.
expect(models.find((m) => m.id === "ap.anthropic.claude-sonnet-4-6")).toBeUndefined();
});
it("gracefully handles ListInferenceProfiles permission errors", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [baseActiveAnthropicSummary],
})
// Simulate AccessDeniedException for ListInferenceProfiles.
.mockRejectedValueOnce(new Error("AccessDeniedException"));
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
// Foundation model should still be discovered despite profile discovery failure.
expect(models).toHaveLength(1);
expect(models[0]?.id).toBe("anthropic.claude-3-7-sonnet-20250219-v1:0");
});
it("keeps matching inference profiles when provider filters are enabled", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-sonnet-4-6",
modelName: "Claude Sonnet 4.6",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "global.anthropic.claude-sonnet-4-6",
inferenceProfileName: "Global Anthropic Claude Sonnet 4.6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-6",
},
],
},
],
});
const models = await discoverBedrockModels({
region: "us-east-1",
config: { providerFilter: ["anthropic"] },
clientFactory,
});
expect(models.map((model) => model.id)).toEqual([
"global.anthropic.claude-sonnet-4-6",
"anthropic.claude-sonnet-4-6",
]);
});
it("prefers backing model ARNs for application profiles with region-like ids", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-sonnet-4-6",
modelName: "Claude Sonnet 4.6",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.my-prod-profile",
inferenceProfileName: "Prod Claude Profile",
status: "ACTIVE",
type: "APPLICATION",
models: [
{
modelArn: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-6",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
const profile = models.find((model) => model.id === "us.my-prod-profile");
expectModelFields(profile, {
id: "us.my-prod-profile",
input: ["text", "image"],
contextWindow: 1000000,
maxTokens: 4096,
});
});
it("uses the resolved base model id for application-profile context fallback", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "us.my-prod-profile",
inferenceProfileName: "Prod Claude Profile",
status: "ACTIVE",
type: "APPLICATION",
models: [
{
modelArn:
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1:0",
},
],
},
],
});
const models = await discoverBedrockModels({ region: "us-east-1", clientFactory });
expectModelFields(models[0], {
id: "us.my-prod-profile",
contextWindow: 1_000_000,
maxTokens: 4096,
input: ["text"],
params: { canonicalModelId: "claude-opus-4-6-v1:0" },
thinkingLevelMap: { xhigh: null, max: "max" },
});
});
it("merges implicit Bedrock models into explicit provider overrides", () => {
expect(
mergeImplicitBedrockProvider({
existing: {
baseUrl: "https://override.example.com",
headers: { "x-test-header": "1" },
models: [],
},
implicit: {
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
api: "bedrock-converse-stream",
auth: "aws-sdk",
models: [
{
id: "amazon.nova-micro-v1:0",
name: "Nova",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1,
maxTokens: 1,
},
],
},
}).models?.map((model) => model.id),
).toEqual(["amazon.nova-micro-v1:0"]);
});
it("uses plugin-owned discovery config without runtime legacy fallback", async () => {
mockSingleActiveSummary();
const pluginEnabled = await resolveImplicitBedrockProvider({
pluginConfig: {
discovery: {
enabled: true,
region: "us-east-1",
},
},
env: {} as NodeJS.ProcessEnv,
clientFactory,
});
expect(pluginEnabled?.baseUrl).toBe("https://bedrock-runtime.us-east-1.amazonaws.com");
// 2 calls per discovery (ListFoundationModels + ListInferenceProfiles).
expect(sendMock).toHaveBeenCalledTimes(2);
});
// Ported from #65449 by @alickgithub2 — extended to also cover apac. prefix
it("resolves au. and apac. prefixes for regional inference profiles", async () => {
sendMock
.mockResolvedValueOnce({
modelSummaries: [
{
modelId: "anthropic.claude-sonnet-4-6",
modelName: "Claude Sonnet 4.6",
providerName: "anthropic",
inputModalities: ["TEXT", "IMAGE"],
outputModalities: ["TEXT"],
responseStreamingSupported: true,
modelLifecycle: { status: "ACTIVE" },
},
],
})
.mockResolvedValueOnce({
inferenceProfileSummaries: [
{
inferenceProfileId: "au.anthropic.claude-sonnet-4-6",
inferenceProfileName: "AU Anthropic Claude Sonnet 4.6",
inferenceProfileArn:
"arn:aws:bedrock:ap-southeast-2::inference-profile/au.anthropic.claude-sonnet-4-6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [], // no ARNs — forces the prefix-regex fallback
},
{
inferenceProfileId: "apac.anthropic.claude-sonnet-4-6",
inferenceProfileName: "APAC Anthropic Claude Sonnet 4.6",
inferenceProfileArn:
"arn:aws:bedrock:ap-northeast-1::inference-profile/apac.anthropic.claude-sonnet-4-6",
status: "ACTIVE",
type: "SYSTEM_DEFINED",
models: [],
},
],
});
const models = await discoverBedrockModels({ region: "ap-southeast-2", clientFactory });
// Foundation model + 2 regional inference profiles
expect(models).toHaveLength(3);
const auProfile = models.find((m) => m.id === "au.anthropic.claude-sonnet-4-6");
expectModelFields(auProfile, {
id: "au.anthropic.claude-sonnet-4-6",
name: "AU Anthropic Claude Sonnet 4.6",
input: ["text", "image"],
});
const apacProfile = models.find((m) => m.id === "apac.anthropic.claude-sonnet-4-6");
expectModelFields(apacProfile, {
id: "apac.anthropic.claude-sonnet-4-6",
name: "APAC Anthropic Claude Sonnet 4.6",
input: ["text", "image"],
});
});
});