oauth-codex 2.2.0__tar.gz → 2.3.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.
Files changed (64) hide show
  1. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/PKG-INFO +17 -9
  2. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/README.md +16 -8
  3. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/pyproject.toml +1 -1
  4. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/__init__.py +2 -0
  5. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_client.py +19 -77
  6. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_version.py +1 -1
  7. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/core_types.py +1 -0
  8. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex.egg-info/PKG-INFO +17 -9
  9. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/tests/test_generate_async.py +58 -7
  10. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/tests/test_generate_sync.py +64 -25
  11. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/tests/test_public_surface.py +2 -0
  12. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/setup.cfg +0 -0
  13. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_base_client.py +0 -0
  14. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_engine.py +0 -0
  15. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_exceptions.py +0 -0
  16. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_models.py +0 -0
  17. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_module_client.py +0 -0
  18. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_resource.py +0 -0
  19. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/_types.py +0 -0
  20. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/auth/__init__.py +0 -0
  21. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/auth/config.py +0 -0
  22. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/auth/pkce.py +0 -0
  23. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/auth/store.py +0 -0
  24. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/auth/token_manager.py +0 -0
  25. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/compat_store.py +0 -0
  26. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/errors.py +0 -0
  27. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/py.typed +0 -0
  28. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/__init__.py +0 -0
  29. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/_wrappers.py +0 -0
  30. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/files.py +0 -0
  31. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/models.py +0 -0
  32. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/responses/__init__.py +0 -0
  33. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/responses/_helpers.py +0 -0
  34. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
  35. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/responses/responses.py +0 -0
  36. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
  37. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
  38. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/vector_stores/files.py +0 -0
  39. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
  40. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/store.py +0 -0
  41. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/tooling.py +0 -0
  42. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/__init__.py +0 -0
  43. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/file_deleted.py +0 -0
  44. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/file_object.py +0 -0
  45. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/responses/__init__.py +0 -0
  46. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
  47. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/responses/response.py +0 -0
  48. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
  49. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/shared/__init__.py +0 -0
  50. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
  51. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/shared/usage.py +0 -0
  52. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
  53. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
  54. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
  55. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
  56. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
  57. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
  58. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex/version.py +0 -0
  59. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex.egg-info/SOURCES.txt +0 -0
  60. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
  61. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex.egg-info/requires.txt +0 -0
  62. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/src/oauth_codex.egg-info/top_level.txt +0 -0
  63. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/tests/test_engine_stream_and_continuity.py +0 -0
  64. {oauth_codex-2.2.0 → oauth_codex-2.3.0}/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.0
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,7 +37,7 @@ 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
 
@@ -45,8 +45,16 @@ print(text)
45
45
 
46
46
  ```python
47
47
  text = client.generate(
48
- "Describe this image",
49
- images=["https://example.com/cat.png", "./local-photo.jpg"],
48
+ [
49
+ {
50
+ "role": "user",
51
+ "content": [
52
+ {"type": "input_text", "text": "Describe this image"},
53
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
54
+ {"type": "input_image", "image_url": "data:image/jpeg;base64,..."},
55
+ ],
56
+ }
57
+ ],
50
58
  )
51
59
  print(text)
52
60
  ```
@@ -58,7 +66,7 @@ def add(a: int, b: int) -> dict:
58
66
  return {"sum": a + b}
59
67
 
60
68
  text = client.generate(
61
- "Calculate 2+3",
69
+ [{"role": "user", "content": "Calculate 2+3"}],
62
70
  tools=[add],
63
71
  )
64
72
  print(text)
@@ -78,7 +86,7 @@ def tool(input: ToolInput) -> str:
78
86
  return f"Tool received query: {input.query}"
79
87
 
80
88
 
81
- text = client.generate("Use the tool", tools=[tool])
89
+ text = client.generate([{"role": "user", "content": "Use the tool"}], tools=[tool])
82
90
  print(text)
83
91
  ```
84
92
 
@@ -100,7 +108,7 @@ class Summary(BaseModel):
100
108
 
101
109
  client = Client()
102
110
  out = client.generate(
103
- "Return JSON with title and score",
111
+ [{"role": "user", "content": "Return JSON with title and score"}],
104
112
  output_schema=Summary,
105
113
  )
106
114
  print(out) # {"title": "...", "score": 1}
@@ -110,7 +118,7 @@ You can also pass a raw JSON schema object.
110
118
 
111
119
  ```python
112
120
  out = client.generate(
113
- "Return {\"ok\": true}",
121
+ [{"role": "user", "content": "Return {\"ok\": true}"}],
114
122
  output_schema={
115
123
  "type": "object",
116
124
  "properties": {"ok": {"type": "boolean"}},
@@ -131,7 +139,7 @@ from oauth_codex import Client
131
139
 
132
140
  async def main() -> None:
133
141
  client = Client()
134
- text = await client.agenerate("hello async")
142
+ text = await client.agenerate([{"role": "user", "content": "hello async"}])
135
143
  print(text)
136
144
 
137
145
 
@@ -23,7 +23,7 @@ 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
 
@@ -31,8 +31,16 @@ print(text)
31
31
 
32
32
  ```python
33
33
  text = client.generate(
34
- "Describe this image",
35
- images=["https://example.com/cat.png", "./local-photo.jpg"],
34
+ [
35
+ {
36
+ "role": "user",
37
+ "content": [
38
+ {"type": "input_text", "text": "Describe this image"},
39
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
40
+ {"type": "input_image", "image_url": "data:image/jpeg;base64,..."},
41
+ ],
42
+ }
43
+ ],
36
44
  )
37
45
  print(text)
38
46
  ```
@@ -44,7 +52,7 @@ def add(a: int, b: int) -> dict:
44
52
  return {"sum": a + b}
45
53
 
46
54
  text = client.generate(
47
- "Calculate 2+3",
55
+ [{"role": "user", "content": "Calculate 2+3"}],
48
56
  tools=[add],
49
57
  )
50
58
  print(text)
@@ -64,7 +72,7 @@ def tool(input: ToolInput) -> str:
64
72
  return f"Tool received query: {input.query}"
65
73
 
66
74
 
67
- text = client.generate("Use the tool", tools=[tool])
75
+ text = client.generate([{"role": "user", "content": "Use the tool"}], tools=[tool])
68
76
  print(text)
69
77
  ```
70
78
 
@@ -86,7 +94,7 @@ class Summary(BaseModel):
86
94
 
87
95
  client = Client()
88
96
  out = client.generate(
89
- "Return JSON with title and score",
97
+ [{"role": "user", "content": "Return JSON with title and score"}],
90
98
  output_schema=Summary,
91
99
  )
92
100
  print(out) # {"title": "...", "score": 1}
@@ -96,7 +104,7 @@ You can also pass a raw JSON schema object.
96
104
 
97
105
  ```python
98
106
  out = client.generate(
99
- "Return {\"ok\": true}",
107
+ [{"role": "user", "content": "Return {\"ok\": true}"}],
100
108
  output_schema={
101
109
  "type": "object",
102
110
  "properties": {"ok": {"type": "boolean"}},
@@ -117,7 +125,7 @@ from oauth_codex import Client
117
125
 
118
126
  async def main() -> None:
119
127
  client = Client()
120
- text = await client.agenerate("hello async")
128
+ text = await client.agenerate([{"role": "user", "content": "hello async"}])
121
129
  print(text)
122
130
 
123
131
 
@@ -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.0"
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
@@ -88,9 +84,8 @@ class OAuthCodexClient(SyncAPIClient):
88
84
 
89
85
  def generate(
90
86
  self,
91
- prompt: str | None = None,
87
+ messages: listMessage | None = None,
92
88
  *,
93
- images: ImageInput | list[ImageInput] | None = None,
94
89
  tools: list[Callable[..., Any]] | None = None,
95
90
  model: str | None = None,
96
91
  reasoning_effort: ReasoningEffort = "medium",
@@ -100,7 +95,7 @@ class OAuthCodexClient(SyncAPIClient):
100
95
  output_schema: StructuredOutputSchema | None = None,
101
96
  strict_output: bool | None = None,
102
97
  ) -> str | dict[str, Any]:
103
- messages = self._build_messages(prompt=prompt, images=images)
98
+ messages = self._normalize_initial_messages(messages)
104
99
  normalized_tools, tools_by_name = self._normalize_tools(tools)
105
100
  response_format, effective_strict_output = self._resolve_structured_output_options(
106
101
  output_schema=output_schema,
@@ -149,9 +144,8 @@ class OAuthCodexClient(SyncAPIClient):
149
144
 
150
145
  async def agenerate(
151
146
  self,
152
- prompt: str | None = None,
147
+ messages: listMessage | None = None,
153
148
  *,
154
- images: ImageInput | list[ImageInput] | None = None,
155
149
  tools: list[Callable[..., Any]] | None = None,
156
150
  model: str | None = None,
157
151
  reasoning_effort: ReasoningEffort = "medium",
@@ -161,7 +155,7 @@ class OAuthCodexClient(SyncAPIClient):
161
155
  output_schema: StructuredOutputSchema | None = None,
162
156
  strict_output: bool | None = None,
163
157
  ) -> str | dict[str, Any]:
164
- messages = self._build_messages(prompt=prompt, images=images)
158
+ messages = self._normalize_initial_messages(messages)
165
159
  normalized_tools, tools_by_name = self._normalize_tools(tools)
166
160
  response_format, effective_strict_output = self._resolve_structured_output_options(
167
161
  output_schema=output_schema,
@@ -210,9 +204,8 @@ class OAuthCodexClient(SyncAPIClient):
210
204
 
211
205
  def stream(
212
206
  self,
213
- prompt: str | None = None,
207
+ messages: listMessage | None = None,
214
208
  *,
215
- images: ImageInput | list[ImageInput] | None = None,
216
209
  tools: list[Callable[..., Any]] | None = None,
217
210
  model: str | None = None,
218
211
  reasoning_effort: ReasoningEffort = "medium",
@@ -222,7 +215,7 @@ class OAuthCodexClient(SyncAPIClient):
222
215
  output_schema: StructuredOutputSchema | None = None,
223
216
  strict_output: bool | None = None,
224
217
  ) -> Iterator[str]:
225
- messages = self._build_messages(prompt=prompt, images=images)
218
+ messages = self._normalize_initial_messages(messages)
226
219
  normalized_tools, tools_by_name = self._normalize_tools(tools)
227
220
  response_format, effective_strict_output = self._resolve_structured_output_options(
228
221
  output_schema=output_schema,
@@ -271,9 +264,8 @@ class OAuthCodexClient(SyncAPIClient):
271
264
 
272
265
  async def astream(
273
266
  self,
274
- prompt: str | None = None,
267
+ messages: listMessage | None = None,
275
268
  *,
276
- images: ImageInput | list[ImageInput] | None = None,
277
269
  tools: list[Callable[..., Any]] | None = None,
278
270
  model: str | None = None,
279
271
  reasoning_effort: ReasoningEffort = "medium",
@@ -283,7 +275,7 @@ class OAuthCodexClient(SyncAPIClient):
283
275
  output_schema: StructuredOutputSchema | None = None,
284
276
  strict_output: bool | None = None,
285
277
  ) -> AsyncIterator[str]:
286
- messages = self._build_messages(prompt=prompt, images=images)
278
+ messages = self._normalize_initial_messages(messages)
287
279
  normalized_tools, tools_by_name = self._normalize_tools(tools)
288
280
  response_format, effective_strict_output = self._resolve_structured_output_options(
289
281
  output_schema=output_schema,
@@ -375,10 +367,10 @@ class OAuthCodexClient(SyncAPIClient):
375
367
  def _messages_for_round(
376
368
  self,
377
369
  *,
378
- messages: list[Message],
370
+ messages: listMessage,
379
371
  previous_response_id: str | None,
380
372
  tool_results: list[ToolResult] | None,
381
- ) -> list[Message]:
373
+ ) -> listMessage:
382
374
  if self._is_tool_continuation_round(
383
375
  previous_response_id=previous_response_id,
384
376
  tool_results=tool_results,
@@ -386,64 +378,14 @@ class OAuthCodexClient(SyncAPIClient):
386
378
  return []
387
379
  return messages
388
380
 
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}"
381
+ def _normalize_initial_messages(self, messages: listMessage | None) -> listMessage:
382
+ if messages is None:
383
+ raise ValueError("`messages` must be a non-empty list")
384
+ if not isinstance(messages, list):
385
+ raise TypeError("`messages` must be a non-empty list")
386
+ if not messages:
387
+ raise ValueError("`messages` must be a non-empty list")
388
+ return messages
447
389
 
448
390
  def _normalize_tools(
449
391
  self,
@@ -1,2 +1,2 @@
1
1
  __title__ = "oauth-codex"
2
- __version__ = "2.2.0"
2
+ __version__ = "2.3.0"
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import Any, Callable, Literal, Protocol, TypeAlias
6
6
 
7
7
  Message: TypeAlias = dict[str, Any]
8
+ listMessage: TypeAlias = list[Message]
8
9
  ValidationMode: TypeAlias = Literal["warn", "error", "ignore"]
9
10
  StoreBehavior: TypeAlias = Literal["auto_disable", "error", "passthrough"]
10
11
  TruncationMode: TypeAlias = Literal["auto", "disabled"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oauth-codex
3
- Version: 2.2.0
3
+ Version: 2.3.0
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,7 +37,7 @@ 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
 
@@ -45,8 +45,16 @@ print(text)
45
45
 
46
46
  ```python
47
47
  text = client.generate(
48
- "Describe this image",
49
- images=["https://example.com/cat.png", "./local-photo.jpg"],
48
+ [
49
+ {
50
+ "role": "user",
51
+ "content": [
52
+ {"type": "input_text", "text": "Describe this image"},
53
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
54
+ {"type": "input_image", "image_url": "data:image/jpeg;base64,..."},
55
+ ],
56
+ }
57
+ ],
50
58
  )
51
59
  print(text)
52
60
  ```
@@ -58,7 +66,7 @@ def add(a: int, b: int) -> dict:
58
66
  return {"sum": a + b}
59
67
 
60
68
  text = client.generate(
61
- "Calculate 2+3",
69
+ [{"role": "user", "content": "Calculate 2+3"}],
62
70
  tools=[add],
63
71
  )
64
72
  print(text)
@@ -78,7 +86,7 @@ def tool(input: ToolInput) -> str:
78
86
  return f"Tool received query: {input.query}"
79
87
 
80
88
 
81
- text = client.generate("Use the tool", tools=[tool])
89
+ text = client.generate([{"role": "user", "content": "Use the tool"}], tools=[tool])
82
90
  print(text)
83
91
  ```
84
92
 
@@ -100,7 +108,7 @@ class Summary(BaseModel):
100
108
 
101
109
  client = Client()
102
110
  out = client.generate(
103
- "Return JSON with title and score",
111
+ [{"role": "user", "content": "Return JSON with title and score"}],
104
112
  output_schema=Summary,
105
113
  )
106
114
  print(out) # {"title": "...", "score": 1}
@@ -110,7 +118,7 @@ You can also pass a raw JSON schema object.
110
118
 
111
119
  ```python
112
120
  out = client.generate(
113
- "Return {\"ok\": true}",
121
+ [{"role": "user", "content": "Return {\"ok\": true}"}],
114
122
  output_schema={
115
123
  "type": "object",
116
124
  "properties": {"ok": {"type": "boolean"}},
@@ -131,7 +139,7 @@ from oauth_codex import Client
131
139
 
132
140
  async def main() -> None:
133
141
  client = Client()
134
- text = await client.agenerate("hello async")
142
+ text = await client.agenerate([{"role": "user", "content": "hello async"}])
135
143
  print(text)
136
144
 
137
145
 
@@ -25,6 +25,51 @@ def _client() -> Client:
25
25
  )
26
26
 
27
27
 
28
+ @pytest.mark.asyncio
29
+ async def test_agenerate_rejects_non_list_messages() -> None:
30
+ client = _client()
31
+
32
+ with pytest.raises(TypeError, match="non-empty list"):
33
+ await client.agenerate("hello") # type: ignore[arg-type]
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_agenerate_rejects_empty_messages() -> None:
38
+ client = _client()
39
+
40
+ with pytest.raises(ValueError, match="non-empty list"):
41
+ await client.agenerate([])
42
+
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_agenerate_preserves_mixed_content_message_order(
46
+ monkeypatch: pytest.MonkeyPatch,
47
+ ) -> None:
48
+ client = _client()
49
+ captured: dict[str, object] = {}
50
+
51
+ async def fake_agenerate(**kwargs):
52
+ captured.update(kwargs)
53
+ return GenerateResult(text="ok", tool_calls=[], finish_reason="stop")
54
+
55
+ monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
56
+
57
+ mixed_messages = [
58
+ {
59
+ "role": "user",
60
+ "content": [
61
+ {"type": "input_text", "text": "describe"},
62
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
63
+ {"type": "input_text", "text": "focus on the cat"},
64
+ ],
65
+ }
66
+ ]
67
+ out = await client.agenerate(messages=mixed_messages)
68
+
69
+ assert out == "ok"
70
+ assert captured["messages"] == mixed_messages
71
+
72
+
28
73
  @pytest.mark.asyncio
29
74
  async def test_agenerate_auto_function_calling(monkeypatch: pytest.MonkeyPatch) -> None:
30
75
  client = _client()
@@ -46,7 +91,7 @@ async def test_agenerate_auto_function_calling(monkeypatch: pytest.MonkeyPatch)
46
91
  async def add_async(a: int, b: int) -> dict[str, int]:
47
92
  return {"sum": a + b}
48
93
 
49
- out = await client.agenerate("5+7", tools=[add_async])
94
+ out = await client.agenerate([{"role": "user", "content": "5+7"}], tools=[add_async])
50
95
 
51
96
  assert out == "12"
52
97
  assert calls[1]["previous_response_id"] == "resp_1"
@@ -86,7 +131,7 @@ async def test_astream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> N
86
131
  return {"product": a * b}
87
132
 
88
133
  out: list[str] = []
89
- async for delta in client.astream("calc", tools=[mul]):
134
+ async for delta in client.astream([{"role": "user", "content": "calc"}], tools=[mul]):
90
135
  out.append(delta)
91
136
 
92
137
  assert out == ["X", "Y"]
@@ -117,7 +162,7 @@ async def test_agenerate_supports_single_pydantic_tool_input(monkeypatch: pytest
117
162
  def tool(input: ToolInput) -> str:
118
163
  return f"Tool received query: {input.query}"
119
164
 
120
- out = await client.agenerate("run", tools=[tool])
165
+ out = await client.agenerate([{"role": "user", "content": "run"}], tools=[tool])
121
166
 
122
167
  assert out == "done"
123
168
  tool_results = calls[1]["tool_results"]
@@ -142,7 +187,10 @@ async def test_agenerate_supports_structured_output_with_pydantic_schema(
142
187
 
143
188
  monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
144
189
 
145
- out = await client.agenerate("return json", output_schema=StructuredOutput)
190
+ out = await client.agenerate(
191
+ [{"role": "user", "content": "return json"}],
192
+ output_schema=StructuredOutput,
193
+ )
146
194
 
147
195
  assert out == {"answer": "ok", "count": 1}
148
196
  response_format = captured["response_format"]
@@ -171,7 +219,7 @@ async def test_agenerate_structured_output_rejects_invalid_json(
171
219
 
172
220
  with pytest.raises(ValueError, match="valid JSON"):
173
221
  await client.agenerate(
174
- "return json",
222
+ [{"role": "user", "content": "return json"}],
175
223
  output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
176
224
  )
177
225
 
@@ -193,7 +241,10 @@ async def test_agenerate_structured_output_enforces_pydantic_strict_validation(
193
241
  monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
194
242
 
195
243
  with pytest.raises(ValidationError):
196
- await client.agenerate("return json", output_schema=StructuredOutput)
244
+ await client.agenerate(
245
+ [{"role": "user", "content": "return json"}],
246
+ output_schema=StructuredOutput,
247
+ )
197
248
 
198
249
 
199
250
  @pytest.mark.asyncio
@@ -217,7 +268,7 @@ async def test_astream_accepts_output_schema_and_keeps_text_stream(
217
268
 
218
269
  out: list[str] = []
219
270
  async for delta in client.astream(
220
- "return json",
271
+ messages=[{"role": "user", "content": "return json"}],
221
272
  output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
222
273
  ):
223
274
  out.append(delta)
@@ -39,32 +39,64 @@ def test_generate_uses_default_model_and_reasoning_effort(monkeypatch: pytest.Mo
39
39
 
40
40
  monkeypatch.setattr(client._engine, "generate", fake_generate)
41
41
 
42
- out = client.generate("hello")
42
+ out = client.generate([{"role": "user", "content": "hello"}])
43
43
 
44
44
  assert out == "ok"
45
45
  assert captured["model"] == "gpt-5.3-codex"
46
46
  assert captured["reasoning"] == {"effort": "medium"}
47
47
 
48
48
 
49
- def test_generate_accepts_url_and_local_image_inputs(
50
- monkeypatch: pytest.MonkeyPatch, tmp_path
49
+ def test_generate_rejects_non_list_messages() -> None:
50
+ client = _client()
51
+
52
+ with pytest.raises(TypeError, match="non-empty list"):
53
+ client.generate("hello") # type: ignore[arg-type]
54
+
55
+
56
+ def test_generate_rejects_empty_messages() -> None:
57
+ client = _client()
58
+
59
+ with pytest.raises(ValueError, match="non-empty list"):
60
+ client.generate([])
61
+
62
+
63
+ def test_generate_rejects_removed_prompt_and_images_kwargs() -> None:
64
+ client = _client()
65
+
66
+ with pytest.raises(TypeError, match="prompt"):
67
+ client.generate(prompt="hello") # type: ignore[call-arg]
68
+
69
+ with pytest.raises(TypeError, match="images"):
70
+ client.generate( # type: ignore[call-arg]
71
+ [{"role": "user", "content": "hello"}],
72
+ images=["https://example.com/cat.png"],
73
+ )
74
+
75
+
76
+ def test_generate_preserves_mixed_content_message_order(
77
+ monkeypatch: pytest.MonkeyPatch,
51
78
  ) -> None:
52
79
  client = _client()
53
80
  captured: dict[str, object] = {}
54
81
 
55
- image_path = tmp_path / "photo.png"
56
- image_path.write_bytes(b"PNGDATA")
57
-
58
82
  def fake_generate(**kwargs):
59
83
  captured.update(kwargs)
60
84
  return GenerateResult(text="ok", tool_calls=[], finish_reason="stop")
61
85
 
62
86
  monkeypatch.setattr(client._engine, "generate", fake_generate)
63
87
 
64
- out = client.generate(
65
- "describe",
66
- images=["https://example.com/cat.png", image_path],
67
- )
88
+ mixed_messages = [
89
+ {
90
+ "role": "user",
91
+ "content": [
92
+ {"type": "input_text", "text": "describe"},
93
+ {"type": "input_image", "image_url": "https://example.com/cat.png"},
94
+ {"type": "input_text", "text": "focus on the cat"},
95
+ {"type": "input_image", "image_url": "data:image/png;base64,AAAA"},
96
+ ],
97
+ }
98
+ ]
99
+ out = client.generate(messages=mixed_messages)
68
100
 
69
101
  assert out == "ok"
70
102
  messages = captured["messages"]
@@ -72,8 +104,9 @@ def test_generate_accepts_url_and_local_image_inputs(
72
104
  content = messages[0]["content"]
73
105
  assert content[0]["type"] == "input_text"
74
106
  assert content[1] == {"type": "input_image", "image_url": "https://example.com/cat.png"}
75
- assert content[2]["type"] == "input_image"
76
- assert content[2]["image_url"].startswith("data:image/png;base64,")
107
+ assert content[2] == {"type": "input_text", "text": "focus on the cat"}
108
+ assert content[3] == {"type": "input_image", "image_url": "data:image/png;base64,AAAA"}
109
+ assert messages == mixed_messages
77
110
 
78
111
 
79
112
  def test_generate_auto_function_calling(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -96,7 +129,7 @@ def test_generate_auto_function_calling(monkeypatch: pytest.MonkeyPatch) -> None
96
129
  def add(a: int, b: int) -> dict[str, int]:
97
130
  return {"sum": a + b}
98
131
 
99
- out = client.generate("2+3", tools=[add])
132
+ out = client.generate([{"role": "user", "content": "2+3"}], tools=[add])
100
133
 
101
134
  assert out == "5"
102
135
  assert calls[1]["previous_response_id"] == "resp_1"
@@ -129,7 +162,7 @@ def test_generate_replays_messages_when_tool_round_has_no_response_id(
129
162
  def add(a: int, b: int) -> dict[str, int]:
130
163
  return {"sum": a + b}
131
164
 
132
- out = client.generate("1+2", tools=[add])
165
+ out = client.generate([{"role": "user", "content": "1+2"}], tools=[add])
133
166
 
134
167
  assert out == "3"
135
168
  assert calls[1]["previous_response_id"] is None
@@ -157,7 +190,7 @@ def test_generate_tool_failure_is_forwarded_to_model(monkeypatch: pytest.MonkeyP
157
190
  _ = x
158
191
  raise ValueError("boom")
159
192
 
160
- out = client.generate("run", tools=[bad_tool])
193
+ out = client.generate([{"role": "user", "content": "run"}], tools=[bad_tool])
161
194
 
162
195
  assert out == "handled"
163
196
  tool_results = calls[1]["tool_results"]
@@ -184,7 +217,7 @@ def test_generate_wraps_string_tool_output_as_dict(monkeypatch: pytest.MonkeyPat
184
217
  def echo(query: str) -> str:
185
218
  return query
186
219
 
187
- out = client.generate("run once", tools=[echo])
220
+ out = client.generate([{"role": "user", "content": "run once"}], tools=[echo])
188
221
 
189
222
  assert out == "done"
190
223
  tool_results = calls[1]["tool_results"]
@@ -213,7 +246,7 @@ def test_generate_supports_single_pydantic_tool_input_with_flat_payload(
213
246
  def tool(input: ToolInputWithDescription) -> str:
214
247
  return f"Tool received query: {input.query}"
215
248
 
216
- out = client.generate("run", tools=[tool])
249
+ out = client.generate([{"role": "user", "content": "run"}], tools=[tool])
217
250
 
218
251
  assert out == "done"
219
252
  first_round_tools = calls[0]["tools"]
@@ -253,7 +286,7 @@ def test_generate_supports_single_pydantic_tool_input_with_nested_payload(
253
286
  def tool(input: ToolInput) -> str:
254
287
  return f"Tool received query: {input.query}"
255
288
 
256
- out = client.generate("run", tools=[tool])
289
+ out = client.generate([{"role": "user", "content": "run"}], tools=[tool])
257
290
 
258
291
  assert out == "done"
259
292
  tool_results = calls[1]["tool_results"]
@@ -279,7 +312,7 @@ def test_generate_raises_when_tool_round_limit_exceeded(monkeypatch: pytest.Monk
279
312
  return {"ok": True}
280
313
 
281
314
  with pytest.raises(RuntimeError, match="exceeded"):
282
- client.generate("loop", tools=[loop])
315
+ client.generate([{"role": "user", "content": "loop"}], tools=[loop])
283
316
 
284
317
 
285
318
  def test_generate_supports_structured_output_with_pydantic_schema(
@@ -299,7 +332,10 @@ def test_generate_supports_structured_output_with_pydantic_schema(
299
332
 
300
333
  monkeypatch.setattr(client._engine, "generate", fake_generate)
301
334
 
302
- out = client.generate("return json", output_schema=StructuredOutput)
335
+ out = client.generate(
336
+ [{"role": "user", "content": "return json"}],
337
+ output_schema=StructuredOutput,
338
+ )
303
339
 
304
340
  assert out == {"answer": "ok", "count": 1}
305
341
  response_format = captured["response_format"]
@@ -329,7 +365,7 @@ def test_generate_supports_structured_output_with_raw_schema_dict(
329
365
  monkeypatch.setattr(client._engine, "generate", fake_generate)
330
366
 
331
367
  out = client.generate(
332
- "return json",
368
+ [{"role": "user", "content": "return json"}],
333
369
  output_schema={
334
370
  "type": "object",
335
371
  "properties": {"ok": {"type": "boolean"}},
@@ -360,7 +396,7 @@ def test_generate_structured_output_rejects_invalid_json(monkeypatch: pytest.Mon
360
396
 
361
397
  with pytest.raises(ValueError, match="valid JSON"):
362
398
  client.generate(
363
- "return json",
399
+ [{"role": "user", "content": "return json"}],
364
400
  output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
365
401
  )
366
402
 
@@ -381,7 +417,10 @@ def test_generate_structured_output_enforces_pydantic_strict_validation(
381
417
  monkeypatch.setattr(client._engine, "generate", fake_generate)
382
418
 
383
419
  with pytest.raises(ValidationError):
384
- client.generate("return json", output_schema=StructuredOutput)
420
+ client.generate(
421
+ [{"role": "user", "content": "return json"}],
422
+ output_schema=StructuredOutput,
423
+ )
385
424
 
386
425
 
387
426
  def test_stream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -408,7 +447,7 @@ def test_stream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
408
447
  def add(a: int, b: int) -> dict[str, int]:
409
448
  return {"sum": a + b}
410
449
 
411
- out = list(client.stream("calc", tools=[add]))
450
+ out = list(client.stream([{"role": "user", "content": "calc"}], tools=[add]))
412
451
 
413
452
  assert out == ["A", "B"]
414
453
  assert calls[1]["previous_response_id"] == "resp_1"
@@ -433,7 +472,7 @@ def test_stream_accepts_output_schema_and_keeps_text_stream(
433
472
 
434
473
  out = list(
435
474
  client.stream(
436
- "return json",
475
+ messages=[{"role": "user", "content": "return json"}],
437
476
  output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
438
477
  )
439
478
  )
@@ -3,11 +3,13 @@ from __future__ import annotations
3
3
  import pytest
4
4
 
5
5
  import oauth_codex
6
+ from oauth_codex.core_types import listMessage
6
7
 
7
8
 
8
9
  def test_single_client_public_surface() -> None:
9
10
  assert oauth_codex.Client is oauth_codex.OAuthCodexClient
10
11
  assert oauth_codex.Client is not None
12
+ assert oauth_codex.listMessage is listMessage
11
13
 
12
14
 
13
15
  def test_removed_async_and_module_level_exports() -> None:
File without changes