diff --git a/.env.example b/.env.example
index 99f367cc..389caeaa 100644
--- a/.env.example
+++ b/.env.example
@@ -47,4 +47,11 @@
# 通用设置(建议始终开启)
# ============================================================
DISABLE_TELEMETRY=1
+
+# Azure OpenAI (Codex)
+CLAUDE_CODE_USE_AZURE_OPENAI=1
+AZURE_OPENAI_BASE_URL=https://your-resource.cognitiveservices.azure.com
+AZURE_OPENAI_API_VERSION=2025-04-01-preview
+AZURE_OPENAI_API_KEY=your_azure_openai_key
+AZURE_OPENAI_CODEX_DEPLOYMENT=your_codex_deployment
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
diff --git a/.gitignore b/.gitignore
index 2dc4818c..9007fabc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.env.*
!.env.example
node_modules
+openspec
diff --git a/README.en.md b/README.en.md
index afb19c87..c2101a88 100644
--- a/README.en.md
+++ b/README.en.md
@@ -1,6 +1,6 @@
-# Claude Code Haha
+# Claude Code Haha
-
中文 | English
+涓枃 | English
A **locally runnable version** repaired from the leaked Claude Code source, with support for any Anthropic-compatible API endpoint such as MiniMax and OpenRouter.
@@ -163,7 +163,7 @@ echo "explain this code" | ./bin/claude-haha -p
The startup script `bin/claude-haha` is a bash script and cannot run directly in cmd or PowerShell. Use one of the following methods:
-**Option 1: PowerShell / cmd — call Bun directly (recommended)**
+**Option 1: PowerShell / cmd 鈥?call Bun directly (recommended)**
```powershell
# Interactive TUI mode
@@ -202,6 +202,56 @@ bun --env-file=.env ./src/localRecoveryCli.ts
| `DISABLE_TELEMETRY` | No | Set to `1` to disable telemetry |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | No | Set to `1` to disable non-essential network traffic |
+### Azure OpenAI (Codex)
+
+```env
+CLAUDE_CODE_USE_AZURE_OPENAI=1
+AZURE_OPENAI_BASE_URL=https://.cognitiveservices.azure.com
+AZURE_OPENAI_API_VERSION=2025-04-01-preview
+AZURE_OPENAI_API_KEY=...
+AZURE_OPENAI_CODEX_DEPLOYMENT=...
+```
+
+You can either set a single default deployment via env:
+
+```env
+AZURE_OPENAI_CODEX_DEPLOYMENT=...
+```
+
+Or map Codex models to deployments via settings (example `~/.claude/settings.json`):
+
+```json
+{
+ "modelOverrides": {
+ "gpt-5.2-codex": "codex-deployment-52",
+ "gpt-5.3-codex": "codex-deployment-53",
+ "gpt-5.4-codex": "codex-deployment-54"
+ }
+}
+```
+
+
+### Azure OpenAI (Codex)
+
+Set these env vars to route requests to Azure OpenAI:
+
+```env
+CLAUDE_CODE_USE_AZURE_OPENAI=1
+AZURE_OPENAI_BASE_URL=https://.cognitiveservices.azure.com
+AZURE_OPENAI_API_VERSION=2025-04-01-preview
+AZURE_OPENAI_API_KEY=...
+```
+
+Map the Codex model to your deployment name via settings (example `~/.claude/settings.json`):
+
+```json
+{
+ "modelOverrides": {
+ "gpt-5.2-codex": "my-codex-deployment"
+ }
+}
+```
+
---
## Fallback Mode
@@ -236,19 +286,19 @@ bin/claude-haha # Entry script
preload.ts # Bun preload (sets MACRO globals)
.env.example # Environment variable template
src/
-├── entrypoints/cli.tsx # Main CLI entry
-├── main.tsx # Main TUI logic (Commander.js + React/Ink)
-├── localRecoveryCli.ts # Fallback Recovery CLI
-├── setup.ts # Startup initialization
-├── screens/REPL.tsx # Interactive REPL screen
-├── ink/ # Ink terminal rendering engine
-├── components/ # UI components
-├── tools/ # Agent tools (Bash, Edit, Grep, etc.)
-├── commands/ # Slash commands (/commit, /review, etc.)
-├── skills/ # Skill system
-├── services/ # Service layer (API, MCP, OAuth, etc.)
-├── hooks/ # React hooks
-└── utils/ # Utility functions
+鈹溾攢鈹€ entrypoints/cli.tsx # Main CLI entry
+鈹溾攢鈹€ main.tsx # Main TUI logic (Commander.js + React/Ink)
+鈹溾攢鈹€ localRecoveryCli.ts # Fallback Recovery CLI
+鈹溾攢鈹€ setup.ts # Startup initialization
+鈹溾攢鈹€ screens/REPL.tsx # Interactive REPL screen
+鈹溾攢鈹€ ink/ # Ink terminal rendering engine
+鈹溾攢鈹€ components/ # UI components
+鈹溾攢鈹€ tools/ # Agent tools (Bash, Edit, Grep, etc.)
+鈹溾攢鈹€ commands/ # Slash commands (/commit, /review, etc.)
+鈹溾攢鈹€ skills/ # Skill system
+鈹溾攢鈹€ services/ # Service layer (API, MCP, OAuth, etc.)
+鈹溾攢鈹€ hooks/ # React hooks
+鈹斺攢鈹€ utils/ # Utility functions
```
---
diff --git a/README.md b/README.md
index c715228d..6d3ad38b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Claude Code Haha
+# Claude Code Haha
中文 | English
@@ -202,6 +202,39 @@ bun --env-file=.env ./src/localRecoveryCli.ts
| `DISABLE_TELEMETRY` | 否 | 设为 `1` 禁用遥测 |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | 否 | 设为 `1` 禁用非必要网络请求 |
+### Azure OpenAI (Codex)
+
+```env
+CLAUDE_CODE_USE_AZURE_OPENAI=1
+AZURE_OPENAI_BASE_URL=https://.cognitiveservices.azure.com
+AZURE_OPENAI_API_VERSION=2025-04-01-preview
+AZURE_OPENAI_API_KEY=...
+```
+
+AZURE_OPENAI_CODEX_DEPLOYMENT=...
+```
+
+也可以只用一个默认 deployment:
+
+```env
+AZURE_OPENAI_CODEX_DEPLOYMENT=...
+```
+
+或在 `~/.claude/settings.json` 中按模型映射:
+
+`~/.claude/settings.json`:
+
+```json
+{
+ "modelOverrides": {
+ "gpt-5.2-codex": "codex-deployment-52",
+ "gpt-5.3-codex": "codex-deployment-53",
+ "gpt-5.4-codex": "codex-deployment-54"
+ }
+}
+```
+
+
---
## 降级模式
diff --git a/src/commands/feedback/index.ts b/src/commands/feedback/index.ts
index ec092c8c..0df5bbb9 100644
--- a/src/commands/feedback/index.ts
+++ b/src/commands/feedback/index.ts
@@ -14,6 +14,7 @@ const feedback = {
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI) ||
isEnvTruthy(process.env.DISABLE_FEEDBACK_COMMAND) ||
isEnvTruthy(process.env.DISABLE_BUG_COMMAND) ||
isEssentialTrafficOnly() ||
diff --git a/src/entrypoints/sdk/coreSchemas.ts b/src/entrypoints/sdk/coreSchemas.ts
index 4d5b9d0a..10dd4f7b 100644
--- a/src/entrypoints/sdk/coreSchemas.ts
+++ b/src/entrypoints/sdk/coreSchemas.ts
@@ -1087,7 +1087,7 @@ export const AccountInfoSchema = lazySchema(() =>
tokenSource: z.string().optional(),
apiKeySource: z.string().optional(),
apiProvider: z
- .enum(['firstParty', 'bedrock', 'vertex', 'foundry'])
+ .enum(['firstParty', 'bedrock', 'vertex', 'foundry', 'azureOpenAI'])
.optional()
.describe(
'Active API backend. Anthropic OAuth login only applies when "firstParty"; for 3P providers the other fields are absent and auth is external (AWS creds, gcloud ADC, etc.).',
diff --git a/src/services/analytics/config.ts b/src/services/analytics/config.ts
index 9e80601b..9334c72c 100644
--- a/src/services/analytics/config.ts
+++ b/src/services/analytics/config.ts
@@ -22,6 +22,7 @@ export function isAnalyticsDisabled(): boolean {
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI) ||
isTelemetryDisabled()
)
}
diff --git a/src/services/api/azureOpenAI.ts b/src/services/api/azureOpenAI.ts
new file mode 100644
index 00000000..ad040f50
--- /dev/null
+++ b/src/services/api/azureOpenAI.ts
@@ -0,0 +1,406 @@
+import type { BetaContentBlock, BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
+import { randomUUID } from 'crypto'
+import type { Tools, ToolPermissionContext } from 'src/Tool.js'
+import { toolMatchesName } from 'src/Tool.js'
+import { TOOL_SEARCH_TOOL_NAME } from 'src/tools/ToolSearchTool/prompt.js'
+import { getUserAgent } from 'src/utils/http.js'
+import { safeParseJSON } from 'src/utils/json.js'
+import { logForDebugging } from 'src/utils/debug.js'
+import { getProxyFetchOptions } from 'src/utils/proxy.js'
+import { getModelStrings } from 'src/utils/model/modelStrings.js'
+import { isEnvTruthy } from 'src/utils/envUtils.js'
+import { toolToAPISchema } from 'src/utils/api.js'
+import type { AgentDefinition } from 'src/tools/AgentTool/loadAgentsDir.js'
+
+const DEFAULT_API_VERSION = '2025-04-01-preview'
+
+type OpenAIToolCall = {
+ id: string
+ type: 'function'
+ function: {
+ name: string
+ arguments: string
+ }
+}
+
+type OpenAIMessage = {
+ role: 'system' | 'user' | 'assistant' | 'tool'
+ content?: string | null
+ tool_calls?: OpenAIToolCall[]
+ tool_call_id?: string
+}
+
+type OpenAIResponseOutputItem = {
+ type?: string
+ role?: string
+ id?: string
+ call_id?: string
+ tool_call_id?: string
+ name?: string
+ arguments?: string
+ function?: { name?: string; arguments?: string }
+ content?: Array<{ type?: string; text?: string }>
+ output?: string
+}
+
+type OpenAIResponse = {
+ id?: string
+ output?: OpenAIResponseOutputItem[]
+ output_text?: string
+ usage?: {
+ input_tokens?: number
+ output_tokens?: number
+ prompt_tokens?: number
+ completion_tokens?: number
+ }
+}
+
+export function resolveAzureOpenAIEndpoint(): string {
+ const baseUrl =
+ process.env.AZURE_OPENAI_BASE_URL || process.env.AZURE_OPENAI_ENDPOINT
+ if (!baseUrl) {
+ throw new Error(
+ 'Missing Azure OpenAI base URL. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_ENDPOINT.',
+ )
+ }
+
+ const apiVersion = process.env.AZURE_OPENAI_API_VERSION || DEFAULT_API_VERSION
+ const url = new URL(baseUrl)
+ const path = url.pathname.replace(/\/$/, '')
+ if (!/\/openai\//i.test(path)) {
+ url.pathname = `${path}/openai/responses`
+ }
+
+ if (!url.searchParams.has('api-version') || process.env.AZURE_OPENAI_API_VERSION) {
+ url.searchParams.set('api-version', apiVersion)
+ }
+
+ return url.toString()
+}
+
+function resolveCodexDeployment(model: string): string | null {
+ const envDefault = process.env.AZURE_OPENAI_CODEX_DEPLOYMENT
+ if (envDefault) {
+ return envDefault
+ }
+
+ switch (model.toLowerCase()) {
+ case 'gpt-5.2-codex':
+ return getModelStrings().gpt52codex
+ case 'gpt-5.3-codex':
+ return getModelStrings().gpt53codex
+ case 'gpt-5.4-codex':
+ return getModelStrings().gpt54codex
+ default:
+ return null
+ }
+}
+
+export function resolveAzureOpenAIDeployment(model: string): string {
+ const trimmed = model.trim()
+ const envDefault = process.env.AZURE_OPENAI_CODEX_DEPLOYMENT
+ if (envDefault) {
+ return envDefault
+ }
+
+ const codex = resolveCodexDeployment(trimmed)
+ if (codex) {
+ if (codex === trimmed || codex.toLowerCase().includes('codex')) {
+ throw new Error(
+ `Missing Azure OpenAI deployment mapping for ${trimmed}. Set AZURE_OPENAI_CODEX_DEPLOYMENT or settings.modelOverrides["${trimmed}"] to your deployment name.`,
+ )
+ }
+ return codex
+ }
+
+ return trimmed
+}
+
+export function getAzureOpenAIHeaders(): Record {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': getUserAgent(),
+ }
+
+ if (!isEnvTruthy(process.env.CLAUDE_CODE_SKIP_AZURE_OPENAI_AUTH)) {
+ const apiKey = process.env.AZURE_OPENAI_API_KEY
+ if (!apiKey) {
+ throw new Error(
+ 'Missing Azure OpenAI API key. Set AZURE_OPENAI_API_KEY or enable CLAUDE_CODE_SKIP_AZURE_OPENAI_AUTH for testing.',
+ )
+ }
+ headers['api-key'] = apiKey
+ }
+
+ return headers
+}
+
+export async function buildAzureOpenAITools(params: {
+ tools: Tools
+ getToolPermissionContext: () => Promise
+ agents: AgentDefinition[]
+ allowedAgentTypes?: string[]
+ model?: string
+}): Promise<
+ {
+ type: 'function'
+ name: string
+ description: string
+ parameters: object
+ }[]
+> {
+ const toolSchemas = await Promise.all(
+ params.tools
+ .filter(t => !toolMatchesName(t, TOOL_SEARCH_TOOL_NAME))
+ .map(tool =>
+ toolToAPISchema(tool, {
+ getToolPermissionContext: params.getToolPermissionContext,
+ tools: params.tools,
+ agents: params.agents,
+ allowedAgentTypes: params.allowedAgentTypes,
+ model: params.model,
+ }),
+ ),
+ )
+
+ return toolSchemas.map(schema => ({
+ type: 'function',
+ name: schema.name,
+ description: schema.description ?? '',
+ parameters: schema.input_schema ?? {},
+ }))
+}
+
+function contentBlocksToText(content: unknown): string {
+ if (typeof content === 'string') return content
+ if (!Array.isArray(content)) return ''
+ return content
+ .map(block => {
+ if (block && typeof block === 'object' && 'type' in block) {
+ const typed = block as { type?: string; text?: string }
+ if (typed.type === 'text' && typeof typed.text === 'string') {
+ return typed.text
+ }
+ }
+ return ''
+ })
+ .filter(Boolean)
+ .join('\n')
+}
+
+export function buildAzureOpenAIInput(messages: Array<{ type: string; message: { content: unknown } }>): OpenAIMessage[] {
+ const inputs: OpenAIMessage[] = []
+
+ for (const msg of messages) {
+ if (msg.type !== 'user' && msg.type !== 'assistant') continue
+
+ const content = msg.message.content
+ if (!Array.isArray(content)) {
+ const text = contentBlocksToText(content)
+ if (text.trim().length > 0) {
+ inputs.push({ role: msg.type, content: text })
+ }
+ continue
+ }
+
+ const textParts: string[] = []
+ const toolCalls: OpenAIToolCall[] = []
+
+ for (const block of content) {
+ if (!block || typeof block !== 'object' || !('type' in block)) continue
+ const typed = block as {
+ type?: string
+ text?: string
+ id?: string
+ name?: string
+ input?: unknown
+ tool_use_id?: string
+ content?: unknown
+ }
+
+ if (typed.type === 'text' && typeof typed.text === 'string') {
+ textParts.push(typed.text)
+ }
+
+ if (typed.type === 'tool_use' && typed.name) {
+ const args =
+ typeof typed.input === 'string'
+ ? typed.input
+ : JSON.stringify(typed.input ?? {})
+ toolCalls.push({
+ id: typed.id ?? randomUUID(),
+ type: 'function',
+ function: {
+ name: typed.name,
+ arguments: args,
+ },
+ })
+ }
+
+ if (typed.type === 'tool_result' && msg.type === 'user') {
+ const resultText = contentBlocksToText(typed.content)
+ inputs.push({
+ role: 'tool',
+ tool_call_id: typed.tool_use_id ?? randomUUID(),
+ content: resultText,
+ })
+ }
+ }
+
+ if (msg.type === 'assistant') {
+ const contentText = textParts.join('\n')
+ if (contentText || toolCalls.length > 0) {
+ inputs.push({
+ role: 'assistant',
+ content: contentText.length > 0 ? contentText : null,
+ ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
+ })
+ }
+ continue
+ }
+
+ if (msg.type === 'user') {
+ const contentText = textParts.join('\n')
+ if (contentText.length > 0) {
+ inputs.push({ role: 'user', content: contentText })
+ }
+ }
+ }
+
+ return inputs
+}
+
+function mapOutputItemToBlocks(item: OpenAIResponseOutputItem): BetaContentBlock[] {
+ const blocks: BetaContentBlock[] = []
+ if (!item) return blocks
+
+ if (item.type === 'message' && Array.isArray(item.content)) {
+ for (const content of item.content) {
+ if (!content || typeof content !== 'object') continue
+ if (content.type === 'output_text' || content.type === 'text') {
+ const text = content.text ?? ''
+ blocks.push({ type: 'text', text })
+ }
+ }
+ }
+
+ if (item.type === 'tool_call' || item.type === 'function_call') {
+ const name = item.name ?? item.function?.name
+ if (name) {
+ const rawArgs = item.arguments ?? item.function?.arguments ?? '{}'
+ const parsed =
+ typeof rawArgs === 'string' ? safeParseJSON(rawArgs) : rawArgs
+ blocks.push({
+ type: 'tool_use',
+ id: item.id ?? item.call_id ?? item.tool_call_id ?? randomUUID(),
+ name,
+ input: parsed ?? {},
+ } as BetaContentBlock)
+ }
+ }
+
+ return blocks
+}
+
+export function parseAzureOpenAIResponse(response: OpenAIResponse): {
+ content: BetaContentBlock[]
+ usage: BetaUsage
+ responseId?: string
+} {
+ const contentBlocks: BetaContentBlock[] = []
+
+ if (Array.isArray(response.output)) {
+ for (const item of response.output) {
+ contentBlocks.push(...mapOutputItemToBlocks(item))
+ }
+ }
+
+ if (contentBlocks.length === 0 && response.output_text) {
+ contentBlocks.push({ type: 'text', text: response.output_text })
+ }
+
+ const usage: BetaUsage = {
+ input_tokens: response.usage?.input_tokens ?? response.usage?.prompt_tokens ?? 0,
+ output_tokens: response.usage?.output_tokens ?? response.usage?.completion_tokens ?? 0,
+ cache_read_input_tokens: 0,
+ cache_creation_input_tokens: 0,
+ } as BetaUsage
+
+ return { content: contentBlocks, usage, responseId: response.id }
+}
+
+export async function requestAzureOpenAI(params: {
+ model: string
+ systemPrompt: string
+ messages: Array<{ type: string; message: { content: unknown } }>
+ tools: Tools
+ toolChoice?: { type?: string; name?: string }
+ maxOutputTokens: number
+ temperature?: number
+ getToolPermissionContext: () => Promise
+ agents: AgentDefinition[]
+ allowedAgentTypes?: string[]
+ signal: AbortSignal
+}): Promise<{ content: BetaContentBlock[]; usage: BetaUsage; responseId?: string }>{
+ const deployment = resolveAzureOpenAIDeployment(params.model)
+ const endpoint = resolveAzureOpenAIEndpoint()
+ const headers = getAzureOpenAIHeaders()
+
+ const tools = await buildAzureOpenAITools({
+ tools: params.tools,
+ getToolPermissionContext: params.getToolPermissionContext,
+ agents: params.agents,
+ allowedAgentTypes: params.allowedAgentTypes,
+ model: params.model,
+ })
+
+ const input = buildAzureOpenAIInput(params.messages)
+
+ const body: Record = {
+ model: deployment,
+ input,
+ instructions: params.systemPrompt,
+ max_output_tokens: params.maxOutputTokens,
+ }
+
+ if (tools.length > 0) {
+ body.tools = tools
+ }
+
+ if (params.toolChoice?.type === 'tool' && params.toolChoice.name) {
+ body.tool_choice = {
+ type: 'function',
+ name: params.toolChoice.name,
+ }
+ } else if (tools.length > 0) {
+ body.tool_choice = 'auto'
+ }
+
+ if (params.temperature !== undefined) {
+ body.temperature = params.temperature
+ }
+
+ logForDebugging(
+ `[AzureOpenAI] POST ${endpoint} model=${deployment} tools=${tools.length}`,
+ )
+
+ const fetchOptions = getProxyFetchOptions()
+ // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(body),
+ signal: params.signal,
+ ...fetchOptions,
+ })
+
+ if (!response.ok) {
+ const errorBody = await response.text()
+ throw new Error(
+ `Azure OpenAI request failed (${response.status}): ${errorBody}`,
+ )
+ }
+
+ const data = (await response.json()) as OpenAIResponse
+ return parseAzureOpenAIResponse(data)
+}
diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts
index 89a6e661..7e2e695c 100644
--- a/src/services/api/claude.ts
+++ b/src/services/api/claude.ts
@@ -1,4 +1,4 @@
-import type {
+import type {
BetaContentBlock,
BetaContentBlockParam,
BetaImageBlockParam,
@@ -229,6 +229,7 @@ import { getInitializationStatus } from '../lsp/manager.js'
import { isToolFromMcpServer } from '../mcp/utils.js'
import { withStreamingVCR, withVCR } from '../vcr.js'
import { CLIENT_REQUEST_ID_HEADER, getAnthropicClient } from './client.js'
+import { requestAzureOpenAI } from './azureOpenAI.js'
import {
API_ERROR_MESSAGE_PREFIX,
CUSTOM_OFF_SWITCH_MESSAGE,
@@ -280,7 +281,7 @@ export function getExtraBodyParams(betaHeaders?: string[]): JsonObject {
const parsed = safeParseJSON(extraBodyStr)
// We expect an object with key-value pairs to spread into API parameters
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
- // Shallow clone — safeParseJSON is LRU-cached and returns the same
+ // Shallow clone 鈥?safeParseJSON is LRU-cached and returns the same
// object reference for the same string. Mutating `result` below
// would poison the cache, causing stale values to persist.
result = { ...(parsed as JsonObject) }
@@ -383,15 +384,15 @@ export function getCacheControl({
* GrowthBook config shape: { allowlist: string[] }
* Patterns support trailing '*' for prefix matching.
* Examples:
- * - { allowlist: ["repl_main_thread*", "sdk"] } — main thread + SDK only
- * - { allowlist: ["repl_main_thread*", "sdk", "agent:*"] } — also subagents
- * - { allowlist: ["*"] } — all sources
+ * - { allowlist: ["repl_main_thread*", "sdk"] } 鈥?main thread + SDK only
+ * - { allowlist: ["repl_main_thread*", "sdk", "agent:*"] } 鈥?also subagents
+ * - { allowlist: ["*"] } 鈥?all sources
*
- * The allowlist is cached in STATE for session stability — prevents mixed
+ * The allowlist is cached in STATE for session stability 鈥?prevents mixed
* TTLs when GrowthBook's disk cache updates mid-request.
*/
function should1hCacheTTL(querySource?: QuerySource): boolean {
- // 3P Bedrock users get 1h TTL when opted in via env var — they manage their own billing
+ // 3P Bedrock users get 1h TTL when opted in via env var 鈥?they manage their own billing
// No GrowthBook gating needed since 3P users don't have GrowthBook configured
if (
getAPIProvider() === 'bedrock' &&
@@ -400,7 +401,7 @@ function should1hCacheTTL(querySource?: QuerySource): boolean {
return true
}
- // Latch eligibility in bootstrap state for session stability — prevents
+ // Latch eligibility in bootstrap state for session stability 鈥?prevents
// mid-session overage flips from changing the cache_control TTL, which
// would bust the server-side prompt cache (~20K tokens per flip).
let userEligible = getPromptCache1hEligible()
@@ -412,7 +413,7 @@ function should1hCacheTTL(querySource?: QuerySource): boolean {
}
if (!userEligible) return false
- // Cache allowlist in bootstrap state for session stability — prevents mixed
+ // Cache allowlist in bootstrap state for session stability 鈥?prevents mixed
// TTLs when GrowthBook's disk cache updates mid-request
let allowlist = getPromptCache1hAllowlist()
if (allowlist === null) {
@@ -465,7 +466,7 @@ function configureEffortParams(
}
}
-// output_config.task_budget — API-side token budget awareness for the model.
+// output_config.task_budget 鈥?API-side token budget awareness for the model.
// Stainless SDK types don't yet include task_budget on BetaOutputConfig, so we
// define the wire shape locally and cast. The API validates on receipt; see
// api/api/schemas/messages/request/output_config.py:12-39 in the monorepo.
@@ -700,7 +701,7 @@ export type Options = {
advisorModel?: string
addNotification?: (notif: Notification) => void
// API-side task budget (output_config.task_budget). Distinct from the
- // tokenBudget.ts +500k auto-continue feature — this one is sent to the API
+ // tokenBudget.ts +500k auto-continue feature 鈥?this one is sent to the API
// so the model can pace itself. `remaining` is computed by the caller
// (query.ts decrements across the agentic loop).
taskBudget?: { total: number; remaining?: number }
@@ -801,7 +802,7 @@ function shouldDeferLspTool(tool: Tool): boolean {
* (~5min) so a hung fallback to a wedged backend surfaces a clean
* APIConnectionTimeoutError instead of stalling past SIGKILL.
*
- * Otherwise defaults to 300s — long enough for slow backends without
+ * Otherwise defaults to 300s 鈥?long enough for slow backends without
* approaching the API's 10-minute non-streaming boundary.
*/
function getNonstreamingFallbackTimeoutMs(): number {
@@ -872,7 +873,7 @@ export async function* executeNonStreamingRequest(
},
)
} catch (err) {
- // User aborts are not errors — re-throw immediately without logging
+ // User aborts are not errors 鈥?re-throw immediately without logging
if (err instanceof APIUserAbortError) throw err
// Instrumentation: record when the non-streaming request errors (including
@@ -1025,7 +1026,54 @@ async function* queryModel(
StreamEvent | AssistantMessage | SystemAPIErrorMessage,
void
> {
- // Check cheap conditions first — the off-switch await blocks on GrowthBook
+ if (getAPIProvider() === 'azureOpenAI') {
+ try {
+ const systemText = systemPrompt.join('\\n')
+ const maxOutputTokens =
+ options.maxOutputTokensOverride ??
+ getModelMaxOutputTokens(options.model).default
+ const temperature =
+ thinkingConfig.type === 'disabled'
+ ? (options.temperatureOverride ?? 1)
+ : undefined
+
+ const result = await requestAzureOpenAI({
+ model: options.model,
+ systemPrompt: systemText,
+ messages,
+ tools,
+ toolChoice: options.toolChoice as { type?: string; name?: string },
+ maxOutputTokens,
+ temperature,
+ getToolPermissionContext: options.getToolPermissionContext,
+ agents: options.agents,
+ allowedAgentTypes: options.allowedAgentTypes,
+ signal,
+ })
+
+ const assistantMessage: AssistantMessage = {
+ message: {
+ id: result.responseId ?? randomUUID(),
+ model: options.model,
+ role: 'assistant',
+ content: result.content,
+ stop_reason: 'end_turn',
+ usage: result.usage,
+ },
+ requestId: result.responseId ?? undefined,
+ type: 'assistant',
+ uuid: randomUUID(),
+ timestamp: new Date().toISOString(),
+ }
+
+ yield assistantMessage
+ } catch (error) {
+ yield getAssistantMessageFromError(error, options.model, { messages })
+ }
+ return
+ }
+
+ // Check cheap conditions first 鈥?the off-switch await blocks on GrowthBook
// init (~10ms). For non-Opus models (haiku, sonnet) this skips the await
// entirely. Subscribers don't hit this path at all.
if (
@@ -1125,7 +1173,7 @@ async function* queryModel(
'query',
)
- // Precompute once — isDeferredTool does 2 GrowthBook lookups per call
+ // Precompute once 鈥?isDeferredTool does 2 GrowthBook lookups per call
const deferredToolNames = new Set()
if (useToolSearch) {
for (const t of tools) {
@@ -1207,7 +1255,7 @@ async function* queryModel(
const useGlobalCacheFeature = shouldUseGlobalCacheScope()
const willDefer = (t: Tool) =>
useToolSearch && (deferredToolNames.has(t.name) || shouldDeferLspTool(t))
- // MCP tools are per-user → dynamic tool section → can't globally cache.
+ // MCP tools are per-user 鈫?dynamic tool section 鈫?can't globally cache.
// Only gate when an MCP tool will actually render (not defer_loading).
const needsToolBasedCacheMarker =
useGlobalCacheFeature &&
@@ -1274,7 +1322,7 @@ async function* queryModel(
// called from ~20 places (analytics, feedback, sharing, etc.), many of which
// don't have model context. Adding model to its signature would be a large refactor.
// - This post-processing uses the model-aware isToolSearchEnabled() check
- // - This handles mid-conversation model switching (e.g., Sonnet → Haiku) where
+ // - This handles mid-conversation model switching (e.g., Sonnet 鈫?Haiku) where
// stale tool-search fields from the previous model would cause 400 errors
//
// Note: For assistant messages, normalizeMessagesForAPI already normalized the
@@ -1300,7 +1348,7 @@ async function* queryModel(
// tool_uses and strips orphaned tool_results referencing non-existent tool_uses.
messagesForAPI = ensureToolResultPairing(messagesForAPI)
- // Strip advisor blocks — the API rejects them without the beta header.
+ // Strip advisor blocks 鈥?the API rejects them without the beta header.
if (!betas.includes(ADVISOR_BETA_HEADER)) {
messagesForAPI = stripAdvisorBlocks(messagesForAPI)
}
@@ -1688,7 +1736,7 @@ async function* queryModel(
)
}
- // Only send temperature when thinking is disabled — the API requires
+ // Only send temperature when thinking is disabled 鈥?the API requires
// temperature: 1 when thinking is enabled, which is already the default.
const temperature = !hasThinking
? (options.temperatureOverride ?? 1)
@@ -1730,7 +1778,7 @@ async function* queryModel(
// Compute log scalars synchronously so the fire-and-forget .then() closure
// captures only primitives instead of paramsFromContext's full closure scope
- // (messagesForAPI, system, allTools, betas — the entire request-building
+ // (messagesForAPI, system, allTools, betas 鈥?the entire request-building
// context), which would otherwise be pinned until the promise resolves.
{
const queryParams = paramsFromContext({
@@ -1809,13 +1857,13 @@ async function* queryModel(
// Generate and track client request ID so timeouts (which return no
// server request ID) can still be correlated with server logs.
- // First-party only — 3P providers don't log it (inc-4029 class).
+ // First-party only 鈥?3P providers don't log it (inc-4029 class).
clientRequestId =
getAPIProvider() === 'firstParty' && isFirstPartyAnthropicBaseUrl()
? randomUUID()
: undefined
- // Use raw stream instead of BetaMessageStream to avoid O(n²) partial JSON parsing
+ // Use raw stream instead of BetaMessageStream to avoid O(n虏) partial JSON parsing
// BetaMessageStream calls partialParse() on every input_json_delta, which we don't need
// since we handle tool input accumulation ourselves
// biome-ignore lint/plugin: main conversation loop handles attribution separately
@@ -2281,7 +2329,7 @@ async function* queryModel(
max_tokens: maxOutputTokens,
output_tokens: usage.output_tokens,
})
- // Reuse the max_output_tokens recovery path — from the model's
+ // Reuse the max_output_tokens recovery path 鈥?from the model's
// perspective, both mean "response was cut off, continue from
// where you left off."
yield createAssistantAPIErrorMessage({
@@ -2598,7 +2646,7 @@ async function* queryModel(
} catch (errorFromRetry) {
// FallbackTriggeredError must propagate to query.ts, which performs the
// actual model switch. Swallowing it here would turn the fallback into a
- // no-op — the user would just see "Model fallback triggered: X -> Y" as
+ // no-op 鈥?the user would just see "Model fallback triggered: X -> Y" as
// an error message with no actual retry on the fallback model.
if (errorFromRetry instanceof FallbackTriggeredError) {
throw errorFromRetry
@@ -2617,7 +2665,7 @@ async function* queryModel(
if (is404StreamCreationError) {
// 404 is thrown at .withResponse() before streamRequestId is assigned,
- // and CannotRetryError means every retry failed — so grab the failed
+ // and CannotRetryError means every retry failed 鈥?so grab the failed
// request's ID from the error header instead.
const failedRequestId =
(errorFromRetry.originalError as APIError).requestID ?? 'unknown'
@@ -2838,7 +2886,7 @@ async function* queryModel(
// Track the last requestId for the main conversation chain so shutdown
// can send a cache eviction hint to inference. Exclude backgrounded
// sessions (Ctrl+B) which share the repl_main_thread querySource but
- // run inside an agent context — they are independent conversation chains
+ // run inside an agent context 鈥?they are independent conversation chains
// whose cache should not be evicted when the foreground session clears.
if (
streamRequestId &&
@@ -3019,7 +3067,7 @@ export function accumulateUsage(
totalUsage.cache_creation.ephemeral_5m_input_tokens +
messageUsage.cache_creation.ephemeral_5m_input_tokens,
},
- // See comment in updateUsage — field is not on NonNullableUsage to keep
+ // See comment in updateUsage 鈥?field is not on NonNullableUsage to keep
// the string out of external builds.
...(feature('CACHED_MICROCOMPACT')
? {
@@ -3080,7 +3128,7 @@ export function addCacheBreakpoints(
// local-attention KV pages at any cached prefix position NOT in
// cache_store_int_token_boundaries. With two markers the second-to-last
// position is protected and its locals survive an extra turn even though
- // nothing will ever resume from there — with one marker they're freed
+ // nothing will ever resume from there 鈥?with one marker they're freed
// immediately. For fire-and-forget forks (skipCacheWrite) we shift the
// marker to the second-to-last message: that's the last shared-prefix
// point, so the write is a no-op merge on mycro (entry already exists)
@@ -3179,7 +3227,7 @@ export function addCacheBreakpoints(
// Add cache_reference to tool_result blocks that are strictly before
// the last cache_control marker. The API requires cache_reference to
- // appear "before or on" the last cache_control — we use strict "before"
+ // appear "before or on" the last cache_control 鈥?we use strict "before"
// to avoid edge cases where cache_edits splicing shifts block indices.
//
// Create new objects instead of mutating in-place to avoid contaminating
@@ -3349,7 +3397,7 @@ export async function queryWithModel({
// Non-streaming requests have a 10min max per the docs:
// https://platform.claude.com/docs/en/api/errors#long-requests
-// The SDK's 21333-token cap is derived from 10min × 128k tokens/hour, but we
+// The SDK's 21333-token cap is derived from 10min 脳 128k tokens/hour, but we
// bypass it by setting a client-level timeout, so we can cap higher.
export const MAX_NON_STREAMING_TOKENS = 64_000
@@ -3400,7 +3448,7 @@ export function getMaxOutputTokensForModel(model: string): number {
const maxOutputTokens = getModelMaxOutputTokens(model)
// Slot-reservation cap: drop default to 8k for all models. BQ p99 output
- // = 4,911 tokens; 32k/64k defaults over-reserve 8-16× slot capacity.
+ // = 4,911 tokens; 32k/64k defaults over-reserve 8-16脳 slot capacity.
// Requests hitting the cap get one clean retry at 64k (query.ts
// max_output_tokens_escalate). Math.min keeps models with lower native
// defaults (e.g. claude-3-opus at 4k) at their native value. Applied
diff --git a/src/utils/apiPreconnect.ts b/src/utils/apiPreconnect.ts
index 6a8de649..7ee24c1b 100644
--- a/src/utils/apiPreconnect.ts
+++ b/src/utils/apiPreconnect.ts
@@ -36,7 +36,8 @@ export function preconnectAnthropicApi(): void {
if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
- isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI)
) {
return
}
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
index 64a61808..386d2f99 100644
--- a/src/utils/auth.ts
+++ b/src/utils/auth.ts
@@ -115,7 +115,8 @@ export function isAnthropicAuthEnabled(): boolean {
const is3P =
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
- isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI)
// Check if user has configured an external API key source
// This allows externally-provided API keys to work (without requiring proxy configuration)
@@ -1594,7 +1595,8 @@ export function is1PApiCustomer(): boolean {
if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
- isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI)
) {
return false
}
@@ -1733,7 +1735,8 @@ export function isUsing3PServices(): boolean {
return !!(
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
- isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI)
)
}
diff --git a/src/utils/log.ts b/src/utils/log.ts
index bc4df3e1..827c3a81 100644
--- a/src/utils/log.ts
+++ b/src/utils/log.ts
@@ -170,6 +170,7 @@ export function logError(error: unknown): void {
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
+ isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI) ||
process.env.DISABLE_ERROR_REPORTING ||
isEssentialTrafficOnly()
) {
diff --git a/src/utils/managedEnvConstants.ts b/src/utils/managedEnvConstants.ts
index 12c56565..23c01eec 100644
--- a/src/utils/managedEnvConstants.ts
+++ b/src/utils/managedEnvConstants.ts
@@ -18,12 +18,15 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
+ 'CLAUDE_CODE_USE_AZURE_OPENAI',
// Endpoint config (base URLs, project/resource identifiers)
'ANTHROPIC_BASE_URL',
'ANTHROPIC_BEDROCK_BASE_URL',
'ANTHROPIC_VERTEX_BASE_URL',
'ANTHROPIC_FOUNDRY_BASE_URL',
'ANTHROPIC_FOUNDRY_RESOURCE',
+ 'AZURE_OPENAI_BASE_URL',
+ 'AZURE_OPENAI_API_VERSION',
'ANTHROPIC_VERTEX_PROJECT_ID',
// Region routing (per-model VERTEX_REGION_CLAUDE_* handled by prefix below)
'CLOUD_ML_REGION',
@@ -33,9 +36,11 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
'CLAUDE_CODE_OAUTH_TOKEN',
'AWS_BEARER_TOKEN_BEDROCK',
'ANTHROPIC_FOUNDRY_API_KEY',
+ 'AZURE_OPENAI_API_KEY',
'CLAUDE_CODE_SKIP_BEDROCK_AUTH',
'CLAUDE_CODE_SKIP_VERTEX_AUTH',
'CLAUDE_CODE_SKIP_FOUNDRY_AUTH',
+ 'CLAUDE_CODE_SKIP_AZURE_OPENAI_AUTH',
// Model defaults — often set to provider-specific ID formats
'ANTHROPIC_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
@@ -148,6 +153,8 @@ export const SAFE_ENV_VARS = new Set([
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_USE_VERTEX',
+ 'CLAUDE_CODE_USE_AZURE_OPENAI',
+ 'AZURE_OPENAI_API_VERSION',
'DISABLE_AUTOUPDATER',
'DISABLE_BUG_COMMAND',
'DISABLE_COST_WARNINGS',
diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts
index 89f243d8..63877dcf 100644
--- a/src/utils/model/configs.ts
+++ b/src/utils/model/configs.ts
@@ -11,6 +11,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = {
bedrock: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
vertex: 'claude-3-7-sonnet@20250219',
foundry: 'claude-3-7-sonnet',
+ azureOpenAI: 'claude-3-7-sonnet-20250219',
} as const satisfies ModelConfig
export const CLAUDE_3_5_V2_SONNET_CONFIG = {
@@ -18,6 +19,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = {
bedrock: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
vertex: 'claude-3-5-sonnet-v2@20241022',
foundry: 'claude-3-5-sonnet',
+ azureOpenAI: 'claude-3-5-sonnet-20241022',
} as const satisfies ModelConfig
export const CLAUDE_3_5_HAIKU_CONFIG = {
@@ -25,6 +27,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = {
bedrock: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
vertex: 'claude-3-5-haiku@20241022',
foundry: 'claude-3-5-haiku',
+ azureOpenAI: 'claude-3-5-haiku-20241022',
} as const satisfies ModelConfig
export const CLAUDE_HAIKU_4_5_CONFIG = {
@@ -32,6 +35,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
vertex: 'claude-haiku-4-5@20251001',
foundry: 'claude-haiku-4-5',
+ azureOpenAI: 'claude-haiku-4-5-20251001',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_CONFIG = {
@@ -39,6 +43,7 @@ export const CLAUDE_SONNET_4_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
vertex: 'claude-sonnet-4@20250514',
foundry: 'claude-sonnet-4',
+ azureOpenAI: 'claude-sonnet-4-20250514',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_5_CONFIG = {
@@ -46,6 +51,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
vertex: 'claude-sonnet-4-5@20250929',
foundry: 'claude-sonnet-4-5',
+ azureOpenAI: 'claude-sonnet-4-5-20250929',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_CONFIG = {
@@ -53,6 +59,7 @@ export const CLAUDE_OPUS_4_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-20250514-v1:0',
vertex: 'claude-opus-4@20250514',
foundry: 'claude-opus-4',
+ azureOpenAI: 'claude-opus-4-20250514',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_1_CONFIG = {
@@ -60,6 +67,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-1-20250805-v1:0',
vertex: 'claude-opus-4-1@20250805',
foundry: 'claude-opus-4-1',
+ azureOpenAI: 'claude-opus-4-1-20250805',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_5_CONFIG = {
@@ -67,6 +75,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-5-20251101-v1:0',
vertex: 'claude-opus-4-5@20251101',
foundry: 'claude-opus-4-5',
+ azureOpenAI: 'claude-opus-4-5-20251101',
} as const satisfies ModelConfig
export const CLAUDE_OPUS_4_6_CONFIG = {
@@ -74,6 +83,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
bedrock: 'us.anthropic.claude-opus-4-6-v1',
vertex: 'claude-opus-4-6',
foundry: 'claude-opus-4-6',
+ azureOpenAI: 'claude-opus-4-6',
} as const satisfies ModelConfig
export const CLAUDE_SONNET_4_6_CONFIG = {
@@ -81,6 +91,31 @@ export const CLAUDE_SONNET_4_6_CONFIG = {
bedrock: 'us.anthropic.claude-sonnet-4-6',
vertex: 'claude-sonnet-4-6',
foundry: 'claude-sonnet-4-6',
+ azureOpenAI: 'claude-sonnet-4-6',
+} as const satisfies ModelConfig
+
+export const GPT_5_2_CODEX_CONFIG = {
+ firstParty: 'gpt-5.2-codex',
+ bedrock: 'gpt-5.2-codex',
+ vertex: 'gpt-5.2-codex',
+ foundry: 'gpt-5.2-codex',
+ azureOpenAI: 'gpt-5.2-codex',
+} as const satisfies ModelConfig
+
+export const GPT_5_3_CODEX_CONFIG = {
+ firstParty: 'gpt-5.3-codex',
+ bedrock: 'gpt-5.3-codex',
+ vertex: 'gpt-5.3-codex',
+ foundry: 'gpt-5.3-codex',
+ azureOpenAI: 'gpt-5.3-codex',
+} as const satisfies ModelConfig
+
+export const GPT_5_4_CODEX_CONFIG = {
+ firstParty: 'gpt-5.4-codex',
+ bedrock: 'gpt-5.4-codex',
+ vertex: 'gpt-5.4-codex',
+ foundry: 'gpt-5.4-codex',
+ azureOpenAI: 'gpt-5.4-codex',
} as const satisfies ModelConfig
// @[MODEL LAUNCH]: Register the new config here.
@@ -96,6 +131,9 @@ export const ALL_MODEL_CONFIGS = {
opus41: CLAUDE_OPUS_4_1_CONFIG,
opus45: CLAUDE_OPUS_4_5_CONFIG,
opus46: CLAUDE_OPUS_4_6_CONFIG,
+ gpt52codex: GPT_5_2_CODEX_CONFIG,
+ gpt53codex: GPT_5_3_CODEX_CONFIG,
+ gpt54codex: GPT_5_4_CODEX_CONFIG,
} as const satisfies Record
export type ModelKey = keyof typeof ALL_MODEL_CONFIGS
diff --git a/src/utils/model/deprecation.ts b/src/utils/model/deprecation.ts
index a8b0ee23..6c057272 100644
--- a/src/utils/model/deprecation.ts
+++ b/src/utils/model/deprecation.ts
@@ -38,6 +38,7 @@ const DEPRECATED_MODELS: Record = {
bedrock: 'January 15, 2026',
vertex: 'January 5, 2026',
foundry: 'January 5, 2026',
+ azureOpenAI: null,
},
},
'claude-3-7-sonnet': {
@@ -47,6 +48,7 @@ const DEPRECATED_MODELS: Record = {
bedrock: 'April 28, 2026',
vertex: 'May 11, 2026',
foundry: 'February 19, 2026',
+ azureOpenAI: null,
},
},
'claude-3-5-haiku': {
@@ -56,6 +58,7 @@ const DEPRECATED_MODELS: Record = {
bedrock: null,
vertex: null,
foundry: null,
+ azureOpenAI: null,
},
},
}
diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts
index 85e369fa..8d2a652f 100644
--- a/src/utils/model/model.ts
+++ b/src/utils/model/model.ts
@@ -176,6 +176,10 @@ export function getRuntimeMainLoopModel(params: {
* @returns The default model setting to use
*/
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
+ if (getAPIProvider() === 'azureOpenAI') {
+ return 'gpt-5.2-codex'
+ }
+
// Ants default to defaultModel from flag config, or Opus 1M if not configured
if (process.env.USER_TYPE === 'ant') {
return (
diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts
index f8ef2966..cf7b0a93 100644
--- a/src/utils/model/modelOptions.ts
+++ b/src/utils/model/modelOptions.ts
@@ -266,9 +266,42 @@ function getOpusPlanOption(): ModelOption {
}
}
+function getAzureOpenAICodexOption(): ModelOption {
+ return {
+ value: 'gpt-5.2-codex',
+ label: 'GPT-5.2 Codex',
+ description: 'Azure OpenAI Codex model',
+ descriptionForModel: 'GPT-5.2 Codex via Azure OpenAI',
+ }
+}
+
+function getAzureOpenAICodexOptions(): ModelOption[] {
+ return [
+ {
+ value: 'gpt-5.2-codex',
+ label: 'GPT-5.2 Codex',
+ description: 'Azure OpenAI Codex model',
+ },
+ {
+ value: 'gpt-5.3-codex',
+ label: 'GPT-5.3 Codex',
+ description: 'Azure OpenAI Codex model',
+ },
+ {
+ value: 'gpt-5.4-codex',
+ label: 'GPT-5.4 Codex',
+ description: 'Azure OpenAI Codex model',
+ },
+ ]
+}
+
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
function getModelOptionsBase(fastMode = false): ModelOption[] {
+ if (getAPIProvider() === 'azureOpenAI') {
+ return [getDefaultOptionForUser(), ...getAzureOpenAICodexOptions()]
+ }
+
if (process.env.USER_TYPE === 'ant') {
// Build options from antModels config
const antModelOptions: ModelOption[] = getAntModels().map(m => ({
diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts
index aba9b7d7..16f7eb28 100644
--- a/src/utils/model/providers.ts
+++ b/src/utils/model/providers.ts
@@ -1,7 +1,12 @@
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js'
import { isEnvTruthy } from '../envUtils.js'
-export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry'
+export type APIProvider =
+ | 'firstParty'
+ | 'bedrock'
+ | 'vertex'
+ | 'foundry'
+ | 'azureOpenAI'
export function getAPIProvider(): APIProvider {
return isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)
@@ -10,6 +15,8 @@ export function getAPIProvider(): APIProvider {
? 'vertex'
: isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
? 'foundry'
+ : isEnvTruthy(process.env.CLAUDE_CODE_USE_AZURE_OPENAI)
+ ? 'azureOpenAI'
: 'firstParty'
}
diff --git a/src/utils/model/validateModel.ts b/src/utils/model/validateModel.ts
index 14b81675..c19f170b 100644
--- a/src/utils/model/validateModel.ts
+++ b/src/utils/model/validateModel.ts
@@ -35,6 +35,69 @@ export async function validateModel(
}
}
+ if (getAPIProvider() === 'azureOpenAI') {
+ const endpoint =
+ process.env.AZURE_OPENAI_BASE_URL || process.env.AZURE_OPENAI_ENDPOINT
+ if (!endpoint) {
+ return {
+ valid: false,
+ error:
+ 'AZURE_OPENAI_BASE_URL is required when using Azure OpenAI provider',
+ }
+ }
+ if (
+ !process.env.AZURE_OPENAI_API_KEY &&
+ !process.env.CLAUDE_CODE_SKIP_AZURE_OPENAI_AUTH
+ ) {
+ return {
+ valid: false,
+ error:
+ 'AZURE_OPENAI_API_KEY is required when using Azure OpenAI provider',
+ }
+ }
+ const lower = normalizedModel.toLowerCase()
+ if (lower === 'gpt-5.2-codex') {
+ if (process.env.AZURE_OPENAI_CODEX_DEPLOYMENT) {
+ return { valid: true }
+ }
+ const mapped = getModelStrings().gpt52codex
+ if (!mapped || mapped === 'gpt-5.2-codex') {
+ return {
+ valid: false,
+ error:
+ 'Missing Azure OpenAI deployment mapping for gpt-5.2-codex. Set AZURE_OPENAI_CODEX_DEPLOYMENT or settings.modelOverrides[\"gpt-5.2-codex\"] to your deployment name.',
+ }
+ }
+ }
+ if (lower === 'gpt-5.3-codex') {
+ if (process.env.AZURE_OPENAI_CODEX_DEPLOYMENT) {
+ return { valid: true }
+ }
+ const mapped = getModelStrings().gpt53codex
+ if (!mapped || mapped === 'gpt-5.3-codex') {
+ return {
+ valid: false,
+ error:
+ 'Missing Azure OpenAI deployment mapping for gpt-5.3-codex. Set AZURE_OPENAI_CODEX_DEPLOYMENT or settings.modelOverrides[\"gpt-5.3-codex\"] to your deployment name.',
+ }
+ }
+ }
+ if (lower === 'gpt-5.4-codex') {
+ if (process.env.AZURE_OPENAI_CODEX_DEPLOYMENT) {
+ return { valid: true }
+ }
+ const mapped = getModelStrings().gpt54codex
+ if (!mapped || mapped === 'gpt-5.4-codex') {
+ return {
+ valid: false,
+ error:
+ 'Missing Azure OpenAI deployment mapping for gpt-5.4-codex. Set AZURE_OPENAI_CODEX_DEPLOYMENT or settings.modelOverrides[\"gpt-5.4-codex\"] to your deployment name.',
+ }
+ }
+ }
+ return { valid: true }
+ }
+
// Check if it's a known alias (these are always valid)
const lowerModel = normalizedModel.toLowerCase()
if ((MODEL_ALIASES as readonly string[]).includes(lowerModel)) {
diff --git a/src/utils/swarm/spawnUtils.ts b/src/utils/swarm/spawnUtils.ts
index cfccdf5a..cc6c2e2d 100644
--- a/src/utils/swarm/spawnUtils.ts
+++ b/src/utils/swarm/spawnUtils.ts
@@ -99,8 +99,13 @@ const TEAMMATE_ENV_VARS = [
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
+ 'CLAUDE_CODE_USE_AZURE_OPENAI',
// Custom API endpoint
'ANTHROPIC_BASE_URL',
+ 'AZURE_OPENAI_BASE_URL',
+ 'AZURE_OPENAI_ENDPOINT',
+ 'AZURE_OPENAI_API_VERSION',
+ 'AZURE_OPENAI_API_KEY',
// Config directory override
'CLAUDE_CONFIG_DIR',
// CCR marker — teammates need this for CCR-aware code paths. Auth finds
diff --git a/tests/azureOpenAI.test.ts b/tests/azureOpenAI.test.ts
new file mode 100644
index 00000000..fb570dc2
--- /dev/null
+++ b/tests/azureOpenAI.test.ts
@@ -0,0 +1,84 @@
+import { expect, test } from "bun:test"
+import {
+ buildAzureOpenAIInput,
+ resolveAzureOpenAIEndpoint,
+ resolveAzureOpenAIDeployment,
+} from "../src/services/api/azureOpenAI.js"
+
+test("resolveAzureOpenAIEndpoint appends responses path and api-version", () => {
+ const prevBase = process.env.AZURE_OPENAI_BASE_URL
+ const prevVersion = process.env.AZURE_OPENAI_API_VERSION
+ process.env.AZURE_OPENAI_BASE_URL =
+ "https://example.cognitiveservices.azure.com/"
+ process.env.AZURE_OPENAI_API_VERSION = "2025-04-01-preview"
+
+ const url = resolveAzureOpenAIEndpoint()
+ expect(url).toContain("/openai/responses")
+ expect(url).toContain("api-version=2025-04-01-preview")
+
+ process.env.AZURE_OPENAI_BASE_URL = prevBase
+ process.env.AZURE_OPENAI_API_VERSION = prevVersion
+})
+
+test("buildAzureOpenAIInput maps tool_use and tool_result", () => {
+ const input = buildAzureOpenAIInput([
+ {
+ type: "assistant",
+ message: {
+ content: [
+ { type: "text", text: "Running tool" },
+ {
+ type: "tool_use",
+ id: "tool_1",
+ name: "my_tool",
+ input: { foo: "bar" },
+ },
+ ],
+ },
+ },
+ {
+ type: "user",
+ message: {
+ content: [
+ {
+ type: "tool_result",
+ tool_use_id: "tool_1",
+ content: [{ type: "text", text: "ok" }],
+ },
+ ],
+ },
+ },
+ ])
+
+ expect(input.some(msg => msg.role === "assistant")).toBe(true)
+ expect(input.some(msg => msg.role === "tool")).toBe(true)
+})
+
+test("resolveAzureOpenAIDeployment throws when codex mapping is missing", () => {
+ const prevBase = process.env.AZURE_OPENAI_BASE_URL
+ const prevEnv = process.env.AZURE_OPENAI_CODEX_DEPLOYMENT
+ process.env.AZURE_OPENAI_BASE_URL =
+ "https://example.cognitiveservices.azure.com/"
+ delete process.env.AZURE_OPENAI_CODEX_DEPLOYMENT
+
+ expect(() => resolveAzureOpenAIDeployment("gpt-5.2-codex")).toThrow()
+ expect(() => resolveAzureOpenAIDeployment("gpt-5.3-codex")).toThrow()
+ expect(() => resolveAzureOpenAIDeployment("gpt-5.4-codex")).toThrow()
+
+ process.env.AZURE_OPENAI_BASE_URL = prevBase
+ process.env.AZURE_OPENAI_CODEX_DEPLOYMENT = prevEnv
+})
+
+test("resolveAzureOpenAIDeployment uses env default even if name matches", () => {
+ const prevBase = process.env.AZURE_OPENAI_BASE_URL
+ const prevEnv = process.env.AZURE_OPENAI_CODEX_DEPLOYMENT
+ process.env.AZURE_OPENAI_BASE_URL =
+ "https://example.cognitiveservices.azure.com/"
+ process.env.AZURE_OPENAI_CODEX_DEPLOYMENT = "gpt-5.2-codex"
+
+ const resolved = resolveAzureOpenAIDeployment("gpt-5.2-codex")
+ expect(resolved).toBe("gpt-5.2-codex")
+
+ process.env.AZURE_OPENAI_BASE_URL = prevBase
+ process.env.AZURE_OPENAI_CODEX_DEPLOYMENT = prevEnv
+})