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.
Files changed (23) hide show
  1. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/PKG-INFO +1 -1
  2. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_chat_model.py +16 -2
  3. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_protocol.py +16 -0
  4. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/pyproject.toml +1 -1
  5. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_chat_model.py +44 -0
  6. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/uv.lock +1 -1
  7. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/.github/workflows/publish.yml +0 -0
  8. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/.gitignore +0 -0
  9. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/LICENSE +0 -0
  10. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/README.md +0 -0
  11. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/__init__.py +0 -0
  12. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/codex_auth.py +0 -0
  13. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/py.typed +0 -0
  14. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/langchain_codex_plus/rate_limits.py +0 -0
  15. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/__init__.py +0 -0
  16. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/conftest.py +0 -0
  17. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_auth.py +0 -0
  18. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_codex_protocol.py +0 -0
  19. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_multimodal.py +0 -0
  20. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_oauth_refresh.py +0 -0
  21. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_rate_limits.py +0 -0
  22. {langchain_codex_plus-0.0.2 → langchain_codex_plus-0.0.3}/tests/test_stop_sequences.py +0 -0
  23. {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.2
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
@@ -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
- # Re-raise with status code prepended so callers can pattern-
556
- # match on it.
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(
@@ -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.2"
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."""
@@ -253,7 +253,7 @@ wheels = [
253
253
 
254
254
  [[package]]
255
255
  name = "langchain-codex-plus"
256
- version = "0.0.1"
256
+ version = "0.0.3"
257
257
  source = { editable = "." }
258
258
  dependencies = [
259
259
  { name = "httpx" },