oauth-codex 2.0.3__tar.gz → 2.2.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.
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/PKG-INFO +39 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/README.md +38 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/pyproject.toml +1 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_client.py +118 -10
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_engine.py +48 -5
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_version.py +1 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/tooling.py +196 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/PKG-INFO +39 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/SOURCES.txt +2 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/tests/test_engine_stream_and_continuity.py +45 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/tests/test_generate_async.py +113 -1
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/tests/test_generate_sync.py +167 -1
- oauth_codex-2.2.0/tests/test_tooling_strict_schema.py +130 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/setup.cfg +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_base_client.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_exceptions.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_models.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_module_client.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_resource.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/_types.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/auth/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/auth/config.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/auth/pkce.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/auth/store.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/auth/token_manager.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/compat_store.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/core_types.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/errors.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/py.typed +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/_wrappers.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/files.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/models.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/_helpers.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/responses.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/files.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/store.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/file_deleted.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/file_object.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/response.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/usage.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex/version.py +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/requires.txt +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/top_level.txt +0 -0
- {oauth_codex-2.0.3 → oauth_codex-2.2.0}/tests/test_public_surface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oauth-codex
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.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
|
|
@@ -84,6 +84,44 @@ print(text)
|
|
|
84
84
|
|
|
85
85
|
If a tool raises an exception, the SDK forwards it to the model as `{\"error\": ...}` and continues the loop.
|
|
86
86
|
|
|
87
|
+
## Structured Output (Strict JSON Schema / Pydantic)
|
|
88
|
+
|
|
89
|
+
`generate` / `agenerate` can produce validated JSON object output via `output_schema`.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pydantic import BaseModel
|
|
93
|
+
from oauth_codex import Client
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Summary(BaseModel):
|
|
97
|
+
title: str
|
|
98
|
+
score: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
client = Client()
|
|
102
|
+
out = client.generate(
|
|
103
|
+
"Return JSON with title and score",
|
|
104
|
+
output_schema=Summary,
|
|
105
|
+
)
|
|
106
|
+
print(out) # {"title": "...", "score": 1}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can also pass a raw JSON schema object.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
out = client.generate(
|
|
113
|
+
"Return {\"ok\": true}",
|
|
114
|
+
output_schema={
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {"ok": {"type": "boolean"}},
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
print(out) # {"ok": True}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When `output_schema` is set, strict mode is enabled by default unless `strict_output` is explicitly provided.
|
|
123
|
+
`stream` / `astream` still yield text deltas only; they do not parse final JSON objects.
|
|
124
|
+
|
|
87
125
|
## Async
|
|
88
126
|
|
|
89
127
|
```python
|
|
@@ -70,6 +70,44 @@ print(text)
|
|
|
70
70
|
|
|
71
71
|
If a tool raises an exception, the SDK forwards it to the model as `{\"error\": ...}` and continues the loop.
|
|
72
72
|
|
|
73
|
+
## Structured Output (Strict JSON Schema / Pydantic)
|
|
74
|
+
|
|
75
|
+
`generate` / `agenerate` can produce validated JSON object output via `output_schema`.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pydantic import BaseModel
|
|
79
|
+
from oauth_codex import Client
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Summary(BaseModel):
|
|
83
|
+
title: str
|
|
84
|
+
score: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
client = Client()
|
|
88
|
+
out = client.generate(
|
|
89
|
+
"Return JSON with title and score",
|
|
90
|
+
output_schema=Summary,
|
|
91
|
+
)
|
|
92
|
+
print(out) # {"title": "...", "score": 1}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You can also pass a raw JSON schema object.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
out = client.generate(
|
|
99
|
+
"Return {\"ok\": true}",
|
|
100
|
+
output_schema={
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {"ok": {"type": "boolean"}},
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
print(out) # {"ok": True}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When `output_schema` is set, strict mode is enabled by default unless `strict_output` is explicitly provided.
|
|
109
|
+
`stream` / `astream` still yield text deltas only; they do not parse final JSON objects.
|
|
110
|
+
|
|
73
111
|
## Async
|
|
74
112
|
|
|
75
113
|
```python
|
|
@@ -22,10 +22,11 @@ from .core_types import (
|
|
|
22
22
|
ToolResult,
|
|
23
23
|
)
|
|
24
24
|
from .store import FallbackTokenStore
|
|
25
|
-
from .tooling import callable_to_tool_schema, normalize_tool_output
|
|
25
|
+
from .tooling import build_strict_response_format, callable_to_tool_schema, normalize_tool_output
|
|
26
26
|
|
|
27
27
|
DEFAULT_MODEL = "gpt-5.3-codex"
|
|
28
|
-
DEFAULT_MAX_TOOL_ROUNDS =
|
|
28
|
+
DEFAULT_MAX_TOOL_ROUNDS = 16
|
|
29
|
+
StructuredOutputSchema = type[BaseModel] | dict[str, Any]
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class OAuthCodexClient(SyncAPIClient):
|
|
@@ -96,9 +97,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
96
97
|
temperature: float | None = None,
|
|
97
98
|
top_p: float | None = None,
|
|
98
99
|
max_output_tokens: int | None = None,
|
|
99
|
-
|
|
100
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
101
|
+
strict_output: bool | None = None,
|
|
102
|
+
) -> str | dict[str, Any]:
|
|
100
103
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
101
104
|
normalized_tools, tools_by_name = self._normalize_tools(tools)
|
|
105
|
+
response_format, effective_strict_output = self._resolve_structured_output_options(
|
|
106
|
+
output_schema=output_schema,
|
|
107
|
+
strict_output=strict_output,
|
|
108
|
+
)
|
|
102
109
|
|
|
103
110
|
previous_response_id: str | None = None
|
|
104
111
|
tool_results: list[ToolResult] | None = None
|
|
@@ -107,9 +114,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
107
114
|
for _ in range(self.max_tool_rounds):
|
|
108
115
|
result = self._engine.generate(
|
|
109
116
|
model=self._resolve_model(model),
|
|
110
|
-
messages=
|
|
117
|
+
messages=self._messages_for_round(
|
|
118
|
+
messages=messages,
|
|
119
|
+
previous_response_id=previous_response_id,
|
|
120
|
+
tool_results=tool_results,
|
|
121
|
+
),
|
|
111
122
|
tools=normalized_tools,
|
|
112
123
|
tool_results=tool_results,
|
|
124
|
+
response_format=response_format,
|
|
125
|
+
strict_output=effective_strict_output,
|
|
113
126
|
reasoning={"effort": reasoning_effort},
|
|
114
127
|
previous_response_id=previous_response_id,
|
|
115
128
|
temperature=temperature,
|
|
@@ -124,7 +137,10 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
124
137
|
output_parts.append(result.text)
|
|
125
138
|
|
|
126
139
|
if not result.tool_calls:
|
|
127
|
-
|
|
140
|
+
text = "".join(output_parts)
|
|
141
|
+
if output_schema is None:
|
|
142
|
+
return text
|
|
143
|
+
return self._parse_structured_output_text(text=text, output_schema=output_schema)
|
|
128
144
|
|
|
129
145
|
tool_results = self._execute_tool_calls_sync(result.tool_calls, tools_by_name)
|
|
130
146
|
previous_response_id = result.response_id
|
|
@@ -142,9 +158,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
142
158
|
temperature: float | None = None,
|
|
143
159
|
top_p: float | None = None,
|
|
144
160
|
max_output_tokens: int | None = None,
|
|
145
|
-
|
|
161
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
162
|
+
strict_output: bool | None = None,
|
|
163
|
+
) -> str | dict[str, Any]:
|
|
146
164
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
147
165
|
normalized_tools, tools_by_name = self._normalize_tools(tools)
|
|
166
|
+
response_format, effective_strict_output = self._resolve_structured_output_options(
|
|
167
|
+
output_schema=output_schema,
|
|
168
|
+
strict_output=strict_output,
|
|
169
|
+
)
|
|
148
170
|
|
|
149
171
|
previous_response_id: str | None = None
|
|
150
172
|
tool_results: list[ToolResult] | None = None
|
|
@@ -153,9 +175,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
153
175
|
for _ in range(self.max_tool_rounds):
|
|
154
176
|
result = await self._engine.agenerate(
|
|
155
177
|
model=self._resolve_model(model),
|
|
156
|
-
messages=
|
|
178
|
+
messages=self._messages_for_round(
|
|
179
|
+
messages=messages,
|
|
180
|
+
previous_response_id=previous_response_id,
|
|
181
|
+
tool_results=tool_results,
|
|
182
|
+
),
|
|
157
183
|
tools=normalized_tools,
|
|
158
184
|
tool_results=tool_results,
|
|
185
|
+
response_format=response_format,
|
|
186
|
+
strict_output=effective_strict_output,
|
|
159
187
|
reasoning={"effort": reasoning_effort},
|
|
160
188
|
previous_response_id=previous_response_id,
|
|
161
189
|
temperature=temperature,
|
|
@@ -170,7 +198,10 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
170
198
|
output_parts.append(result.text)
|
|
171
199
|
|
|
172
200
|
if not result.tool_calls:
|
|
173
|
-
|
|
201
|
+
text = "".join(output_parts)
|
|
202
|
+
if output_schema is None:
|
|
203
|
+
return text
|
|
204
|
+
return self._parse_structured_output_text(text=text, output_schema=output_schema)
|
|
174
205
|
|
|
175
206
|
tool_results = await self._execute_tool_calls_async(result.tool_calls, tools_by_name)
|
|
176
207
|
previous_response_id = result.response_id
|
|
@@ -188,9 +219,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
188
219
|
temperature: float | None = None,
|
|
189
220
|
top_p: float | None = None,
|
|
190
221
|
max_output_tokens: int | None = None,
|
|
222
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
223
|
+
strict_output: bool | None = None,
|
|
191
224
|
) -> Iterator[str]:
|
|
192
225
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
193
226
|
normalized_tools, tools_by_name = self._normalize_tools(tools)
|
|
227
|
+
response_format, effective_strict_output = self._resolve_structured_output_options(
|
|
228
|
+
output_schema=output_schema,
|
|
229
|
+
strict_output=strict_output,
|
|
230
|
+
)
|
|
194
231
|
|
|
195
232
|
previous_response_id: str | None = None
|
|
196
233
|
tool_results: list[ToolResult] | None = None
|
|
@@ -200,9 +237,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
200
237
|
round_response_id: str | None = previous_response_id
|
|
201
238
|
events = self._engine.generate_stream(
|
|
202
239
|
model=self._resolve_model(model),
|
|
203
|
-
messages=
|
|
240
|
+
messages=self._messages_for_round(
|
|
241
|
+
messages=messages,
|
|
242
|
+
previous_response_id=previous_response_id,
|
|
243
|
+
tool_results=tool_results,
|
|
244
|
+
),
|
|
204
245
|
tools=normalized_tools,
|
|
205
246
|
tool_results=tool_results,
|
|
247
|
+
response_format=response_format,
|
|
248
|
+
strict_output=effective_strict_output,
|
|
206
249
|
reasoning={"effort": reasoning_effort},
|
|
207
250
|
previous_response_id=previous_response_id,
|
|
208
251
|
temperature=temperature,
|
|
@@ -237,9 +280,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
237
280
|
temperature: float | None = None,
|
|
238
281
|
top_p: float | None = None,
|
|
239
282
|
max_output_tokens: int | None = None,
|
|
283
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
284
|
+
strict_output: bool | None = None,
|
|
240
285
|
) -> AsyncIterator[str]:
|
|
241
286
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
242
287
|
normalized_tools, tools_by_name = self._normalize_tools(tools)
|
|
288
|
+
response_format, effective_strict_output = self._resolve_structured_output_options(
|
|
289
|
+
output_schema=output_schema,
|
|
290
|
+
strict_output=strict_output,
|
|
291
|
+
)
|
|
243
292
|
|
|
244
293
|
previous_response_id: str | None = None
|
|
245
294
|
tool_results: list[ToolResult] | None = None
|
|
@@ -249,9 +298,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
249
298
|
round_response_id: str | None = previous_response_id
|
|
250
299
|
events = await self._engine.agenerate_stream(
|
|
251
300
|
model=self._resolve_model(model),
|
|
252
|
-
messages=
|
|
301
|
+
messages=self._messages_for_round(
|
|
302
|
+
messages=messages,
|
|
303
|
+
previous_response_id=previous_response_id,
|
|
304
|
+
tool_results=tool_results,
|
|
305
|
+
),
|
|
253
306
|
tools=normalized_tools,
|
|
254
307
|
tool_results=tool_results,
|
|
308
|
+
response_format=response_format,
|
|
309
|
+
strict_output=effective_strict_output,
|
|
255
310
|
reasoning={"effort": reasoning_effort},
|
|
256
311
|
previous_response_id=previous_response_id,
|
|
257
312
|
temperature=temperature,
|
|
@@ -278,6 +333,59 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
278
333
|
def _resolve_model(self, model: str | None) -> str:
|
|
279
334
|
return model or self.default_model
|
|
280
335
|
|
|
336
|
+
def _resolve_structured_output_options(
|
|
337
|
+
self,
|
|
338
|
+
*,
|
|
339
|
+
output_schema: StructuredOutputSchema | None,
|
|
340
|
+
strict_output: bool | None,
|
|
341
|
+
) -> tuple[dict[str, Any] | None, bool]:
|
|
342
|
+
effective_strict_output = strict_output if strict_output is not None else bool(output_schema)
|
|
343
|
+
if output_schema is None:
|
|
344
|
+
return None, effective_strict_output
|
|
345
|
+
return build_strict_response_format(output_schema), effective_strict_output
|
|
346
|
+
|
|
347
|
+
def _parse_structured_output_text(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
text: str,
|
|
351
|
+
output_schema: StructuredOutputSchema,
|
|
352
|
+
) -> dict[str, Any]:
|
|
353
|
+
try:
|
|
354
|
+
parsed = json.loads(text)
|
|
355
|
+
except json.JSONDecodeError as exc:
|
|
356
|
+
raise ValueError("structured output is not valid JSON") from exc
|
|
357
|
+
|
|
358
|
+
if not isinstance(parsed, dict):
|
|
359
|
+
raise TypeError("structured output must be a JSON object")
|
|
360
|
+
|
|
361
|
+
model_type = self._resolve_pydantic_model_type(output_schema)
|
|
362
|
+
if model_type is None:
|
|
363
|
+
return parsed
|
|
364
|
+
validated = model_type.model_validate(parsed, strict=True)
|
|
365
|
+
return validated.model_dump(mode="json")
|
|
366
|
+
|
|
367
|
+
def _is_tool_continuation_round(
|
|
368
|
+
self,
|
|
369
|
+
*,
|
|
370
|
+
previous_response_id: str | None,
|
|
371
|
+
tool_results: list[ToolResult] | None,
|
|
372
|
+
) -> bool:
|
|
373
|
+
return bool(previous_response_id) and bool(tool_results)
|
|
374
|
+
|
|
375
|
+
def _messages_for_round(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
messages: list[Message],
|
|
379
|
+
previous_response_id: str | None,
|
|
380
|
+
tool_results: list[ToolResult] | None,
|
|
381
|
+
) -> list[Message]:
|
|
382
|
+
if self._is_tool_continuation_round(
|
|
383
|
+
previous_response_id=previous_response_id,
|
|
384
|
+
tool_results=tool_results,
|
|
385
|
+
):
|
|
386
|
+
return []
|
|
387
|
+
return messages
|
|
388
|
+
|
|
281
389
|
def _build_messages(
|
|
282
390
|
self,
|
|
283
391
|
*,
|
|
@@ -199,7 +199,15 @@ class OAuthCodexClient:
|
|
|
199
199
|
) -> str | GenerateResult:
|
|
200
200
|
self._require_responses_mode(api_mode)
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
allows_empty_messages = self._is_tool_continuation_request(
|
|
203
|
+
previous_response_id=previous_response_id,
|
|
204
|
+
tool_results=tool_results,
|
|
205
|
+
)
|
|
206
|
+
normalized_messages = self._normalize_messages(
|
|
207
|
+
prompt=prompt,
|
|
208
|
+
messages=messages,
|
|
209
|
+
allow_empty_messages=allows_empty_messages,
|
|
210
|
+
)
|
|
203
211
|
normalized_tools = normalize_tool_inputs(tools)
|
|
204
212
|
normalized_tool_results = self._normalize_tool_results(tool_results)
|
|
205
213
|
|
|
@@ -276,7 +284,15 @@ class OAuthCodexClient:
|
|
|
276
284
|
) -> str | GenerateResult:
|
|
277
285
|
self._require_responses_mode(api_mode)
|
|
278
286
|
|
|
279
|
-
|
|
287
|
+
allows_empty_messages = self._is_tool_continuation_request(
|
|
288
|
+
previous_response_id=previous_response_id,
|
|
289
|
+
tool_results=tool_results,
|
|
290
|
+
)
|
|
291
|
+
normalized_messages = self._normalize_messages(
|
|
292
|
+
prompt=prompt,
|
|
293
|
+
messages=messages,
|
|
294
|
+
allow_empty_messages=allows_empty_messages,
|
|
295
|
+
)
|
|
280
296
|
normalized_tools = normalize_tool_inputs(tools)
|
|
281
297
|
normalized_tool_results = self._normalize_tool_results(tool_results)
|
|
282
298
|
|
|
@@ -353,7 +369,15 @@ class OAuthCodexClient:
|
|
|
353
369
|
) -> Iterator[str] | Iterator[StreamEvent]:
|
|
354
370
|
self._require_responses_mode(api_mode)
|
|
355
371
|
|
|
356
|
-
|
|
372
|
+
allows_empty_messages = self._is_tool_continuation_request(
|
|
373
|
+
previous_response_id=previous_response_id,
|
|
374
|
+
tool_results=tool_results,
|
|
375
|
+
)
|
|
376
|
+
normalized_messages = self._normalize_messages(
|
|
377
|
+
prompt=prompt,
|
|
378
|
+
messages=messages,
|
|
379
|
+
allow_empty_messages=allows_empty_messages,
|
|
380
|
+
)
|
|
357
381
|
normalized_tools = normalize_tool_inputs(tools)
|
|
358
382
|
normalized_tool_results = self._normalize_tool_results(tool_results)
|
|
359
383
|
|
|
@@ -426,7 +450,15 @@ class OAuthCodexClient:
|
|
|
426
450
|
) -> AsyncIterator[str] | AsyncIterator[StreamEvent]:
|
|
427
451
|
self._require_responses_mode(api_mode)
|
|
428
452
|
|
|
429
|
-
|
|
453
|
+
allows_empty_messages = self._is_tool_continuation_request(
|
|
454
|
+
previous_response_id=previous_response_id,
|
|
455
|
+
tool_results=tool_results,
|
|
456
|
+
)
|
|
457
|
+
normalized_messages = self._normalize_messages(
|
|
458
|
+
prompt=prompt,
|
|
459
|
+
messages=messages,
|
|
460
|
+
allow_empty_messages=allows_empty_messages,
|
|
461
|
+
)
|
|
430
462
|
normalized_tools = normalize_tool_inputs(tools)
|
|
431
463
|
normalized_tool_results = self._normalize_tool_results(tool_results)
|
|
432
464
|
|
|
@@ -1392,6 +1424,7 @@ class OAuthCodexClient:
|
|
|
1392
1424
|
*,
|
|
1393
1425
|
prompt: str | None,
|
|
1394
1426
|
messages: list[Message] | None,
|
|
1427
|
+
allow_empty_messages: bool = False,
|
|
1395
1428
|
) -> list[Message]:
|
|
1396
1429
|
if (prompt is None and messages is None) or (prompt is not None and messages is not None):
|
|
1397
1430
|
raise ValueError("Provide exactly one of `prompt` or `messages`")
|
|
@@ -1399,10 +1432,20 @@ class OAuthCodexClient:
|
|
|
1399
1432
|
if prompt is not None:
|
|
1400
1433
|
return [{"role": "user", "content": prompt}]
|
|
1401
1434
|
|
|
1402
|
-
if not isinstance(messages, list)
|
|
1435
|
+
if not isinstance(messages, list):
|
|
1436
|
+
raise ValueError("`messages` must be a non-empty list")
|
|
1437
|
+
if not messages and not allow_empty_messages:
|
|
1403
1438
|
raise ValueError("`messages` must be a non-empty list")
|
|
1404
1439
|
return [dict(item) for item in messages]
|
|
1405
1440
|
|
|
1441
|
+
def _is_tool_continuation_request(
|
|
1442
|
+
self,
|
|
1443
|
+
*,
|
|
1444
|
+
previous_response_id: str | None,
|
|
1445
|
+
tool_results: list[ToolResult] | None,
|
|
1446
|
+
) -> bool:
|
|
1447
|
+
return bool(previous_response_id) and bool(tool_results)
|
|
1448
|
+
|
|
1406
1449
|
def _normalize_tool_results(
|
|
1407
1450
|
self,
|
|
1408
1451
|
tool_results: list[ToolResult] | None,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__title__ = "oauth-codex"
|
|
2
|
-
__version__ = "2.0
|
|
2
|
+
__version__ = "2.2.0"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import inspect
|
|
4
5
|
import json
|
|
5
6
|
from types import UnionType
|
|
@@ -30,6 +31,200 @@ def _pydantic_model_to_schema(model_type: type[Any]) -> dict[str, Any]:
|
|
|
30
31
|
return {"type": "object"}
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
def _resolve_json_pointer_ref(*, root: dict[str, Any], ref: str) -> dict[str, Any]:
|
|
35
|
+
if not ref.startswith("#/"):
|
|
36
|
+
raise ValueError(f"Only local JSON pointer refs are supported, got: {ref}")
|
|
37
|
+
|
|
38
|
+
target: Any = root
|
|
39
|
+
for raw_part in ref[2:].split("/"):
|
|
40
|
+
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
41
|
+
if not isinstance(target, dict) or part not in target:
|
|
42
|
+
raise ValueError(f"Unresolvable JSON pointer ref: {ref}")
|
|
43
|
+
target = target[part]
|
|
44
|
+
|
|
45
|
+
if not isinstance(target, dict):
|
|
46
|
+
raise ValueError(f"JSON pointer ref must resolve to an object schema: {ref}")
|
|
47
|
+
return copy.deepcopy(target)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ensure_strict_json_schema(
|
|
51
|
+
json_schema: dict[str, Any],
|
|
52
|
+
*,
|
|
53
|
+
path: tuple[str, ...] = (),
|
|
54
|
+
root: dict[str, Any] | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
if not isinstance(json_schema, dict):
|
|
57
|
+
raise TypeError(f"Expected dict at path={path!r}, got {type(json_schema).__name__}")
|
|
58
|
+
|
|
59
|
+
if root is None:
|
|
60
|
+
root = json_schema
|
|
61
|
+
|
|
62
|
+
defs = json_schema.get("$defs")
|
|
63
|
+
if defs is not None:
|
|
64
|
+
if not isinstance(defs, dict):
|
|
65
|
+
raise TypeError(f"Expected $defs to be a dict at path={path!r}")
|
|
66
|
+
for def_name, def_schema in defs.items():
|
|
67
|
+
if not isinstance(def_schema, dict):
|
|
68
|
+
raise TypeError(f"Expected object schema in $defs[{def_name!r}]")
|
|
69
|
+
_ensure_strict_json_schema(
|
|
70
|
+
def_schema,
|
|
71
|
+
path=(*path, "$defs", str(def_name)),
|
|
72
|
+
root=root,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
definitions = json_schema.get("definitions")
|
|
76
|
+
if definitions is not None:
|
|
77
|
+
if not isinstance(definitions, dict):
|
|
78
|
+
raise TypeError(f"Expected definitions to be a dict at path={path!r}")
|
|
79
|
+
for definition_name, definition_schema in definitions.items():
|
|
80
|
+
if not isinstance(definition_schema, dict):
|
|
81
|
+
raise TypeError(f"Expected object schema in definitions[{definition_name!r}]")
|
|
82
|
+
_ensure_strict_json_schema(
|
|
83
|
+
definition_schema,
|
|
84
|
+
path=(*path, "definitions", str(definition_name)),
|
|
85
|
+
root=root,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if json_schema.get("type") == "object" and "additionalProperties" not in json_schema:
|
|
89
|
+
json_schema["additionalProperties"] = False
|
|
90
|
+
|
|
91
|
+
properties = json_schema.get("properties")
|
|
92
|
+
if properties is not None:
|
|
93
|
+
if not isinstance(properties, dict):
|
|
94
|
+
raise TypeError(f"Expected properties to be a dict at path={path!r}")
|
|
95
|
+
json_schema["required"] = list(properties.keys())
|
|
96
|
+
json_schema["properties"] = {
|
|
97
|
+
key: _ensure_strict_json_schema(
|
|
98
|
+
prop_schema,
|
|
99
|
+
path=(*path, "properties", str(key)),
|
|
100
|
+
root=root,
|
|
101
|
+
)
|
|
102
|
+
for key, prop_schema in properties.items()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
items = json_schema.get("items")
|
|
106
|
+
if items is not None:
|
|
107
|
+
if isinstance(items, dict):
|
|
108
|
+
json_schema["items"] = _ensure_strict_json_schema(
|
|
109
|
+
items,
|
|
110
|
+
path=(*path, "items"),
|
|
111
|
+
root=root,
|
|
112
|
+
)
|
|
113
|
+
elif isinstance(items, list):
|
|
114
|
+
json_schema["items"] = [
|
|
115
|
+
_ensure_strict_json_schema(
|
|
116
|
+
entry,
|
|
117
|
+
path=(*path, "items", str(i)),
|
|
118
|
+
root=root,
|
|
119
|
+
)
|
|
120
|
+
for i, entry in enumerate(items)
|
|
121
|
+
]
|
|
122
|
+
else:
|
|
123
|
+
raise TypeError(f"Expected items to be a dict or list at path={path!r}")
|
|
124
|
+
|
|
125
|
+
any_of = json_schema.get("anyOf")
|
|
126
|
+
if any_of is not None:
|
|
127
|
+
if not isinstance(any_of, list):
|
|
128
|
+
raise TypeError(f"Expected anyOf to be a list at path={path!r}")
|
|
129
|
+
json_schema["anyOf"] = [
|
|
130
|
+
_ensure_strict_json_schema(
|
|
131
|
+
variant,
|
|
132
|
+
path=(*path, "anyOf", str(i)),
|
|
133
|
+
root=root,
|
|
134
|
+
)
|
|
135
|
+
for i, variant in enumerate(any_of)
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
all_of = json_schema.get("allOf")
|
|
139
|
+
if all_of is not None:
|
|
140
|
+
if not isinstance(all_of, list):
|
|
141
|
+
raise TypeError(f"Expected allOf to be a list at path={path!r}")
|
|
142
|
+
if len(all_of) == 1:
|
|
143
|
+
entry = _ensure_strict_json_schema(
|
|
144
|
+
all_of[0],
|
|
145
|
+
path=(*path, "allOf", "0"),
|
|
146
|
+
root=root,
|
|
147
|
+
)
|
|
148
|
+
json_schema.update(entry)
|
|
149
|
+
json_schema.pop("allOf", None)
|
|
150
|
+
else:
|
|
151
|
+
json_schema["allOf"] = [
|
|
152
|
+
_ensure_strict_json_schema(
|
|
153
|
+
entry,
|
|
154
|
+
path=(*path, "allOf", str(i)),
|
|
155
|
+
root=root,
|
|
156
|
+
)
|
|
157
|
+
for i, entry in enumerate(all_of)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
if json_schema.get("default", object()) is None:
|
|
161
|
+
json_schema.pop("default", None)
|
|
162
|
+
|
|
163
|
+
ref = json_schema.get("$ref")
|
|
164
|
+
if ref is not None and len(json_schema) > 1:
|
|
165
|
+
if not isinstance(ref, str):
|
|
166
|
+
raise TypeError(f"Expected $ref to be a string at path={path!r}")
|
|
167
|
+
resolved = _resolve_json_pointer_ref(root=root, ref=ref)
|
|
168
|
+
json_schema.update({**resolved, **json_schema})
|
|
169
|
+
json_schema.pop("$ref", None)
|
|
170
|
+
return _ensure_strict_json_schema(json_schema, path=path, root=root)
|
|
171
|
+
|
|
172
|
+
return json_schema
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_strict_response_format(output_schema: type[Any] | dict[str, Any]) -> dict[str, Any]:
|
|
176
|
+
name = "output"
|
|
177
|
+
description: str | None = None
|
|
178
|
+
|
|
179
|
+
if _is_pydantic_model_type(output_schema):
|
|
180
|
+
model_type = output_schema
|
|
181
|
+
schema = _pydantic_model_to_schema(model_type)
|
|
182
|
+
name = getattr(model_type, "__name__", "output") or "output"
|
|
183
|
+
elif isinstance(output_schema, dict):
|
|
184
|
+
schema: dict[str, Any]
|
|
185
|
+
if output_schema.get("type") == "json_schema":
|
|
186
|
+
nested = output_schema.get("json_schema")
|
|
187
|
+
if nested is not None:
|
|
188
|
+
if not isinstance(nested, dict):
|
|
189
|
+
raise TypeError("response format json_schema payload must be a dictionary")
|
|
190
|
+
schema = nested.get("schema")
|
|
191
|
+
if not isinstance(schema, dict):
|
|
192
|
+
raise TypeError("response format json_schema.schema must be a dictionary")
|
|
193
|
+
name = str(nested.get("name") or output_schema.get("name") or "output")
|
|
194
|
+
nested_description = nested.get("description")
|
|
195
|
+
root_description = output_schema.get("description")
|
|
196
|
+
if isinstance(nested_description, str):
|
|
197
|
+
description = nested_description
|
|
198
|
+
elif isinstance(root_description, str):
|
|
199
|
+
description = root_description
|
|
200
|
+
else:
|
|
201
|
+
schema = output_schema.get("schema")
|
|
202
|
+
if not isinstance(schema, dict):
|
|
203
|
+
raise TypeError("response format schema must be a dictionary")
|
|
204
|
+
name = str(output_schema.get("name") or "output")
|
|
205
|
+
root_description = output_schema.get("description")
|
|
206
|
+
if isinstance(root_description, str):
|
|
207
|
+
description = root_description
|
|
208
|
+
else:
|
|
209
|
+
schema = output_schema
|
|
210
|
+
else:
|
|
211
|
+
raise TypeError("output_schema must be a pydantic model type or a dictionary")
|
|
212
|
+
|
|
213
|
+
strict_schema = _ensure_strict_json_schema(copy.deepcopy(schema))
|
|
214
|
+
if strict_schema.get("type") != "object":
|
|
215
|
+
raise ValueError("structured output schema root must be a JSON object")
|
|
216
|
+
|
|
217
|
+
response_format: dict[str, Any] = {
|
|
218
|
+
"type": "json_schema",
|
|
219
|
+
"name": name,
|
|
220
|
+
"schema": strict_schema,
|
|
221
|
+
"strict": True,
|
|
222
|
+
}
|
|
223
|
+
if description:
|
|
224
|
+
response_format["description"] = description
|
|
225
|
+
return response_format
|
|
226
|
+
|
|
227
|
+
|
|
33
228
|
def _python_type_to_schema(annotation: Any) -> dict[str, Any]:
|
|
34
229
|
if annotation is inspect._empty:
|
|
35
230
|
return {"type": "string"}
|
|
@@ -199,6 +394,7 @@ def to_responses_tools(
|
|
|
199
394
|
"parameters": dict(tool.get("parameters", {"type": "object", "properties": {}})),
|
|
200
395
|
}
|
|
201
396
|
if strict_output:
|
|
397
|
+
item["parameters"] = _ensure_strict_json_schema(copy.deepcopy(item["parameters"]))
|
|
202
398
|
item["strict"] = True
|
|
203
399
|
normalized.append(item)
|
|
204
400
|
return normalized
|