diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 200cd4c822..4d83617e9f 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -1103,7 +1103,10 @@ export async function POST(request: NextRequest): Promise byok.providerId), }); if (errorResponse) { await logUnrewrittenResponse({ diff --git a/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx b/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx index 00f677ab16..ed96468d5a 100644 --- a/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx +++ b/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx @@ -44,6 +44,7 @@ import { UserByokProviderIdSchema, VercelUserByokInferenceProviderIdSchema, AwsCredentialsSchema, + VertexCredentialsSchema, type VercelUserByokInferenceProviderId, } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import { DIRECT_BYOK_PROVIDERS_META } from '@/lib/ai-gateway/providers/direct-byok/direct-byok-meta'; @@ -66,6 +67,7 @@ const VERCEL_BYOK_PROVIDER_NAMES = { moonshotai: 'Moonshot AI', novita: 'Novita', perplexity: 'Perplexity', + vertex: 'Google Vertex AI', xai: 'SpaceXAI', xiaomi: 'Xiaomi (pay as you go)', zai: 'Z.ai (pay as you go)', @@ -151,7 +153,7 @@ type BYOKDialogState = { selectedProvider: string; apiKey: string; showApiKey: boolean; - awsCredentialError: string | null; + credentialError: string | null; }; const INITIAL_BYOK_DIALOG_STATE: BYOKDialogState = { @@ -160,7 +162,7 @@ const INITIAL_BYOK_DIALOG_STATE: BYOKDialogState = { selectedProvider: '', apiKey: '', showApiKey: false, - awsCredentialError: null, + credentialError: null, }; function updateBYOKDialogState(state: BYOKDialogState, update: Partial) { @@ -172,15 +174,15 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) { updateBYOKDialogState, INITIAL_BYOK_DIALOG_STATE ); - const { isDialogOpen, editingKeyId, selectedProvider, apiKey, showApiKey, awsCredentialError } = + const { isDialogOpen, editingKeyId, selectedProvider, apiKey, showApiKey, credentialError } = dialogState; const setIsDialogOpen = (isDialogOpen: boolean) => updateDialogState({ isDialogOpen }); const setEditingKeyId = (editingKeyId: string | null) => updateDialogState({ editingKeyId }); const setSelectedProvider = (selectedProvider: string) => updateDialogState({ selectedProvider }); const setApiKey = (apiKey: string) => updateDialogState({ apiKey }); const setShowApiKey = (showApiKey: boolean) => updateDialogState({ showApiKey }); - const setAwsCredentialError = (awsCredentialError: string | null) => - updateDialogState({ awsCredentialError }); + const setCredentialError = (credentialError: string | null) => + updateDialogState({ credentialError }); const trpc = useTRPC(); const queryClient = useQueryClient(); const confirm = useConfirm(); @@ -274,17 +276,28 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) { return keys?.some(k => k.provider_id === providerSlug) ?? false; }; - const validateAwsCredentials = (value: string): string | null => { + const validateStructuredCredentials = (providerId: string, value: string): string | null => { if (!value) return null; + const schema = + providerId === VercelUserByokInferenceProviderIdSchema.enum.bedrock + ? AwsCredentialsSchema + : providerId === VercelUserByokInferenceProviderIdSchema.enum.vertex + ? VertexCredentialsSchema + : null; + if (!schema) return null; let parsed: unknown; try { parsed = JSON.parse(value); } catch { - return 'Invalid JSON — please enter a valid JSON object.'; + return 'Invalid JSON. Enter a valid JSON object.'; } - const result = AwsCredentialsSchema.safeParse(parsed); + const result = schema.safeParse(parsed); if (!result.success) { - return `Invalid AWS credentials:\n${z.prettifyError(result.error)}`; + const providerName = + providerId === VercelUserByokInferenceProviderIdSchema.enum.bedrock + ? 'AWS' + : 'Google Vertex'; + return `Invalid ${providerName} credentials:\n${z.prettifyError(result.error)}`; } return null; }; @@ -295,15 +308,13 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) { setSelectedProvider(''); setApiKey(''); setShowApiKey(false); - setAwsCredentialError(null); + setCredentialError(null); }; const handleSave = () => { - if (selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock) { - const error = validateAwsCredentials(apiKey); - setAwsCredentialError(error); - if (error) return; - } + const error = validateStructuredCredentials(selectedProvider, apiKey); + setCredentialError(error); + if (error) return; if (editingKeyId) { updateMutation.mutate({ ...(organizationId && { organizationId }), @@ -526,7 +537,10 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {