From 447c5f75845c463a83c5109aa476c417f7b6846e Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 14 Aug 2026 16:40:48 +0200 Subject: [PATCH 01/12] feat(ai-gateway): add Vertex user BYOK --- .../organizations/byok/BYOKKeysManager.tsx | 102 +++++++++++++----- apps/web/src/lib/ai-gateway/byok/index.ts | 13 ++- .../lib/ai-gateway/llm-proxy-helpers.test.ts | 25 +++++ .../src/lib/ai-gateway/llm-proxy-helpers.ts | 35 ++++-- .../src/lib/ai-gateway/processUsage.test.ts | 27 +++++ apps/web/src/lib/ai-gateway/processUsage.ts | 19 +++- .../openrouter/inference-provider-id.test.ts | 17 +++ .../openrouter/inference-provider-id.ts | 34 +++++- .../inject-extra-provider-models.ts | 11 +- .../ai-gateway/providers/openrouter/types.ts | 10 +- .../ai-gateway/providers/vercel/index.test.ts | 53 +++++++++ .../lib/ai-gateway/providers/vercel/index.ts | 31 ++++-- apps/web/src/routers/byok-router.test.ts | 50 +++++++++ apps/web/src/routers/byok-router.ts | 26 ++++- 14 files changed, 398 insertions(+), 55 deletions(-) diff --git a/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx b/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx index 00f677ab16..d6ed491a63 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) {