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
64 changes: 48 additions & 16 deletions src/components/organizations/byok/BYOKKeysManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
// Hardcoded BYOK providers list
const BYOK_PROVIDERS = [
{ id: VercelUserByokInferenceProviderIdSchema.enum.anthropic, name: 'Anthropic' },
{ id: VercelUserByokInferenceProviderIdSchema.enum.bedrock, name: 'AWS Bedrock' },
{ id: VercelUserByokInferenceProviderIdSchema.enum.openai, name: 'OpenAI' },
{ id: VercelUserByokInferenceProviderIdSchema.enum.google, name: 'Google AI Studio' },
{ id: VercelUserByokInferenceProviderIdSchema.enum.minimax, name: 'MiniMax' },
Expand Down Expand Up @@ -390,26 +391,57 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {
</div>

<div className="space-y-2">
<Label htmlFor="apiKey">API Key</Label>
<div className="relative">
<Input
<Label htmlFor="apiKey">
{selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock
? 'AWS Credentials'
: 'API Key'}
</Label>
{selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock ? (
<textarea
id="apiKey"
type={showApiKey ? 'text' : 'password'}
value={apiKey}
onChange={e => setApiKey(e.target.value)}
placeholder="Enter API key"
className="pr-10"
placeholder='{"accessKeyId": "...", "secretAccessKey": "...", "region": "us-east-1"}'
className="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
rows={4}
/>
<Button
type="button"
variant="secondary"
size="sm"
className="absolute top-0 right-0 h-full px-3"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
) : (
<div className="relative">
<Input
id="apiKey"
type={showApiKey ? 'text' : 'password'}
value={apiKey}
onChange={e => setApiKey(e.target.value)}
placeholder="Enter API key"
className="pr-10"
/>
<Button
type="button"
variant="secondary"
size="sm"
className="absolute top-0 right-0 h-full px-3"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
)}
{selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock && (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<p>Enter your AWS credentials as JSON:</p>
<code className="mt-1 block text-xs break-all">
{'{"accessKeyId": "...", "secretAccessKey": "...", "region": "us-east-1"}'}
</code>
<p className="mt-1">
Your IAM user needs <code className="text-xs">bedrock:InvokeModel</code> and{' '}
<code className="text-xs">bedrock:InvokeModelWithResponseStream</code>{' '}
permissions.
</p>
</AlertDescription>
</Alert>
)}
{editingKeyId ? (
<Alert>
<Lock className="h-4 w-4" />
Expand Down
13 changes: 11 additions & 2 deletions src/lib/providers/openrouter/inference-provider-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const OpenRouterInferenceProviderIdSchema = z.enum([

export const VercelUserByokInferenceProviderIdSchema = z.enum([
'anthropic',
'bedrock',
'google', // Google AI Studio
'openai',
'minimax',
Expand All @@ -47,7 +48,7 @@ export const UserByokProviderIdSchema = VercelUserByokInferenceProviderIdSchema.

export type UserByokProviderId = z.infer<typeof UserByokProviderIdSchema>;

export const VercelNonUserByokInferenceProviderIdSchema = z.enum(['alibaba', 'bedrock', 'vertex']);
export const VercelNonUserByokInferenceProviderIdSchema = z.enum(['alibaba', 'vertex']);

export const VercelInferenceProviderIdSchema = VercelUserByokInferenceProviderIdSchema.or(
VercelNonUserByokInferenceProviderIdSchema
Expand All @@ -59,7 +60,7 @@ export type VercelInferenceProviderId = z.infer<typeof VercelInferenceProviderId

const openRouterToVercelInferenceProviderMapping = {
[OpenRouterInferenceProviderIdSchema.enum['amazon-bedrock']]:
VercelNonUserByokInferenceProviderIdSchema.enum.bedrock,
VercelUserByokInferenceProviderIdSchema.enum.bedrock,
[OpenRouterInferenceProviderIdSchema.enum['google-ai-studio']]:
VercelUserByokInferenceProviderIdSchema.enum.google,
[OpenRouterInferenceProviderIdSchema.enum['google-vertex']]:
Expand Down Expand Up @@ -94,3 +95,11 @@ export function inferVercelFirstPartyInferenceProviderForModel(
? null
: (modelPrefixToVercelInferenceProviderMapping[model.split('/')[0]] ?? null);
}

export const AwsCredentialsSchema = z.object({
accessKeyId: z.string(),
secretAccessKey: z.string(),
region: z.string(),
});

export type AwsCredentials = z.infer<typeof AwsCredentialsSchema>;
3 changes: 2 additions & 1 deletion src/lib/providers/openrouter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type OpenAI from 'openai';
import type { GatewayProviderOptions } from '@ai-sdk/gateway';
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import type { ReasoningDetailUnion } from '@/lib/custom-llm/reasoning-details';
import type { AwsCredentials } from '@/lib/providers/openrouter/inference-provider-id';

// Base types for OpenRouter API that don't depend on other lib files
// This breaks circular dependencies with mistral.ts, minimax.ts, etc.
Expand All @@ -13,7 +14,7 @@ export type OpenRouterProviderConfig = {
zdr?: boolean;
};

export type VercelInferenceProviderConfig = { apiKey: string; baseURL?: string };
export type VercelInferenceProviderConfig = { apiKey: string; baseURL?: string } | AwsCredentials;

export type VercelProviderConfig = {
gateway?: GatewayProviderOptions & {
Expand Down
18 changes: 17 additions & 1 deletion src/lib/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isAnthropicModel } from '@/lib/providers/anthropic';
import { getGatewayErrorRate } from '@/lib/providers/gateway-error-rate';
import {
AutocompleteUserByokProviderIdSchema,
AwsCredentialsSchema,
openRouterToVercelInferenceProviderId,
VercelUserByokInferenceProviderIdSchema,
} from '@/lib/providers/openrouter/inference-provider-id';
Expand Down Expand Up @@ -101,6 +102,14 @@ function convertProviderOptions(
};
}

function parseAwsCredentials(input: string) {
try {
return AwsCredentialsSchema.parse(JSON.parse(input));
} catch {
throw new Error('Failed to parse AWS credentials');
}
}

export function applyVercelSettings(
requestedModel: string,
requestToMutate: OpenRouterChatCompletionRequest,
Expand Down Expand Up @@ -128,14 +137,21 @@ export function applyVercelSettings(
? VercelUserByokInferenceProviderIdSchema.enum.mistral
: provider.providerId;
const list = new Array<VercelInferenceProviderConfig>();

if (key === VercelUserByokInferenceProviderIdSchema.enum.zai) {
// Z.AI Coding Plan support
list.push({
apiKey: provider.decryptedAPIKey,
baseURL: 'https://api.z.ai/api/coding/paas/v4',
});
}
list.push({ apiKey: provider.decryptedAPIKey });

if (key === VercelUserByokInferenceProviderIdSchema.enum.bedrock) {
list.push(parseAwsCredentials(provider.decryptedAPIKey));
} else {
list.push({ apiKey: provider.decryptedAPIKey });
}

byokProviders[key] = [...(byokProviders[key] ?? []), ...list];
}

Expand Down