Merge 9a2c873d33fe25a2e2c20765b08845fc98cf9b66 into 8a05c05596ca9ba0735dafd8e359885de4c2c969
This commit is contained in:
commit
b2e583d59e
@ -20,6 +20,104 @@ describe("amazon-bedrock provider plugin", () => {
|
|||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enables prompt caching for Application Inference Profile ARNs with Claude model name", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile";
|
||||||
|
const result = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: arn,
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
models: [{ id: arn, name: "Claude Sonnet 4.6 via Inference Profile" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
// Should return the original streamFn (no no-cache wrapper)
|
||||||
|
expect(result).toBe(baseFn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables prompt caching for inference profile when config uses provider alias", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile";
|
||||||
|
const result = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: arn,
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
// "bedrock" is a known alias that normalizeProviderId maps to "amazon-bedrock"
|
||||||
|
bedrock: {
|
||||||
|
models: [{ id: arn, name: "Claude Sonnet 4.6 via Inference Profile" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
// Should return the original streamFn even when config key is "bedrock" alias
|
||||||
|
expect(result).toBe(baseFn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables prompt caching for Application Inference Profile ARNs with non-Claude model name", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/llama-profile";
|
||||||
|
const wrapped = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: arn,
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
models: [{ id: arn, name: "Llama 2 via Inference Profile" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapped?.(
|
||||||
|
{ api: "openai-completions", provider: "amazon-bedrock", id: arn } as never,
|
||||||
|
{ messages: [] } as never,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
).toMatchObject({ cacheRetention: "none" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables prompt caching for Application Inference Profile ARNs with no config entry", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/unknown-profile";
|
||||||
|
const wrapped = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: arn,
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
wrapped?.(
|
||||||
|
{ api: "openai-completions", provider: "amazon-bedrock", id: arn } as never,
|
||||||
|
{ messages: [] } as never,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
).toMatchObject({ cacheRetention: "none" });
|
||||||
|
});
|
||||||
|
|
||||||
it("disables prompt caching for non-Anthropic Bedrock models", () => {
|
it("disables prompt caching for non-Anthropic Bedrock models", () => {
|
||||||
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
const wrapped = provider.wrapStreamFn?.({
|
const wrapped = provider.wrapStreamFn?.({
|
||||||
@ -42,4 +140,74 @@ describe("amazon-bedrock provider plugin", () => {
|
|||||||
cacheRetention: "none",
|
cacheRetention: "none",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("injects region from bedrockDiscovery config into stream options", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const wrapped = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: "eu.anthropic.claude-sonnet-4-6",
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
bedrockDiscovery: { region: "eu-west-1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const result = wrapped?.(
|
||||||
|
{
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
id: "eu.anthropic.claude-sonnet-4-6",
|
||||||
|
} as never,
|
||||||
|
{ messages: [] } as never,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({ region: "eu-west-1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("injects region extracted from provider baseUrl into stream options", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const wrapped = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: "eu.anthropic.claude-sonnet-4-6",
|
||||||
|
config: {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const result = wrapped?.(
|
||||||
|
{
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
id: "eu.anthropic.claude-sonnet-4-6",
|
||||||
|
} as never,
|
||||||
|
{ messages: [] } as never,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({ region: "eu-central-1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not inject region when neither bedrockDiscovery nor baseUrl is configured", () => {
|
||||||
|
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||||
|
const baseFn = (_model: never, _context: never, options: Record<string, unknown>) => options;
|
||||||
|
const result = provider.wrapStreamFn?.({
|
||||||
|
provider: "amazon-bedrock",
|
||||||
|
modelId: "anthropic.claude-sonnet-4-6",
|
||||||
|
streamFn: baseFn,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
// Without region config, Claude model returns the base streamFn directly
|
||||||
|
expect(result).toBe(baseFn);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||||
|
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-models";
|
||||||
import {
|
import {
|
||||||
createBedrockNoCacheWrapper,
|
createBedrockNoCacheWrapper,
|
||||||
isAnthropicBedrockModel,
|
isAnthropicBedrockModel,
|
||||||
@ -7,6 +8,15 @@ import {
|
|||||||
const PROVIDER_ID = "amazon-bedrock";
|
const PROVIDER_ID = "amazon-bedrock";
|
||||||
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
||||||
|
|
||||||
|
/** Extract the AWS region from a bedrock-runtime baseUrl, e.g. "https://bedrock-runtime.eu-west-1.amazonaws.com". */
|
||||||
|
function extractRegionFromBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||||
|
if (!baseUrl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const match = /bedrock-runtime\.([a-z0-9-]+)\.amazonaws\.com/.exec(baseUrl);
|
||||||
|
return match?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
export default definePluginEntry({
|
export default definePluginEntry({
|
||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
name: "Amazon Bedrock Provider",
|
name: "Amazon Bedrock Provider",
|
||||||
@ -17,8 +27,60 @@ export default definePluginEntry({
|
|||||||
label: "Amazon Bedrock",
|
label: "Amazon Bedrock",
|
||||||
docsPath: "/providers/models",
|
docsPath: "/providers/models",
|
||||||
auth: [],
|
auth: [],
|
||||||
wrapStreamFn: ({ modelId, streamFn }) =>
|
wrapStreamFn: ({ modelId, config, streamFn }) => {
|
||||||
isAnthropicBedrockModel(modelId) ? streamFn : createBedrockNoCacheWrapper(streamFn),
|
// Look up model name and region from provider config.
|
||||||
|
// Use normalized key matching so aliases like "bedrock" / "aws-bedrock" are found.
|
||||||
|
let modelName: string | undefined;
|
||||||
|
let providerBaseUrl: string | undefined;
|
||||||
|
const providers = config?.models?.providers;
|
||||||
|
if (providers) {
|
||||||
|
for (const [key, value] of Object.entries(providers)) {
|
||||||
|
if (normalizeProviderId(key) !== PROVIDER_ID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const typedValue = value as {
|
||||||
|
baseUrl?: string;
|
||||||
|
models?: Array<{ id?: string; name?: string }>;
|
||||||
|
};
|
||||||
|
if (!providerBaseUrl && typedValue.baseUrl) {
|
||||||
|
providerBaseUrl = typedValue.baseUrl;
|
||||||
|
}
|
||||||
|
const modelDef = typedValue.models?.find((m) => m.id === modelId);
|
||||||
|
if (modelDef?.name) {
|
||||||
|
modelName = modelDef.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract region from provider baseUrl or bedrockDiscovery config so the
|
||||||
|
// pi-ai BedrockRuntimeClient uses the correct endpoint. Without this, the
|
||||||
|
// gateway process (which may not inherit AWS_REGION) falls back to us-east-1
|
||||||
|
// and rejects cross-region inference profile IDs like "eu.anthropic.claude-*".
|
||||||
|
const region =
|
||||||
|
config?.models?.bedrockDiscovery?.region ?? extractRegionFromBaseUrl(providerBaseUrl);
|
||||||
|
|
||||||
|
const baseFn = isAnthropicBedrockModel(modelId, modelName)
|
||||||
|
? streamFn
|
||||||
|
: createBedrockNoCacheWrapper(streamFn);
|
||||||
|
|
||||||
|
if (!region) {
|
||||||
|
return baseFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap to inject the region into every stream call.
|
||||||
|
const underlying = baseFn ?? streamFn;
|
||||||
|
if (!underlying) {
|
||||||
|
return baseFn;
|
||||||
|
}
|
||||||
|
return (model, context, options) => {
|
||||||
|
// pi-ai's bedrock provider reads `options.region` at runtime but the
|
||||||
|
// StreamFn type does not declare it. Merge via Object.assign to avoid
|
||||||
|
// an unsafe type assertion.
|
||||||
|
const merged = Object.assign({}, options, { region });
|
||||||
|
return underlying(model, context, merged);
|
||||||
|
};
|
||||||
|
},
|
||||||
resolveDefaultThinkingLevel: ({ modelId }) =>
|
resolveDefaultThinkingLevel: ({ modelId }) =>
|
||||||
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
|
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
156
src/agents/pi-embedded-runner/anthropic-stream-wrappers.test.ts
Normal file
156
src/agents/pi-embedded-runner/anthropic-stream-wrappers.test.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isAnthropicBedrockModel } from "./anthropic-stream-wrappers.js";
|
||||||
|
|
||||||
|
describe("isAnthropicBedrockModel", () => {
|
||||||
|
describe("standard Bedrock model IDs", () => {
|
||||||
|
it("should return true for standard Anthropic Bedrock model IDs with dot notation", () => {
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-3-opus-20240229-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-3-sonnet-20240229-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-3-haiku-20240307-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-v2:1")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-instant-v1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for standard Anthropic Bedrock model IDs with slash notation", () => {
|
||||||
|
expect(isAnthropicBedrockModel("anthropic/claude-3-opus-20240229-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("anthropic/claude-3-sonnet-20240229-v1:0")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for new US Anthropic Bedrock model IDs", () => {
|
||||||
|
expect(isAnthropicBedrockModel("us.anthropic.claude-opus-4-6-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("us.anthropic.claude-sonnet-4-6-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("us.anthropic.claude-haiku-4-5-v1:0")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle case-insensitive matching", () => {
|
||||||
|
expect(isAnthropicBedrockModel("ANTHROPIC.CLAUDE-3-OPUS-20240229-V1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("Anthropic.Claude-3-Opus-20240229-v1:0")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non-Anthropic Bedrock model IDs", () => {
|
||||||
|
expect(isAnthropicBedrockModel("amazon.titan-text-express-v1")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("amazon.titan-embed-text-v1")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("meta.llama2-13b-chat-v1")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("cohere.command-text-v14")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("ai21.j2-ultra-v1")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("stability.stable-diffusion-xl-v1")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Application Inference Profile ARNs", () => {
|
||||||
|
it("should return true for Application Inference Profile ARNs when model name contains 'claude'", () => {
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile";
|
||||||
|
expect(isAnthropicBedrockModel(arn, "Claude 3 Opus Profile")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "My Claude Model")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "claude-profile")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "CLAUDE-PROFILE")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for Application Inference Profile ARNs when model name does not contain 'claude'", () => {
|
||||||
|
const arn =
|
||||||
|
"arn:aws:bedrock:us-west-2:123456789012:application-inference-profile/llama-profile";
|
||||||
|
expect(isAnthropicBedrockModel(arn, "Llama 2 Profile")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "Titan Model")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "General Model")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for Application Inference Profile ARNs when neither profile ID nor model name contains 'claude'", () => {
|
||||||
|
const arn = "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile";
|
||||||
|
expect(isAnthropicBedrockModel(arn)).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel(arn, undefined)).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel(arn, "Titan Model")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for Application Inference Profile ARNs when profile ID contains 'claude' (no model name needed)", () => {
|
||||||
|
expect(
|
||||||
|
isAnthropicBedrockModel(
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isAnthropicBedrockModel(
|
||||||
|
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/claude-sonnet",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle Application Inference Profile ARNs with various formats", () => {
|
||||||
|
expect(
|
||||||
|
isAnthropicBedrockModel(
|
||||||
|
"arn:aws:bedrock:eu-west-1:987654321098:application-inference-profile/prod-claude",
|
||||||
|
"Production Claude Model",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isAnthropicBedrockModel(
|
||||||
|
"arn:aws:bedrock:ap-southeast-1:111222333444:application-inference-profile/test",
|
||||||
|
"Test Claude Instance",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("short Application Inference Profile IDs", () => {
|
||||||
|
it("should return true when short ID is provided with model name containing 'claude'", () => {
|
||||||
|
expect(isAnthropicBedrockModel("my-profile-id", "Claude Profile")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("prod-inference", "Production Claude")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("test-profile", "claude-test")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when short ID is provided with model name not containing 'claude'", () => {
|
||||||
|
expect(isAnthropicBedrockModel("my-profile-id", "Titan Profile")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("prod-inference", "Production Llama")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("test-profile", "test-model")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for short IDs without model name and without 'claude' in ID", () => {
|
||||||
|
expect(isAnthropicBedrockModel("my-profile-id")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("prod-inference")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("test-profile")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for short IDs containing 'claude' without model name", () => {
|
||||||
|
expect(isAnthropicBedrockModel("my-claude-profile")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("claude-sonnet-profile")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("should handle empty strings", () => {
|
||||||
|
expect(isAnthropicBedrockModel("")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("", "")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("", "Claude Model")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle model names with 'claude' in different positions", () => {
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "claude")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "claude-at-start")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "middle-claude-here")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "ends-with-claude")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "CLAUDE")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "ClAuDe")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should match claude as a substring in any position within model names", () => {
|
||||||
|
// These should still match because they contain 'claude' as a substring
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "unclaude")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("some-id", "claudette")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle special characters in model IDs and names", () => {
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-3_opus@v1:0")).toBe(true);
|
||||||
|
// IDs with special characters don't match short profile ID pattern
|
||||||
|
expect(isAnthropicBedrockModel("model-with-special-chars!@#", "Claude Model!")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("backward compatibility", () => {
|
||||||
|
it("should maintain backward compatibility when modelName parameter is not provided", () => {
|
||||||
|
// These should work exactly as before when no modelName is provided
|
||||||
|
expect(isAnthropicBedrockModel("anthropic.claude-3-opus-20240229-v1:0")).toBe(true);
|
||||||
|
expect(isAnthropicBedrockModel("amazon.titan-text-express-v1")).toBe(false);
|
||||||
|
expect(isAnthropicBedrockModel("us.anthropic.claude-opus-4-6-v1:0")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -390,7 +390,52 @@ export function createBedrockNoCacheWrapper(baseStreamFn: StreamFn | undefined):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAnthropicBedrockModel(modelId: string): boolean {
|
export function isAnthropicBedrockModel(modelId: string, modelName?: string): boolean {
|
||||||
const normalized = modelId.toLowerCase();
|
const normalized = modelId.toLowerCase();
|
||||||
return normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude");
|
|
||||||
|
// Direct Anthropic Claude model IDs (e.g., anthropic.claude-sonnet-4-6, global.anthropic.claude-opus-4-6-v1)
|
||||||
|
if (normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application Inference Profile ARN — detect Claude via profile ID segment or model name.
|
||||||
|
// ARN format: arn:<partition>:bedrock:<region>:<account>:application-inference-profile/<id>
|
||||||
|
// Supports all AWS partitions with Bedrock: aws, aws-cn, aws-us-gov.
|
||||||
|
// Note: model name (`models[].name`) is a user-chosen display label, so this is best-effort.
|
||||||
|
// A profile ID or name containing "claude" is treated as an Anthropic Claude model; if neither
|
||||||
|
// contains "claude", the no-cache wrapper is applied (safe default).
|
||||||
|
if (
|
||||||
|
/^arn:aws(-cn|-us-gov)?:bedrock:/.test(normalized) &&
|
||||||
|
normalized.includes(":application-inference-profile/")
|
||||||
|
) {
|
||||||
|
const profileId = normalized.split(":application-inference-profile/")[1] ?? "";
|
||||||
|
if (profileId.includes("claude")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return modelName ? modelName.toLowerCase().includes("claude") : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short/opaque inference profile IDs — fall back to ID or model name for IDs
|
||||||
|
// that look like short profile IDs (alphanumeric, no dots/colons/slashes).
|
||||||
|
// Excludes standard model IDs, other ARN resource types, and any dotted identifiers.
|
||||||
|
// Note: the regex is intentionally broad; it is safe because standard Bedrock model IDs
|
||||||
|
// always contain dots or colons (e.g. "amazon.nova-micro-v1:0") which exclude them here.
|
||||||
|
if (looksLikeShortProfileId(normalized)) {
|
||||||
|
if (normalized.includes("claude")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return modelName ? modelName.toLowerCase().includes("claude") : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the ID looks like a short Application Inference Profile ID
|
||||||
|
* (opaque alphanumeric-and-hyphen string, cannot start/end with a hyphen).
|
||||||
|
* Requires at least 2 characters; single-char IDs are not realistic model IDs.
|
||||||
|
* Examples: "gdkqufd9flgg", "s3rr0t98ews8", "my-claude-profile"
|
||||||
|
*/
|
||||||
|
function looksLikeShortProfileId(normalizedId: string): boolean {
|
||||||
|
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(normalizedId);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user