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.
- openai_codex_auth-0.1.0/PKG-INFO +73 -0
- openai_codex_auth-0.1.0/README.md +50 -0
- openai_codex_auth-0.1.0/pyproject.toml +47 -0
- openai_codex_auth-0.1.0/src/openai_codex_auth/LICENSE +21 -0
- openai_codex_auth-0.1.0/src/openai_codex_auth/__init__.py +140 -0
- openai_codex_auth-0.1.0/src/openai_codex_auth/py.typed +0 -0
|
@@ -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
|