Skip to content

Commit 5ae6ff4

Browse files
chitalianclaude
andauthored
feat: add support for OpenAI US data residency endpoint (us.api.openai.com) (#5537)
* feat: add support for OpenAI US data residency endpoint (us.api.openai.com) - Update OpenAI URL pattern to accept us.api.openai.com for US data residency - Add custom base URL support in OpenAIProvider.buildUrl() via userConfig.baseUri - Add UI configuration for OpenAI Base URL in provider key settings - Enables BYOK customers to comply with OpenAI's January 12, 2026 deadline Customers can now configure their OpenAI provider key with a custom base URL (e.g., https://us.api.openai.com) to route requests to the US regional endpoint. * feat: use dropdown select for OpenAI endpoint and add validation - Replace free-form text input with dropdown select for OpenAI endpoint - Add predefined options: Default (api.openai.com) and US Data Residency (us.api.openai.com) - Add server-side validation to only allow whitelisted OpenAI base URLs - Invalid or unknown URLs fall back to the default api.openai.com endpoint --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1ab78af commit 5ae6ff4

File tree

4 files changed

+87
-7
lines changed

4 files changed

+87
-7
lines changed

packages/cost/models/providers/openai.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,47 @@
11
import { BaseProvider } from "./base";
22
import type { Endpoint, RequestParams, RequestBodyContext } from "../types";
33

4+
// Allowed OpenAI base URLs for security
5+
const ALLOWED_OPENAI_BASE_URLS = [
6+
"https://api.openai.com",
7+
"https://us.api.openai.com", // US data residency
8+
] as const;
9+
410
export class OpenAIProvider extends BaseProvider {
511
readonly displayName = "OpenAI";
612
readonly baseUrl = "https://api.openai.com";
713
readonly auth = "api-key" as const;
814
readonly pricingPages = ["https://openai.com/api/pricing"];
915
readonly modelPages = ["https://platform.openai.com/docs/models"];
1016

17+
private validateBaseUrl(baseUrl: string): string {
18+
// Normalize the URL by removing trailing slash
19+
const normalized = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
20+
21+
// Check if the URL is in the allowed list
22+
if (
23+
!ALLOWED_OPENAI_BASE_URLS.includes(
24+
normalized as (typeof ALLOWED_OPENAI_BASE_URLS)[number]
25+
)
26+
) {
27+
// Fall back to default if not allowed
28+
return "https://api.openai.com";
29+
}
30+
31+
return normalized;
32+
}
33+
1134
buildUrl(endpoint: Endpoint, requestParams: RequestParams): string {
35+
// Use custom base URL if provided and valid, otherwise fall back to default
36+
const baseUrl = this.validateBaseUrl(
37+
endpoint.userConfig.baseUri || "https://api.openai.com"
38+
);
39+
1240
switch (requestParams.bodyMapping) {
1341
case "RESPONSES":
14-
return "https://api.openai.com/v1/responses";
42+
return `${baseUrl}/v1/responses`;
1543
default:
16-
return "https://api.openai.com/v1/chat/completions";
44+
return `${baseUrl}/v1/chat/completions`;
1745
}
1846
}
1947

packages/cost/models/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export interface UserEndpointConfig {
249249
region?: string;
250250
location?: string;
251251
projectId?: string;
252-
baseUri?: string; // Azure OpenAI
252+
baseUri?: string; // Custom base URL (e.g., Azure OpenAI, OpenAI US data residency)
253253
deploymentName?: string;
254254
resourceName?: string;
255255
apiVersion?: string; // Azure OpenAI

packages/cost/providers/mappings.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { costs as xCosts } from "./x";
2828
import { googleProvider } from "./google";
2929
import { costs as vercelCosts } from "./vercel";
3030

31-
const openAiPattern = /^https:\/\/api\.openai\.com/;
31+
// Matches both standard api.openai.com and US data residency us.api.openai.com
32+
const openAiPattern = /^https:\/\/(us\.)?api\.openai\.com(\/|$)/;
3233
const anthropicPattern = /^https:\/\/api\.anthropic\.com/;
3334
export const azurePattern =
3435
/^(https?:\/\/)?([^.]*\.)?(openai\.azure\.com|azure-api\.net|cognitiveservices\.azure\.com|services\.ai\.azure\.com)(\/.*)?$/;

web/components/providers/ProviderCard.tsx

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,22 @@ import useNotification from "@/components/shared/notification/useNotification";
3737
import { Small } from "@/components/ui/typography";
3838
import { Label } from "../ui/label";
3939
import { Checkbox } from "../ui/checkbox";
40+
import {
41+
Select,
42+
SelectContent,
43+
SelectItem,
44+
SelectTrigger,
45+
SelectValue,
46+
} from "../ui/select";
4047
import { cn } from "@/lib/utils";
4148
import { logger } from "@/lib/telemetry/logger";
4249

50+
// Allowed OpenAI endpoints for data residency
51+
const OPENAI_ENDPOINTS = [
52+
{ value: "", label: "Default (api.openai.com)" },
53+
{ value: "https://us.api.openai.com", label: "US Data Residency (us.api.openai.com)" },
54+
] as const;
55+
4356
// ====== Types ======
4457
interface ProviderCardProps {
4558
provider: Provider;
@@ -98,6 +111,7 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
98111
// ====== Derived state ======
99112
const isEditMode = !!existingKey;
100113
const hasAdvancedConfig =
114+
provider.id === "openai" ||
101115
provider.id === "azure" ||
102116
provider.id === "bedrock" ||
103117
provider.id === "vertex";
@@ -119,7 +133,11 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
119133
useEffect(() => {
120134
// Initialize default config based on provider
121135
let initialConfig = {};
122-
if (provider.id === "azure") {
136+
if (provider.id === "openai") {
137+
initialConfig = {
138+
baseUri: "",
139+
};
140+
} else if (provider.id === "azure") {
123141
initialConfig = {
124142
baseUri: "",
125143
apiVersion: "",
@@ -279,9 +297,11 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
279297
};
280298

281299
const handleUpdateConfigField = (key: string, value: string) => {
300+
// Convert "default" placeholder value to empty string for storage
301+
const normalizedValue = value === "default" ? "" : value;
282302
setConfigValues((prev) => ({
283303
...prev,
284-
[key]: value,
304+
[key]: normalizedValue,
285305
}));
286306
};
287307

@@ -393,7 +413,16 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
393413
type?: string;
394414
}[] = [];
395415

396-
if (provider.id === "azure") {
416+
if (provider.id === "openai") {
417+
configFields = [
418+
{
419+
label: "Endpoint Region",
420+
key: "baseUri",
421+
placeholder: "",
422+
type: "select",
423+
},
424+
];
425+
} else if (provider.id === "azure") {
397426
configFields = [
398427
{
399428
label: "Base URI",
@@ -470,6 +499,28 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
470499
: field.label}
471500
</Label>
472501
</div>
502+
) : field.type === "select" && provider.id === "openai" ? (
503+
<Select
504+
value={configValues[field.key] || "default"}
505+
onValueChange={(value) =>
506+
handleUpdateConfigField(field.key, value)
507+
}
508+
disabled={isEditMode && !isEditingKey}
509+
>
510+
<SelectTrigger className="h-7 text-xs">
511+
<SelectValue placeholder="Select endpoint region" />
512+
</SelectTrigger>
513+
<SelectContent>
514+
{OPENAI_ENDPOINTS.map((endpoint) => (
515+
<SelectItem
516+
key={endpoint.value || "default"}
517+
value={endpoint.value || "default"}
518+
>
519+
{endpoint.label}
520+
</SelectItem>
521+
))}
522+
</SelectContent>
523+
</Select>
473524
) : (
474525
<Input
475526
type={field.type ?? "text"}

0 commit comments

Comments
 (0)