diff --git a/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx b/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx
index a2b417c6a5..7e686433b7 100644
--- a/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx
+++ b/apps/web/src/components/organizations/byok/BYOKKeysManager.tsx
@@ -43,12 +43,14 @@ import {
DirectUserByokInferenceProviderIdSchema,
UserByokProviderIdSchema,
VercelUserByokInferenceProviderIdSchema,
+ AzureCredentialsSchema,
BedrockCredentialsSchema,
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';
import { getCodingPlanManagedKeyLabel } from '@/components/subscriptions/coding-plans/coding-plan-provider';
+import { cn } from '@/lib/utils';
import * as z from 'zod';
// Exhaustive map of Vercel BYOK providers to their display names. The `satisfies`
@@ -56,6 +58,7 @@ import * as z from 'zod';
// VercelUserByokInferenceProviderIdSchema.
const VERCEL_BYOK_PROVIDER_NAMES = {
anthropic: 'Anthropic',
+ azure: 'Azure Foundry (experimental)',
bedrock: 'AWS Bedrock',
deepseek: 'DeepSeek',
openai: 'OpenAI',
@@ -279,11 +282,13 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {
const validateStructuredCredentials = (providerId: string, value: string): string | null => {
if (!value) return null;
const schema =
- providerId === VercelUserByokInferenceProviderIdSchema.enum.bedrock
- ? BedrockCredentialsSchema
- : providerId === VercelUserByokInferenceProviderIdSchema.enum.vertex
- ? VertexCredentialsSchema
- : null;
+ providerId === VercelUserByokInferenceProviderIdSchema.enum.azure
+ ? AzureCredentialsSchema
+ : providerId === VercelUserByokInferenceProviderIdSchema.enum.bedrock
+ ? BedrockCredentialsSchema
+ : providerId === VercelUserByokInferenceProviderIdSchema.enum.vertex
+ ? VertexCredentialsSchema
+ : null;
if (!schema) return null;
let parsed: unknown;
try {
@@ -293,6 +298,11 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {
}
const result = schema.safeParse(parsed);
if (!result.success) {
+ if (providerId === VercelUserByokInferenceProviderIdSchema.enum.azure) {
+ return result.error.issues.some(issue => issue.path[0] === 'modelMappings')
+ ? 'Each model mapping must include gatewayModelSlug and customModelId.'
+ : 'Enter JSON with apiKey and resourceName.';
+ }
if (providerId === VercelUserByokInferenceProviderIdSchema.enum.bedrock) {
return 'Enter JSON with apiKey and region, or accessKeyId, secretAccessKey, and region. Use only one authentication method.';
}
@@ -394,6 +404,10 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {
const getProviderModels = (providerId: string): string[] => {
return supportedModels?.[providerId] ?? [];
};
+ const usesStructuredCredentials =
+ selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.azure ||
+ selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock ||
+ selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.vertex;
return (
@@ -579,42 +593,71 @@ export function BYOKKeysManager({ organizationId }: BYOKKeysManagerProps) {
- {selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock ||
- selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.vertex ? (
+ {usesStructuredCredentials ? (
<>
-
)}
+ {selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.azure && (
+
+
+
+ Enter your Azure OpenAI API key and resource name as JSON:
+
+ {'{"apiKey": "...", "resourceName": "..."}'}
+
+
+ The resource name is the subdomain from your Azure OpenAI endpoint, for
+ example my-resource from{' '}
+ my-resource.openai.azure.com.
+
+
+ For custom deployment names, add{' '}
+ modelMappings with{' '}
+ gatewayModelSlug and{' '}
+ customModelId entries.
+
+
+
+ )}
{selectedProvider === VercelUserByokInferenceProviderIdSchema.enum.bedrock && (
diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.test.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.test.ts
index f26832acf5..3fbbba3ae5 100644
--- a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.test.ts
+++ b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.test.ts
@@ -1,4 +1,5 @@
import {
+ AzureCredentialsSchema,
BedrockCredentialsSchema,
DirectUserByokInferenceProviderIdSchema,
getVercelUserByokProviderIdForEndpoint,
@@ -10,6 +11,40 @@ import {
VercelUserByokInferenceProviderIdSchema,
} from './inference-provider-id';
+describe('AzureCredentialsSchema', () => {
+ test('accepts Azure credentials with optional model mappings', () => {
+ const credentials = {
+ apiKey: 'azure-api-key',
+ resourceName: 'example-resource',
+ modelMappings: [
+ {
+ gatewayModelSlug: 'openai/gpt-5.4-nano',
+ customModelId: 'custom-gpt-5.4-nano',
+ },
+ ],
+ };
+
+ expect(AzureCredentialsSchema.parse(credentials)).toEqual(credentials);
+ });
+
+ test.each([
+ null,
+ 'azure-api-key',
+ {},
+ { apiKey: 'azure-api-key' },
+ { resourceName: 'example-resource' },
+ { apiKey: '', resourceName: 'example-resource' },
+ { apiKey: 'azure-api-key', resourceName: '' },
+ {
+ apiKey: 'azure-api-key',
+ resourceName: 'example-resource',
+ modelMappings: [{ gatewayModelSlug: 'openai/gpt-5.4-nano' }],
+ },
+ ])('rejects incomplete or invalid Azure credentials: %j', credentials => {
+ expect(AzureCredentialsSchema.safeParse(credentials).success).toBe(false);
+ });
+});
+
describe('BedrockCredentialsSchema', () => {
test.each([
{ accessKeyId: 'AKIAEXAMPLE', secretAccessKey: 'secret', region: 'us-east-1' },
@@ -76,6 +111,12 @@ describe('inference provider ids', () => {
expect(openRouterToVercelInferenceProviderId('google-vertex')).toBe('vertex');
});
+ test('promotes Azure to a user BYOK provider', () => {
+ expect(VercelUserByokInferenceProviderIdSchema.safeParse('azure').success).toBe(true);
+ expect(VercelNonUserByokInferenceProviderIdSchema.safeParse('azure').success).toBe(false);
+ expect(getVercelUserByokProviderIdForEndpoint('azure')).toBe('azure');
+ });
+
test('uses the Vertex user key for Vertex Anthropic endpoints', () => {
expect(normalizeVercelInferenceProviderIdForRouting('vertexAnthropic')).toBe('vertex');
expect(getVercelUserByokProviderIdForEndpoint('vertexAnthropic')).toBe('vertex');
diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts
index 1daf812b72..09e1a1c275 100644
--- a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts
+++ b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts
@@ -81,6 +81,7 @@ export const OpenRouterInferenceProviderIdSchema = z.enum([
export const VercelUserByokInferenceProviderIdSchema = z.enum([
'anthropic',
+ 'azure',
'bedrock',
'deepseek',
'fireworks',
@@ -136,6 +137,7 @@ export type UserByokProviderId = z.infer;
export const UserByokTestModels = {
[VercelUserByokInferenceProviderIdSchema.enum.anthropic]: 'anthropic/claude-haiku-4.5',
+ [VercelUserByokInferenceProviderIdSchema.enum.azure]: 'openai/gpt-5.4-nano',
[VercelUserByokInferenceProviderIdSchema.enum.bedrock]: 'anthropic/claude-haiku-4.5',
[VercelUserByokInferenceProviderIdSchema.enum.deepseek]: 'deepseek/deepseek-v3.2',
[VercelUserByokInferenceProviderIdSchema.enum.fireworks]: 'openai/gpt-oss-20b',
@@ -178,7 +180,6 @@ export const UserByokTestModels = {
export const VercelNonUserByokInferenceProviderIdSchema = z.enum([
'alibaba',
'arcee-ai',
- 'azure',
'baseten',
'bfl',
'blackbox',
@@ -305,6 +306,21 @@ export const BedrockCredentialsSchema = z.union([
export type BedrockCredentials = z.infer;
+export const AzureCredentialsSchema = z.object({
+ apiKey: z.string().trim().min(1),
+ resourceName: z.string().trim().min(1),
+ modelMappings: z
+ .array(
+ z.object({
+ gatewayModelSlug: z.string().trim().min(1),
+ customModelId: z.string().trim().min(1),
+ })
+ )
+ .optional(),
+});
+
+export type AzureCredentials = z.infer;
+
export const VertexCredentialsSchema = z.object({
project: z.string().min(1),
location: z.string().min(1),
diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts
index 0ec1f2e115..8a1343cdd7 100644
--- a/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts
+++ b/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts
@@ -3,6 +3,7 @@ import type { GatewayProviderOptions } from '@ai-sdk/gateway';
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import type { ReasoningDetailUnion } from '@/lib/ai-gateway/custom-llm/reasoning-details';
import type {
+ AzureCredentials,
BedrockCredentials,
VertexCredentials,
} from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
@@ -27,6 +28,7 @@ export function isOpenRouterProviderConfig(value: unknown): value is OpenRouterP
export type VercelInferenceProviderConfig =
| { apiKey: string; baseURL?: string }
+ | AzureCredentials
| BedrockCredentials
| VertexCredentials;
diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts
index 5d4839093e..9be95ff3ca 100644
--- a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts
+++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts
@@ -395,6 +395,16 @@ describe('applyVercelSettings BYOK pinning', () => {
secretAccessKey: 'secret',
region: 'us-east-1',
});
+ const azureCredentials = JSON.stringify({
+ apiKey: 'azure-api-key',
+ resourceName: 'example-resource',
+ modelMappings: [
+ {
+ gatewayModelSlug: 'openai/gpt-5.4-nano',
+ customModelId: 'custom-gpt-5.4-nano',
+ },
+ ],
+ });
const vertexCredentials = JSON.stringify({
project: 'example-project',
location: 'us-east5',
@@ -511,6 +521,27 @@ describe('applyVercelSettings BYOK pinning', () => {
expect(request.body.providerOptions?.gateway?.only).toEqual(['bedrock']);
});
+ it('forwards structured Azure credentials', async () => {
+ const request = byokRequest([]);
+
+ await applyVercelSettings('openai/gpt-5.4-nano', request, [
+ { decryptedAPIKey: azureCredentials, providerId: 'azure' },
+ ]);
+
+ expect(request.body.providerOptions?.gateway?.byok).toEqual({
+ azure: [JSON.parse(azureCredentials)],
+ });
+ expect(request.body.providerOptions?.gateway?.only).toEqual(['azure']);
+ });
+
+ it('rejects malformed Azure credentials without including credential contents', async () => {
+ await expect(
+ applyVercelSettings('openai/gpt-5.4-nano', byokRequest([]), [
+ { decryptedAPIKey: '{"apiKey":"secret"}', providerId: 'azure' },
+ ])
+ ).rejects.toEqual(new Error('Failed to parse Azure credentials'));
+ });
+
it.each([
'bedrock-secret',
'{"apiKey":"bedrock-secret"',
diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts
index fc6a46d9e0..83ed0a20c8 100644
--- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts
+++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts
@@ -2,6 +2,7 @@ import type { BYOKResult } from '@/lib/ai-gateway/providers/types';
import type { VercelUserByokInferenceProviderId } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id';
import {
DirectUserByokInferenceProviderIdSchema,
+ AzureCredentialsSchema,
BedrockCredentialsSchema,
normalizeVercelInferenceProviderIdForRouting,
openRouterToVercelInferenceProviderId,
@@ -189,19 +190,15 @@ export function convertProviderOptions(
};
}
-function parseBedrockCredentials(input: string) {
+function parseCredentials(
+ input: string,
+ schema: { parse(input: unknown): T },
+ providerName: string
+): T {
try {
- return BedrockCredentialsSchema.parse(JSON.parse(input));
+ return schema.parse(JSON.parse(input));
} catch {
- throw new Error('Failed to parse AWS credentials');
- }
-}
-
-function parseVertexCredentials(input: string) {
- try {
- return VertexCredentialsSchema.parse(JSON.parse(input));
- } catch {
- throw new Error('Failed to parse Google Vertex credentials');
+ throw new Error(`Failed to parse ${providerName} credentials`);
}
}
@@ -244,10 +241,12 @@ export function getVercelInferenceProviderConfigForUserByok(
});
}
- if (key === VercelUserByokInferenceProviderIdSchema.enum.bedrock) {
- list.push(parseBedrockCredentials(provider.decryptedAPIKey));
+ if (key === VercelUserByokInferenceProviderIdSchema.enum.azure) {
+ list.push(parseCredentials(provider.decryptedAPIKey, AzureCredentialsSchema, 'Azure'));
+ } else if (key === VercelUserByokInferenceProviderIdSchema.enum.bedrock) {
+ list.push(parseCredentials(provider.decryptedAPIKey, BedrockCredentialsSchema, 'AWS'));
} else if (key === VercelUserByokInferenceProviderIdSchema.enum.vertex) {
- list.push(parseVertexCredentials(provider.decryptedAPIKey));
+ list.push(parseCredentials(provider.decryptedAPIKey, VertexCredentialsSchema, 'Google Vertex'));
} else {
list.push({ apiKey: provider.decryptedAPIKey });
}
diff --git a/apps/web/src/routers/byok-router.test.ts b/apps/web/src/routers/byok-router.test.ts
index 47809313aa..1b52f765fa 100644
--- a/apps/web/src/routers/byok-router.test.ts
+++ b/apps/web/src/routers/byok-router.test.ts
@@ -24,6 +24,11 @@ const VERTEX_CREDENTIALS = JSON.stringify({
},
});
+const AZURE_CREDENTIALS = JSON.stringify({
+ apiKey: 'azure-api-key',
+ resourceName: 'example-resource',
+});
+
const BEDROCK_IAM_CREDENTIALS = {
accessKeyId: 'AKIAEXAMPLE',
secretAccessKey: 'bedrock-test-secret',
@@ -203,6 +208,30 @@ describe('BYOK Router', () => {
expect(result.provider_id).toBe('vertex');
});
+ test('should create an Azure BYOK credential', async () => {
+ const caller = await createCallerForUser(ownerUser.id);
+
+ const result = await caller.byok.create({
+ organizationId: organizationA.id,
+ provider_id: 'azure',
+ api_key: AZURE_CREDENTIALS,
+ });
+
+ expect(result.provider_id).toBe('azure');
+ });
+
+ test('should reject malformed Azure credentials', async () => {
+ const caller = await createCallerForUser(ownerUser.id);
+
+ await expect(
+ caller.byok.create({
+ organizationId: organizationA.id,
+ provider_id: 'azure',
+ api_key: '{"apiKey":"secret"}',
+ })
+ ).rejects.toThrow('Invalid credentials for provider: azure');
+ });
+
test('should reject malformed Vertex credentials', async () => {
const caller = await createCallerForUser(ownerUser.id);
diff --git a/apps/web/src/routers/byok-router.ts b/apps/web/src/routers/byok-router.ts
index c3294fb7da..58e1d5cff5 100644
--- a/apps/web/src/routers/byok-router.ts
+++ b/apps/web/src/routers/byok-router.ts
@@ -30,7 +30,7 @@ import {
getVercelModelsMetadataFromDatabase,
getOpenRouterModelsMetadataFromDatabase,
} from '@/lib/ai-gateway/providers/gateway-models-cache';
-import { AISDKError, createGateway, generateText } from 'ai';
+import { createGateway, generateText } from 'ai';
import { VERCEL_AI_GATEWAY } from '@/lib/ai-gateway/providers/provider-definitions';
import { getVercelInferenceProviderConfigForUserByok } from '@/lib/ai-gateway/providers/vercel';
import { decryptByokRow } from '@/lib/ai-gateway/byok';
@@ -484,7 +484,7 @@ export const byokRouter = createTRPCRouter({
message: `API key test success. Provider: ${metadata?.finalProvider ?? finalProvider}. Model: ${metadata?.originalModelId ?? model.modelId}.`,
};
} catch (e) {
- const message = AISDKError.isInstance(e) ? e.message : undefined;
+ const message = e instanceof Error ? e.message : undefined;
logByokWarning('BYOK key test request failed', {
providerId: decryptedKey.providerId,
message,