From 7e4a76a6d00a682ae519992bade11a61b49096a7 Mon Sep 17 00:00:00 2001 From: Wynne668 Date: Sun, 19 Jul 2026 05:38:51 +0800 Subject: [PATCH] fix(amazon-bedrock): ignore blank region env overrides (#110676) * fix(amazon-bedrock): ignore blank region env overrides * fix(amazon-bedrock): normalize region envs across runtimes Co-authored-by: ZengWen-DT --------- Co-authored-by: Peter Steinberger --- .../amazon-bedrock-mantle/discovery.test.ts | 32 +++++++++++++++ extensions/amazon-bedrock-mantle/discovery.ts | 11 ++++- extensions/amazon-bedrock/discovery.test.ts | 28 +++++++++++++ extensions/amazon-bedrock/discovery.ts | 7 +++- .../amazon-bedrock/embedding-provider.test.ts | 41 ++++++++++++++++++- .../amazon-bedrock/embedding-provider.ts | 5 ++- .../amazon-bedrock/stream.runtime.test.ts | 19 ++++++++- extensions/amazon-bedrock/stream.runtime.ts | 7 +++- 8 files changed, 141 insertions(+), 9 deletions(-) diff --git a/extensions/amazon-bedrock-mantle/discovery.test.ts b/extensions/amazon-bedrock-mantle/discovery.test.ts index 5efe210b838f..275aa37407fd 100644 --- a/extensions/amazon-bedrock-mantle/discovery.test.ts +++ b/extensions/amazon-bedrock-mantle/discovery.test.ts @@ -710,6 +710,38 @@ describe("bedrock mantle discovery", () => { objectArgAt(mockFetch, 0, 1); }); + it.each([ + { + name: "the fallback region when the primary env is blank", + env: { AWS_REGION: " ", AWS_DEFAULT_REGION: "eu-west-1" }, + expectedRegion: "eu-west-1", + }, + { + name: "the default region when both env values are blank", + env: { AWS_REGION: "", AWS_DEFAULT_REGION: "\t" }, + expectedRegion: "us-east-1", + }, + ])("uses $name", async ({ env, expectedRegion }) => { + const mockFetch = vi + .fn() + .mockResolvedValue( + modelDiscoveryResponse({ data: [{ id: "openai.gpt-oss-120b", object: "model" }] }), + ); + + const provider = await resolveImplicitMantleProvider({ + env: { + AWS_BEARER_TOKEN_BEDROCK: MANTLE_IAM_TOKEN_MARKER, + ...env, + } as NodeJS.ProcessEnv, + fetchFn: mockFetch as unknown as typeof fetch, + }); + + expect(provider?.baseUrl).toBe(`https://bedrock-mantle.${expectedRegion}.api.aws/v1`); + expect(stringArgAt(mockFetch, 0, 0)).toBe( + `https://bedrock-mantle.${expectedRegion}.api.aws/v1/models`, + ); + }); + // --------------------------------------------------------------------------- // Provider merging // --------------------------------------------------------------------------- diff --git a/extensions/amazon-bedrock-mantle/discovery.ts b/extensions/amazon-bedrock-mantle/discovery.ts index 4020c5fd66f6..352aa52dd6f2 100644 --- a/extensions/amazon-bedrock-mantle/discovery.ts +++ b/extensions/amazon-bedrock-mantle/discovery.ts @@ -13,7 +13,10 @@ import type { ModelProviderConfig, } from "openclaw/plugin-sdk/provider-model-shared"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/string-coerce-runtime"; const log = createSubsystemLogger("bedrock-mantle-discovery"); @@ -116,7 +119,11 @@ const iamTokenCache = new Map(); const IAM_TOKEN_TTL_MS = 7200_000; // Matches the 2h token lifetime we request below. function resolveMantleRegion(env: NodeJS.ProcessEnv): string { - return env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1"; + return ( + normalizeOptionalString(env.AWS_REGION) ?? + normalizeOptionalString(env.AWS_DEFAULT_REGION) ?? + "us-east-1" + ); } function getCachedIamTokenEntry( diff --git a/extensions/amazon-bedrock/discovery.test.ts b/extensions/amazon-bedrock/discovery.test.ts index 7f5daf2a91bd..cf0e21164539 100644 --- a/extensions/amazon-bedrock/discovery.test.ts +++ b/extensions/amazon-bedrock/discovery.test.ts @@ -795,6 +795,34 @@ describe("bedrock discovery", () => { expect(sendMock).toHaveBeenCalledTimes(2); }); + it.each([ + { + name: "secondary region when the primary env override is blank", + env: { AWS_REGION: " ", AWS_DEFAULT_REGION: "eu-west-1" }, + expectedRegion: "eu-west-1", + }, + { + name: "plugin default when both region env overrides are blank", + env: { AWS_REGION: "", AWS_DEFAULT_REGION: " " }, + expectedRegion: "us-east-1", + }, + { + name: "primary region when both env overrides are nonblank", + env: { AWS_REGION: "ap-southeast-2", AWS_DEFAULT_REGION: "eu-west-1" }, + expectedRegion: "ap-southeast-2", + }, + ])("uses $name", async ({ env, expectedRegion }) => { + mockSingleActiveSummary(); + + const provider = await resolveImplicitBedrockProvider({ + pluginConfig: { discovery: { enabled: true } }, + env, + clientFactory, + }); + + expect(provider?.baseUrl).toBe(`https://bedrock-runtime.${expectedRegion}.amazonaws.com`); + }); + // Ported from #65449 by @alickgithub2 — extended to also cover apac. prefix it("resolves au. and apac. prefixes for regional inference profiles", async () => { sendMock diff --git a/extensions/amazon-bedrock/discovery.ts b/extensions/amazon-bedrock/discovery.ts index c54b11d3c335..f3c55e96cd64 100644 --- a/extensions/amazon-bedrock/discovery.ts +++ b/extensions/amazon-bedrock/discovery.ts @@ -28,6 +28,7 @@ import { import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, + normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js"; import { @@ -675,7 +676,11 @@ export async function resolveImplicitBedrockProvider(params: { return null; } - const region = discoveryConfig?.region ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION ?? "us-east-1"; + const region = + discoveryConfig?.region ?? + normalizeOptionalString(env.AWS_REGION) ?? + normalizeOptionalString(env.AWS_DEFAULT_REGION) ?? + "us-east-1"; const models = await discoverBedrockModels({ region, config: discoveryConfig, diff --git a/extensions/amazon-bedrock/embedding-provider.test.ts b/extensions/amazon-bedrock/embedding-provider.test.ts index d2a309b8d4f0..f9e989c46abf 100644 --- a/extensions/amazon-bedrock/embedding-provider.test.ts +++ b/extensions/amazon-bedrock/embedding-provider.test.ts @@ -1,8 +1,45 @@ // Amazon Bedrock tests cover embedding provider plugin behavior. -import { describe, expect, it, vi } from "vitest"; -import { hasAwsCredentials } from "./embedding-provider.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createBedrockEmbeddingProvider, hasAwsCredentials } from "./embedding-provider.js"; import { embeddingTesting as testing } from "./test-support.js"; +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe("bedrock embedding region resolution", () => { + it.each([ + { + name: "secondary region when the primary env override is blank", + primary: " ", + secondary: "eu-west-1", + expected: "eu-west-1", + }, + { + name: "plugin default when both env overrides are blank", + primary: "", + secondary: " ", + expected: "us-east-1", + }, + { + name: "primary region when both env overrides are nonblank", + primary: "ap-southeast-2", + secondary: "eu-west-1", + expected: "ap-southeast-2", + }, + ])("uses $name", async ({ primary, secondary, expected }) => { + vi.stubEnv("AWS_REGION", primary); + vi.stubEnv("AWS_DEFAULT_REGION", secondary); + + const { client } = await createBedrockEmbeddingProvider({ + config: {}, + model: "", + }); + + expect(client.region).toBe(expected); + }); +}); + describe("hasAwsCredentials", () => { it("accepts static AWS key credentials without loading the credential chain", async () => { const loadCredentialProvider = vi.fn(); diff --git a/extensions/amazon-bedrock/embedding-provider.ts b/extensions/amazon-bedrock/embedding-provider.ts index 761a11ebe653..c33a5d6421ed 100644 --- a/extensions/amazon-bedrock/embedding-provider.ts +++ b/extensions/amazon-bedrock/embedding-provider.ts @@ -11,6 +11,7 @@ import { import { asOptionalRecord as asRecord, normalizeLowercaseStringOrEmpty, + normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js"; @@ -406,8 +407,8 @@ function resolveBedrockEmbeddingClient( const region = regionFromUrl(options.remote?.baseUrl) ?? regionFromUrl(providerConfig?.baseUrl) ?? - process.env.AWS_REGION ?? - process.env.AWS_DEFAULT_REGION ?? + normalizeOptionalString(process.env.AWS_REGION) ?? + normalizeOptionalString(process.env.AWS_DEFAULT_REGION) ?? "us-east-1"; let dimensions: number | undefined; diff --git a/extensions/amazon-bedrock/stream.runtime.test.ts b/extensions/amazon-bedrock/stream.runtime.test.ts index ecfdad442359..1b1dcbe59dbe 100644 --- a/extensions/amazon-bedrock/stream.runtime.test.ts +++ b/extensions/amazon-bedrock/stream.runtime.test.ts @@ -238,6 +238,20 @@ describe("Bedrock profile endpoint resolution", () => { ambientRegion: "eu-west-1", expectedRegion: "eu-west-1", }, + { + name: "blank primary region with a fallback env", + modelId: "amazon.nova-micro-v1:0", + ambientRegion: " ", + fallbackRegion: "eu-west-1", + expectedRegion: "eu-west-1", + }, + { + name: "blank region env vars", + modelId: "amazon.nova-micro-v1:0", + ambientRegion: " ", + fallbackRegion: "\t", + expectedRegion: "us-east-1", + }, { name: "application inference-profile ARN", modelId: "arn:aws:bedrock:us-west-2:123456789012:application-inference-profile/profile-abc", @@ -260,8 +274,11 @@ describe("Bedrock profile endpoint resolution", () => { }, ])( "resolves $name to $expectedRegion", - async ({ modelId, ambientRegion, explicitRegion, expectedRegion }) => { + async ({ modelId, ambientRegion, fallbackRegion, explicitRegion, expectedRegion }) => { vi.stubEnv("AWS_REGION", ambientRegion); + if (fallbackRegion !== undefined) { + vi.stubEnv("AWS_DEFAULT_REGION", fallbackRegion); + } await expect( captureClientRegion( diff --git a/extensions/amazon-bedrock/stream.runtime.ts b/extensions/amazon-bedrock/stream.runtime.ts index 55caf747d62a..ffea69ebaf4b 100644 --- a/extensions/amazon-bedrock/stream.runtime.ts +++ b/extensions/amazon-bedrock/stream.runtime.ts @@ -69,6 +69,7 @@ import { notifyLlmRequestActivity, } from "openclaw/plugin-sdk/provider-stream-shared"; import { describeToolResultMediaPlaceholder } from "openclaw/plugin-sdk/provider-transport-runtime"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { supportsBedrockPromptCaching, type BedrockOptions } from "./bedrock-options.js"; import { supportsBedrockNativeMaxEffort } from "./thinking-policy.js"; @@ -1038,7 +1039,11 @@ function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined return options.region; } - return options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined; + return ( + options.region || + normalizeOptionalString(process.env.AWS_REGION) || + normalizeOptionalString(process.env.AWS_DEFAULT_REGION) + ); } function hasConfiguredBedrockProfile(options: BedrockOptions): boolean {