Skip to content

Commit c6e08ad

Browse files
authored
add: sign in with chatgpt (#963)
Sign in with ChatGPT to get an API key (flow to grant API credits for Plus/Pro coming later today!)
1 parent cabf83f commit c6e08ad

File tree

7 files changed

+1052
-7
lines changed

7 files changed

+1052
-7
lines changed

codex-cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"chalk": "^5.2.0",
3232
"diff": "^7.0.0",
3333
"dotenv": "^16.1.4",
34+
"express": "^5.1.0",
3435
"fast-deep-equal": "^3.1.3",
3536
"fast-npm-meta": "^0.4.2",
3637
"figures": "^6.1.0",
@@ -54,6 +55,7 @@
5455
"devDependencies": {
5556
"@eslint/js": "^9.22.0",
5657
"@types/diff": "^7.0.2",
58+
"@types/express": "^5.0.1",
5759
"@types/js-yaml": "^4.0.9",
5860
"@types/marked-terminal": "^6.1.1",
5961
"@types/react": "^18.0.32",

codex-cli/src/cli.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ import { ReviewDecision } from "./utils/agent/review";
2020
import { AutoApprovalMode } from "./utils/auto-approval-mode";
2121
import { checkForUpdates } from "./utils/check-updates";
2222
import {
23-
getApiKey,
2423
loadConfig,
2524
PRETTY_PRINT,
2625
INSTRUCTIONS_FILEPATH,
2726
} from "./utils/config";
27+
import { getApiKey as fetchApiKey } from "./utils/get-api-key";
2828
import { createInputItem } from "./utils/input-utils";
2929
import { initLogger } from "./utils/logger/log";
3030
import { isModelSupportedForResponses } from "./utils/model-utils.js";
@@ -35,6 +35,7 @@ import { spawnSync } from "child_process";
3535
import fs from "fs";
3636
import { render } from "ink";
3737
import meow from "meow";
38+
import os from "os";
3839
import path from "path";
3940
import React from "react";
4041

@@ -271,7 +272,38 @@ let prompt = cli.input[0];
271272
const model = cli.flags.model ?? config.model;
272273
const imagePaths = cli.flags.image;
273274
const provider = cli.flags.provider ?? config.provider ?? "openai";
274-
const apiKey = getApiKey(provider);
275+
276+
const client = {
277+
issuer: "https://auth.openai.com",
278+
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
279+
};
280+
281+
let apiKey = "";
282+
283+
// Try to load existing auth file if present
284+
try {
285+
const home = os.homedir();
286+
const authDir = path.join(home, ".codex");
287+
const authFile = path.join(authDir, "auth.json");
288+
if (fs.existsSync(authFile)) {
289+
const data = JSON.parse(fs.readFileSync(authFile, "utf-8"));
290+
const lastRefreshTime = data.last_refresh
291+
? new Date(data.last_refresh).getTime()
292+
: 0;
293+
const expired = Date.now() - lastRefreshTime > 28 * 24 * 60 * 60 * 1000;
294+
if (data.OPENAI_API_KEY && !expired) {
295+
apiKey = data.OPENAI_API_KEY;
296+
}
297+
}
298+
} catch {
299+
// ignore errors
300+
}
301+
302+
if (!apiKey) {
303+
apiKey = await fetchApiKey(client.issuer, client.client_id);
304+
}
305+
// Ensure the API key is available as an environment variable for legacy code
306+
process.env["OPENAI_API_KEY"] = apiKey;
275307

276308
// Set of providers that don't require API keys
277309
const NO_API_KEY_REQUIRED = new Set(["ollama"]);

codex-cli/src/utils/agent/agent-loop.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
OPENAI_TIMEOUT_MS,
1818
OPENAI_ORGANIZATION,
1919
OPENAI_PROJECT,
20-
getApiKey,
2120
getBaseUrl,
2221
AZURE_OPENAI_API_VERSION,
2322
} from "../config.js";
@@ -307,7 +306,7 @@ export class AgentLoop {
307306
this.sessionId = getSessionId() || randomUUID().replaceAll("-", "");
308307
// Configure OpenAI client with optional timeout (ms) from environment
309308
const timeoutMs = OPENAI_TIMEOUT_MS;
310-
const apiKey = getApiKey(this.provider);
309+
const apiKey = this.config.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
311310
const baseURL = getBaseUrl(this.provider);
312311

313312
this.oai = new OpenAI({
@@ -766,7 +765,7 @@ export class AgentLoop {
766765
// prompts) and so that freshly generated `function_call_output`s are
767766
// shown immediately.
768767
// Figure out what subset of `turnInput` constitutes *new* information
769-
// for the UI so that we dont spam the interface with repeats of the
768+
// for the UI so that we don't spam the interface with repeats of the
770769
// entire transcript on every iteration when response storage is
771770
// disabled.
772771
const deltaInput = this.disableResponseStorage
@@ -1645,7 +1644,6 @@ You MUST adhere to the following criteria when executing the task:
16451644
- If there is a .pre-commit-config.yaml, use \`pre-commit run --files ...\` to check that your changes pass the pre-commit checks. However, do not fix pre-existing errors on lines you didn't touch.
16461645
- If pre-commit doesn't work after a few retries, politely inform the user that the pre-commit setup is broken.
16471646
- Once you finish coding, you must
1648-
- Check \`git status\` to sanity check your changes; revert any scratch files or changes.
16491647
- Remove all inline comments you added as much as possible, even if they look normal. Check using \`git diff\`. Inline comments must be generally avoided, unless active maintainers of the repo, after long careful study of the code and the issue, will still misinterpret the code without the comments.
16501648
- Check if you accidentally add copyright or license headers. If so, remove them.
16511649
- Try to run pre-commit if it is available.

codex-cli/src/utils/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function getApiKey(provider: string = "openai"): string | undefined {
120120
return process.env[providerInfo.envKey];
121121
}
122122

123-
// Checking `PROVIDER_API_KEY feels more intuitive with a custom provider.
123+
// Checking `PROVIDER_API_KEY` feels more intuitive with a custom provider.
124124
const customApiKey = process.env[`${provider.toUpperCase()}_API_KEY`];
125125
if (customApiKey) {
126126
return customApiKey;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import SelectInput from "../components/select-input/select-input.js";
2+
import Spinner from "../components/vendor/ink-spinner.js";
3+
import TextInput from "../components/vendor/ink-text-input.js";
4+
import { Box, Text } from "ink";
5+
import React, { useState } from "react";
6+
7+
export type Choice = { type: "signin" } | { type: "apikey"; key: string };
8+
9+
export function ApiKeyPrompt({
10+
onDone,
11+
}: {
12+
onDone: (choice: Choice) => void;
13+
}): JSX.Element {
14+
const [step, setStep] = useState<"select" | "paste">("select");
15+
const [apiKey, setApiKey] = useState("");
16+
17+
if (step === "select") {
18+
return (
19+
<Box flexDirection="column" gap={1}>
20+
<Box flexDirection="column">
21+
<Text>
22+
Sign in with ChatGPT to generate an API key or paste one you already
23+
have.
24+
</Text>
25+
<Text dimColor>[use arrows to move, enter to select]</Text>
26+
</Box>
27+
<SelectInput
28+
items={[
29+
{ label: "Sign in with ChatGPT", value: "signin" },
30+
{
31+
label: "Paste an API key (or set as OPENAI_API_KEY)",
32+
value: "paste",
33+
},
34+
]}
35+
onSelect={(item: { value: string }) => {
36+
if (item.value === "signin") {
37+
onDone({ type: "signin" });
38+
} else {
39+
setStep("paste");
40+
}
41+
}}
42+
/>
43+
</Box>
44+
);
45+
}
46+
47+
return (
48+
<Box flexDirection="column">
49+
<Text>Paste your OpenAI API key and press &lt;Enter&gt;:</Text>
50+
<TextInput
51+
value={apiKey}
52+
onChange={setApiKey}
53+
onSubmit={(value: string) => {
54+
if (value.trim() !== "") {
55+
onDone({ type: "apikey", key: value.trim() });
56+
}
57+
}}
58+
placeholder="sk-..."
59+
mask="*"
60+
/>
61+
</Box>
62+
);
63+
}
64+
65+
export function WaitingForAuth(): JSX.Element {
66+
return (
67+
<Box flexDirection="row" marginTop={1}>
68+
<Spinner type="ball" />
69+
<Text>
70+
{" "}
71+
Waiting for authentication… <Text dimColor>ctrl + c to quit</Text>
72+
</Text>
73+
</Box>
74+
);
75+
}

0 commit comments

Comments
 (0)