Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
67 changes: 48 additions & 19 deletions docs/config/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ Most providers only need an API key. The UI handles validation and shows which p

## Supported Providers

| Provider | Models | Get API Key |
| -------------- | -------------------------- | ------------------------------------------------------- |
| **Anthropic** | Claude Opus, Sonnet, Haiku | [console.anthropic.com](https://console.anthropic.com/) |
| **OpenAI** | GPT-5, Codex | [platform.openai.com](https://platform.openai.com/) |
| **Google** | Gemini Pro, Flash | [aistudio.google.com](https://aistudio.google.com/) |
| **xAI** | Grok | [console.x.ai](https://console.x.ai/) |
| **DeepSeek** | DeepSeek Chat, Reasoner | [platform.deepseek.com](https://platform.deepseek.com/) |
| **OpenRouter** | 300+ models | [openrouter.ai](https://openrouter.ai/) |
| **Ollama** | Local models | [ollama.com](https://ollama.com/) (no key needed) |
| **Bedrock** | Claude via AWS | AWS Console |
| Provider | Models | Get API Key |
| ----------------- | -------------------------- | ------------------------------------------------------- |
| **Anthropic** | Claude Opus, Sonnet, Haiku | [console.anthropic.com](https://console.anthropic.com/) |
| **Azure Foundry** | Claude via Azure Foundry | [ai.azure.com](https://ai.azure.com/) |
| **OpenAI** | GPT-5, Codex | [platform.openai.com](https://platform.openai.com/) |
| **Google** | Gemini Pro, Flash | [aistudio.google.com](https://aistudio.google.com/) |
| **xAI** | Grok | [console.x.ai](https://console.x.ai/) |
| **DeepSeek** | DeepSeek Chat, Reasoner | [platform.deepseek.com](https://platform.deepseek.com/) |
| **OpenRouter** | 300+ models | [openrouter.ai](https://openrouter.ai/) |
| **Ollama** | Local models | [ollama.com](https://ollama.com/) (no key needed) |
| **Bedrock** | Claude via AWS | AWS Console |

## Environment Variables

Providers also read from environment variables as fallback:

{/* BEGIN PROVIDER_ENV_VARS */}

| Provider | Environment Variable |
| ---------- | -------------------------------------------------- |
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` |
| OpenAI | `OPENAI_API_KEY` |
| Google | `GOOGLE_GENERATIVE_AI_API_KEY` or `GOOGLE_API_KEY` |
| xAI | `XAI_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| DeepSeek | `DEEPSEEK_API_KEY` |
| Bedrock | `AWS_REGION` (credentials via AWS SDK chain) |
| Provider | Environment Variable |
| ------------- | ----------------------------------------------------- |
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` |
| Azure Foundry | `AZURE_FOUNDRY_API_KEY` and `AZURE_FOUNDRY_RESOURCE` |
| OpenAI | `OPENAI_API_KEY` |
| Google | `GOOGLE_GENERATIVE_AI_API_KEY` or `GOOGLE_API_KEY` |
| xAI | `XAI_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| DeepSeek | `DEEPSEEK_API_KEY` |
| Bedrock | `AWS_REGION` (credentials via AWS SDK chain) |

<details>
<summary>Additional environment variables</summary>
Expand Down Expand Up @@ -95,6 +97,33 @@ For advanced options not exposed in the UI, edit `~/.mux/providers.jsonc` direct
}
```

### Azure Foundry

Azure Foundry provides access to Claude models through Microsoft's AI marketplace. It uses Anthropic's native API format.

```jsonc
{
"azure-foundry": {
"apiKey": "your-azure-api-key",
"resource": "your-resource-name" // Just the resource name, not full URL
}
}
```

**Getting your credentials:**
1. Go to [Azure AI Foundry](https://ai.azure.com/)
2. Create a project and deploy a Claude model
3. Find your resource name (e.g., `my-resource` from `https://my-resource.services.ai.azure.com/`)
4. Copy your API key from the Keys section

**Environment variables:**
```bash
export AZURE_FOUNDRY_RESOURCE=your-resource-name # Just the name, not full URL
export AZURE_FOUNDRY_API_KEY=your-azure-api-key
```

**Note:** Azure Foundry is separate from Azure OpenAI. All Claude features work identically: streaming, tool calling, thinking, and prompt caching.

### Bedrock Authentication

Bedrock supports multiple authentication methods (tried in order):
Expand Down
1 change: 1 addition & 0 deletions src/browser/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { cn } from "@/common/lib/utils";
*/
const PROVIDER_ICONS: Partial<Record<ProviderName, React.FC>> = {
anthropic: AnthropicIcon,
"azure-foundry": AnthropicIcon, // Same icon as Anthropic (Claude branding)
openai: OpenAIIcon,
google: GoogleIcon,
xai: XAIIcon,
Expand Down
17 changes: 17 additions & 0 deletions src/browser/components/Settings/sections/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ function getProviderFields(provider: ProviderName): FieldConfig[] {
return [];
}

if (provider === "azure-foundry") {
return [
{ key: "apiKey", label: "API Key", placeholder: "Enter API key", type: "secret" },
{
key: "resource",
label: "Resource",
placeholder: "your-resource-name",
type: "text",
},
];
}

// Default for most providers
return [
{ key: "apiKey", label: "API Key", placeholder: "Enter API key", type: "secret" },
Expand All @@ -134,6 +146,7 @@ function getProviderFields(provider: ProviderName): FieldConfig[] {
*/
const PROVIDER_KEY_URLS: Partial<Record<ProviderName, string>> = {
anthropic: "https://console.anthropic.com/settings/keys",
"azure-foundry": "https://ai.azure.com/",
openai: "https://platform.openai.com/api-keys",
google: "https://aistudio.google.com/app/apikey",
xai: "https://console.x.ai/team/default/api-keys",
Expand Down Expand Up @@ -430,6 +443,8 @@ export function ProvidersSection() {
updateOptimistically(provider, { apiKeySet: editValue !== "" });
} else if (field === "baseUrl") {
updateOptimistically(provider, { baseUrl: editValue || undefined });
} else if (field === "resource") {
updateOptimistically(provider, { resource: editValue || undefined });
}

setEditingField(null);
Expand All @@ -449,6 +464,8 @@ export function ProvidersSection() {
updateOptimistically(provider, { apiKeySet: false });
} else if (field === "baseUrl") {
updateOptimistically(provider, { baseUrl: undefined });
} else if (field === "resource") {
updateOptimistically(provider, { resource: undefined });
}

// Save in background
Expand Down
19 changes: 18 additions & 1 deletion src/common/constants/knownModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { formatModelDisplayName } from "../utils/ai/modelDisplay";

type ModelProvider = "anthropic" | "openai" | "google" | "xai";
type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "azure-foundry";

interface KnownModelDefinition {
/** Provider identifier used by SDK factories */
Expand Down Expand Up @@ -46,6 +46,23 @@ const MODEL_DEFINITIONS = {
aliases: ["haiku"],
tokenizerOverride: "anthropic/claude-3.5-haiku",
},
AZURE_FOUNDRY_OPUS: {
provider: "azure-foundry",
providerModelId: "claude-opus-4-5",
aliases: ["azure-opus", "foundry-opus"],
},
AZURE_FOUNDRY_SONNET: {
provider: "azure-foundry",
providerModelId: "claude-sonnet-4-5",
aliases: ["azure-sonnet", "foundry-sonnet"],
tokenizerOverride: "anthropic/claude-sonnet-4.5",
},
AZURE_FOUNDRY_HAIKU: {
provider: "azure-foundry",
providerModelId: "claude-haiku-4-5",
aliases: ["azure-haiku", "foundry-haiku"],
tokenizerOverride: "anthropic/claude-3.5-haiku",
},
GPT: {
provider: "openai",
providerModelId: "gpt-5.2",
Expand Down
6 changes: 6 additions & 0 deletions src/common/constants/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export const PROVIDER_DEFINITIONS = {
factoryName: "createAnthropic",
requiresApiKey: true,
},
"azure-foundry": {
displayName: "Azure Foundry",
import: () => import("@ai-sdk/anthropic"), // Uses Anthropic SDK with Azure baseURL
factoryName: "createAnthropic",
requiresApiKey: true,
},
openai: {
displayName: "OpenAI",
import: () => import("@ai-sdk/openai"),
Expand Down
2 changes: 2 additions & 0 deletions src/common/orpc/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export const ProviderConfigInfoSchema = z.object({
/** Whether this provider is configured and ready to use */
isConfigured: z.boolean(),
baseUrl: z.string().optional(),
/** Azure Foundry resource name */
resource: z.string().optional(),
models: z.array(z.string()).optional(),
/** OpenAI-specific fields */
serviceTier: z.enum(["auto", "default", "flex", "priority"]).optional(),
Expand Down
25 changes: 25 additions & 0 deletions src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,31 @@ export class AIService extends EventEmitter {
return Ok(provider(modelId));
}

// Handle Azure Foundry provider (uses @ai-sdk/anthropic with Azure baseURL)
if (providerName === "azure-foundry") {
// Resolve credentials from config + env
const creds = resolveProviderCredentials("azure-foundry", providerConfig);
if (!creds.isConfigured || !creds.apiKey || !creds.resource) {
return Err({ type: "api_key_not_found", provider: providerName });
}

// Build Azure Foundry baseURL from resource name
const baseURL = `https://${creds.resource}.services.ai.azure.com/anthropic/v1/`;
log.debug(`Azure Foundry baseURL: ${baseURL}`);

// Use @ai-sdk/anthropic with Azure baseURL - no special adapter needed
const { createAnthropic } = await import("@ai-sdk/anthropic");

const provider = createAnthropic({
apiKey: creds.apiKey,
baseURL,
headers: providerConfig.headers,
fetch: getProviderFetch(providerConfig),
});

return Ok(provider(modelId));
}

// Handle OpenAI provider (using Responses API)
if (providerName === "openai") {
// Resolve credentials from config + env (single source of truth)
Expand Down
2 changes: 2 additions & 0 deletions src/node/services/providerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class ProviderService {
const config = (providersConfig[provider] ?? {}) as {
apiKey?: string;
baseUrl?: string;
resource?: string;
models?: string[];
serviceTier?: unknown;
region?: string;
Expand All @@ -67,6 +68,7 @@ export class ProviderService {
apiKeySet: !!config.apiKey,
isConfigured: false, // computed below
baseUrl: config.baseUrl,
resource: config.resource,
models: config.models,
};

Expand Down
21 changes: 21 additions & 0 deletions src/node/utils/providerRequirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ export const PROVIDER_ENV_VARS: Partial<
baseUrl?: string[];
organization?: string[];
region?: string[];
resource?: string[];
}
>
> = {
anthropic: {
apiKey: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
baseUrl: ["ANTHROPIC_BASE_URL"],
},
"azure-foundry": {
apiKey: ["AZURE_FOUNDRY_API_KEY", "AZURE_API_KEY"],
resource: ["AZURE_FOUNDRY_RESOURCE"],
},
openai: {
apiKey: ["OPENAI_API_KEY"],
baseUrl: ["OPENAI_BASE_URL", "OPENAI_API_BASE"],
Expand Down Expand Up @@ -91,6 +96,7 @@ export interface ProviderConfigRaw {
couponCode?: string;
voucher?: string; // legacy mux-gateway field
organization?: string; // OpenAI org ID
resource?: string; // Azure Foundry resource name
}

/** Result of resolving provider credentials */
Expand All @@ -105,6 +111,7 @@ export interface ResolvedCredentials {
couponCode?: string; // mux-gateway
baseUrl?: string; // from config or env
organization?: string; // openai
resource?: string; // azure-foundry
}

/** Legacy alias for backward compatibility */
Expand Down Expand Up @@ -144,6 +151,20 @@ export function resolveProviderCredentials(
: { isConfigured: false, missingRequirement: "coupon_code" };
}

// Azure Foundry: requires both API key and resource name
if (provider === "azure-foundry") {
const envMapping = PROVIDER_ENV_VARS[provider];
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string should be treated as unset
const configKey = config.apiKey || null;
const apiKey = configKey ?? resolveEnv(envMapping?.apiKey, env);
const resource = config.resource ?? resolveEnv(envMapping?.resource, env);

if (apiKey && resource) {
return { isConfigured: true, apiKey, resource };
}
return { isConfigured: false, missingRequirement: "api_key" };
}

// Keyless providers (e.g., ollama): require explicit opt-in via baseUrl or models
const def = PROVIDER_DEFINITIONS[provider];
if (!def.requiresApiKey) {
Expand Down