oauth-codex 2.2.0__tar.gz → 2.3.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.
Files changed (66) hide show
  1. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/PKG-INFO +23 -9
  2. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/README.md +22 -8
  3. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/pyproject.toml +1 -1
  4. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/__init__.py +2 -0
  5. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_client.py +249 -77
  6. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_exceptions.py +93 -0
  7. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_version.py +1 -1
  8. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/core_types.py +17 -0
  9. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/PKG-INFO +23 -9
  10. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/SOURCES.txt +2 -0
  11. oauth_codex-2.3.1/tests/test_client_authentication.py +46 -0
  12. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/tests/test_generate_async.py +58 -7
  13. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/tests/test_generate_sync.py +64 -25
  14. oauth_codex-2.3.1/tests/test_public_api_docstrings.py +48 -0
  15. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/tests/test_public_surface.py +2 -0
  16. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/setup.cfg +0 -0
  17. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_base_client.py +0 -0
  18. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_engine.py +0 -0
  19. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_models.py +0 -0
  20. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_module_client.py +0 -0
  21. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_resource.py +0 -0
  22. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/_types.py +0 -0
  23. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/__init__.py +0 -0
  24. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/config.py +0 -0
  25. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/pkce.py +0 -0
  26. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/store.py +0 -0
  27. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/auth/token_manager.py +0 -0
  28. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/compat_store.py +0 -0
  29. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/errors.py +0 -0
  30. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/py.typed +0 -0
  31. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/__init__.py +0 -0
  32. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/_wrappers.py +0 -0
  33. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/files.py +0 -0
  34. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/models.py +0 -0
  35. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/__init__.py +0 -0
  36. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/_helpers.py +0 -0
  37. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
  38. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/responses/responses.py +0 -0
  39. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
  40. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
  41. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/files.py +0 -0
  42. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
  43. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/store.py +0 -0
  44. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/tooling.py +0 -0
  45. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/__init__.py +0 -0
  46. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/file_deleted.py +0 -0
  47. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/file_object.py +0 -0
  48. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/__init__.py +0 -0
  49. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
  50. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/response.py +0 -0
  51. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
  52. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/__init__.py +0 -0
  53. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
  54. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/shared/usage.py +0 -0
  55. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
  56. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
  57. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
  58. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
  59. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
  60. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
  61. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex/version.py +0 -0
  62. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
  63. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/requires.txt +0 -0
  64. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/src/oauth_codex.egg-info/top_level.txt +0 -0
  65. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/tests/test_engine_stream_and_continuity.py +0 -0
  66. {oauth_codex-2.2.0 → oauth_codex-2.3.1}/tests/test_tooling_strict_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: Codex OAuth-based Python SDK with a single Client and generate-first API
5
5
  Author: Codex
6
6
  Requires-Python: >=3.11
@@ -37,16 +37,30 @@ pip install oauth-codex
37
37
  from oauth_codex import Client
38
38
 
39
39
  client = Client()
40
- text = client.generate("hello")
40
+ text = client.generate([{"role": "user", "content": "hello"}])
41
41
  print(text)
42
42
  ```
43
43
 
44
+ Authenticate immediately during client construction:
45
+
46
+ ```python
47
+ client = Client(authenticate_on_init=True)
48
+ ```
49
+
44
50
  ## Image Input
45
51
 
46
52
  ```python
47
53
  text = client.generate(
48
- "Describe this image",
49
- images=["https://example.com/cat.png", "./local-photo.jpg"],
54
+ [
55
+ {
56
+ "role": "user",
57
+ "content": [
58
+ {"type": "input_text", "text": "Describe this image"},
59
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
60
+ {"type": "input_image", "image_url": "data:image/jpeg;base64,..."},
61
+ ],
62
+ }
63
+ ],
50
64
  )
51
65
  print(text)
52
66
  ```
@@ -58,7 +72,7 @@ def add(a: int, b: int) -> dict:
58
72
  return {"sum": a + b}
59
73
 
60
74
  text = client.generate(
61
- "Calculate 2+3",
75
+ [{"role": "user", "content": "Calculate 2+3"}],
62
76
  tools=[add],
63
77
  )
64
78
  print(text)
@@ -78,7 +92,7 @@ def tool(input: ToolInput) -> str:
78
92
  return f"Tool received query: {input.query}"
79
93
 
80
94
 
81
- text = client.generate("Use the tool", tools=[tool])
95
+ text = client.generate([{"role": "user", "content": "Use the tool"}], tools=[tool])
82
96
  print(text)
83
97
  ```
84
98
 
@@ -100,7 +114,7 @@ class Summary(BaseModel):
100
114
 
101
115
  client = Client()
102
116
  out = client.generate(
103
- "Return JSON with title and score",
117
+ [{"role": "user", "content": "Return JSON with title and score"}],
104
118
  output_schema=Summary,
105
119
  )
106
120
  print(out) # {"title": "...", "score": 1}
@@ -110,7 +124,7 @@ You can also pass a raw JSON schema object.
110
124
 
111
125
  ```python
112
126
  out = client.generate(
113
- "Return {\"ok\": true}",
127
+ [{"role": "user", "content": "Return {\"ok\": true}"}],
114
128
  output_schema={
115
129
  "type": "object",
116
130
  "properties": {"ok": {"type": "boolean"}},
@@ -131,7 +145,7 @@ from oauth_codex import Client
131
145
 
132
146
  async def main() -> None:
133
147
  client = Client()
134
- text = await client.agenerate("hello async")
148
+ text = await client.agenerate([{"role": "user", "content": "hello async"}])
135
149
  print(text)
136
150
 
137
151
 
@@ -23,16 +23,30 @@ pip install oauth-codex
23
23
  from oauth_codex import Client
24
24
 
25
25
  client = Client()
26
- text = client.generate("hello")
26
+ text = client.generate([{"role": "user", "content": "hello"}])
27
27
  print(text)
28
28
  ```
29
29
 
30
+ Authenticate immediately during client construction:
31
+
32
+ ```python
33
+ client = Client(authenticate_on_init=True)
34
+ ```
35
+
30
36
  ## Image Input
31
37
 
32
38
  ```python
33
39
  text = client.generate(
34
- "Describe this image",
35
- images=["https://example.com/cat.png", "./local-photo.jpg"],
40
+ [
41
+ {
42
+ "role": "user",
43
+ "content": [
44
+ {"type": "input_text", "text": "Describe this image"},
45
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
46
+ {"type": "input_image", "image_url": "data:image/jpeg;base64,..."},
47
+ ],
48
+ }
49
+ ],
36
50
  )
37
51
  print(text)
38
52
  ```
@@ -44,7 +58,7 @@ def add(a: int, b: int) -> dict:
44
58
  return {"sum": a + b}
45
59
 
46
60
  text = client.generate(
47
- "Calculate 2+3",
61
+ [{"role": "user", "content": "Calculate 2+3"}],
48
62
  tools=[add],
49
63
  )
50
64
  print(text)
@@ -64,7 +78,7 @@ def tool(input: ToolInput) -> str:
64
78
  return f"Tool received query: {input.query}"
65
79
 
66
80
 
67
- text = client.generate("Use the tool", tools=[tool])
81
+ text = client.generate([{"role": "user", "content": "Use the tool"}], tools=[tool])
68
82
  print(text)
69
83
  ```
70
84
 
@@ -86,7 +100,7 @@ class Summary(BaseModel):
86
100
 
87
101
  client = Client()
88
102
  out = client.generate(
89
- "Return JSON with title and score",
103
+ [{"role": "user", "content": "Return JSON with title and score"}],
90
104
  output_schema=Summary,
91
105
  )
92
106
  print(out) # {"title": "...", "score": 1}
@@ -96,7 +110,7 @@ You can also pass a raw JSON schema object.
96
110
 
97
111
  ```python
98
112
  out = client.generate(
99
- "Return {\"ok\": true}",
113
+ [{"role": "user", "content": "Return {\"ok\": true}"}],
100
114
  output_schema={
101
115
  "type": "object",
102
116
  "properties": {"ok": {"type": "boolean"}},
@@ -117,7 +131,7 @@ from oauth_codex import Client
117
131
 
118
132
  async def main() -> None:
119
133
  client = Client()
120
- text = await client.agenerate("hello async")
134
+ text = await client.agenerate([{"role": "user", "content": "hello async"}])
121
135
  print(text)
122
136
 
123
137
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oauth-codex"
7
- version = "2.2.0"
7
+ version = "2.3.1"
8
8
  description = "Codex OAuth-based Python SDK with a single Client and generate-first API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -34,6 +34,7 @@ from ._exceptions import (
34
34
  UnprocessableEntityError,
35
35
  )
36
36
  from ._version import __title__, __version__
37
+ from .core_types import listMessage
37
38
 
38
39
  __all__ = [
39
40
  "types",
@@ -41,6 +42,7 @@ __all__ = [
41
42
  "__version__",
42
43
  "Client",
43
44
  "OAuthCodexClient",
45
+ "listMessage",
44
46
  "OAuthCodexError",
45
47
  "OpenAIError",
46
48
  "APIError",
@@ -1,11 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
3
  import asyncio
5
4
  import inspect
6
5
  import json
7
- import mimetypes
8
- from pathlib import Path
9
6
  from typing import Any, AsyncIterator, Callable, Iterator, get_type_hints
10
7
 
11
8
  from pydantic import BaseModel
@@ -15,11 +12,10 @@ from ._engine import OAuthCodexClient as _EngineClient
15
12
  from .auth.config import OAuthConfig
16
13
  from .core_types import (
17
14
  GenerateResult,
18
- ImageInput,
19
- Message,
20
15
  ReasoningEffort,
21
16
  ToolCall,
22
17
  ToolResult,
18
+ listMessage,
23
19
  )
24
20
  from .store import FallbackTokenStore
25
21
  from .tooling import build_strict_response_format, callable_to_tool_schema, normalize_tool_output
@@ -30,6 +26,29 @@ StructuredOutputSchema = type[BaseModel] | dict[str, Any]
30
26
 
31
27
 
32
28
  class OAuthCodexClient(SyncAPIClient):
29
+ """Public single-entry client for oauth-codex.
30
+
31
+ This client wraps the OAuth-backed responses engine and exposes a
32
+ generate-first workflow:
33
+
34
+ - `generate` and `agenerate` return final text, or a validated JSON object
35
+ when `output_schema` is provided.
36
+ - `stream` and `astream` yield text deltas while still supporting
37
+ automatic tool-call continuation rounds.
38
+
39
+ Public attributes:
40
+ default_model: Model used when `model` is omitted. Defaults to
41
+ `"gpt-5.3-codex"`.
42
+ max_tool_rounds: Maximum number of automatic tool continuation rounds
43
+ before raising `RuntimeError`.
44
+
45
+ Example:
46
+ from oauth_codex import Client
47
+
48
+ client = Client(authenticate_on_init=True)
49
+ text = client.generate([{"role": "user", "content": "hello"}])
50
+ """
51
+
33
52
  def __init__(
34
53
  self,
35
54
  *,
@@ -44,7 +63,28 @@ class OAuthCodexClient(SyncAPIClient):
44
63
  on_request_end: Any | None = None,
45
64
  on_auth_refresh: Any | None = None,
46
65
  on_error: Any | None = None,
66
+ authenticate_on_init: bool = False,
47
67
  ) -> None:
68
+ """Create a `Client` instance.
69
+
70
+ Args:
71
+ oauth_config: Optional OAuth endpoint/client configuration.
72
+ token_store: Token persistence backend. If omitted, a fallback
73
+ keyring/file store is used.
74
+ base_url: Base URL for Codex backend requests.
75
+ chatgpt_base_url: Legacy alias for `base_url`. `base_url` takes
76
+ precedence when both are provided.
77
+ timeout: Request timeout in seconds.
78
+ max_retries: Number of retry attempts for retryable failures.
79
+ compat_storage_dir: Optional local compatibility storage path.
80
+ on_request_start: Optional callback invoked when a request starts.
81
+ on_request_end: Optional callback invoked when a request ends.
82
+ on_auth_refresh: Optional callback invoked after token refresh.
83
+ on_error: Optional callback invoked when request/auth errors occur.
84
+ authenticate_on_init: When `True`, perform authentication during
85
+ client construction. If no stored token exists, interactive
86
+ login is started immediately.
87
+ """
48
88
  super().__init__()
49
89
 
50
90
  resolved_base_url = (
@@ -67,30 +107,82 @@ class OAuthCodexClient(SyncAPIClient):
67
107
  )
68
108
  self.default_model = DEFAULT_MODEL
69
109
  self.max_tool_rounds = DEFAULT_MAX_TOOL_ROUNDS
110
+ if authenticate_on_init:
111
+ self.authenticate()
70
112
 
71
113
  def is_authenticated(self) -> bool:
114
+ """Return whether usable OAuth tokens are currently available.
115
+
116
+ Returns:
117
+ `True` when a token is available, otherwise `False`.
118
+ """
72
119
  return self._engine.is_authenticated()
73
120
 
74
121
  def is_expired(self, *, leeway_seconds: int = 60) -> bool:
122
+ """Return whether the current token is expired or near expiry.
123
+
124
+ Args:
125
+ leeway_seconds: Safety margin in seconds added before expiry check.
126
+
127
+ Returns:
128
+ `True` if the token is expired within the given leeway.
129
+ """
75
130
  return self._engine.is_expired(leeway_seconds=leeway_seconds)
76
131
 
77
132
  def refresh_if_needed(self, *, force: bool = False) -> bool:
133
+ """Refresh tokens when needed.
134
+
135
+ Args:
136
+ force: Refresh even when token is not currently expired.
137
+
138
+ Returns:
139
+ `True` if refresh happened and succeeded, otherwise `False`.
140
+ """
78
141
  return self._engine.refresh_if_needed(force=force)
79
142
 
80
143
  async def arefresh_if_needed(self, *, force: bool = False) -> bool:
144
+ """Async version of `refresh_if_needed`.
145
+
146
+ Args:
147
+ force: Refresh even when token is not currently expired.
148
+
149
+ Returns:
150
+ `True` if refresh happened and succeeded, otherwise `False`.
151
+ """
81
152
  return await self._engine.arefresh_if_needed(force=force)
82
153
 
154
+ def authenticate(self) -> None:
155
+ """Ensure usable OAuth credentials are available now.
156
+
157
+ This loads stored credentials, starts interactive login when missing,
158
+ and refreshes tokens when expired.
159
+ """
160
+ self._engine._ensure_authenticated_sync()
161
+
83
162
  def login(self) -> None:
163
+ """Run interactive OAuth login flow in the current thread.
164
+
165
+ Raises:
166
+ OAuthCallbackParseError: If callback URL parsing fails.
167
+ OAuthStateMismatchError: If OAuth state verification fails.
168
+ TokenExchangeError: If code-to-token exchange fails.
169
+ """
84
170
  self._engine.login()
85
171
 
86
172
  async def alogin(self) -> None:
173
+ """Run interactive OAuth login flow without blocking the event loop.
174
+
175
+ Raises:
176
+ OAuthCallbackParseError: If callback URL parsing fails.
177
+ OAuthStateMismatchError: If OAuth state verification fails.
178
+ TokenExchangeError: If code-to-token exchange fails.
179
+ """
87
180
  await asyncio.to_thread(self._engine.login)
88
181
 
89
182
  def generate(
90
183
  self,
91
- prompt: str | None = None,
184
+ messages: listMessage | None = None,
92
185
  *,
93
- images: ImageInput | list[ImageInput] | None = None,
94
186
  tools: list[Callable[..., Any]] | None = None,
95
187
  model: str | None = None,
96
188
  reasoning_effort: ReasoningEffort = "medium",
@@ -100,7 +192,54 @@ class OAuthCodexClient(SyncAPIClient):
100
192
  output_schema: StructuredOutputSchema | None = None,
101
193
  strict_output: bool | None = None,
102
194
  ) -> str | dict[str, Any]:
103
- messages = self._build_messages(prompt=prompt, images=images)
195
+ """Generate a final response with automatic tool execution.
196
+
197
+ Args:
198
+ messages: Non-empty list of response input messages.
199
+ tools: Optional list of Python callables used for automatic
200
+ function-calling rounds.
201
+ model: Optional model override. Uses `default_model` when omitted.
202
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
203
+ `"high"`).
204
+ temperature: Sampling temperature.
205
+ top_p: Nucleus sampling probability.
206
+ max_output_tokens: Maximum generated output tokens.
207
+ output_schema: Optional structured-output schema. Accepts a
208
+ Pydantic model type or JSON schema dict.
209
+ strict_output: Strict schema mode. When `output_schema` is set and
210
+ `strict_output` is omitted, strict mode is enabled by default.
211
+
212
+ Returns:
213
+ Final text output, or a validated JSON object when `output_schema`
214
+ is provided.
215
+
216
+ Raises:
217
+ ValueError: If `messages` is missing/empty, or structured output is
218
+ not valid JSON.
219
+ TypeError: If `messages` is not a list, `tools` are invalid, or
220
+ structured output is not a JSON object.
221
+ RuntimeError: If automatic tool execution exceeds
222
+ `max_tool_rounds`.
223
+ pydantic.ValidationError: If a Pydantic `output_schema` fails
224
+ strict validation.
225
+
226
+ Examples:
227
+ Basic text generation:
228
+ text = client.generate([{"role": "user", "content": "hello"}])
229
+
230
+ Structured output:
231
+ from pydantic import BaseModel
232
+
233
+ class Summary(BaseModel):
234
+ title: str
235
+ score: int
236
+
237
+ data = client.generate(
238
+ [{"role": "user", "content": "Return JSON"}],
239
+ output_schema=Summary,
240
+ )
241
+ """
242
+ messages = self._normalize_initial_messages(messages)
104
243
  normalized_tools, tools_by_name = self._normalize_tools(tools)
105
244
  response_format, effective_strict_output = self._resolve_structured_output_options(
106
245
  output_schema=output_schema,
@@ -149,9 +288,8 @@ class OAuthCodexClient(SyncAPIClient):
149
288
 
150
289
  async def agenerate(
151
290
  self,
152
- prompt: str | None = None,
291
+ messages: listMessage | None = None,
153
292
  *,
154
- images: ImageInput | list[ImageInput] | None = None,
155
293
  tools: list[Callable[..., Any]] | None = None,
156
294
  model: str | None = None,
157
295
  reasoning_effort: ReasoningEffort = "medium",
@@ -161,7 +299,41 @@ class OAuthCodexClient(SyncAPIClient):
161
299
  output_schema: StructuredOutputSchema | None = None,
162
300
  strict_output: bool | None = None,
163
301
  ) -> str | dict[str, Any]:
164
- messages = self._build_messages(prompt=prompt, images=images)
302
+ """Async response generation with automatic tool execution.
303
+
304
+ Args:
305
+ messages: Non-empty list of response input messages.
306
+ tools: Optional list of Python callables used for automatic
307
+ function-calling rounds.
308
+ model: Optional model override. Uses `default_model` when omitted.
309
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
310
+ `"high"`).
311
+ temperature: Sampling temperature.
312
+ top_p: Nucleus sampling probability.
313
+ max_output_tokens: Maximum generated output tokens.
314
+ output_schema: Optional structured-output schema. Accepts a
315
+ Pydantic model type or JSON schema dict.
316
+ strict_output: Strict schema mode. When `output_schema` is set and
317
+ `strict_output` is omitted, strict mode is enabled by default.
318
+
319
+ Returns:
320
+ Final text output, or a validated JSON object when `output_schema`
321
+ is provided.
322
+
323
+ Raises:
324
+ ValueError: If `messages` is missing/empty, or structured output is
325
+ not valid JSON.
326
+ TypeError: If `messages` is not a list, `tools` are invalid, or
327
+ structured output is not a JSON object.
328
+ RuntimeError: If automatic tool execution exceeds
329
+ `max_tool_rounds`.
330
+ pydantic.ValidationError: If a Pydantic `output_schema` fails
331
+ strict validation.
332
+
333
+ Examples:
334
+ text = await client.agenerate([{"role": "user", "content": "hello"}])
335
+ """
336
+ messages = self._normalize_initial_messages(messages)
165
337
  normalized_tools, tools_by_name = self._normalize_tools(tools)
166
338
  response_format, effective_strict_output = self._resolve_structured_output_options(
167
339
  output_schema=output_schema,
@@ -210,9 +382,8 @@ class OAuthCodexClient(SyncAPIClient):
210
382
 
211
383
  def stream(
212
384
  self,
213
- prompt: str | None = None,
385
+ messages: listMessage | None = None,
214
386
  *,
215
- images: ImageInput | list[ImageInput] | None = None,
216
387
  tools: list[Callable[..., Any]] | None = None,
217
388
  model: str | None = None,
218
389
  reasoning_effort: ReasoningEffort = "medium",
@@ -222,7 +393,33 @@ class OAuthCodexClient(SyncAPIClient):
222
393
  output_schema: StructuredOutputSchema | None = None,
223
394
  strict_output: bool | None = None,
224
395
  ) -> Iterator[str]:
225
- messages = self._build_messages(prompt=prompt, images=images)
396
+ """Stream text deltas with automatic tool execution rounds.
397
+
398
+ Args:
399
+ messages: Non-empty list of response input messages.
400
+ tools: Optional list of Python callables used for automatic
401
+ function-calling rounds.
402
+ model: Optional model override. Uses `default_model` when omitted.
403
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
404
+ `"high"`).
405
+ temperature: Sampling temperature.
406
+ top_p: Nucleus sampling probability.
407
+ max_output_tokens: Maximum generated output tokens.
408
+ output_schema: Optional structured-output schema forwarded to the
409
+ backend. Stream output is still text deltas only.
410
+ strict_output: Strict schema mode. When `output_schema` is set and
411
+ `strict_output` is omitted, strict mode is enabled by default.
412
+
413
+ Yields:
414
+ Text delta chunks as they arrive.
415
+
416
+ Raises:
417
+ ValueError: If `messages` is missing/empty.
418
+ TypeError: If `messages` is not a list or `tools` are invalid.
419
+ RuntimeError: If automatic tool execution exceeds
420
+ `max_tool_rounds`.
421
+ """
422
+ messages = self._normalize_initial_messages(messages)
226
423
  normalized_tools, tools_by_name = self._normalize_tools(tools)
227
424
  response_format, effective_strict_output = self._resolve_structured_output_options(
228
425
  output_schema=output_schema,
@@ -271,9 +468,8 @@ class OAuthCodexClient(SyncAPIClient):
271
468
 
272
469
  async def astream(
273
470
  self,
274
- prompt: str | None = None,
471
+ messages: listMessage | None = None,
275
472
  *,
276
- images: ImageInput | list[ImageInput] | None = None,
277
473
  tools: list[Callable[..., Any]] | None = None,
278
474
  model: str | None = None,
279
475
  reasoning_effort: ReasoningEffort = "medium",
@@ -283,7 +479,33 @@ class OAuthCodexClient(SyncAPIClient):
283
479
  output_schema: StructuredOutputSchema | None = None,
284
480
  strict_output: bool | None = None,
285
481
  ) -> AsyncIterator[str]:
286
- messages = self._build_messages(prompt=prompt, images=images)
482
+ """Async stream of text deltas with automatic tool execution rounds.
483
+
484
+ Args:
485
+ messages: Non-empty list of response input messages.
486
+ tools: Optional list of Python callables used for automatic
487
+ function-calling rounds.
488
+ model: Optional model override. Uses `default_model` when omitted.
489
+ reasoning_effort: Reasoning intensity (`"low"`, `"medium"`,
490
+ `"high"`).
491
+ temperature: Sampling temperature.
492
+ top_p: Nucleus sampling probability.
493
+ max_output_tokens: Maximum generated output tokens.
494
+ output_schema: Optional structured-output schema forwarded to the
495
+ backend. Stream output is still text deltas only.
496
+ strict_output: Strict schema mode. When `output_schema` is set and
497
+ `strict_output` is omitted, strict mode is enabled by default.
498
+
499
+ Yields:
500
+ Text delta chunks as they arrive.
501
+
502
+ Raises:
503
+ ValueError: If `messages` is missing/empty.
504
+ TypeError: If `messages` is not a list or `tools` are invalid.
505
+ RuntimeError: If automatic tool execution exceeds
506
+ `max_tool_rounds`.
507
+ """
508
+ messages = self._normalize_initial_messages(messages)
287
509
  normalized_tools, tools_by_name = self._normalize_tools(tools)
288
510
  response_format, effective_strict_output = self._resolve_structured_output_options(
289
511
  output_schema=output_schema,
@@ -375,10 +597,10 @@ class OAuthCodexClient(SyncAPIClient):
375
597
  def _messages_for_round(
376
598
  self,
377
599
  *,
378
- messages: list[Message],
600
+ messages: listMessage,
379
601
  previous_response_id: str | None,
380
602
  tool_results: list[ToolResult] | None,
381
- ) -> list[Message]:
603
+ ) -> listMessage:
382
604
  if self._is_tool_continuation_round(
383
605
  previous_response_id=previous_response_id,
384
606
  tool_results=tool_results,
@@ -386,64 +608,14 @@ class OAuthCodexClient(SyncAPIClient):
386
608
  return []
387
609
  return messages
388
610
 
389
- def _build_messages(
390
- self,
391
- *,
392
- prompt: str | None,
393
- images: ImageInput | list[ImageInput] | None,
394
- ) -> list[Message]:
395
- image_urls = self._normalize_images(images)
396
-
397
- if prompt is not None and not isinstance(prompt, str):
398
- raise TypeError("prompt must be a string")
399
- if prompt is None and not image_urls:
400
- raise ValueError("Either prompt or images must be provided")
401
-
402
- content: list[dict[str, Any]] = []
403
- if prompt:
404
- content.append({"type": "input_text", "text": prompt})
405
- for image_url in image_urls:
406
- content.append({"type": "input_image", "image_url": image_url})
407
-
408
- if not content:
409
- raise ValueError("Either prompt or images must be provided")
410
- if len(content) == 1 and content[0]["type"] == "input_text":
411
- return [{"role": "user", "content": content[0]["text"]}]
412
- return [{"role": "user", "content": content}]
413
-
414
- def _normalize_images(self, images: ImageInput | list[ImageInput] | None) -> list[str]:
415
- if images is None:
416
- return []
417
-
418
- raw_items: list[ImageInput]
419
- if isinstance(images, (str, Path)):
420
- raw_items = [images]
421
- elif isinstance(images, list):
422
- raw_items = images
423
- else:
424
- raise TypeError("images must be a string/Path or list of string/Path")
425
-
426
- normalized: list[str] = []
427
- for item in raw_items:
428
- normalized.append(self._coerce_image_to_url(item))
429
- return normalized
430
-
431
- def _coerce_image_to_url(self, image: ImageInput) -> str:
432
- if isinstance(image, Path):
433
- return self._path_to_data_url(image)
434
-
435
- value = image.strip()
436
- if value.startswith(("http://", "https://", "data:")):
437
- return value
438
- return self._path_to_data_url(Path(value).expanduser())
439
-
440
- def _path_to_data_url(self, path: Path) -> str:
441
- if not path.is_file():
442
- raise FileNotFoundError(f"image file not found: {path}")
443
-
444
- mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
445
- encoded = base64.b64encode(path.read_bytes()).decode("ascii")
446
- return f"data:{mime_type};base64,{encoded}"
611
+ def _normalize_initial_messages(self, messages: listMessage | None) -> listMessage:
612
+ if messages is None:
613
+ raise ValueError("`messages` must be a non-empty list")
614
+ if not isinstance(messages, list):
615
+ raise TypeError("`messages` must be a non-empty list")
616
+ if not messages:
617
+ raise ValueError("`messages` must be a non-empty list")
618
+ return messages
447
619
 
448
620
  def _normalize_tools(
449
621
  self,