langchain-codex-plus 0.0.1__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,53 @@
1
+ name: Publish to PyPI
2
+
3
+ # Triggered by pushing a version tag (e.g. ``v0.0.1``). PyPI Trusted
4
+ # Publishing uses GitHub's OIDC token to authenticate — no API tokens
5
+ # stored anywhere. The ``pypi`` environment adds an extra gate (you
6
+ # can require manual approval on it in the repo settings).
7
+ on:
8
+ push:
9
+ tags:
10
+ - "v*"
11
+
12
+ jobs:
13
+ build:
14
+ name: Build distributions
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v3
20
+ with:
21
+ enable-cache: true
22
+ - name: Set up Python
23
+ run: uv python install 3.12
24
+ - name: Build sdist + wheel
25
+ run: uv build
26
+ - name: Upload dist artifact
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: python-package-distributions
30
+ path: dist/
31
+
32
+ publish:
33
+ name: Publish to PyPI (trusted publishing)
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ # Must match the environment name configured on the PyPI pending
37
+ # publisher. If you skipped the environment field there, remove
38
+ # this line.
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/p/langchain-codex-plus
42
+ permissions:
43
+ # Required for trusted publishing — PyPI verifies the GH OIDC
44
+ # token, which only this job is allowed to mint.
45
+ id-token: write
46
+ steps:
47
+ - name: Download dist artifact
48
+ uses: actions/download-artifact@v4
49
+ with:
50
+ name: python-package-distributions
51
+ path: dist/
52
+ - name: Publish to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ dist/
9
+ build/
10
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Carreira
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,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-codex-plus
3
+ Version: 0.0.1
4
+ Summary: LangChain ChatModel for OpenAI Codex Plus / Pro (ChatGPT-account subscription protocol, not api.openai.com).
5
+ Project-URL: Homepage, https://github.com/jasoncarreira/langchain-codex-plus
6
+ Project-URL: Issues, https://github.com/jasoncarreira/langchain-codex-plus/issues
7
+ Author: Jason Carreira
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: chatgpt,codex,langchain,oauth,openai,subscription
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: langchain-core>=0.3.0
21
+ Requires-Dist: pydantic>=2.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.10; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # langchain-codex-plus
30
+
31
+ LangChain `ChatModel` for OpenAI's **ChatGPT-account-backed Codex** —
32
+ the subscription protocol (Codex Plus / Pro plans), NOT the public
33
+ `api.openai.com` API.
34
+
35
+ ## What this is
36
+
37
+ OpenAI's Codex CLI signs you in with a **ChatGPT account** (browser
38
+ OAuth) and routes traffic through:
39
+
40
+ ```
41
+ https://chatgpt.com/backend-api/codex/responses
42
+ ```
43
+
44
+ — a different protocol than `api.openai.com/v1/chat/completions`. It
45
+ has its own request shape, its own auth (OAuth bearer instead of
46
+ `OPENAI_API_KEY`), and exposes quota-window utilization via response
47
+ headers (`x-codex-primary-*`, `x-codex-secondary-*`).
48
+
49
+ This package wraps that protocol in a LangChain `BaseChatModel` so
50
+ you can use a Codex Plus subscription from any LangChain-built agent
51
+ the way you'd use `ChatOpenAI` or `ChatAnthropic`.
52
+
53
+ ## What this is NOT
54
+
55
+ * Not for `api.openai.com` traffic — use `langchain-openai` for that.
56
+ * Not for Claude — use `langchain-anthropic` or `langchain-claude-code`.
57
+ * Not a re-implementation of the Codex CLI's agent loop — just the
58
+ chat-model surface.
59
+
60
+ ## Status
61
+
62
+ Alpha. v0.0.1. 134 tests + a gated real-account smoke test pass.
63
+
64
+ ## Auth
65
+
66
+ Run `codex login` once. The CLI writes OAuth credentials to
67
+ `$CODEX_HOME/auth.json` (defaults to `~/.codex/auth.json`). This
68
+ package reads the file directly — there's no separate setup.
69
+
70
+ ```python
71
+ from langchain_codex_plus import ChatCodexPlus
72
+
73
+ llm = ChatCodexPlus(model="gpt-5.4")
74
+ llm.invoke("Say ok.")
75
+ ```
76
+
77
+ When the access token expires (~1h TTL), a 401 response triggers an
78
+ automatic refresh against `auth.openai.com/oauth/token`, then the
79
+ call retries once. Permanent refresh failures (expired / revoked /
80
+ already-used refresh token) raise `CodexAuthRefreshError` with
81
+ `permanent=True` — the operator must re-run `codex login`. Opt out
82
+ with `auto_refresh=False` if you want to handle 401s yourself.
83
+
84
+ ## Tool calling
85
+
86
+ Use `bind_tools` exactly like `ChatOpenAI.bind_tools`:
87
+
88
+ ```python
89
+ from langchain_core.tools import tool
90
+ from langchain_codex_plus import ChatCodexPlus
91
+
92
+ @tool
93
+ def get_weather(location: str) -> str:
94
+ """Look up the weather."""
95
+ return f"sunny in {location}"
96
+
97
+ llm = ChatCodexPlus().bind_tools([get_weather])
98
+ msg = llm.invoke("Weather in Boston?")
99
+ # msg.tool_calls → [{"name": "get_weather", "args": {"location": "Boston"}, "id": "call_..."}]
100
+ ```
101
+
102
+ Send tool results back via `ToolMessage(content=..., tool_call_id=...)` —
103
+ the protocol layer serializes them as Codex `function_call_output`
104
+ entries.
105
+
106
+ ## Multimodal
107
+
108
+ `HumanMessage` content can be a list mixing text and image blocks:
109
+
110
+ ```python
111
+ from langchain_core.messages import HumanMessage
112
+
113
+ llm.invoke([HumanMessage(content=[
114
+ {"type": "text", "text": "What's in this image?"},
115
+ {"type": "image_url", "image_url": "https://example.com/cat.png"},
116
+ ])])
117
+ ```
118
+
119
+ Both LangChain image-block conventions are accepted (`{type: image_url,
120
+ image_url: {url, detail}}` and `{type: image, source_type: "url"|"base64",
121
+ ...}`). Base64 data is auto-encoded as a `data:` URL.
122
+
123
+ ## Stop sequences
124
+
125
+ Codex's `/codex/responses` rejects the `stop` parameter, so we match
126
+ client-side. Streaming uses a buffered matcher so stop sequences
127
+ split across SSE chunks (the common tokenization case) still
128
+ truncate cleanly:
129
+
130
+ ```python
131
+ llm.invoke("Count from 1 to 100", stop=["50"])
132
+ # → "1, 2, 3, ... 49, "
133
+ ```
134
+
135
+ ## Rate-limit hook
136
+
137
+ Every successful `/codex/responses` response carries quota headers
138
+ (`x-codex-primary-*` / `-secondary-*`). The chat model parses these
139
+ into a `CodexRateLimits` dataclass and (optionally) calls a callback
140
+ so your monitoring layer can persist them:
141
+
142
+ ```python
143
+ from langchain_codex_plus import ChatCodexPlus, CodexRateLimits
144
+
145
+ def on_rate_limits(rl: CodexRateLimits) -> None:
146
+ print(f"5h: {rl.primary.used_percent}% / 7d: {rl.secondary.used_percent}%")
147
+
148
+ llm = ChatCodexPlus(model="gpt-5.4", rate_limit_callback=on_rate_limits)
149
+ ```
150
+
151
+ Callback exceptions are caught and logged — they never break the
152
+ response path.
153
+
154
+ ## License
155
+
156
+ MIT. See `LICENSE`.
@@ -0,0 +1,128 @@
1
+ # langchain-codex-plus
2
+
3
+ LangChain `ChatModel` for OpenAI's **ChatGPT-account-backed Codex** —
4
+ the subscription protocol (Codex Plus / Pro plans), NOT the public
5
+ `api.openai.com` API.
6
+
7
+ ## What this is
8
+
9
+ OpenAI's Codex CLI signs you in with a **ChatGPT account** (browser
10
+ OAuth) and routes traffic through:
11
+
12
+ ```
13
+ https://chatgpt.com/backend-api/codex/responses
14
+ ```
15
+
16
+ — a different protocol than `api.openai.com/v1/chat/completions`. It
17
+ has its own request shape, its own auth (OAuth bearer instead of
18
+ `OPENAI_API_KEY`), and exposes quota-window utilization via response
19
+ headers (`x-codex-primary-*`, `x-codex-secondary-*`).
20
+
21
+ This package wraps that protocol in a LangChain `BaseChatModel` so
22
+ you can use a Codex Plus subscription from any LangChain-built agent
23
+ the way you'd use `ChatOpenAI` or `ChatAnthropic`.
24
+
25
+ ## What this is NOT
26
+
27
+ * Not for `api.openai.com` traffic — use `langchain-openai` for that.
28
+ * Not for Claude — use `langchain-anthropic` or `langchain-claude-code`.
29
+ * Not a re-implementation of the Codex CLI's agent loop — just the
30
+ chat-model surface.
31
+
32
+ ## Status
33
+
34
+ Alpha. v0.0.1. 134 tests + a gated real-account smoke test pass.
35
+
36
+ ## Auth
37
+
38
+ Run `codex login` once. The CLI writes OAuth credentials to
39
+ `$CODEX_HOME/auth.json` (defaults to `~/.codex/auth.json`). This
40
+ package reads the file directly — there's no separate setup.
41
+
42
+ ```python
43
+ from langchain_codex_plus import ChatCodexPlus
44
+
45
+ llm = ChatCodexPlus(model="gpt-5.4")
46
+ llm.invoke("Say ok.")
47
+ ```
48
+
49
+ When the access token expires (~1h TTL), a 401 response triggers an
50
+ automatic refresh against `auth.openai.com/oauth/token`, then the
51
+ call retries once. Permanent refresh failures (expired / revoked /
52
+ already-used refresh token) raise `CodexAuthRefreshError` with
53
+ `permanent=True` — the operator must re-run `codex login`. Opt out
54
+ with `auto_refresh=False` if you want to handle 401s yourself.
55
+
56
+ ## Tool calling
57
+
58
+ Use `bind_tools` exactly like `ChatOpenAI.bind_tools`:
59
+
60
+ ```python
61
+ from langchain_core.tools import tool
62
+ from langchain_codex_plus import ChatCodexPlus
63
+
64
+ @tool
65
+ def get_weather(location: str) -> str:
66
+ """Look up the weather."""
67
+ return f"sunny in {location}"
68
+
69
+ llm = ChatCodexPlus().bind_tools([get_weather])
70
+ msg = llm.invoke("Weather in Boston?")
71
+ # msg.tool_calls → [{"name": "get_weather", "args": {"location": "Boston"}, "id": "call_..."}]
72
+ ```
73
+
74
+ Send tool results back via `ToolMessage(content=..., tool_call_id=...)` —
75
+ the protocol layer serializes them as Codex `function_call_output`
76
+ entries.
77
+
78
+ ## Multimodal
79
+
80
+ `HumanMessage` content can be a list mixing text and image blocks:
81
+
82
+ ```python
83
+ from langchain_core.messages import HumanMessage
84
+
85
+ llm.invoke([HumanMessage(content=[
86
+ {"type": "text", "text": "What's in this image?"},
87
+ {"type": "image_url", "image_url": "https://example.com/cat.png"},
88
+ ])])
89
+ ```
90
+
91
+ Both LangChain image-block conventions are accepted (`{type: image_url,
92
+ image_url: {url, detail}}` and `{type: image, source_type: "url"|"base64",
93
+ ...}`). Base64 data is auto-encoded as a `data:` URL.
94
+
95
+ ## Stop sequences
96
+
97
+ Codex's `/codex/responses` rejects the `stop` parameter, so we match
98
+ client-side. Streaming uses a buffered matcher so stop sequences
99
+ split across SSE chunks (the common tokenization case) still
100
+ truncate cleanly:
101
+
102
+ ```python
103
+ llm.invoke("Count from 1 to 100", stop=["50"])
104
+ # → "1, 2, 3, ... 49, "
105
+ ```
106
+
107
+ ## Rate-limit hook
108
+
109
+ Every successful `/codex/responses` response carries quota headers
110
+ (`x-codex-primary-*` / `-secondary-*`). The chat model parses these
111
+ into a `CodexRateLimits` dataclass and (optionally) calls a callback
112
+ so your monitoring layer can persist them:
113
+
114
+ ```python
115
+ from langchain_codex_plus import ChatCodexPlus, CodexRateLimits
116
+
117
+ def on_rate_limits(rl: CodexRateLimits) -> None:
118
+ print(f"5h: {rl.primary.used_percent}% / 7d: {rl.secondary.used_percent}%")
119
+
120
+ llm = ChatCodexPlus(model="gpt-5.4", rate_limit_callback=on_rate_limits)
121
+ ```
122
+
123
+ Callback exceptions are caught and logged — they never break the
124
+ response path.
125
+
126
+ ## License
127
+
128
+ MIT. See `LICENSE`.
@@ -0,0 +1,77 @@
1
+ """langchain-codex-plus: LangChain ChatModel for OpenAI Codex Plus.
2
+
3
+ Wraps OpenAI's ChatGPT-account-backed Codex subscription protocol
4
+ (``chatgpt.com/backend-api/codex/responses``) as a LangChain
5
+ ``BaseChatModel``. See README for what this is and isn't.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from langchain_codex_plus.codex_auth import (
10
+ CODEX_API_BASE,
11
+ CODEX_OAUTH_CLIENT_ID,
12
+ REFRESH_TOKEN_URL,
13
+ CodexAuth,
14
+ CodexAuthInvalidError,
15
+ CodexAuthNotFoundError,
16
+ CodexAuthRefreshError,
17
+ arefresh_codex_auth,
18
+ auth_file_path,
19
+ codex_home,
20
+ is_likely_expired,
21
+ load_codex_auth,
22
+ refresh_codex_auth,
23
+ )
24
+ from langchain_codex_plus.codex_chat_model import ChatCodexPlus
25
+ from langchain_codex_plus.codex_protocol import (
26
+ CodexCompletion,
27
+ CodexResponseError,
28
+ CodexToolCall,
29
+ SseEvent,
30
+ ToolChoice,
31
+ build_request_body,
32
+ consume_events,
33
+ parse_error_body,
34
+ parse_sse_stream,
35
+ )
36
+ from langchain_codex_plus.rate_limits import (
37
+ CodexCredits,
38
+ CodexQuotaWindow,
39
+ CodexRateLimits,
40
+ parse_codex_rate_limits,
41
+ )
42
+
43
+ __version__ = "0.0.1"
44
+
45
+ __all__ = [
46
+ # codex_auth
47
+ "CODEX_API_BASE",
48
+ "CODEX_OAUTH_CLIENT_ID",
49
+ "REFRESH_TOKEN_URL",
50
+ "CodexAuth",
51
+ "CodexAuthInvalidError",
52
+ "CodexAuthNotFoundError",
53
+ "CodexAuthRefreshError",
54
+ "arefresh_codex_auth",
55
+ "auth_file_path",
56
+ "codex_home",
57
+ "is_likely_expired",
58
+ "load_codex_auth",
59
+ "refresh_codex_auth",
60
+ # codex_protocol
61
+ "CodexCompletion",
62
+ "CodexResponseError",
63
+ "CodexToolCall",
64
+ "SseEvent",
65
+ "ToolChoice",
66
+ "build_request_body",
67
+ "consume_events",
68
+ "parse_error_body",
69
+ "parse_sse_stream",
70
+ # rate_limits
71
+ "CodexCredits",
72
+ "CodexQuotaWindow",
73
+ "CodexRateLimits",
74
+ "parse_codex_rate_limits",
75
+ # chat model
76
+ "ChatCodexPlus",
77
+ ]