fix(bedrock): pass configured region to pi-ai BedrockRuntimeClient
The gateway process may not inherit AWS_REGION from the shell environment, causing pi-ai to fall back to us-east-1. This breaks cross-region inference profile IDs (eu.*, us.*, global.*) when bedrockDiscovery.region is set to a different region. Extract the region from bedrockDiscovery.region (primary) or the provider baseUrl (fallback) and inject it into stream options so the Bedrock client connects to the correct regional endpoint.
This commit is contained in:
parent
8e3987d66f
commit
1ba19140d5
@ -140,4 +140,74 @@ describe("amazon-bedrock provider plugin", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,6 +8,15 @@ import {
|
||||
const PROVIDER_ID = "amazon-bedrock";
|
||||
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({
|
||||
id: PROVIDER_ID,
|
||||
name: "Amazon Bedrock Provider",
|
||||
@ -19,26 +28,58 @@ export default definePluginEntry({
|
||||
docsPath: "/providers/models",
|
||||
auth: [],
|
||||
wrapStreamFn: ({ modelId, config, streamFn }) => {
|
||||
// Look up model name from provider config for inference profile detection.
|
||||
// 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 models = (value as { models?: Array<{ id?: string; name?: string }> })?.models;
|
||||
const modelDef = models?.find((m) => m.id === modelId);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAnthropicBedrockModel(modelId, modelName)
|
||||
|
||||
// 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 }) =>
|
||||
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user