Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/api/src/routes/keys-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
})
Expand Down
149 changes: 148 additions & 1 deletion apps/gateway/src/lib/openai-gpt56-pricing.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -118,3 +123,145 @@ 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). 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;

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();
},
);

// 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<string, string[]> = {
"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
// 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,
);
expect(entry.outputPrice, `${modelId} ${entry.region}`).toBe(
provider.outputPrice,
);
}
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -547,6 +569,11 @@ export function CreateProviderKeyDialog({
<SelectValue placeholder="Select region" />
</SelectTrigger>
<SelectContent>
{regionOptional && (
<SelectItem value={ANY_REGION}>
Any region (recommended)
</SelectItem>
)}
{selectedProviderDef.regionConfig.regions.map((r) => (
<SelectItem key={r.id} value={r.id}>
{r.label}
Expand All @@ -555,8 +582,9 @@ export function CreateProviderKeyDialog({
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
API keys are region-specific. Make sure your key matches the
selected region.
{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."}
</p>
</div>
)}
Expand Down
124 changes: 124 additions & 0 deletions packages/actions/src/get-provider-endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -834,4 +841,121 @@ 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("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",
"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",
);
});
});
});
Loading
Loading