Skip to content

Commit ea7ac40

Browse files
committed
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
1 parent 822d124 commit ea7ac40

File tree

2 files changed

+70
-10
lines changed

2 files changed

+70
-10
lines changed

packages/cost/models/providers/openai.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +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 {
12-
// Use custom base URL if provided (e.g., for US data residency: us.api.openai.com)
13-
const baseUrl = endpoint.userConfig.baseUri || "https://api.openai.com";
14-
const normalizedBaseUrl = baseUrl.endsWith("/")
15-
? baseUrl.slice(0, -1)
16-
: baseUrl;
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+
);
1739

1840
switch (requestParams.bodyMapping) {
1941
case "RESPONSES":
20-
return `${normalizedBaseUrl}/v1/responses`;
42+
return `${baseUrl}/v1/responses`;
2143
default:
22-
return `${normalizedBaseUrl}/v1/chat/completions`;
44+
return `${baseUrl}/v1/chat/completions`;
2345
}
2446
}
2547

web/components/providers/ProviderCard.tsx

Lines changed: 41 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;
@@ -284,9 +297,11 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
284297
};
285298

286299
const handleUpdateConfigField = (key: string, value: string) => {
300+
// Convert "default" placeholder value to empty string for storage
301+
const normalizedValue = value === "default" ? "" : value;
287302
setConfigValues((prev) => ({
288303
...prev,
289-
[key]: value,
304+
[key]: normalizedValue,
290305
}));
291306
};
292307

@@ -401,9 +416,10 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
401416
if (provider.id === "openai") {
402417
configFields = [
403418
{
404-
label: "Base URL (optional)",
419+
label: "Endpoint Region",
405420
key: "baseUri",
406-
placeholder: "https://us.api.openai.com (for US data residency)",
421+
placeholder: "",
422+
type: "select",
407423
},
408424
];
409425
} else if (provider.id === "azure") {
@@ -483,6 +499,28 @@ const ProviderInstance: React.FC<ProviderInstanceProps> = ({
483499
: field.label}
484500
</Label>
485501
</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>
486524
) : (
487525
<Input
488526
type={field.type ?? "text"}

0 commit comments

Comments
 (0)