openai-codex-auth 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: openai-codex-auth
3
+ Version: 0.1.0
4
+ Summary: Use the Codex CLI's ChatGPT login (~/.codex/auth.json) as a bearer credential for the Codex backend
5
+ Keywords: codex,chatgpt,oauth,openai
6
+ Author: hrbatra
7
+ Author-email: hrbatra <hrbatra@utexas.edu>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Dist: requests>=2.32.0
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://github.com/hrbatra/openai-codex-auth
20
+ Project-URL: Issues, https://github.com/hrbatra/openai-codex-auth/issues
21
+ Project-URL: Repository, https://github.com/hrbatra/openai-codex-auth
22
+ Description-Content-Type: text/markdown
23
+
24
+ # openai-codex-auth
25
+
26
+ Use the Codex CLI's ChatGPT login as a bearer credential, so OpenAI model calls
27
+ bill to a ChatGPT (Codex) subscription instead of an API key.
28
+
29
+ `codex login` stores tokens in `~/.codex/auth.json`. This package reads them,
30
+ refreshes the access token when it has expired, and writes the result back in
31
+ the CLI's format so the two stay in sync. One runtime dependency: `requests`.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ uv add openai-codex-auth
37
+ codex login # once, choosing "Sign in with ChatGPT"
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ import openai_codex_auth
44
+
45
+ auth = openai_codex_auth.CodexAuth() # or CodexAuth("/path/to/auth.json")
46
+ token = auth.token() # refreshed if expired
47
+ headers = openai_codex_auth.codex_headers(token, account_id=auth.account_id(), originator="my_app")
48
+ base_url = openai_codex_auth.DEFAULT_CODEX_API_BASE
49
+ ```
50
+
51
+ Send Responses API requests to `base_url` with `Authorization: Bearer <token>`
52
+ plus `headers`. The backend requires `stream: true`, rejects `system` role
53
+ input items (use `instructions` or `developer`), and rejects
54
+ `max_output_tokens`.
55
+
56
+ `openai_codex_auth.getauthtoken()` is the one-call form of the above.
57
+
58
+ Refreshes take an exclusive lock on the file, so parallel processes sharing
59
+ one login do not race each other. A missing file, or a CLI signed in with an
60
+ API key rather than ChatGPT, raises `CodexAuthError` naming `codex login`.
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ uv sync --dev
66
+ uv run pytest
67
+ uv run ruff check .
68
+ uv build --no-sources
69
+ ```
70
+
71
+ ## Release
72
+
73
+ See [RELEASING.md](RELEASING.md).
@@ -0,0 +1,50 @@
1
+ # openai-codex-auth
2
+
3
+ Use the Codex CLI's ChatGPT login as a bearer credential, so OpenAI model calls
4
+ bill to a ChatGPT (Codex) subscription instead of an API key.
5
+
6
+ `codex login` stores tokens in `~/.codex/auth.json`. This package reads them,
7
+ refreshes the access token when it has expired, and writes the result back in
8
+ the CLI's format so the two stay in sync. One runtime dependency: `requests`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ uv add openai-codex-auth
14
+ codex login # once, choosing "Sign in with ChatGPT"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import openai_codex_auth
21
+
22
+ auth = openai_codex_auth.CodexAuth() # or CodexAuth("/path/to/auth.json")
23
+ token = auth.token() # refreshed if expired
24
+ headers = openai_codex_auth.codex_headers(token, account_id=auth.account_id(), originator="my_app")
25
+ base_url = openai_codex_auth.DEFAULT_CODEX_API_BASE
26
+ ```
27
+
28
+ Send Responses API requests to `base_url` with `Authorization: Bearer <token>`
29
+ plus `headers`. The backend requires `stream: true`, rejects `system` role
30
+ input items (use `instructions` or `developer`), and rejects
31
+ `max_output_tokens`.
32
+
33
+ `openai_codex_auth.getauthtoken()` is the one-call form of the above.
34
+
35
+ Refreshes take an exclusive lock on the file, so parallel processes sharing
36
+ one login do not race each other. A missing file, or a CLI signed in with an
37
+ API key rather than ChatGPT, raises `CodexAuthError` naming `codex login`.
38
+
39
+ ## Development
40
+
41
+ ```bash
42
+ uv sync --dev
43
+ uv run pytest
44
+ uv run ruff check .
45
+ uv build --no-sources
46
+ ```
47
+
48
+ ## Release
49
+
50
+ See [RELEASING.md](RELEASING.md).
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "openai-codex-auth"
3
+ version = "0.1.0"
4
+ description = "Use the Codex CLI's ChatGPT login (~/.codex/auth.json) as a bearer credential for the Codex backend"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "hrbatra", email = "hrbatra@utexas.edu" }
8
+ ]
9
+ license = "MIT"
10
+ requires-python = ">=3.12"
11
+ keywords = ["codex", "chatgpt", "oauth", "openai"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ dependencies = [
23
+ "requests>=2.32.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/hrbatra/openai-codex-auth"
28
+ Repository = "https://github.com/hrbatra/openai-codex-auth"
29
+ Issues = "https://github.com/hrbatra/openai-codex-auth/issues"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.9.17,<0.10.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "ruff>=0.15",
39
+ ]
40
+
41
+ [tool.ruff]
42
+ target-version = "py312"
43
+
44
+ [tool.ruff.lint]
45
+ # Pin the rule set: ruff 0.16 widened its defaults, and the auth module is kept
46
+ # byte-for-byte close to its dspy-codex-auth origin rather than restyled.
47
+ select = ["E4", "E7", "E9", "F", "I"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hrbatra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ """Use the Codex CLI's ChatGPT login as a bearer credential.
2
+
3
+ Run ``codex login`` once. This module reads the tokens the CLI stores in
4
+ ``~/.codex/auth.json``, refreshes the access token when it has expired, and
5
+ writes the result back in the CLI's own format so both stay in sync.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import fcntl
12
+ import json
13
+ import os
14
+ import time
15
+ from datetime import UTC, datetime
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import requests
20
+
21
+ DEFAULT_AUTH_PATH = Path("~/.codex/auth.json").expanduser()
22
+ DEFAULT_CODEX_API_BASE = "https://chatgpt.com/backend-api/codex"
23
+ DEFAULT_CODEX_ORIGINATOR = "openai_codex_auth"
24
+ CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
25
+ CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token"
26
+ _ACCOUNT_CLAIM = "https://api.openai.com/auth"
27
+ _EXPIRY_LEEWAY_SECONDS = 60
28
+
29
+
30
+ class CodexAuthError(RuntimeError):
31
+ """No usable ChatGPT credential in the Codex CLI auth file."""
32
+
33
+
34
+ def _jwt_claims(token: str) -> dict[str, Any]:
35
+ payload = token.split(".")[1]
36
+ payload += "=" * (-len(payload) % 4)
37
+ return json.loads(base64.urlsafe_b64decode(payload))
38
+
39
+
40
+ def _is_fresh(access_token: str) -> bool:
41
+ return _jwt_claims(access_token).get("exp", 0) > time.time() + _EXPIRY_LEEWAY_SECONDS
42
+
43
+
44
+ class CodexAuth:
45
+ """Access token and account id from the Codex CLI's ``auth.json``."""
46
+
47
+ def __init__(self, path: str | os.PathLike[str] = DEFAULT_AUTH_PATH):
48
+ self.path = Path(path).expanduser()
49
+
50
+ def _read(self) -> dict[str, Any]:
51
+ try:
52
+ data = json.loads(self.path.read_text())
53
+ except FileNotFoundError:
54
+ raise CodexAuthError(
55
+ f"No Codex credential at {self.path}. Run `codex login`."
56
+ ) from None
57
+ tokens = data.get("tokens") or {}
58
+ if not tokens.get("access_token") or not tokens.get("refresh_token"):
59
+ raise CodexAuthError(
60
+ f"{self.path} has no ChatGPT login (auth_mode="
61
+ f"{data.get('auth_mode')!r}). Run `codex login` and sign in with ChatGPT."
62
+ )
63
+ return data
64
+
65
+ def token(self) -> str:
66
+ """The current access token, refreshed first if it has expired."""
67
+ access = self._read()["tokens"]["access_token"]
68
+ if _is_fresh(access):
69
+ return access
70
+ return self._refresh()["tokens"]["access_token"]
71
+
72
+ def account_id(self) -> str:
73
+ tokens = self._read()["tokens"]
74
+ return tokens.get("account_id") or _jwt_claims(tokens["access_token"])[
75
+ _ACCOUNT_CLAIM
76
+ ]["chatgpt_account_id"]
77
+
78
+ def _refresh(self) -> dict[str, Any]:
79
+ with self.path.open("r+") as handle:
80
+ fcntl.flock(handle, fcntl.LOCK_EX)
81
+ data = json.loads(handle.read())
82
+ tokens = data["tokens"]
83
+ if _is_fresh(tokens["access_token"]):
84
+ return data # another process refreshed while we waited for the lock
85
+ response = requests.post(
86
+ CODEX_TOKEN_URL,
87
+ data={
88
+ "grant_type": "refresh_token",
89
+ "refresh_token": tokens["refresh_token"],
90
+ "client_id": CODEX_CLIENT_ID,
91
+ },
92
+ timeout=30,
93
+ )
94
+ response.raise_for_status()
95
+ fresh = response.json()
96
+ tokens["access_token"] = fresh["access_token"]
97
+ tokens["refresh_token"] = fresh.get("refresh_token") or tokens["refresh_token"]
98
+ if fresh.get("id_token"):
99
+ tokens["id_token"] = fresh["id_token"]
100
+ data["last_refresh"] = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
101
+ handle.seek(0)
102
+ handle.truncate()
103
+ json.dump(data, handle, indent=2)
104
+ return data
105
+
106
+
107
+ def getauthtoken(path: str | os.PathLike[str] = DEFAULT_AUTH_PATH) -> str:
108
+ return CodexAuth(path).token()
109
+
110
+
111
+ def codex_headers(
112
+ token: str,
113
+ *,
114
+ account_id: str | None = None,
115
+ originator: str = DEFAULT_CODEX_ORIGINATOR,
116
+ extra_headers: dict[str, Any] | None = None,
117
+ ) -> dict[str, str]:
118
+ """Headers the Codex backend expects alongside ``Authorization: Bearer``."""
119
+ headers = {
120
+ "chatgpt-account-id": account_id
121
+ or _jwt_claims(token)[_ACCOUNT_CLAIM]["chatgpt_account_id"],
122
+ "OpenAI-Beta": "responses=experimental",
123
+ "originator": originator,
124
+ }
125
+ if extra_headers:
126
+ headers.update({str(key): str(value) for key, value in extra_headers.items()})
127
+ return headers
128
+
129
+
130
+ __all__ = [
131
+ "CODEX_CLIENT_ID",
132
+ "CODEX_TOKEN_URL",
133
+ "DEFAULT_AUTH_PATH",
134
+ "DEFAULT_CODEX_API_BASE",
135
+ "DEFAULT_CODEX_ORIGINATOR",
136
+ "CodexAuth",
137
+ "CodexAuthError",
138
+ "codex_headers",
139
+ "getauthtoken",
140
+ ]
File without changes