From 28910c443af5ba79ddb787be345915bd47fa81aa Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sat, 1 Aug 2026 23:19:04 +0100 Subject: [PATCH 1/5] fix(models): correct AWS Mantle GPT-5.6 context + price MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS advertises a "272K" context window for GPT-5.6 Sol/Terra/Luna on Bedrock Mantle, which the catalogue read as 272,000 tokens. The real limit is 272 * 1024 = 278,528: upstream accepts prompts up to 278,527 tokens and rejects 278,528 or more with a validation_error naming the exact ceiling. Verified live against bedrock-mantle for all three models, plus a gateway request at 275,494 prompt tokens — inside the band the old value wrongly excluded from routing. Bedrock in-region inference is also priced at OpenAI's data-residency tier, a flat 10% premium over the standard first-party rates, so the mappings were undercharging by 10% on every token type. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/openai-gpt56-pricing.spec.ts | 78 +++++++++++++++++++ packages/models/src/models/openai.ts | 43 ++++++---- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts index 2129760313..b6f39daa34 100644 --- a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts +++ b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts @@ -118,3 +118,81 @@ describe("OpenAI GPT-5.6 family pricing", () => { }, ); }); + +// Bedrock Mantle in-region inference is priced at OpenAI's data-residency +// tier, a flat 10% premium over the first-party rates, and AWS caps the +// deployment at a 272 * 1024 token context (upstream rejects prompts of +// 278528 tokens or more), so the over-272K tier can never apply. +const BEDROCK_PREMIUM = 1.1; +const MANTLE_CONTEXT_SIZE = 272 * 1024; + +describe("GPT-5.6 on AWS Bedrock Mantle", () => { + const mantleEntries = models.flatMap((model) => + model.id.startsWith("gpt-5.6") + ? model.providers + .filter((provider) => provider.providerId === "aws-mantle") + .map((provider) => ({ + modelId: model.id, + provider: provider as ProviderModelMapping, + openai: model.providers.find( + (p) => p.providerId === "openai", + ) as ProviderModelMapping, + })) + : [], + ); + + it("has the three aws-mantle mappings to validate", () => { + expect(mantleEntries.map((e) => e.modelId).sort()).toEqual([ + "gpt-5.6-luna", + "gpt-5.6-sol", + "gpt-5.6-terra", + ]); + }); + + it.each(mantleEntries)( + "$modelId bills at a 10% premium over the first-party rates", + ({ modelId, provider, openai }) => { + expectRatio( + `${modelId} aws-mantle inputPrice`, + provider.inputPrice, + openai.inputPrice, + BEDROCK_PREMIUM, + ); + expectRatio( + `${modelId} aws-mantle outputPrice`, + provider.outputPrice, + openai.outputPrice, + BEDROCK_PREMIUM, + ); + expectRatio( + `${modelId} aws-mantle cachedInputPrice`, + provider.cachedInputPrice, + openai.cachedInputPrice, + BEDROCK_PREMIUM, + ); + expectRatio( + `${modelId} aws-mantle cacheWriteInputPrice`, + provider.cacheWriteInputPrice, + openai.cacheWriteInputPrice, + BEDROCK_PREMIUM, + ); + // The 1.25x cache-write relationship survives the premium. + expectRatio( + `${modelId} aws-mantle cacheWriteInputPrice vs inputPrice`, + provider.cacheWriteInputPrice, + provider.inputPrice, + CACHE_WRITE_MULTIPLIER, + ); + }, + ); + + it.each(mantleEntries)( + "$modelId caps the context at 272 * 1024 with no long-context tier", + ({ modelId, provider }) => { + expect(provider.contextSize, `${modelId}: aws-mantle context size`).toBe( + MANTLE_CONTEXT_SIZE, + ); + expect(provider.pricingTiers).toBeUndefined(); + }, + ); +}); diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index fd305de5ac..4a0bc17358 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1944,14 +1944,19 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-sol", - inputPrice: "5.0e-6", - outputPrice: "30.0e-6", - cachedInputPrice: "0.5e-6", - cacheWriteInputPrice: "6.25e-6", + // Bedrock in-region inference is priced at OpenAI's data-residency + // tier, a flat 10% premium over OpenAI's standard first-party rates. + // AWS displays the cache-write rate rounded to $6.88/M. + inputPrice: "5.5e-6", + outputPrice: "33.0e-6", + cachedInputPrice: "0.55e-6", + cacheWriteInputPrice: "6.875e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M // first-party), so OpenAI's over-272K pricing tier never applies. - contextSize: 272000, + // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 + // tokens or more. + contextSize: 278528, maxOutput: 128000, streaming: true, // Bedrock Mantle only accepts data:/s3:// image URLs; the gateway @@ -2089,14 +2094,18 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-terra", - inputPrice: "2.0e-6", - outputPrice: "12.0e-6", - cachedInputPrice: "0.2e-6", - cacheWriteInputPrice: "2.5e-6", + // Bedrock in-region inference is priced at OpenAI's data-residency + // tier, a flat 10% premium over OpenAI's standard first-party rates. + inputPrice: "2.2e-6", + outputPrice: "13.2e-6", + cachedInputPrice: "0.22e-6", + cacheWriteInputPrice: "2.75e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M // first-party), so OpenAI's over-272K pricing tier never applies. - contextSize: 272000, + // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 + // tokens or more. + contextSize: 278528, maxOutput: 128000, streaming: true, // Bedrock Mantle only accepts data:/s3:// image URLs; the gateway @@ -2234,14 +2243,18 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-luna", - inputPrice: "0.2e-6", - outputPrice: "1.2e-6", - cachedInputPrice: "0.02e-6", - cacheWriteInputPrice: "0.25e-6", + // Bedrock in-region inference is priced at OpenAI's data-residency + // tier, a flat 10% premium over OpenAI's standard first-party rates. + inputPrice: "0.22e-6", + outputPrice: "1.32e-6", + cachedInputPrice: "0.022e-6", + cacheWriteInputPrice: "0.275e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M // first-party), so OpenAI's over-272K pricing tier never applies. - contextSize: 272000, + // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 + // tokens or more. + contextSize: 278528, maxOutput: 128000, streaming: true, // Bedrock Mantle only accepts data:/s3:// image URLs; the gateway From 31767abf6b80d5736aa64726d34a9e76f3c7bee3 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 2 Aug 2026 00:23:20 +0100 Subject: [PATCH 2/5] docs(models): explain Mantle in-region-only pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AWS lists global cross-region pricing for Mantle as "coming soon" and the model cards mark Geo/Global as unsupported, so the usual Bedrock geo-premium/global-parity split does not apply — in-region is the only rate that exists, and AWS prices it at OpenAI's data-residency tier. Co-Authored-By: Claude Opus 5 (1M context) --- packages/models/src/models/openai.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index 4a0bc17358..ea2e2db38b 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1944,8 +1944,12 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-sol", - // Bedrock in-region inference is priced at OpenAI's data-residency - // tier, a flat 10% premium over OpenAI's standard first-party rates. + // Mantle is in-region-only (the model cards list Geo and Global + // cross-region as unsupported, and AWS lists global cross-region + // pricing as "coming soon"), and AWS prices in-region inference at + // OpenAI's data-residency tier — a flat 10% premium over the + // standard first-party rates. Unlike the usual Bedrock geo/global + // split there is no cheaper global rate to route to yet. // AWS displays the cache-write rate rounded to $6.88/M. inputPrice: "5.5e-6", outputPrice: "33.0e-6", @@ -2094,8 +2098,12 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-terra", - // Bedrock in-region inference is priced at OpenAI's data-residency - // tier, a flat 10% premium over OpenAI's standard first-party rates. + // Mantle is in-region-only (the model cards list Geo and Global + // cross-region as unsupported, and AWS lists global cross-region + // pricing as "coming soon"), and AWS prices in-region inference at + // OpenAI's data-residency tier — a flat 10% premium over the + // standard first-party rates. Unlike the usual Bedrock geo/global + // split there is no cheaper global rate to route to yet. inputPrice: "2.2e-6", outputPrice: "13.2e-6", cachedInputPrice: "0.22e-6", @@ -2243,8 +2251,12 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-luna", - // Bedrock in-region inference is priced at OpenAI's data-residency - // tier, a flat 10% premium over OpenAI's standard first-party rates. + // Mantle is in-region-only (the model cards list Geo and Global + // cross-region as unsupported, and AWS lists global cross-region + // pricing as "coming soon"), and AWS prices in-region inference at + // OpenAI's data-residency tier — a flat 10% premium over the + // standard first-party rates. Unlike the usual Bedrock geo/global + // split there is no cheaper global rate to route to yet. inputPrice: "0.22e-6", outputPrice: "1.32e-6", cachedInputPrice: "0.022e-6", From bfe90779af621a0d3f0a8a5c165ef3c663185bf8 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 2 Aug 2026 05:43:33 +0100 Subject: [PATCH 3/5] feat(models): make AWS Mantle region-selectable Mantle serves each model from concrete AWS regions with no cross-region inference profiles, so a single env-var-pinned region hid two thirds of the deployment. Give aws-mantle a regionConfig and per-model region lists instead, following the Alibaba pattern: every entry is a real region, there is no synthetic "global", and pinDefaultRegion stays unset so the gateway routes across regions rather than to a fixed default. Regions come from the model cards and were confirmed against the live endpoint: Sol is us-east-1/us-east-2 only (us-west-2 404s "the model does not exist"), Terra and Luna add us-west-2. Pricing is identical in every region, so the entries carry no per-region overrides. Bedrock long-term API keys are IAM-global, so sharedCredentialAcrossRegions lets one key reach every region without per-region env keys, and the provider-key dialog now says so instead of claiming keys are regional. Co-Authored-By: Claude Opus 5 (1M context) --- apps/api/src/routes/keys-provider.ts | 6 ++ .../src/lib/openai-gpt56-pricing.spec.ts | 66 +++++++++++++++++- .../create-provider-key-dialog.tsx | 5 +- .../actions/src/get-provider-endpoint.spec.ts | 69 +++++++++++++++++++ packages/actions/src/get-provider-endpoint.ts | 12 ++-- packages/db/src/schema.ts | 1 + packages/models/src/models/openai.ts | 19 +++++ packages/models/src/providers.ts | 23 +++++++ 8 files changed, 194 insertions(+), 7 deletions(-) diff --git a/apps/api/src/routes/keys-provider.ts b/apps/api/src/routes/keys-provider.ts index 8079fde386..e1cf722071 100644 --- a/apps/api/src/routes/keys-provider.ts +++ b/apps/api/src/routes/keys-provider.ts @@ -87,6 +87,9 @@ export const providerKeySchema = z.object({ alibaba_region: z .enum(["singapore", "us-virginia", "cn-beijing"]) .optional(), + aws_mantle_region: z + .enum(["us-east-1", "us-east-2", "us-west-2"]) + .optional(), vertex_openai_project_id: z.string().optional(), }) .nullable(), @@ -244,6 +247,9 @@ const createProviderKeySchema = z.object({ alibaba_region: z .enum(["singapore", "us-virginia", "cn-beijing"]) .optional(), + aws_mantle_region: z + .enum(["us-east-1", "us-east-2", "us-west-2"]) + .optional(), google_vertex_project_id: z.string().optional(), vertex_openai_project_id: z.string().optional(), }) diff --git a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts index b6f39daa34..8b8039311e 100644 --- a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts +++ b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vitest"; -import { models, type ProviderModelMapping } from "@llmgateway/models"; +import { + expandProviderRegions, + models, + providers, + type ProviderModelMapping, +} from "@llmgateway/models"; // OpenAI GPT-5.6 pricing invariants per // https://developers.openai.com/api/docs/pricing and @@ -195,4 +200,63 @@ describe("GPT-5.6 on AWS Bedrock Mantle", () => { expect(provider.pricingTiers).toBeUndefined(); }, ); + + // Sol is not deployed to us-west-2 — that region 404s "The model + // 'openai.gpt-5.6-sol' does not exist" — while Terra and Luna are in all + // three. Mantle offers no cross-region profiles, so no "global"/"us" entry + // may appear here. + const EXPECTED_REGIONS: Record = { + "gpt-5.6-sol": ["us-east-1", "us-east-2"], + "gpt-5.6-terra": ["us-east-1", "us-east-2", "us-west-2"], + "gpt-5.6-luna": ["us-east-1", "us-east-2", "us-west-2"], + }; + + it.each(mantleEntries)( + "$modelId declares exactly the regions AWS deploys it to", + ({ modelId, provider }) => { + expect(provider.regions?.map((r) => r.id)).toEqual( + EXPECTED_REGIONS[modelId], + ); + }, + ); + + it("only offers concrete AWS regions, never a cross-region profile", () => { + const def = providers.find((p) => p.id === "aws-mantle"); + const configured = def?.regionConfig?.regions.map((r) => r.id) ?? []; + + expect(configured).toEqual(["us-east-1", "us-east-2", "us-west-2"]); + // A synthetic default like aws-bedrock's `global` would be unroutable: + // Mantle has no cross-region inference profiles. + expect(def?.regionConfig?.pinDefaultRegion).toBeUndefined(); + expect(configured).not.toContain("global"); + // Every declared region needs an endpoint, and every model region must be + // one the provider actually configures. + for (const region of configured) { + expect(def?.regionConfig?.endpointMap[region]).toBe( + `https://bedrock-mantle.${region}.api.aws`, + ); + } + const usedRegions = new Set( + mantleEntries.flatMap((e) => e.provider.regions?.map((r) => r.id) ?? []), + ); + expect([...usedRegions].sort()).toEqual(configured); + }); + + it("expands each region without altering pricing", () => { + for (const { modelId, provider } of mantleEntries) { + const expanded = expandProviderRegions(provider); + // synthetic root + one entry per region + expect(expanded).toHaveLength( + (EXPECTED_REGIONS[modelId]?.length ?? 0) + 1, + ); + for (const entry of expanded) { + expect(entry.inputPrice, `${modelId} ${entry.region}`).toBe( + provider.inputPrice, + ); + expect(entry.outputPrice, `${modelId} ${entry.region}`).toBe( + provider.outputPrice, + ); + } + } + }); }); diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index 1eb5acc14b..dac9207cd9 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -555,8 +555,9 @@ export function CreateProviderKeyDialog({

- API keys are region-specific. Make sure your key matches the - selected region. + {selectedProviderDef.regionConfig.sharedCredentialAcrossRegions + ? "One key works across every region for this provider." + : "API keys are region-specific. Make sure your key matches the selected region."}

)} diff --git a/packages/actions/src/get-provider-endpoint.spec.ts b/packages/actions/src/get-provider-endpoint.spec.ts index 5dc1d36ab1..c1d89dbaa7 100644 --- a/packages/actions/src/get-provider-endpoint.spec.ts +++ b/packages/actions/src/get-provider-endpoint.spec.ts @@ -834,4 +834,73 @@ describe("getProviderEndpoint", () => { ); }); }); + + describe("aws-mantle regions", () => { + it.each([ + { region: "us-east-1" }, + { region: "us-east-2" }, + { region: "us-west-2" }, + ])("routes to the $region Mantle endpoint", ({ region }) => { + const endpoint = getProviderEndpoint( + "aws-mantle", + undefined, + "openai.gpt-5.6-terra", + undefined, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + region, + true, + ); + + expect(endpoint).toBe( + `https://bedrock-mantle.${region}.api.aws/openai/v1/responses`, + ); + }); + + it("falls back to us-east-1 when no region is selected", () => { + const endpoint = getProviderEndpoint( + "aws-mantle", + undefined, + "openai.gpt-5.6-sol", + undefined, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); + + expect(endpoint).toBe( + "https://bedrock-mantle.us-east-1.api.aws/openai/v1/responses", + ); + }); + + it("keeps an explicit base URL over the region-derived endpoint", () => { + const endpoint = getProviderEndpoint( + "aws-mantle", + "https://mantle-proxy.internal", + "openai.gpt-5.6-luna", + undefined, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + "us-west-2", + true, + ); + + expect(endpoint).toBe( + "https://mantle-proxy.internal/openai/v1/responses", + ); + }); + }); }); diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index a9159d3cb7..e25102a760 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -383,15 +383,19 @@ export function getProviderEndpoint( } case "aws-mantle": { // Bedrock Mantle: OpenAI frontier models on AWS, Responses API only. - // GPT-5.6 Sol is deployed in us-east-1/us-east-2 only (Terra/Luna - // additionally in us-west-2), so default to us-east-1 where the - // whole family is available. + // The selected region normally resolves through regionConfig's + // endpointMap; the env var stays supported as a deployment-level + // override, and us-east-1 is the fallback because it is the only + // region carrying the whole GPT-5.6 family (Sol is not in us-west-2). const envBaseUrl = skipEnvVars ? undefined : getProviderEnvValue("aws-mantle", "baseUrl", configIndex); const mantleRegion = envValueOrDefault("aws-mantle", "region", "us-east-1") ?? "us-east-1"; - url = envBaseUrl ?? `https://bedrock-mantle.${mantleRegion}.api.aws`; + url = + envBaseUrl ?? + regionBaseUrl ?? + `https://bedrock-mantle.${mantleRegion}.api.aws`; break; } case "azure": { diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index f01f377874..d58df64971 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1534,6 +1534,7 @@ export interface ProviderKeyOptions { azure_ai_foundry_resource?: string; azure_ai_foundry_api_version?: string; alibaba_region?: "singapore" | "us-virginia" | "cn-beijing"; + aws_mantle_region?: "us-east-1" | "us-east-2" | "us-west-2"; google_vertex_project_id?: string; google_vertex_token_type?: "api-key" | "oauth"; vertex_openai_project_id?: string; diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index ea2e2db38b..300a6f91f8 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1944,6 +1944,11 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-sol", + // Sol is the one family member AWS has not deployed to us-west-2 — + // that region 404s with "The model 'openai.gpt-5.6-sol' does not + // exist". Pricing is identical across regions, so the entries carry + // no per-region overrides. + regions: [{ id: "us-east-1" }, { id: "us-east-2" }], // Mantle is in-region-only (the model cards list Geo and Global // cross-region as unsupported, and AWS lists global cross-region // pricing as "coming soon"), and AWS prices in-region inference at @@ -2098,6 +2103,13 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-terra", + // Pricing is identical across regions, so the entries carry no + // per-region overrides. + regions: [ + { id: "us-east-1" }, + { id: "us-east-2" }, + { id: "us-west-2" }, + ], // Mantle is in-region-only (the model cards list Geo and Global // cross-region as unsupported, and AWS lists global cross-region // pricing as "coming soon"), and AWS prices in-region inference at @@ -2251,6 +2263,13 @@ export const openaiModels = [ { providerId: "aws-mantle", externalId: "openai.gpt-5.6-luna", + // Pricing is identical across regions, so the entries carry no + // per-region overrides. + regions: [ + { id: "us-east-1" }, + { id: "us-east-2" }, + { id: "us-west-2" }, + ], // Mantle is in-region-only (the model cards list Geo and Global // cross-region as unsupported, and AWS lists global cross-region // pricing as "coming soon"), and AWS prices in-region inference at diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index ba8405e1c0..419c73f6a6 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -962,6 +962,29 @@ export const providers: ProviderDefinition[] = [ website: "https://aws.amazon.com/bedrock", statusPageUrl: "https://health.aws.amazon.com/health/status", announcement: null, + regionConfig: { + optionsKey: "aws_mantle_region", + defaultRegion: "us-east-1", + // Mantle has no cross-region inference profiles at all — the model + // cards mark Geo and Global as unsupported — so every entry is a + // concrete AWS region and `pinDefaultRegion` stays unset, letting the + // gateway route across regions like Alibaba instead of pinning to a + // synthetic global default the way aws-bedrock does. + regions: [ + { id: "us-east-1", label: "US East (N. Virginia)" }, + { id: "us-east-2", label: "US East (Ohio)" }, + { id: "us-west-2", label: "US West (Oregon)" }, + ], + endpointMap: { + "us-east-1": "https://bedrock-mantle.us-east-1.api.aws", + "us-east-2": "https://bedrock-mantle.us-east-2.api.aws", + "us-west-2": "https://bedrock-mantle.us-west-2.api.aws", + }, + // Bedrock long-term API keys are IAM-global: one ABSK key authenticates + // against every regional Mantle endpoint, so non-default regions do not + // need their own `LLM_AWS_MANTLE_API_KEY__` env key. + sharedCredentialAcrossRegions: true, + }, apiKeyInstructions: "Use AWS Bedrock Long-Term API Keys (not IAM service account or private keys)", termsUrl: "https://aws.amazon.com/service-terms", From 3d916770baecd092934d3d5470186e43791ab3c2 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Sun, 2 Aug 2026 19:03:18 +0100 Subject: [PATCH 4/5] fix(ui): stop pinning shared-credential keys to a region Storing a region on a provider key locks routing to it, which is right for data residency but wrong as a default: the dialog pre-filled the provider's defaultRegion, so a BYOK user who never opened the dropdown was silently pinned to one region. For providers whose credential works everywhere (aws-mantle, aws-bedrock) the dropdown now defaults to "Any region" and stores nothing, leaving all regions as routing candidates. Region-scoped providers like Alibaba keep defaulting, since their keys really do belong to one region. Picking a region explicitly still locks it. Also addresses PR review: pass `variant` through Mantle's baseUrl lookup so variant-specific overrides are honoured, and reword the pricing-tier comments, which claimed the over-272K tier was unreachable when the real reason is that AWS never exposes it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/openai-gpt56-pricing.spec.ts | 13 +++-- .../create-provider-key-dialog.tsx | 37 +++++++++++-- .../actions/src/get-provider-endpoint.spec.ts | 55 +++++++++++++++++++ packages/actions/src/get-provider-endpoint.ts | 8 ++- packages/models/src/models/openai.ts | 21 ++++--- 5 files changed, 115 insertions(+), 19 deletions(-) diff --git a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts index 8b8039311e..020cfe54d0 100644 --- a/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts +++ b/apps/gateway/src/lib/openai-gpt56-pricing.spec.ts @@ -127,7 +127,8 @@ describe("OpenAI GPT-5.6 family pricing", () => { // Bedrock Mantle in-region inference is priced at OpenAI's data-residency // tier, a flat 10% premium over the first-party rates, and AWS caps the // deployment at a 272 * 1024 token context (upstream rejects prompts of -// 278528 tokens or more), so the over-272K tier can never apply. +// 278528 tokens or more). AWS publishes a single flat rate per model and does +// not expose OpenAI's long-context tier, so pricingTiers stays unset. const BEDROCK_PREMIUM = 1.1; const MANTLE_CONTEXT_SIZE = 272 * 1024; @@ -246,9 +247,13 @@ describe("GPT-5.6 on AWS Bedrock Mantle", () => { for (const { modelId, provider } of mantleEntries) { const expanded = expandProviderRegions(provider); // synthetic root + one entry per region - expect(expanded).toHaveLength( - (EXPECTED_REGIONS[modelId]?.length ?? 0) + 1, - ); + // The synthetic root carries no region and must come first, followed by + // exactly the declared regions — a count check alone would pass if a + // region were duplicated or mislabelled. + expect(expanded.map((entry) => entry.region)).toEqual([ + undefined, + ...EXPECTED_REGIONS[modelId], + ]); for (const entry of expanded) { expect(entry.inputPrice, `${modelId} ${entry.region}`).toBe( provider.inputPrice, diff --git a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx index dac9207cd9..904418ad6d 100644 --- a/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx +++ b/apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx @@ -81,8 +81,24 @@ export function CreateProviderKeyDialog({ (p) => p.id === selectedProvider, ) as ProviderDefinition | undefined; + // Sentinel for "let the gateway pick". Radix Select cannot hold an empty + // string value, so the no-preference choice needs its own id. + const ANY_REGION = "__any__"; + + // When one credential works in every region (AWS), don't pre-select a region: + // storing one pins the key to it and forfeits cross-region failover, for no + // gain when the regions are priced identically. Providers whose keys are + // region-scoped (Alibaba — a Singapore key does not work in Beijing) keep + // defaulting, since the key really does belong to one region. + const regionOptional = + selectedProviderDef?.regionConfig?.sharedCredentialAcrossRegions === true; + const effectiveRegion = - (selectedRegion || selectedProviderDef?.regionConfig?.defaultRegion) ?? ""; + (selectedRegion || + (regionOptional + ? ANY_REGION + : selectedProviderDef?.regionConfig?.defaultRegion)) ?? + ""; // Exclude the gateway itself and stealth providers (no default base URL): // users can't configure a stealth provider key because the platform behind @@ -166,8 +182,14 @@ export function CreateProviderKeyDialog({ if (selectedProvider === "custom" && customName) { payload.name = customName; } - // Include region in options for providers that support it - if (selectedProviderDef?.regionConfig && effectiveRegion) { + // Include region in options for providers that support it. Storing a + // region locks routing to it (a data-residency guarantee), so the + // no-preference choice deliberately stores nothing. + if ( + selectedProviderDef?.regionConfig && + effectiveRegion && + effectiveRegion !== ANY_REGION + ) { payload.options = { ...payload.options, [selectedProviderDef.regionConfig.optionsKey]: effectiveRegion, @@ -547,6 +569,11 @@ export function CreateProviderKeyDialog({ + {regionOptional && ( + + Any region (recommended) + + )} {selectedProviderDef.regionConfig.regions.map((r) => ( {r.label} @@ -555,8 +582,8 @@ export function CreateProviderKeyDialog({

- {selectedProviderDef.regionConfig.sharedCredentialAcrossRegions - ? "One key works across every region for this provider." + {regionOptional + ? "One key works across every region. Leave this on “Any region” to let the gateway route across all of them; pick one to keep requests in a single region." : "API keys are region-specific. Make sure your key matches the selected region."}

diff --git a/packages/actions/src/get-provider-endpoint.spec.ts b/packages/actions/src/get-provider-endpoint.spec.ts index c1d89dbaa7..808fa9788a 100644 --- a/packages/actions/src/get-provider-endpoint.spec.ts +++ b/packages/actions/src/get-provider-endpoint.spec.ts @@ -15,6 +15,7 @@ const originalXiaomiBaseUrl = process.env.LLM_XIAOMI_BASE_URL; const originalOpenaiBaseUrl = process.env.LLM_OPENAI_BASE_URL; const originalBedrockBaseUrl = process.env.LLM_AWS_BEDROCK_BASE_URL; const originalBedrockRegion = process.env.LLM_AWS_BEDROCK_REGION; +const originalMantleRegion = process.env.LLM_AWS_MANTLE_REGION; afterEach(() => { if (originalAiStudioBaseUrl === undefined) { @@ -89,6 +90,12 @@ afterEach(() => { } else { process.env.LLM_AWS_BEDROCK_REGION = originalBedrockRegion; } + + if (originalMantleRegion === undefined) { + delete process.env.LLM_AWS_MANTLE_REGION; + } else { + process.env.LLM_AWS_MANTLE_REGION = originalMantleRegion; + } }); describe("getProviderEndpoint", () => { @@ -882,6 +889,54 @@ describe("getProviderEndpoint", () => { ); }); + it("prefers the selected region over LLM_AWS_MANTLE_REGION", () => { + // The env var is a deployment-level default; a region resolved from the + // request or from a provider key's options must still win. + process.env.LLM_AWS_MANTLE_REGION = "us-east-1"; + + const endpoint = getProviderEndpoint( + "aws-mantle", + undefined, + "openai.gpt-5.6-terra", + undefined, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + "us-west-2", + false, // read env vars + ); + + expect(endpoint).toBe( + "https://bedrock-mantle.us-west-2.api.aws/openai/v1/responses", + ); + }); + + it("falls back to LLM_AWS_MANTLE_REGION when no region is selected", () => { + process.env.LLM_AWS_MANTLE_REGION = "us-east-2"; + + const endpoint = getProviderEndpoint( + "aws-mantle", + undefined, + "openai.gpt-5.6-terra", + undefined, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + false, // read env vars + ); + + expect(endpoint).toBe( + "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses", + ); + }); + it("keeps an explicit base URL over the region-derived endpoint", () => { const endpoint = getProviderEndpoint( "aws-mantle", diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index e25102a760..b660cf33c2 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -389,7 +389,13 @@ export function getProviderEndpoint( // region carrying the whole GPT-5.6 family (Sol is not in us-west-2). const envBaseUrl = skipEnvVars ? undefined - : getProviderEnvValue("aws-mantle", "baseUrl", configIndex); + : getProviderEnvValue( + "aws-mantle", + "baseUrl", + configIndex, + undefined, + variant, + ); const mantleRegion = envValueOrDefault("aws-mantle", "region", "us-east-1") ?? "us-east-1"; url = diff --git a/packages/models/src/models/openai.ts b/packages/models/src/models/openai.ts index 300a6f91f8..a7632c7639 100644 --- a/packages/models/src/models/openai.ts +++ b/packages/models/src/models/openai.ts @@ -1962,9 +1962,10 @@ export const openaiModels = [ cacheWriteInputPrice: "6.875e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M - // first-party), so OpenAI's over-272K pricing tier never applies. - // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 - // tokens or more. + // first-party). AWS's "272K" is 272 * 1024: upstream rejects prompts + // of 278528 tokens or more. No pricingTiers because AWS publishes a + // single flat rate for Mantle and does not expose OpenAI's + // long-context tier at all. contextSize: 278528, maxOutput: 128000, streaming: true, @@ -2122,9 +2123,10 @@ export const openaiModels = [ cacheWriteInputPrice: "2.75e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M - // first-party), so OpenAI's over-272K pricing tier never applies. - // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 - // tokens or more. + // first-party). AWS's "272K" is 272 * 1024: upstream rejects prompts + // of 278528 tokens or more. No pricingTiers because AWS publishes a + // single flat rate for Mantle and does not expose OpenAI's + // long-context tier at all. contextSize: 278528, maxOutput: 128000, streaming: true, @@ -2282,9 +2284,10 @@ export const openaiModels = [ cacheWriteInputPrice: "0.275e-6", requestPrice: "0", // AWS caps the Mantle deployment at a 272K context (vs 1.05M - // first-party), so OpenAI's over-272K pricing tier never applies. - // AWS's "272K" is 272 * 1024: upstream rejects prompts of 278528 - // tokens or more. + // first-party). AWS's "272K" is 272 * 1024: upstream rejects prompts + // of 278528 tokens or more. No pricingTiers because AWS publishes a + // single flat rate for Mantle and does not expose OpenAI's + // long-context tier at all. contextSize: 278528, maxOutput: 128000, streaming: true, From 3b3fa89cfa3acfc024ffb23c48a1c0d022b77576 Mon Sep 17 00:00:00 2001 From: Luca Steeb Date: Mon, 3 Aug 2026 17:20:07 +0100 Subject: [PATCH 5/5] test(actions): cover Mantle BYOK region resolution The endpoint spec pinned region precedence by passing the region directly, which skipped the chain the BYOK path actually uses: the region stored on a provider key resolves through regionConfig.optionsKey into the request. These drive validateProviderKey from provider-key options and assert the resulting Mantle URL, plus that a key pinned to us-west-2 does not validate against Sol, which AWS does not deploy there. Co-Authored-By: Claude Opus 5 (1M context) --- .../actions/src/validate-provider-key.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/actions/src/validate-provider-key.spec.ts b/packages/actions/src/validate-provider-key.spec.ts index 7645d4f2aa..bcdf195fb9 100644 --- a/packages/actions/src/validate-provider-key.spec.ts +++ b/packages/actions/src/validate-provider-key.spec.ts @@ -153,3 +153,61 @@ describe("validateProviderKey error reporting", () => { expect(result.error).toBe("401 Unauthorized"); }); }); + +describe("validateProviderKey region resolution", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + // Covers the whole BYOK chain rather than just the endpoint helper: the + // region stored on the provider key must reach the Mantle URL. Region-aware + // providers resolve it from regionConfig.optionsKey, so a break anywhere + // between the stored option and the request would silently send traffic to + // the default region. + it.each([ + { region: "us-east-1" as const }, + { region: "us-east-2" as const }, + { region: "us-west-2" as const }, + ])( + "sends validation to $region when the provider key selects it", + async ({ region }) => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response("{}", { status: 200 })); + + const result = await validateProviderKey( + "aws-mantle", + "ABSKtest", + undefined, + false, + { aws_mantle_region: region }, + ); + + expect(result.valid).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toBe( + `https://bedrock-mantle.${region}.api.aws/openai/v1/responses`, + ); + }, + ); + + // Sol is not deployed to us-west-2, so a key pinned there must validate + // against a model that actually exists in the region. + it("picks a us-west-2 model when the key is pinned to us-west-2", async () => { + const fetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response("{}", { status: 200 })); + + const result = await validateProviderKey( + "aws-mantle", + "ABSKtest", + undefined, + false, + { aws_mantle_region: "us-west-2" }, + ); + + expect(result.model).not.toBe("gpt-5.6-sol"); + const body = JSON.parse(String(fetchMock.mock.calls[0][1]?.body)); + expect(body.model).not.toBe("openai.gpt-5.6-sol"); + }); +});