Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/setup-mux
- uses: ./.github/actions/setup-playwright
with:
# Storybook test-runner may launch chromium headless shell; install it alongside chromium.
browsers: "chromium chromium-headless-shell"
- run: make storybook-build
- run: |
bun x http-server storybook-static -p 6006 &
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Mux is a desktop & browser application for parallel agentic development. It enab
- **Multi-model** (`sonnet-4-*`, `grok-*`, `gpt-5-*`, `opus-4-*`)
- Ollama supported for local LLMs ([docs](https://mux.coder.com/config/models#ollama-local))
- OpenRouter supported for long-tail of LLMs ([docs](https://mux.coder.com/config/models#openrouter-cloud))
- Azure OpenAI keyless auth via Microsoft Entra ID (`OPENAI_AUTH_MODE=entra` + `OPENAI_BASE_URL`) ([docs](https://mux.coder.com/config/providers#openai-azure-entra-id-keyless))
- **VS Code Extension**: Jump into Mux workspaces directly from VS Code ([docs](https://mux.coder.com/integrations/vscode-extension))
- Supporting UI and keybinds for efficiently managing a suite of agents
- Rich markdown outputs (mermaid diagrams, LaTeX, etc.)
Expand Down
1,538 changes: 1,019 additions & 519 deletions bun.lock

Large diffs are not rendered by default.

65 changes: 54 additions & 11 deletions docs/config/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@ Providers also read from environment variables as fallback:
<details>
<summary>Additional environment variables</summary>

| Provider | Variable | Purpose |
| ------------ | -------------------------- | ------------------- |
| Anthropic | `ANTHROPIC_BASE_URL` | Custom API endpoint |
| OpenAI | `OPENAI_BASE_URL` | Custom API endpoint |
| OpenAI | `OPENAI_ORG_ID` | Organization ID |
| Google | `GOOGLE_BASE_URL` | Custom API endpoint |
| xAI | `XAI_BASE_URL` | Custom API endpoint |
| Azure OpenAI | `AZURE_OPENAI_API_KEY` | API key |
| Azure OpenAI | `AZURE_OPENAI_ENDPOINT` | Endpoint URL |
| Azure OpenAI | `AZURE_OPENAI_DEPLOYMENT` | Deployment name |
| Azure OpenAI | `AZURE_OPENAI_API_VERSION` | API version |
| Provider | Variable | Purpose |
| ------------ | -------------------------- | ---------------------------- |
| Anthropic | `ANTHROPIC_BASE_URL` | Custom API endpoint |
| OpenAI | `OPENAI_BASE_URL` | Custom API endpoint |
| OpenAI | `OPENAI_ORG_ID` | Organization ID |
| OpenAI | `OPENAI_AUTH_MODE` | Auth mode (`apiKey`/`entra`) |
| Google | `GOOGLE_BASE_URL` | Custom API endpoint |
| xAI | `XAI_BASE_URL` | Custom API endpoint |
| Azure OpenAI | `AZURE_OPENAI_API_KEY` | API key |
| Azure OpenAI | `AZURE_OPENAI_ENDPOINT` | Endpoint URL |
| Azure OpenAI | `AZURE_OPENAI_DEPLOYMENT` | Deployment name |
| Azure OpenAI | `AZURE_OPENAI_API_VERSION` | API version |

Azure OpenAI env vars configure the OpenAI provider with Azure backend.

Expand Down Expand Up @@ -97,6 +98,48 @@ For advanced options not exposed in the UI, edit `~/.mux/providers.jsonc` direct
}
```

### OpenAI: Azure Entra ID (Keyless)

Use Azure Entra ID credentials instead of API keys when routing the OpenAI provider through an Azure OpenAI endpoint.

**Required configuration**

- Set `OPENAI_AUTH_MODE=entra` (or `"authMode": "entra"` under `"openai"` in `~/.mux/providers.jsonc`).
- Set `OPENAI_BASE_URL` to your Azure OpenAI endpoint (for example `https://my-resource.openai.azure.com`).
- Do not set `OPENAI_API_KEY` for this mode.

**How it works**

Mux uses `DefaultAzureCredential` from `@azure/identity`, which supports:

- Local development with `az login`
- CI/CD with managed identity or workload identity

**OpenAI auth priority**

1. API key (if set)
2. Codex OAuth (if applicable)
3. Entra keyless (`authMode: "entra"` + Azure `baseUrl`)
4. Error if no auth path is configured

**`providers.jsonc` example**

```jsonc
{
"openai": {
"authMode": "entra",
"baseUrl": "https://my-resource.openai.azure.com",
},
}
```

**Environment-only example**

```sh
OPENAI_AUTH_MODE=entra
OPENAI_BASE_URL=https://my-resource.openai.azure.com
```

### Bedrock Authentication

Bedrock supports multiple authentication methods (tried in order):
Expand Down
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@

outputHashMode = "recursive";
# Marker used by scripts/update_flake_hash.sh to update this hash in place.
outputHash = "sha256-+6o2twg8KOUBuq2RoEqY/OwqCnWSrUiXFuaeLUiuF3k="; # mux-offline-cache-hash
outputHash = "sha256-ExkCcDObxrht0hTilYOUBx0L82NhFr0PMJAEukILibA="; # mux-offline-cache-hash
};

configurePhase = ''
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@ai-sdk/openai-compatible": "^2.0.27",
"@ai-sdk/xai": "^3.0.47",
"@aws-sdk/credential-providers": "^3.940.0",
"@azure/identity": "^4.13.0",
"@coder/mux-md-client": "0.1.0-main.29",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
Expand Down
6 changes: 6 additions & 0 deletions scripts/gen_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ function generateProviderEnvVarsBlock(): string {
" | Organization ID |"
);
}
if (vars.authMode?.length) {
lines.push(
`| ${displayName.padEnd(12)} | \`${vars.authMode[0]}\``.padEnd(42) +
" | Auth mode (`apiKey`/`entra`) |"
);
}
}

// Azure OpenAI (special case)
Expand Down
48 changes: 47 additions & 1 deletion src/browser/components/Settings/sections/ProvidersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export function ProvidersSection() {

const codexOauthIsConnected = config?.openai?.codexOauthSet === true;
const openaiApiKeySet = config?.openai?.apiKeySet === true;
const openaiAuthMode = config?.openai?.openaiAuthMode ?? "apiKey";
const codexOauthDefaultAuth =
config?.openai?.codexOauthDefaultAuth === "apiKey" ? "apiKey" : "oauth";
const codexOauthDefaultAuthIsEditable = codexOauthIsConnected && openaiApiKeySet;
Expand Down Expand Up @@ -1505,9 +1506,54 @@ export function ProvidersSection() {
</div>
)}

{/* OpenAI: ChatGPT OAuth + service tier */}
{/* OpenAI: auth mode, ChatGPT OAuth, and service tier */}
{provider === "openai" && (
<div className="border-border-light space-y-3 border-t pt-3">
<div className="space-y-2">
<div>
<label className="text-muted block text-xs">Auth Mode</label>
<p className="text-muted text-xs">
Choose how to authenticate OpenAI requests.
</p>
</div>
<Select
value={openaiAuthMode}
onValueChange={(next) => {
if (!api) return;
if (next !== "apiKey" && next !== "entra") {
return;
}

// Provider config info exposes this as `openaiAuthMode`, but the
// persisted providers.jsonc key is `authMode`.
updateOptimistically("openai", { openaiAuthMode: next });
void api.providers.setProviderConfig({
provider: "openai",
keyPath: ["authMode"],
value: next,
});
}}
>
<SelectTrigger className="w-52">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="apiKey">API Key</SelectItem>
<SelectItem value="entra">Azure Entra ID (keyless)</SelectItem>
</SelectContent>
</Select>
</div>

{openaiAuthMode === "entra" && (
// Keep this explicit: Entra auth is environment-driven (DefaultAzureCredential),
// so users should not look for a dedicated in-app Entra login button.
<p className="text-muted text-xs">
Entra keyless auth uses Azure credentials from your environment (for
example, run az login locally, or use managed/workload identity in cloud).
There is no separate Entra login button in Mux.
</p>
)}

<div>
<label className="text-foreground block text-xs font-medium">
ChatGPT (Codex) OAuth
Expand Down
2 changes: 1 addition & 1 deletion src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ async function main(): Promise<number> {
config.saveProvidersConfig(providersFromEnv);
} else {
throw new Error(
"No provider credentials found. Configure providers.jsonc or set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY."
"No provider credentials found. Configure providers.jsonc or set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY, or use OPENAI_AUTH_MODE=entra with OPENAI_BASE_URL for Azure keyless auth."
);
}
}
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 @@ -152,6 +152,8 @@ export const ProviderConfigInfoSchema = z.object({
* ChatGPT OAuth and an OpenAI API key are configured.
*/
codexOauthDefaultAuth: z.enum(["oauth", "apiKey"]).optional(),
/** OpenAI-only: auth mode — API key (default) or Azure Entra ID keyless auth. */
openaiAuthMode: z.enum(["apiKey", "entra"]).optional(),
/** AWS-specific fields (only present for bedrock provider) */
aws: AWSCredentialStatusSchema.optional(),
/** Mux Gateway-specific fields */
Expand Down
2 changes: 1 addition & 1 deletion src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export class AgentSession {
}
}

if (streamCursor && streamInfo && streamCursor.messageId === streamInfo.messageId) {
if (streamCursor && streamCursor.messageId === streamInfo?.messageId) {
// Stream cursor is advisory: only apply it when the same stream is still active.
// If the stream ended or rotated while offline, keep since-mode history replay
// and skip stream filtering by leaving afterTimestamp undefined.
Expand Down
Loading
Loading