Skip to content

Adds Azure OpenAI support #769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export OPENAI_API_KEY="your-api-key-here"
>
> - openai (default)
> - openrouter
> - azure
> - gemini
> - ollama
> - mistral
Expand Down Expand Up @@ -394,6 +395,11 @@ Below is a comprehensive example of `config.json` with multiple custom providers
"baseURL": "https://api.openai.com/v1",
"envKey": "OPENAI_API_KEY"
},
"azure": {
"name": "AzureOpenAI",
"baseURL": "https://YOUR_PROJECT_NAME.openai.azure.com",
"envKey": "AZURE_OPENAI_API_KEY"
},
"openrouter": {
"name": "OpenRouter",
"baseURL": "https://openrouter.ai/api/v1",
Expand Down Expand Up @@ -455,6 +461,10 @@ For each AI provider, you need to set the corresponding API key in your environm
# OpenAI
export OPENAI_API_KEY="your-api-key-here"
# Azure OpenAI
export AZURE_OPENAI_API_KEY="your-azure-api-key-here"
export AZURE_OPENAI_API_VERSION="2025-03-01-preview" (Optional)
# OpenRouter
export OPENROUTER_API_KEY="your-openrouter-key-here"
Expand Down
9 changes: 3 additions & 6 deletions codex-cli/src/components/chat/terminal-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useTerminalSize } from "../../hooks/use-terminal-size.js";
import { AgentLoop } from "../../utils/agent/agent-loop.js";
import { ReviewDecision } from "../../utils/agent/review.js";
import { generateCompactSummary } from "../../utils/compact-summary.js";
import { getBaseUrl, getApiKey, saveConfig } from "../../utils/config.js";
import { saveConfig } from "../../utils/config.js";
import { extractAppliedPatches as _extractAppliedPatches } from "../../utils/extract-applied-patches.js";
import { getGitDiff } from "../../utils/get-diff.js";
import { createInputItem } from "../../utils/input-utils.js";
Expand All @@ -23,6 +23,7 @@ import {
calculateContextPercentRemaining,
uniqueById,
} from "../../utils/model-utils.js";
import { createOpenAIClient } from "../../utils/openai-client.js";
import { CLI_VERSION } from "../../utils/session.js";
import { shortCwd } from "../../utils/short-path.js";
import { saveRollout } from "../../utils/storage/save-rollout.js";
Expand All @@ -34,7 +35,6 @@ import ModelOverlay from "../model-overlay.js";
import chalk from "chalk";
import { Box, Text } from "ink";
import { spawn } from "node:child_process";
import OpenAI from "openai";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { inspect } from "util";

Expand Down Expand Up @@ -78,10 +78,7 @@ async function generateCommandExplanation(
): Promise<string> {
try {
// Create a temporary OpenAI client
const oai = new OpenAI({
apiKey: getApiKey(config.provider),
baseURL: getBaseUrl(config.provider),
});
const oai = createOpenAIClient(config);

// Format the command for display
const commandForDisplay = formatCommandForDisplay(command);
Expand Down
24 changes: 2 additions & 22 deletions codex-cli/src/components/singlepass-cli-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import type { FileOperation } from "../utils/singlepass/file_ops";

import Spinner from "./vendor/ink-spinner"; // Third‑party / vendor components
import TextInput from "./vendor/ink-text-input";
import {
OPENAI_TIMEOUT_MS,
OPENAI_ORGANIZATION,
OPENAI_PROJECT,
getBaseUrl,
getApiKey,
} from "../utils/config";
import { createOpenAIClient } from "../utils/openai-client";
import {
generateDiffSummary,
generateEditSummary,
Expand All @@ -26,7 +20,6 @@ import { EditedFilesSchema } from "../utils/singlepass/file_ops";
import * as fsSync from "fs";
import * as fsPromises from "fs/promises";
import { Box, Text, useApp, useInput } from "ink";
import OpenAI from "openai";
import { zodResponseFormat } from "openai/helpers/zod";
import path from "path";
import React, { useEffect, useState, useRef } from "react";
Expand Down Expand Up @@ -399,20 +392,7 @@ export function SinglePassApp({
files,
});

const headers: Record<string, string> = {};
if (OPENAI_ORGANIZATION) {
headers["OpenAI-Organization"] = OPENAI_ORGANIZATION;
}
if (OPENAI_PROJECT) {
headers["OpenAI-Project"] = OPENAI_PROJECT;
}

const openai = new OpenAI({
apiKey: getApiKey(config.provider),
baseURL: getBaseUrl(config.provider),
timeout: OPENAI_TIMEOUT_MS,
defaultHeaders: headers,
});
const openai = createOpenAIClient(config);
const chatResp = await openai.beta.chat.completions.parse({
model: config.model,
...(config.flexMode ? { service_tier: "flex" } : {}),
Expand Down
22 changes: 21 additions & 1 deletion codex-cli/src/utils/agent/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
OPENAI_PROJECT,
getApiKey,
getBaseUrl,
AZURE_OPENAI_API_VERSION,
} from "../config.js";
import { log } from "../logger/log.js";
import { parseToolCallArguments } from "../parsers.js";
Expand All @@ -31,7 +32,7 @@ import {
import { handleExecCommand } from "./handle-exec-command.js";
import { HttpsProxyAgent } from "https-proxy-agent";
import { randomUUID } from "node:crypto";
import OpenAI, { APIConnectionTimeoutError } from "openai";
import OpenAI, { APIConnectionTimeoutError, AzureOpenAI } from "openai";

// Wait time before retrying after rate limit errors (ms).
const RATE_LIMIT_RETRY_WAIT_MS = parseInt(
Expand Down Expand Up @@ -322,6 +323,25 @@ export class AgentLoop {
...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
});

if (this.provider.toLowerCase() === "azure") {
this.oai = new AzureOpenAI({
apiKey,
baseURL,
apiVersion: AZURE_OPENAI_API_VERSION,
defaultHeaders: {
originator: ORIGIN,
version: CLI_VERSION,
session_id: this.sessionId,
...(OPENAI_ORGANIZATION
? { "OpenAI-Organization": OPENAI_ORGANIZATION }
: {}),
...(OPENAI_PROJECT ? { "OpenAI-Project": OPENAI_PROJECT } : {}),
},
httpAgent: PROXY_URL ? new HttpsProxyAgent(PROXY_URL) : undefined,
...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
});
}

setSessionId(this.sessionId);
setCurrentModel(this.model);

Expand Down
11 changes: 5 additions & 6 deletions codex-cli/src/utils/compact-summary.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { AppConfig } from "./config.js";
import type { ResponseItem } from "openai/resources/responses/responses.mjs";

import { getBaseUrl, getApiKey } from "./config.js";
import OpenAI from "openai";
import { createOpenAIClient } from "./openai-client.js";

/**
* Generate a condensed summary of the conversation items.
* @param items The list of conversation items to summarize
* @param model The model to use for generating the summary
* @param flexMode Whether to use the flex-mode service tier
* @param config The configuration object
* @returns A concise structured summary string
*/
/**
Expand All @@ -23,10 +25,7 @@ export async function generateCompactSummary(
flexMode = false,
config: AppConfig,
): Promise<string> {
const oai = new OpenAI({
apiKey: getApiKey(config.provider),
baseURL: getBaseUrl(config.provider),
});
const oai = createOpenAIClient(config);

const conversationText = items
.filter(
Expand Down
3 changes: 3 additions & 0 deletions codex-cli/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export const OPENAI_TIMEOUT_MS =
export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || "";
export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || "";

export const AZURE_OPENAI_API_VERSION =
process.env["AZURE_OPENAI_API_VERSION"] || "2025-03-01-preview";

export const DEFAULT_REASONING_EFFORT = "high";
export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || "";
export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || "";
Expand Down
23 changes: 3 additions & 20 deletions codex-cli/src/utils/model-utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import type { ResponseItem } from "openai/resources/responses/responses.mjs";

import { approximateTokensUsed } from "./approximate-tokens-used.js";
import {
OPENAI_ORGANIZATION,
OPENAI_PROJECT,
getBaseUrl,
getApiKey,
} from "./config";
import { getApiKey } from "./config.js";
import { type SupportedModelId, openAiModelInfo } from "./model-info.js";
import OpenAI from "openai";
import { createOpenAIClient } from "./openai-client.js";

const MODEL_LIST_TIMEOUT_MS = 2_000; // 2 seconds
export const RECOMMENDED_MODELS: Array<string> = ["o4-mini", "o3"];
Expand All @@ -27,19 +22,7 @@ async function fetchModels(provider: string): Promise<Array<string>> {
}

try {
const headers: Record<string, string> = {};
if (OPENAI_ORGANIZATION) {
headers["OpenAI-Organization"] = OPENAI_ORGANIZATION;
}
if (OPENAI_PROJECT) {
headers["OpenAI-Project"] = OPENAI_PROJECT;
}

const openai = new OpenAI({
apiKey: getApiKey(provider),
baseURL: getBaseUrl(provider),
defaultHeaders: headers,
});
const openai = createOpenAIClient({ provider });
const list = await openai.models.list();
const models: Array<string> = [];
for await (const model of list as AsyncIterable<{ id?: string }>) {
Expand Down
51 changes: 51 additions & 0 deletions codex-cli/src/utils/openai-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AppConfig } from "./config.js";

import {
getBaseUrl,
getApiKey,
AZURE_OPENAI_API_VERSION,
OPENAI_TIMEOUT_MS,
OPENAI_ORGANIZATION,
OPENAI_PROJECT,
} from "./config.js";
import OpenAI, { AzureOpenAI } from "openai";

type OpenAIClientConfig = {
provider: string;
};

/**
* Creates an OpenAI client instance based on the provided configuration.
* Handles both standard OpenAI and Azure OpenAI configurations.
*
* @param config The configuration containing provider information
* @returns An instance of either OpenAI or AzureOpenAI client
*/
export function createOpenAIClient(
config: OpenAIClientConfig | AppConfig,
): OpenAI | AzureOpenAI {
const headers: Record<string, string> = {};
if (OPENAI_ORGANIZATION) {
headers["OpenAI-Organization"] = OPENAI_ORGANIZATION;
}
if (OPENAI_PROJECT) {
headers["OpenAI-Project"] = OPENAI_PROJECT;
}

if (config.provider?.toLowerCase() === "azure") {
return new AzureOpenAI({
apiKey: getApiKey(config.provider),
baseURL: getBaseUrl(config.provider),
apiVersion: AZURE_OPENAI_API_VERSION,
timeout: OPENAI_TIMEOUT_MS,
defaultHeaders: headers,
});
}

return new OpenAI({
apiKey: getApiKey(config.provider),
baseURL: getBaseUrl(config.provider),
timeout: OPENAI_TIMEOUT_MS,
defaultHeaders: headers,
});
}
5 changes: 5 additions & 0 deletions codex-cli/src/utils/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const providers: Record<
baseURL: "https://openrouter.ai/api/v1",
envKey: "OPENROUTER_API_KEY",
},
azure: {
name: "AzureOpenAI",
baseURL: "https://YOUR_PROJECT_NAME.openai.azure.com",
envKey: "AZURE_OPENAI_API_KEY",
},
gemini: {
name: "Gemini",
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
Expand Down