diff --git a/README.md b/README.md
index 7b8400bc8..7aebe3f4b 100644
--- a/README.md
+++ b/README.md
@@ -369,7 +369,7 @@ Coverage output is written to `coverage/lcov.info`, and OpenClaude also generate
## VS Code Extension
-The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration, provider-aware control-center UI, and theme support.
+The repo includes a VS Code extension in [`vscode-extension/openclaude-vscode`](vscode-extension/openclaude-vscode) for OpenClaude launch integration, provider-aware Control Center, in-editor chat, theme support, and optional **Microsoft Foundry / Azure OpenAI** configuration (endpoint, API version, deployment, API key via Secret Storage) injected into launched terminals. See that folder’s [README](vscode-extension/openclaude-vscode/README.md).
## Security
diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index 5b54bd573..abaeec4c9 100644
--- a/docs/advanced-setup.md
+++ b/docs/advanced-setup.md
@@ -256,6 +256,30 @@ export OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments
export OPENAI_MODEL=gpt-4o
```
+### Microsoft Foundry / Azure OpenAI (resource URL + deployment)
+
+When your endpoint is the **resource base URL** (not the full `.../deployments/.../v1` path), set `OPENAI_MODEL` to the **deployment name** and `AZURE_OPENAI_API_VERSION` to your API version. The OpenAI shim builds:
+
+`{base}/openai/deployments/{OPENAI_MODEL}/chat/completions?api-version={AZURE_OPENAI_API_VERSION}`
+
+and sends the key in the `api-key` header for Azure hosts.
+
+```bash
+export CLAUDE_CODE_USE_OPENAI=1
+export OPENAI_API_KEY=your-azure-key
+export OPENAI_BASE_URL=https://your-resource.openai.azure.com
+export OPENAI_MODEL=your-deployment-name
+export AZURE_OPENAI_API_VERSION=2024-12-01-preview
+```
+
+If your hostname is not detected as Azure (for example some inference endpoints), force Azure URL and header behavior:
+
+```bash
+export OPENAI_AZURE_STYLE=1
+```
+
+The **OpenClaude VS Code extension** can store the key in Secret Storage and set these variables for you when you launch from the Control Center. See `vscode-extension/openclaude-vscode/README.md`.
+
## Environment Variables
| Variable | Required | Description |
diff --git a/docs/quick-start-windows.md b/docs/quick-start-windows.md
index 58d6aa886..1dbe59f48 100644
--- a/docs/quick-start-windows.md
+++ b/docs/quick-start-windows.md
@@ -155,9 +155,13 @@ npm install -g @gitlawb/openclaude@latest
npm uninstall -g @gitlawb/openclaude
```
+
## Need Advanced Setup?
-Use:
+For advanced provider setup, custom endpoints, environment variables, and enterprise launch workflows, see the advanced setup guide:
-- [Advanced Setup](advanced-setup.md)
- For Codex, Gemini, Mistral, LiteLLM, provider profiles, and runtime diagnostics.
+- [Advanced setup](advanced-setup.md)
+
+For Windows helper aliases and launcher shortcuts such as `oc`, `oc-init`, `oc-local`, `oc-provider`, and `oc-check`, see:
+
+- [Windows aliases and launchers](windows-aliases-and-launchers.md)
\ No newline at end of file
diff --git a/docs/windows-aliases-and-launchers.md b/docs/windows-aliases-and-launchers.md
new file mode 100644
index 000000000..0ce55647a
--- /dev/null
+++ b/docs/windows-aliases-and-launchers.md
@@ -0,0 +1,163 @@
+# Windows aliases and launchers
+
+This page documents optional PowerShell helper functions for launching OpenClaude on Windows after a global npm install.
+
+These helpers are designed for the installed package workflow:
+
+~~~powershell
+npm install -g @gitlawb/openclaude
+~~~
+
+The helpers use the installed `openclaude` CLI command. They do not require a source checkout and do not call source-only `bun run scripts/*.ts` entrypoints.
+
+## One-time setup
+
+Run this once in PowerShell:
+
+~~~powershell
+$packageRoot = Join-Path (npm root -g) "@gitlawb/openclaude"
+$aliases = Join-Path $packageRoot "scripts\windows\openclaude-aliases.ps1"
+
+if (-not (Test-Path $aliases)) {
+ throw "Alias script not found at $aliases. Update or reinstall @gitlawb/openclaude."
+}
+
+if (-not (Test-Path $PROFILE)) {
+ New-Item -ItemType File -Path $PROFILE -Force | Out-Null
+}
+
+$profileLine = ". `"$aliases`""
+
+if (-not (Select-String -Path $PROFILE -Pattern ([regex]::Escape($profileLine)) -Quiet)) {
+ Add-Content -Path $PROFILE -Value "`n$profileLine"
+}
+
+. $aliases
+oc-help
+~~~
+
+Open a new PowerShell window after setup, or dot-source the profile:
+
+~~~powershell
+. $PROFILE
+~~~
+
+## Daily commands
+
+### Launch OpenClaude using the installed CLI
+
+~~~powershell
+oc
+~~~
+
+You can pass normal CLI arguments through `oc`:
+
+~~~powershell
+oc --version
+oc --help
+~~~
+
+### Launch with local Ollama/OpenAI-compatible environment hints
+
+~~~powershell
+oc-local
+~~~
+
+By default, this uses local Ollama through the OpenAI-compatible API:
+
+~~~text
+CLAUDE_CODE_USE_OPENAI=1
+OPENAI_BASE_URL=http://localhost:11434/v1
+OPENAI_MODEL=llama3.1:8b
+~~~
+
+To use a different local model for that invocation:
+
+~~~powershell
+oc-local -Model "qwen2.5-coder:7b"
+~~~
+
+The environment overrides are scoped to that single `openclaude` invocation. A later plain `oc` call returns to normal installed CLI behavior and saved-provider-profile behavior.
+
+### Launch with low-latency local defaults
+
+~~~powershell
+oc-fast
+~~~
+
+To use a different model:
+
+~~~powershell
+oc-fast -Model "qwen2.5-coder:7b"
+~~~
+
+Like `oc-local`, the environment overrides are scoped to that single invocation.
+
+### Open the provider manager
+
+~~~powershell
+oc-provider
+~~~
+
+This opens the provider manager through the installed OpenClaude CLI.
+
+### Check local Ollama state
+
+~~~powershell
+oc-check
+~~~
+
+To check a specific model:
+
+~~~powershell
+oc-check -Model "qwen2.5-coder:7b"
+~~~
+
+### Pull/check a local model, then launch local mode
+
+~~~powershell
+oc-init
+~~~
+
+To choose a model:
+
+~~~powershell
+oc-init -Model "qwen2.5-coder:7b"
+~~~
+
+To skip pulling the model and only check/launch:
+
+~~~powershell
+oc-init -Model "qwen2.5-coder:7b" -SkipModelPull
+~~~
+
+`oc-init` does not save a provider profile. It pulls/checks the local Ollama model and then launches `oc-local`.
+
+### Show quick help
+
+~~~powershell
+oc-help
+~~~
+
+## Command summary
+
+| Command | Purpose |
+| --- | --- |
+| `oc` | Launch OpenClaude using the installed CLI and saved/default behavior |
+| `oc-local` | Launch once with local Ollama/OpenAI-compatible environment hints |
+| `oc-fast` | Launch once with local Ollama/OpenAI-compatible low-latency hints |
+| `oc-provider` | Open the provider manager |
+| `oc-check` | Show local Ollama install/listening/model state |
+| `oc-init` | Pull/check a local Ollama model, then launch local mode |
+| `oc-help` | Show quick command help |
+
+## Notes
+
+These helpers are intentionally global-install oriented. They use the installed CLI instead of source-checkout development scripts.
+
+For advanced provider setup, use the built-in provider manager:
+
+~~~powershell
+oc-provider
+~~~
+
diff --git a/package.json b/package.json
index f708068f4..4b64ba5e9 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,8 @@
"dist/sdk.mjs",
"src/entrypoints/sdk.d.ts",
"src/entrypoints/sdk/coreTypes.generated.ts",
+ "scripts/windows/openclaude-aliases.ps1",
+ "docs/windows-aliases-and-launchers.md",
"README.md"
],
"scripts": {
diff --git a/scripts/windows/openclaude-aliases.ps1 b/scripts/windows/openclaude-aliases.ps1
new file mode 100644
index 000000000..1774336f8
--- /dev/null
+++ b/scripts/windows/openclaude-aliases.ps1
@@ -0,0 +1,206 @@
+Set-StrictMode -Version Latest
+
+function Test-OpenClaudeCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name
+ )
+
+ return [bool](Get-Command -Name $Name -ErrorAction SilentlyContinue)
+}
+
+function Assert-OpenClaudeCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name,
+ [Parameter(Mandatory = $true)]
+ [string]$InstallHint
+ )
+
+ if (-not (Test-OpenClaudeCommand -Name $Name)) {
+ throw "Required command '$Name' was not found. $InstallHint"
+ }
+}
+
+function Invoke-OpenClaude {
+ [CmdletBinding()]
+ param(
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$OpenClaudeArgs
+ )
+
+ Assert-OpenClaudeCommand -Name "openclaude" -InstallHint "Install with: npm install -g @gitlawb/openclaude"
+
+ & openclaude @OpenClaudeArgs
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "openclaude failed with exit code $LASTEXITCODE."
+ }
+}
+
+function Invoke-OpenClaudeWithEnvironment {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [hashtable]$Environment,
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$OpenClaudeArgs
+ )
+
+ $previousValues = @{}
+
+ foreach ($name in $Environment.Keys) {
+ $previousValues[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
+ Set-Item -Path "Env:$name" -Value $Environment[$name]
+ }
+
+ try {
+ Invoke-OpenClaude @OpenClaudeArgs
+ }
+ finally {
+ foreach ($name in $Environment.Keys) {
+ if ($null -eq $previousValues[$name]) {
+ Remove-Item -Path "Env:$name" -ErrorAction SilentlyContinue
+ }
+ else {
+ Set-Item -Path "Env:$name" -Value $previousValues[$name]
+ }
+ }
+ }
+}
+
+function Get-OpenClaudeQuickHelp {
+ [CmdletBinding()]
+ param()
+
+ @(
+ "OpenClaude quick commands:",
+ " oc [args...] -> launch OpenClaude using the installed CLI",
+ " oc-local [args...] -> launch OpenClaude with local/Ollama OpenAI-compatible environment hints for this invocation only",
+ " oc-fast [args...] -> launch OpenClaude with low-latency local defaults for this invocation only",
+ " oc-provider -> open the provider manager in OpenClaude",
+ " oc-check -> show Ollama install/listening/model state",
+ " oc-init -> pull/check the local model, then launch local/Ollama mode",
+ " oc-help -> show this help"
+ ) -join [Environment]::NewLine
+}
+
+function oc {
+ [CmdletBinding()]
+ param(
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$OpenClaudeArgs
+ )
+
+ Invoke-OpenClaude @OpenClaudeArgs
+}
+
+function oc-local {
+ [CmdletBinding()]
+ param(
+ [string]$Model = "llama3.1:8b",
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$OpenClaudeArgs
+ )
+
+ Invoke-OpenClaudeWithEnvironment `
+ -Environment @{
+ CLAUDE_CODE_USE_OPENAI = "1"
+ OPENAI_BASE_URL = "http://localhost:11434/v1"
+ OPENAI_MODEL = $Model
+ } `
+ @OpenClaudeArgs
+}
+
+function oc-fast {
+ [CmdletBinding()]
+ param(
+ [string]$Model = "llama3.1:8b",
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$OpenClaudeArgs
+ )
+
+ Invoke-OpenClaudeWithEnvironment `
+ -Environment @{
+ CLAUDE_CODE_USE_OPENAI = "1"
+ OPENAI_BASE_URL = "http://localhost:11434/v1"
+ OPENAI_MODEL = $Model
+ OPENCLAUDE_FAST_MODE = "1"
+ } `
+ @OpenClaudeArgs
+}
+
+function oc-provider {
+ [CmdletBinding()]
+ param()
+
+ Invoke-OpenClaude "/provider"
+}
+
+function oc-check {
+ [CmdletBinding()]
+ param(
+ [string]$Model = "llama3.1:8b"
+ )
+
+ Assert-OpenClaudeCommand -Name "ollama" -InstallHint "Install Ollama from https://ollama.com/download/windows."
+
+ $version = & ollama --version 2>$null
+ $modelNames = (& ollama list 2>$null | Select-Object -Skip 1 | ForEach-Object {
+ ($_ -split "\s+")[0]
+ }) | Where-Object { $_ }
+
+ $isModelAvailable = $modelNames -contains $Model
+ $probeSucceeded = $false
+
+ try {
+ $response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get -TimeoutSec 3
+ if ($response.models) {
+ $probeSucceeded = $true
+ }
+ }
+ catch {
+ $probeSucceeded = $false
+ }
+
+ [PSCustomObject]@{
+ OllamaInstalled = $true
+ OllamaVersion = $version
+ OllamaListening = $probeSucceeded
+ Model = $Model
+ ModelAvailable = $isModelAvailable
+ }
+}
+
+function oc-init {
+ [CmdletBinding()]
+ param(
+ [string]$Model = "llama3.1:8b",
+ [switch]$SkipModelPull
+ )
+
+ Assert-OpenClaudeCommand -Name "ollama" -InstallHint "Install Ollama from https://ollama.com/download/windows."
+
+ if (-not $SkipModelPull) {
+ & ollama pull $Model
+ if ($LASTEXITCODE -ne 0) {
+ throw "ollama pull $Model failed with exit code $LASTEXITCODE."
+ }
+ }
+
+ $health = oc-check -Model $Model
+ if (-not $health.OllamaListening) {
+ Write-Warning "Ollama is installed but API probe to localhost:11434 did not succeed. Start Ollama and retry."
+ }
+
+ oc-local -Model $Model
+}
+
+function oc-help {
+ [CmdletBinding()]
+ param()
+
+ Get-OpenClaudeQuickHelp
+}
diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts
index e26452fd0..6e218048b 100644
--- a/src/services/api/openaiShim.ts
+++ b/src/services/api/openaiShim.ts
@@ -23,6 +23,11 @@
* CLAUDE_CODE_USE_GITHUB=1 — enable GitHub inference (no need for USE_OPENAI)
* GITHUB_TOKEN or GH_TOKEN — Copilot API token (mapped to Bearer auth)
* OPENAI_MODEL — optional; use github:copilot or openai/gpt-4.1 style IDs
+ *
+ * Azure OpenAI / Microsoft Foundry (OpenAI-compatible chat):
+ * AZURE_OPENAI_API_VERSION — query param for chat/completions (default: 2024-12-01-preview)
+ * OPENAI_AZURE_STYLE=1 — force Azure deployment URL + api-key header when the hostname
+ * would not otherwise match (for example inference.ml.azure.com)
*/
import { APIError } from '@anthropic-ai/sdk'
@@ -2441,12 +2446,20 @@ class OpenAIShimMessages {
: apiKey
// Detect Azure endpoints by hostname (not raw URL) to prevent bypass via
// path segments like https://evil.com/cognitiveservices.azure.com/
- let isAzure = false
- try {
- const { hostname } = new URL(request.baseUrl)
- isAzure = hostname.endsWith('.azure.com') &&
- (hostname.includes('cognitiveservices') || hostname.includes('openai') || hostname.includes('services.ai'))
- } catch { /* malformed URL — not Azure */ }
+ let isAzure = isEnvTruthy(process.env.OPENAI_AZURE_STYLE)
+ if (!isAzure) {
+ try {
+ const { hostname } = new URL(request.baseUrl)
+ isAzure =
+ hostname.endsWith('.azure.com') &&
+ (hostname.includes('cognitiveservices') ||
+ hostname.includes('openai') ||
+ hostname.includes('services.ai') ||
+ hostname.includes('inference.ml'))
+ } catch {
+ /* malformed URL — not Azure */
+ }
+ }
let isBankr = false
try {
diff --git a/vscode-extension/openclaude-vscode/.vscode/launch.json b/vscode-extension/openclaude-vscode/.vscode/launch.json
index b521eca09..662dd021f 100644
--- a/vscode-extension/openclaude-vscode/.vscode/launch.json
+++ b/vscode-extension/openclaude-vscode/.vscode/launch.json
@@ -6,8 +6,7 @@
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
- "outFiles": ["${workspaceFolder}/out/**/*.js"],
- "preLaunchTask": "${defaultBuildTask}"
+ "outFiles": ["${workspaceFolder}/out/**/*.js"]
}
]
}
diff --git a/vscode-extension/openclaude-vscode/README.md b/vscode-extension/openclaude-vscode/README.md
index 0bc3ae07b..c7e254437 100644
--- a/vscode-extension/openclaude-vscode/README.md
+++ b/vscode-extension/openclaude-vscode/README.md
@@ -24,6 +24,7 @@ A practical VS Code companion for OpenClaude with a project-aware **Control Cent
- Open Setup Guide
- Open Command Palette
- **Built-in dark theme**: `OpenClaude Terminal Black`
+- **Microsoft Foundry / Azure OpenAI**: optional wizard and settings store endpoint, API version, deployment name, and API key (Secret Storage); launch injects `OPENAI_*` and `AZURE_OPENAI_API_VERSION` into the OpenClaude terminal (see `docs/advanced-setup.md` on the repo).
## Requirements
@@ -38,14 +39,29 @@ A practical VS Code companion for OpenClaude with a project-aware **Control Cent
- `OpenClaude: Open Repository`
- `OpenClaude: Open Setup Guide`
- `OpenClaude: Open Workspace Profile`
+- `OpenClaude: New Chat` / `OpenClaude: Open Chat Panel` / `OpenClaude: Resume Session` / `OpenClaude: Abort Generation`
+- `OpenClaude: Configure Azure / Foundry Chat (wizard)`
+- `OpenClaude: Set Azure / Foundry API Key (Secret Storage)`
+- `OpenClaude: Clear Azure / Foundry API Key`
+- `OpenClaude: Open Azure / Foundry Settings`
+
+## Microsoft Foundry / Azure OpenAI (terminal chat)
+
+1. Command Palette → **OpenClaude: Configure Azure / Foundry Chat (wizard)** and enter endpoint, API version, deployment name, and API key; or set `openclaude.azure.*` in Settings and use **OpenClaude: Set Azure / Foundry API Key**.
+2. Enable **OpenClaude: Azure: Enabled** (the wizard turns this on).
+3. **OpenClaude: Launch in Terminal** — the extension merges env vars the OpenAI shim expects (`CLAUDE_CODE_USE_OPENAI`, `OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_MODEL`, `AZURE_OPENAI_API_VERSION`, and `OPENAI_AZURE_STYLE` when forced).
+
+If you use `.openclaude-profile.json` for the same workspace, leave Azure injection off to avoid conflicting provider configuration.
## Settings
- `openclaude.launchCommand` (default: `openclaude`)
- `openclaude.terminalName` (default: `OpenClaude`)
- `openclaude.useOpenAIShim` (default: `false`)
+- `openclaude.azure.*` — Foundry / Azure OpenAI terminal injection (see Settings UI)
+- `openclaude.permissionMode` — chat permission mode
-`openclaude.useOpenAIShim` only injects `CLAUDE_CODE_USE_OPENAI=1` into terminals launched by the extension. It does not guess or configure a provider by itself.
+`openclaude.useOpenAIShim` only injects `CLAUDE_CODE_USE_OPENAI=1` when Azure injection did not already set it. It does not configure endpoints or keys by itself.
## Notes on Status Detection
diff --git a/vscode-extension/openclaude-vscode/package.json b/vscode-extension/openclaude-vscode/package.json
index f5ecba320..79194cfdc 100644
--- a/vscode-extension/openclaude-vscode/package.json
+++ b/vscode-extension/openclaude-vscode/package.json
@@ -1,7 +1,7 @@
{
"name": "openclaude-vscode",
"displayName": "OpenClaude",
- "description": "Practical VS Code companion for OpenClaude with project-aware launch behavior and a real Control Center.",
+ "description": "Practical VS Code companion for OpenClaude with project-aware launch, Control Center, optional Microsoft Foundry / Azure OpenAI terminal env, and chat.",
"version": "0.2.0",
"publisher": "devnull-bootloader",
"engines": {
@@ -23,6 +23,10 @@
"onCommand:openclaude.openChat",
"onCommand:openclaude.resumeSession",
"onCommand:openclaude.abortChat",
+ "onCommand:openclaude.setAzureApiKey",
+ "onCommand:openclaude.clearAzureApiKey",
+ "onCommand:openclaude.configureAzureChat",
+ "onCommand:openclaude.openAzureSettings",
"onView:openclaude.controlCenter",
"onView:openclaude.chat"
],
@@ -87,6 +91,26 @@
"command": "openclaude.abortChat",
"title": "OpenClaude: Abort Generation",
"category": "OpenClaude"
+ },
+ {
+ "command": "openclaude.setAzureApiKey",
+ "title": "OpenClaude: Set Azure / Foundry API Key (Secret Storage)",
+ "category": "OpenClaude"
+ },
+ {
+ "command": "openclaude.clearAzureApiKey",
+ "title": "OpenClaude: Clear Azure / Foundry API Key",
+ "category": "OpenClaude"
+ },
+ {
+ "command": "openclaude.configureAzureChat",
+ "title": "OpenClaude: Configure Azure / Foundry Chat (wizard)",
+ "category": "OpenClaude"
+ },
+ {
+ "command": "openclaude.openAzureSettings",
+ "title": "OpenClaude: Open Azure / Foundry Settings",
+ "category": "OpenClaude"
}
],
"viewsContainers": {
@@ -135,7 +159,37 @@
"openclaude.useOpenAIShim": {
"type": "boolean",
"default": false,
- "description": "Optionally set CLAUDE_CODE_USE_OPENAI=1 in launched OpenClaude terminals."
+ "description": "Optionally set CLAUDE_CODE_USE_OPENAI=1 in launched OpenClaude terminals when Azure injection is off or incomplete."
+ },
+ "openclaude.azure.enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "When true, launch injects Microsoft Foundry / Azure OpenAI-compatible chat env (OPENAI_* + AZURE_OPENAI_API_VERSION) into the OpenClaude terminal."
+ },
+ "openclaude.azure.endpoint": {
+ "type": "string",
+ "default": "",
+ "markdownDescription": "Azure resource base URL for OpenAI-compatible chat (example: `https://YOUR_RESOURCE.openai.azure.com`). Do not put `api-version` here; use **OpenClaude: Azure API Version**."
+ },
+ "openclaude.azure.apiVersion": {
+ "type": "string",
+ "default": "2024-12-01-preview",
+ "description": "Azure API version for chat completions (AZURE_OPENAI_API_VERSION)."
+ },
+ "openclaude.azure.deployment": {
+ "type": "string",
+ "default": "",
+ "markdownDescription": "Azure deployment name (maps to `OPENAI_MODEL` for the OpenAI shim)."
+ },
+ "openclaude.azure.forceAzureUrlStyle": {
+ "type": "boolean",
+ "default": true,
+ "description": "Sets OPENAI_AZURE_STYLE=1 so deployment URLs and api-key header work on Foundry and non-standard Azure hosts."
+ },
+ "openclaude.azure.apiKey": {
+ "type": "string",
+ "default": "",
+ "markdownDescription": "Optional API key in settings (not recommended). Prefer **OpenClaude: Set Azure / Foundry API Key**. Secret wins if both are set."
},
"openclaude.permissionMode": {
"type": "string",
@@ -169,7 +223,9 @@
"terminal",
"theme",
"cli",
- "llm"
+ "llm",
+ "azure",
+ "foundry"
],
"repository": {
"type": "git",
diff --git a/vscode-extension/openclaude-vscode/src/extension.js b/vscode-extension/openclaude-vscode/src/extension.js
index 2cfcf06ae..18243e7a3 100644
--- a/vscode-extension/openclaude-vscode/src/extension.js
+++ b/vscode-extension/openclaude-vscode/src/extension.js
@@ -19,6 +19,10 @@ const { DiffContentProvider, SCHEME: DIFF_SCHEME } = require('./chat/diffControl
const OPENCLAUDE_REPO_URL = 'https://github.com/Gitlawb/openclaude';
const OPENCLAUDE_SETUP_URL = 'https://github.com/Gitlawb/openclaude/blob/main/README.md#quick-start';
const PROFILE_FILE_NAME = '.openclaude-profile.json';
+const SECRET_AZURE_API_KEY = 'openclaude.azure.apiKey';
+
+/** @type {vscode.ExtensionContext | null} */
+let extensionContext = null;
function escapeHtml(value) {
return String(value)
@@ -52,6 +56,172 @@ function getExecutableFromCommand(command) {
return normalized.split(/\s+/)[0];
}
+/**
+ * @param {string} raw
+ */
+function normalizeAzureEndpoint(raw) {
+ const t = (raw || '').trim();
+ if (!t) {
+ return '';
+ }
+ try {
+ const u = new URL(t);
+ const pathname = u.pathname.replace(/\/+$/, '');
+ return `${u.origin}${pathname}`;
+ } catch {
+ return t.replace(/\/+$/, '');
+ }
+}
+
+/**
+ * @param {vscode.ExtensionContext | null} context
+ * @param {vscode.WorkspaceConfiguration} configured
+ */
+async function resolveAzureApiKey(context, configured) {
+ if (!context) {
+ return '';
+ }
+ const fromSecret = await context.secrets.get(SECRET_AZURE_API_KEY);
+ if (fromSecret) {
+ return fromSecret;
+ }
+ return (configured.get('azure.apiKey', '') || '').trim();
+}
+
+/**
+ * @param {vscode.WorkspaceConfiguration} configured
+ */
+async function buildLaunchAzureEnv(configured) {
+ const env = {};
+ const ctx = extensionContext;
+ if (!ctx) {
+ return env;
+ }
+
+ const azureEnabled = configured.get('azure.enabled', false);
+ const endpoint = normalizeAzureEndpoint(configured.get('azure.endpoint', ''));
+ const apiVersion = (configured.get('azure.apiVersion', '2024-12-01-preview') || '').trim();
+ const deployment = (configured.get('azure.deployment', '') || '').trim();
+ const forceStyle = configured.get('azure.forceAzureUrlStyle', true);
+
+ if (!azureEnabled) {
+ return env;
+ }
+
+ const apiKey = await resolveAzureApiKey(ctx, configured);
+ if (!endpoint || !deployment) {
+ void vscode.window.showWarningMessage(
+ 'OpenClaude Azure chat is enabled but endpoint or deployment is missing. Run "OpenClaude: Configure Azure / Foundry Chat" or set openclaude.azure.* in settings.',
+ );
+ return env;
+ }
+ if (!apiKey) {
+ void vscode.window.showWarningMessage(
+ 'OpenClaude Azure chat is enabled but no API key is set. Use "OpenClaude: Set Azure / Foundry API Key" or openclaude.azure.apiKey (not recommended).',
+ );
+ return env;
+ }
+
+ env.CLAUDE_CODE_USE_OPENAI = '1';
+ env.OPENAI_BASE_URL = endpoint;
+ env.OPENAI_API_KEY = apiKey;
+ env.OPENAI_MODEL = deployment;
+ env.AZURE_OPENAI_API_VERSION = apiVersion || '2024-12-01-preview';
+ if (forceStyle) {
+ env.OPENAI_AZURE_STYLE = '1';
+ }
+ return env;
+}
+
+/**
+ * @param {vscode.ExtensionContext} context
+ */
+async function setAzureApiKey(context) {
+ const key = await vscode.window.showInputBox({
+ title: 'OpenClaude — Azure / Foundry API key',
+ prompt: 'Stored in VS Code Secret Storage (not committed to the repo).',
+ password: true,
+ ignoreFocusOut: true,
+ validateInput: v => (v && v.trim() ? null : 'Enter a non-empty key'),
+ });
+ if (key == null) {
+ return;
+ }
+ await context.secrets.store(SECRET_AZURE_API_KEY, key.trim());
+ void vscode.window.showInformationMessage('OpenClaude Azure / Foundry API key saved to Secret Storage.');
+}
+
+/**
+ * @param {vscode.ExtensionContext} context
+ */
+async function clearAzureApiKey(context) {
+ await context.secrets.delete(SECRET_AZURE_API_KEY);
+ void vscode.window.showInformationMessage('OpenClaude Azure / Foundry API key removed from Secret Storage.');
+}
+
+/**
+ * @param {vscode.ExtensionContext} context
+ */
+async function configureAzureChat(context) {
+ const cfg = vscode.workspace.getConfiguration('openclaude');
+ const target = vscode.ConfigurationTarget.Global;
+
+ const endpoint = await vscode.window.showInputBox({
+ title: 'OpenClaude — Azure / Foundry API endpoint',
+ prompt: 'Resource base URL only (no api-version query). Example: https://YOUR_RESOURCE.openai.azure.com',
+ ignoreFocusOut: true,
+ value: cfg.get('azure.endpoint', ''),
+ validateInput: v => (v && v.trim() ? null : 'Required'),
+ });
+ if (endpoint == null) {
+ return;
+ }
+
+ const apiVersion = await vscode.window.showInputBox({
+ title: 'OpenClaude — Azure API version',
+ prompt: 'Matches the api-version used by your deployment (e.g. 2024-12-01-preview).',
+ value: (cfg.get('azure.apiVersion', '2024-12-01-preview') || '').trim(),
+ ignoreFocusOut: true,
+ validateInput: v => (v && v.trim() ? null : 'Required'),
+ });
+ if (apiVersion == null) {
+ return;
+ }
+
+ const deployment = await vscode.window.showInputBox({
+ title: 'OpenClaude — Azure deployment / model',
+ prompt: 'Deployment name in Azure (this becomes OPENAI_MODEL for the OpenAI shim).',
+ value: cfg.get('azure.deployment', ''),
+ ignoreFocusOut: true,
+ validateInput: v => (v && v.trim() ? null : 'Required'),
+ });
+ if (deployment == null) {
+ return;
+ }
+
+ const key = await vscode.window.showInputBox({
+ title: 'OpenClaude — Azure / Foundry API key',
+ prompt: 'Stored in VS Code Secret Storage.',
+ password: true,
+ ignoreFocusOut: true,
+ validateInput: v => (v && v.trim() ? null : 'Required'),
+ });
+ if (key == null) {
+ return;
+ }
+
+ await cfg.update('azure.endpoint', normalizeAzureEndpoint(endpoint), target);
+ await cfg.update('azure.apiVersion', apiVersion.trim(), target);
+ await cfg.update('azure.deployment', deployment.trim(), target);
+ await cfg.update('azure.forceAzureUrlStyle', true, target);
+ await cfg.update('azure.enabled', true, target);
+ await context.secrets.store(SECRET_AZURE_API_KEY, key.trim());
+
+ void vscode.window.showInformationMessage(
+ 'OpenClaude Azure / Foundry chat saved. Launch OpenClaude from the Control Center or command palette.',
+ );
+}
+
function getWorkspacePaths() {
return (vscode.workspace.workspaceFolders || []).map(folder => folder.uri.fsPath);
}
@@ -305,8 +475,8 @@ async function launchOpenClaude(options = {}) {
return;
}
- const env = {};
- if (shimEnabled) {
+ const env = await buildLaunchAzureEnv(configured);
+ if (shimEnabled && !env.CLAUDE_CODE_USE_OPENAI) {
env.CLAUDE_CODE_USE_OPENAI = '1';
}
@@ -887,6 +1057,10 @@ function renderControlCenterHtml(status, options = {}) {
Open Command Palette
Access VS Code and OpenClaude commands quickly.
+
@@ -904,6 +1078,7 @@ function renderControlCenterHtml(status, options = {}) {
document.getElementById('repo').addEventListener('click', () => vscode.postMessage({ type: 'repo' }));
document.getElementById('setup').addEventListener('click', () => vscode.postMessage({ type: 'setup' }));
document.getElementById('commands').addEventListener('click', () => vscode.postMessage({ type: 'commands' }));
+ document.getElementById('azureFoundry').addEventListener('click', () => vscode.postMessage({ type: 'azureSettings' }));
document.getElementById('refresh').addEventListener('click', () => vscode.postMessage({ type: 'refresh' }));
const profileButton = document.getElementById('openProfile');
@@ -950,6 +1125,9 @@ class OpenClaudeControlCenterProvider {
case 'commands':
await vscode.commands.executeCommand('workbench.action.showCommands');
break;
+ case 'azureSettings':
+ await vscode.commands.executeCommand('workbench.action.openSettings', 'openclaude.azure');
+ break;
case 'refresh':
default:
break;
@@ -1044,6 +1222,8 @@ class OpenClaudeControlCenterProvider {
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
+ extensionContext = context;
+
// ── Control Center (existing) ──
const provider = new OpenClaudeControlCenterProvider();
const refreshProvider = () => {
@@ -1165,6 +1345,22 @@ function activate(context) {
chatController.abort();
});
+ const setAzureApiKeyCommand = vscode.commands.registerCommand('openclaude.setAzureApiKey', async () => {
+ await setAzureApiKey(context);
+ });
+
+ const clearAzureApiKeyCommand = vscode.commands.registerCommand('openclaude.clearAzureApiKey', async () => {
+ await clearAzureApiKey(context);
+ });
+
+ const configureAzureChatCommand = vscode.commands.registerCommand('openclaude.configureAzureChat', async () => {
+ await configureAzureChat(context);
+ });
+
+ const openAzureSettingsCommand = vscode.commands.registerCommand('openclaude.openAzureSettings', async () => {
+ await vscode.commands.executeCommand('workbench.action.openSettings', 'openclaude.azure');
+ });
+
// ── Register providers ──
const controlCenterProviderReg = vscode.window.registerWebviewViewProvider(
'openclaude.controlCenter',
@@ -1193,6 +1389,10 @@ function activate(context) {
openChatCommand,
resumeSessionCommand,
abortChatCommand,
+ setAzureApiKeyCommand,
+ clearAzureApiKeyCommand,
+ configureAzureChatCommand,
+ openAzureSettingsCommand,
chatViewProviderReg,
diffProviderReg,
statusBarItem,
@@ -1221,7 +1421,9 @@ function activate(context) {
);
}
-function deactivate() {}
+function deactivate() {
+ extensionContext = null;
+}
module.exports = {
activate,