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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export OPENAI_API_KEY="your-api-key-here"
> - xai
> - groq
> - arceeai
> - githubcopilot
> - any other provider that is compatible with the OpenAI API
>
> If you use a provider other than OpenAI, you will need to set the API key for the provider in the config file or in the environment variable as:
Expand Down Expand Up @@ -440,6 +441,11 @@ Below is a comprehensive example of `config.json` with multiple custom providers
"name": "ArceeAI",
"baseURL": "https://conductor.arcee.ai/v1",
"envKey": "ARCEEAI_API_KEY"
},
"githubcopilot": {
"name": "GitHub Copilot",
"baseURL": "https://api.githubcopilot.com",
"envKey": "GITHUBCOPILOT_API_KEY"
}
},
"history": {
Expand Down Expand Up @@ -474,6 +480,9 @@ export AZURE_OPENAI_API_VERSION="2025-03-01-preview" (Optional)
# OpenRouter
export OPENROUTER_API_KEY="your-openrouter-key-here"

# GitHub Copilot
export GITHUBCOPILOT_API_KEY="your-copilot-token-here"

# Similarly for other providers
```

Expand Down
36 changes: 33 additions & 3 deletions codex-cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import {
} from "./utils/config";
import {
getApiKey as fetchApiKey,
getGithubCopilotApiKey as fetchGithubCopilotApiKey,
maybeRedeemCredits,
fetchGithubCopilotApiKey,
} from "./utils/get-api-key";
import { createInputItem } from "./utils/input-utils";
import { initLogger } from "./utils/logger/log";
Expand Down Expand Up @@ -117,6 +117,10 @@ const cli = meow(
$ codex "Write and run a python program that prints ASCII art"
$ codex -q "fix build issues"
$ codex completion bash

Supported providers:
openai (default), openrouter, azure, gemini, ollama, mistral,
deepseek, xai, groq, arceeai, githubcopilot
`,
{
importMeta: import.meta,
Expand Down Expand Up @@ -323,12 +327,38 @@ try {
if (data.OPENAI_API_KEY && !expired) {
apiKey = data.OPENAI_API_KEY;
}
if (
data.GITHUBCOPILOT_API_KEY &&
provider.toLowerCase() === "githubcopilot"
) {
apiKey = data.GITHUBCOPILOT_API_KEY;
}
}
} catch {
// ignore errors
}

if (cli.flags.login) {
if (provider.toLowerCase() === "githubcopilot" && !apiKey) {
apiKey = await fetchGithubCopilotApiKey();
try {
const home = os.homedir();
const authDir = path.join(home, ".codex");
const authFile = path.join(authDir, "auth.json");
fs.writeFileSync(
Copy link

Copilot AI Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid blocking the event loop in async code—use fs.promises.writeFile instead of writeFileSync.

Suggested change
fs.writeFileSync(
await fs.promises.writeFile(

Copilot uses AI. Check for mistakes.
authFile,
JSON.stringify(
{
GITHUBCOPILOT_API_KEY: apiKey,
},
null,
2,
),
"utf-8",
);
} catch {
/* ignore */
}
} else if (cli.flags.login) {
Copy link

Copilot AI Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The GitHub Copilot login block duplicates logic in getGithubCopilotApiKey. Consider consolidating into a shared helper.

Copilot uses AI. Check for mistakes.
if (provider.toLowerCase() === "githubcopilot") {
apiKey = await fetchGithubCopilotApiKey();
} else {
Expand All @@ -353,7 +383,7 @@ if (cli.flags.login) {
}
}
// Ensure the API key is available as an environment variable for legacy code
process.env["OPENAI_API_KEY"] = apiKey;
process.env[`${provider.toUpperCase()}_API_KEY`] = apiKey;
Copy link

Copilot AI Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deriving the env var name from provider.toUpperCase() will produce GITHUBCOPILOT_API_KEY (no underscore). Consider mapping Copilot explicitly to the correct key.

Suggested change
process.env[`${provider.toUpperCase()}_API_KEY`] = apiKey;
const envVarName = provider.toLowerCase() === "githubcopilot"
? "GITHUB_COPILOT_API_KEY"
: `${provider.toUpperCase()}_API_KEY`;
process.env[envVarName] = apiKey;

Copilot uses AI. Check for mistakes.

if (cli.flags.free) {
// eslint-disable-next-line no-console
Expand Down
19 changes: 19 additions & 0 deletions codex-cli/src/utils/agent/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "../session.js";
import { applyPatchToolInstructions } from "./apply-patch.js";
import { handleExecCommand } from "./handle-exec-command.js";
import { GithubCopilotClient } from "../openai-client.js";
import { HttpsProxyAgent } from "https-proxy-agent";
import { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
Expand Down Expand Up @@ -350,6 +351,24 @@ export class AgentLoop {
});
}

if (this.provider.toLowerCase() === "githubcopilot") {
this.oai = new GithubCopilotClient({
...(apiKey ? { apiKey } : {}),
baseURL,
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
2 changes: 2 additions & 0 deletions codex-cli/src/utils/agent/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function execApplyPatch(
stdout: result,
stderr: "",
exitCode: 0,
pid: 0,
};
} catch (error: unknown) {
// @ts-expect-error error might not be an object or have a message property.
Expand All @@ -128,6 +129,7 @@ export function execApplyPatch(
stdout: "",
stderr: stderr,
exitCode: 1,
pid: 0,
};
}
}
Expand Down
2 changes: 2 additions & 0 deletions codex-cli/src/utils/agent/sandbox/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type ExecResult = {
stdout: string;
stderr: string;
exitCode: number;
/** PID of the spawned process. 0 if spawn failed */
pid: number;
};

/**
Expand Down
8 changes: 6 additions & 2 deletions codex-cli/src/utils/agent/sandbox/raw-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function exec(
stdout: "",
stderr: "command[0] is not a string",
exitCode: 1,
pid: 0,
});
}

Expand Down Expand Up @@ -124,7 +125,7 @@ export function exec(
if (!child.killed) {
killTarget("SIGKILL");
}
}, 2000).unref();
}, 250).unref();
};
if (abortSignal.aborted) {
abortHandler();
Expand Down Expand Up @@ -186,6 +187,7 @@ export function exec(
stdout,
stderr,
exitCode,
pid: child.pid ?? 0,
};
resolve(
addTruncationWarningsIfNecessary(
Expand All @@ -201,6 +203,7 @@ export function exec(
stdout: "",
stderr: String(err),
exitCode: 1,
pid: child.pid ?? 0,
};
resolve(
addTruncationWarningsIfNecessary(
Expand All @@ -224,7 +227,7 @@ function addTruncationWarningsIfNecessary(
if (!hitMaxStdout && !hitMaxStderr) {
return execResult;
} else {
const { stdout, stderr, exitCode } = execResult;
const { stdout, stderr, exitCode, pid } = execResult;
return {
stdout: hitMaxStdout
? stdout + "\n\n[Output truncated: too many lines or bytes]"
Expand All @@ -233,6 +236,7 @@ function addTruncationWarningsIfNecessary(
? stderr + "\n\n[Output truncated: too many lines or bytes]"
: stderr,
exitCode,
pid,
};
}
}
49 changes: 27 additions & 22 deletions codex-cli/src/utils/get-api-key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { Choice } from "./get-api-key-components";
import type { Request, Response } from "express";

import { ApiKeyPrompt, WaitingForAuth } from "./get-api-key-components";
import { GithubCopilotClient } from "./openai-client.js";
import Spinner from "../components/vendor/ink-spinner.js";
import chalk from "chalk";
import express from "express";
import fs from "fs/promises";
import { render } from "ink";
import { Box, Text, render } from "ink";
import crypto from "node:crypto";
import { URL } from "node:url";
import open from "open";
Expand Down Expand Up @@ -763,26 +765,29 @@ export async function getApiKey(
}
}

export { maybeRedeemCredits };

export async function fetchGithubCopilotApiKey(): Promise<string> {
if (process.env["GITHUB_COPILOT_TOKEN"]) {
return process.env["GITHUB_COPILOT_TOKEN"]!;
}

const choice = await promptUserForChoice();
if (choice.type === "apikey") {
process.env["GITHUB_COPILOT_TOKEN"] = choice.key;
return choice.key;
}

// Sign in via GitHub is not yet supported; instruct the user
// eslint-disable-next-line no-console
console.error(
"\n" +
"GitHub OAuth login is not yet implemented for Codex. " +
"Please generate a token manually and set it as GITHUB_COPILOT_TOKEN." +
"\n"
export async function getGithubCopilotApiKey(): Promise<string> {
const { device_code, user_code, verification_uri } =
await GithubCopilotClient.getLoginURL();
const spinner = render(
<Box flexDirection="row" marginTop={1}>
<Spinner type="ball" />
<Text>
{" "}
Please visit {verification_uri} and enter code {user_code}
</Text>
</Box>,
);
process.exit(1);
try {
const key = await GithubCopilotClient.pollForAccessToken(device_code);
spinner.clear();
spinner.unmount();
process.env["GITHUBCOPILOT_API_KEY"] = key;
return key;
} catch (err) {
spinner.clear();
spinner.unmount();
throw err;
}
}

export { maybeRedeemCredits };
Loading