Skip to content
Merged
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
32 changes: 29 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 @@ -323,12 +323,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(
authFile,
JSON.stringify(
{
GITHUBCOPILOT_API_KEY: apiKey,
},
null,
2,
),
"utf-8",
);
} catch {
/* ignore */
}
} else if (cli.flags.login) {
if (provider.toLowerCase() === "githubcopilot") {
apiKey = await fetchGithubCopilotApiKey();
Comment on lines +337 to 359
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 initial githubcopilot login block will always take precedence, making the subsequent cli.flags.login branch unreachable for Copilot; consider merging or removing redundant logic.

Suggested change
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(
authFile,
JSON.stringify(
{
GITHUBCOPILOT_API_KEY: apiKey,
},
null,
2,
),
"utf-8",
);
} catch {
/* ignore */
}
} else if (cli.flags.login) {
if (provider.toLowerCase() === "githubcopilot") {
apiKey = await fetchGithubCopilotApiKey();
if ((provider.toLowerCase() === "githubcopilot" && !apiKey) || cli.flags.login) {
if (provider.toLowerCase() === "githubcopilot") {
apiKey = await fetchGithubCopilotApiKey();
try {
const home = os.homedir();
const authDir = path.join(home, ".codex");
const authFile = path.join(authDir, "auth.json");
fs.writeFileSync(
authFile,
JSON.stringify(
{
GITHUBCOPILOT_API_KEY: apiKey,
},
null,
2,
),
"utf-8",
);
} catch {
/* ignore */
}

Copilot uses AI. Check for mistakes.
} else {
Expand All @@ -353,7 +379,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.

Dynamic assignment of process.env[${provider}_API_KEY] overrides the expected OPENAI_API_KEY for non-openai providers (e.g., Azure); consider special-casing openai/azure to maintain compatibility.

Suggested change
process.env[`${provider.toUpperCase()}_API_KEY`] = apiKey;
if (provider.toLowerCase() === "openai") {
process.env["OPENAI_API_KEY"] = apiKey;
} else if (provider.toLowerCase() === "azure") {
process.env["AZURE_API_KEY"] = apiKey;
} else {
process.env[`${provider.toUpperCase()}_API_KEY`] = 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> {
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] Provide a fallback for manual API key entry or prompt when device flow isn't available, as removing the previous manual entry option may limit usability.

Suggested change
export async function getGithubCopilotApiKey(): Promise<string> {
export async function getGithubCopilotApiKey(): Promise<string> {
const choice = await promptUserForChoice();
if (choice.type === "apikey") {
process.env["GITHUBCOPILOT_API_KEY"] = choice.key;
return choice.key;
}

Copilot uses AI. Check for mistakes.
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