langchain-codex-plus 0.0.2__tar.gz → 0.0.3__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.
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/PKG-INFO +1 -1
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_chat_model.py +16 -2
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_protocol.py +16 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/pyproject.toml +1 -1
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_chat_model.py +44 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/uv.lock +1 -1
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/.github/workflows/publish.yml +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/.gitignore +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/LICENSE +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/README.md +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/__init__.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_auth.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/py.typed +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/rate_limits.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/__init__.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/conftest.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_auth.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_protocol.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_multimodal.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_oauth_refresh.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_rate_limits.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_stop_sequences.py +0 -0
- {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_tool_calling.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-codex-plus
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: LangChain ChatModel for OpenAI Codex Plus / Pro (ChatGPT-account subscription protocol, not api.openai.com).
|
|
5
5
|
Project-URL: Homepage, https://github.com/jasoncarreira/langchain-codex-plus
|
|
6
6
|
Project-URL: Issues, https://github.com/jasoncarreira/langchain-codex-plus/issues
|
{langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_chat_model.py
RENAMED
|
@@ -552,13 +552,21 @@ class ChatCodexPlus(BaseChatModel):
|
|
|
552
552
|
return
|
|
553
553
|
body_bytes = response.read()
|
|
554
554
|
err = parse_error_body(body_bytes)
|
|
555
|
-
#
|
|
556
|
-
#
|
|
555
|
+
# Surface the rate-limit headers on errors too (esp. 429): fire
|
|
556
|
+
# the callback so the usage snapshot updates even on a refusal,
|
|
557
|
+
# and attach status_code + headers + parsed limits to the
|
|
558
|
+
# exception. Previously these were discarded — this method ran
|
|
559
|
+
# BEFORE _fire_rate_limit_callback on the success path, so a 429
|
|
560
|
+
# gave callers no reset timestamp to pause on.
|
|
561
|
+
rate_limits = self._fire_rate_limit_callback(response.headers)
|
|
557
562
|
raise CodexResponseError(
|
|
558
563
|
message=f"HTTP {response.status_code}: {err.message}",
|
|
559
564
|
code=err.code,
|
|
560
565
|
type=err.type,
|
|
561
566
|
raw=err.raw,
|
|
567
|
+
status_code=response.status_code,
|
|
568
|
+
headers=dict(response.headers),
|
|
569
|
+
rate_limits=rate_limits,
|
|
562
570
|
)
|
|
563
571
|
|
|
564
572
|
def _consume_sync(
|
|
@@ -686,11 +694,17 @@ class ChatCodexPlus(BaseChatModel):
|
|
|
686
694
|
return
|
|
687
695
|
body_bytes = await response.aread()
|
|
688
696
|
err = parse_error_body(body_bytes)
|
|
697
|
+
# See _raise_for_http_error: surface rate-limit headers on errors
|
|
698
|
+
# so a 429 carries its reset timestamp instead of being opaque.
|
|
699
|
+
rate_limits = self._fire_rate_limit_callback(response.headers)
|
|
689
700
|
raise CodexResponseError(
|
|
690
701
|
message=f"HTTP {response.status_code}: {err.message}",
|
|
691
702
|
code=err.code,
|
|
692
703
|
type=err.type,
|
|
693
704
|
raw=err.raw,
|
|
705
|
+
status_code=response.status_code,
|
|
706
|
+
headers=dict(response.headers),
|
|
707
|
+
rate_limits=rate_limits,
|
|
694
708
|
)
|
|
695
709
|
|
|
696
710
|
async def _consume_async(
|
{langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_protocol.py
RENAMED
|
@@ -746,15 +746,31 @@ class CodexResponseError(RuntimeError):
|
|
|
746
746
|
code: str | None = None,
|
|
747
747
|
type: str | None = None,
|
|
748
748
|
raw: Any = None,
|
|
749
|
+
status_code: int | None = None,
|
|
750
|
+
headers: dict[str, str] | None = None,
|
|
751
|
+
rate_limits: Any = None,
|
|
749
752
|
) -> None:
|
|
750
753
|
super().__init__(message)
|
|
751
754
|
self.message = message
|
|
752
755
|
self.code = code
|
|
753
756
|
self.type = type
|
|
754
757
|
self.raw = raw
|
|
758
|
+
# HTTP envelope context — populated for non-2xx responses so
|
|
759
|
+
# callers can pattern-match on the status and, crucially, read
|
|
760
|
+
# the rate-limit headers on a 429 (the reset timestamp lives in
|
|
761
|
+
# ``x-codex-primary-reset-at`` / ``-reset-after-seconds``). These
|
|
762
|
+
# used to be discarded; a caller hitting a 429 had no way to know
|
|
763
|
+
# when the window would roll over. ``rate_limits`` is the parsed
|
|
764
|
+
# :class:`~langchain_codex_plus.rate_limits.CodexRateLimits` (or
|
|
765
|
+
# ``None`` when the response carried no ``x-codex-*`` headers).
|
|
766
|
+
self.status_code = status_code
|
|
767
|
+
self.headers = headers
|
|
768
|
+
self.rate_limits = rate_limits
|
|
755
769
|
|
|
756
770
|
def __repr__(self) -> str:
|
|
757
771
|
bits = [f"message={self.message!r}"]
|
|
772
|
+
if self.status_code is not None:
|
|
773
|
+
bits.append(f"status_code={self.status_code!r}")
|
|
758
774
|
if self.code:
|
|
759
775
|
bits.append(f"code={self.code!r}")
|
|
760
776
|
if self.type:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langchain-codex-plus"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.3"
|
|
8
8
|
description = "LangChain ChatModel for OpenAI Codex Plus / Pro (ChatGPT-account subscription protocol, not api.openai.com)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -290,6 +290,50 @@ def test_generate_raises_on_oai_error_shape(auth_file):
|
|
|
290
290
|
assert exc.value.type == "invalid_request_error"
|
|
291
291
|
|
|
292
292
|
|
|
293
|
+
def test_generate_429_surfaces_rate_limit_headers(auth_file):
|
|
294
|
+
"""A 429 must surface status_code + headers + parsed rate_limits on
|
|
295
|
+
the exception (so callers can pause until the real window reset) AND
|
|
296
|
+
fire the rate-limit callback even on the refusal. Previously the
|
|
297
|
+
headers were discarded — _raise_for_http_error ran before the
|
|
298
|
+
callback on the success path, so a 429 was opaque."""
|
|
299
|
+
transport = _CaptureTransport(
|
|
300
|
+
status_code=429,
|
|
301
|
+
body=b'{"detail":"Rate limit exceeded"}',
|
|
302
|
+
headers=_real_rl_headers(),
|
|
303
|
+
)
|
|
304
|
+
seen: list[CodexRateLimits] = []
|
|
305
|
+
llm = _make_llm(
|
|
306
|
+
auth_file, transport=transport, rate_limit_callback=seen.append
|
|
307
|
+
)
|
|
308
|
+
with pytest.raises(CodexResponseError) as exc:
|
|
309
|
+
llm.invoke([HumanMessage("hi")])
|
|
310
|
+
err = exc.value
|
|
311
|
+
assert err.status_code == 429
|
|
312
|
+
assert err.headers is not None
|
|
313
|
+
assert err.headers.get("x-codex-primary-reset-at") == "1779343790"
|
|
314
|
+
assert err.rate_limits is not None
|
|
315
|
+
assert err.rate_limits.primary is not None
|
|
316
|
+
assert err.rate_limits.primary.reset_at == 1779343790
|
|
317
|
+
# The callback fired on the 429 (usage snapshot updates on refusals).
|
|
318
|
+
assert len(seen) == 1
|
|
319
|
+
assert seen[0].primary.reset_at == 1779343790
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_generate_429_without_codex_headers_still_sets_status(auth_file):
|
|
323
|
+
"""A 429 with no ``x-codex-*`` headers still carries status_code (and
|
|
324
|
+
rate_limits is None) — callers fall back to a backoff."""
|
|
325
|
+
transport = _CaptureTransport(
|
|
326
|
+
status_code=429,
|
|
327
|
+
body=b'{"detail":"Rate limit exceeded"}',
|
|
328
|
+
headers={"Content-Type": "application/json"},
|
|
329
|
+
)
|
|
330
|
+
llm = _make_llm(auth_file, transport=transport)
|
|
331
|
+
with pytest.raises(CodexResponseError) as exc:
|
|
332
|
+
llm.invoke([HumanMessage("hi")])
|
|
333
|
+
assert exc.value.status_code == 429
|
|
334
|
+
assert exc.value.rate_limits is None
|
|
335
|
+
|
|
336
|
+
|
|
293
337
|
def test_generate_stop_argument_is_ignored_silently(auth_file, caplog):
|
|
294
338
|
"""Codex Responses API doesn't expose stop sequences. We log at
|
|
295
339
|
DEBUG and proceed — silent drop in production logs."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/rate_limits.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|