oauth-codex 2.1.0__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.1.0 → oauth_codex-2.2.0}/PKG-INFO +39 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/README.md +38 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/pyproject.toml +1 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_client.py +75 -5
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_version.py +1 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/tooling.py +196 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/PKG-INFO +39 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/SOURCES.txt +2 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/tests/test_generate_async.py +111 -1
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/tests/test_generate_sync.py +136 -1
- oauth_codex-2.2.0/tests/test_tooling_strict_schema.py +130 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/setup.cfg +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_base_client.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_engine.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_exceptions.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_models.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_module_client.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_resource.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/_types.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/auth/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/auth/config.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/auth/pkce.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/auth/store.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/auth/token_manager.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/compat_store.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/core_types.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/errors.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/py.typed +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/_wrappers.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/files.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/models.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/_helpers.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/input_tokens.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/responses/responses.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/file_batches.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/files.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/vector_stores.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/store.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/file_deleted.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/file_object.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/input_token_count_response.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/response.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/response_stream_event.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/model_capabilities.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/shared/usage.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/__init__.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_deleted.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_file.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_file_batch.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_search_response.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/version.py +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/dependency_links.txt +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/requires.txt +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex.egg-info/top_level.txt +0 -0
- {oauth_codex-2.1.0 → oauth_codex-2.2.0}/tests/test_engine_stream_and_continuity.py +0 -0
- {oauth_codex-2.1.0 → 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.
|
|
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
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
|
|
@@ -114,6 +121,8 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
114
121
|
),
|
|
115
122
|
tools=normalized_tools,
|
|
116
123
|
tool_results=tool_results,
|
|
124
|
+
response_format=response_format,
|
|
125
|
+
strict_output=effective_strict_output,
|
|
117
126
|
reasoning={"effort": reasoning_effort},
|
|
118
127
|
previous_response_id=previous_response_id,
|
|
119
128
|
temperature=temperature,
|
|
@@ -128,7 +137,10 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
128
137
|
output_parts.append(result.text)
|
|
129
138
|
|
|
130
139
|
if not result.tool_calls:
|
|
131
|
-
|
|
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)
|
|
132
144
|
|
|
133
145
|
tool_results = self._execute_tool_calls_sync(result.tool_calls, tools_by_name)
|
|
134
146
|
previous_response_id = result.response_id
|
|
@@ -146,9 +158,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
146
158
|
temperature: float | None = None,
|
|
147
159
|
top_p: float | None = None,
|
|
148
160
|
max_output_tokens: int | None = None,
|
|
149
|
-
|
|
161
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
162
|
+
strict_output: bool | None = None,
|
|
163
|
+
) -> str | dict[str, Any]:
|
|
150
164
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
151
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
|
+
)
|
|
152
170
|
|
|
153
171
|
previous_response_id: str | None = None
|
|
154
172
|
tool_results: list[ToolResult] | None = None
|
|
@@ -164,6 +182,8 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
164
182
|
),
|
|
165
183
|
tools=normalized_tools,
|
|
166
184
|
tool_results=tool_results,
|
|
185
|
+
response_format=response_format,
|
|
186
|
+
strict_output=effective_strict_output,
|
|
167
187
|
reasoning={"effort": reasoning_effort},
|
|
168
188
|
previous_response_id=previous_response_id,
|
|
169
189
|
temperature=temperature,
|
|
@@ -178,7 +198,10 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
178
198
|
output_parts.append(result.text)
|
|
179
199
|
|
|
180
200
|
if not result.tool_calls:
|
|
181
|
-
|
|
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)
|
|
182
205
|
|
|
183
206
|
tool_results = await self._execute_tool_calls_async(result.tool_calls, tools_by_name)
|
|
184
207
|
previous_response_id = result.response_id
|
|
@@ -196,9 +219,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
196
219
|
temperature: float | None = None,
|
|
197
220
|
top_p: float | None = None,
|
|
198
221
|
max_output_tokens: int | None = None,
|
|
222
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
223
|
+
strict_output: bool | None = None,
|
|
199
224
|
) -> Iterator[str]:
|
|
200
225
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
201
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
|
+
)
|
|
202
231
|
|
|
203
232
|
previous_response_id: str | None = None
|
|
204
233
|
tool_results: list[ToolResult] | None = None
|
|
@@ -215,6 +244,8 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
215
244
|
),
|
|
216
245
|
tools=normalized_tools,
|
|
217
246
|
tool_results=tool_results,
|
|
247
|
+
response_format=response_format,
|
|
248
|
+
strict_output=effective_strict_output,
|
|
218
249
|
reasoning={"effort": reasoning_effort},
|
|
219
250
|
previous_response_id=previous_response_id,
|
|
220
251
|
temperature=temperature,
|
|
@@ -249,9 +280,15 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
249
280
|
temperature: float | None = None,
|
|
250
281
|
top_p: float | None = None,
|
|
251
282
|
max_output_tokens: int | None = None,
|
|
283
|
+
output_schema: StructuredOutputSchema | None = None,
|
|
284
|
+
strict_output: bool | None = None,
|
|
252
285
|
) -> AsyncIterator[str]:
|
|
253
286
|
messages = self._build_messages(prompt=prompt, images=images)
|
|
254
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
|
+
)
|
|
255
292
|
|
|
256
293
|
previous_response_id: str | None = None
|
|
257
294
|
tool_results: list[ToolResult] | None = None
|
|
@@ -268,6 +305,8 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
268
305
|
),
|
|
269
306
|
tools=normalized_tools,
|
|
270
307
|
tool_results=tool_results,
|
|
308
|
+
response_format=response_format,
|
|
309
|
+
strict_output=effective_strict_output,
|
|
271
310
|
reasoning={"effort": reasoning_effort},
|
|
272
311
|
previous_response_id=previous_response_id,
|
|
273
312
|
temperature=temperature,
|
|
@@ -294,6 +333,37 @@ class OAuthCodexClient(SyncAPIClient):
|
|
|
294
333
|
def _resolve_model(self, model: str | None) -> str:
|
|
295
334
|
return model or self.default_model
|
|
296
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
|
+
|
|
297
367
|
def _is_tool_continuation_round(
|
|
298
368
|
self,
|
|
299
369
|
*,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__title__ = "oauth-codex"
|
|
2
|
-
__version__ = "2.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oauth-codex
|
|
3
|
-
Version: 2.
|
|
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
|
|
@@ -58,4 +58,5 @@ src/oauth_codex/types/vector_stores/vector_store_search_response.py
|
|
|
58
58
|
tests/test_engine_stream_and_continuity.py
|
|
59
59
|
tests/test_generate_async.py
|
|
60
60
|
tests/test_generate_sync.py
|
|
61
|
-
tests/test_public_surface.py
|
|
61
|
+
tests/test_public_surface.py
|
|
62
|
+
tests/test_tooling_strict_schema.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
from pydantic import BaseModel
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
5
|
|
|
6
6
|
from conftest import InMemoryTokenStore
|
|
7
7
|
from oauth_codex import Client
|
|
@@ -12,6 +12,11 @@ class ToolInput(BaseModel):
|
|
|
12
12
|
query: str
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class StructuredOutput(BaseModel):
|
|
16
|
+
answer: str
|
|
17
|
+
count: int
|
|
18
|
+
|
|
19
|
+
|
|
15
20
|
def _client() -> Client:
|
|
16
21
|
return Client(
|
|
17
22
|
token_store=InMemoryTokenStore(
|
|
@@ -117,3 +122,108 @@ async def test_agenerate_supports_single_pydantic_tool_input(monkeypatch: pytest
|
|
|
117
122
|
assert out == "done"
|
|
118
123
|
tool_results = calls[1]["tool_results"]
|
|
119
124
|
assert tool_results[0].output == {"output": "Tool received query: hello"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_agenerate_supports_structured_output_with_pydantic_schema(
|
|
129
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
130
|
+
) -> None:
|
|
131
|
+
client = _client()
|
|
132
|
+
captured: dict[str, object] = {}
|
|
133
|
+
|
|
134
|
+
async def fake_agenerate(**kwargs):
|
|
135
|
+
captured.update(kwargs)
|
|
136
|
+
return GenerateResult(
|
|
137
|
+
text='{"answer":"ok","count":1}',
|
|
138
|
+
tool_calls=[],
|
|
139
|
+
finish_reason="stop",
|
|
140
|
+
response_id="resp_1",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
|
|
144
|
+
|
|
145
|
+
out = await client.agenerate("return json", output_schema=StructuredOutput)
|
|
146
|
+
|
|
147
|
+
assert out == {"answer": "ok", "count": 1}
|
|
148
|
+
response_format = captured["response_format"]
|
|
149
|
+
assert response_format["type"] == "json_schema"
|
|
150
|
+
assert response_format["name"] == "StructuredOutput"
|
|
151
|
+
assert response_format["strict"] is True
|
|
152
|
+
assert response_format["schema"]["required"] == ["answer", "count"]
|
|
153
|
+
assert captured["strict_output"] is True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
async def test_agenerate_structured_output_rejects_invalid_json(
|
|
158
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
159
|
+
) -> None:
|
|
160
|
+
client = _client()
|
|
161
|
+
|
|
162
|
+
async def fake_agenerate(**kwargs):
|
|
163
|
+
_ = kwargs
|
|
164
|
+
return GenerateResult(
|
|
165
|
+
text="not-json",
|
|
166
|
+
tool_calls=[],
|
|
167
|
+
finish_reason="stop",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
|
|
171
|
+
|
|
172
|
+
with pytest.raises(ValueError, match="valid JSON"):
|
|
173
|
+
await client.agenerate(
|
|
174
|
+
"return json",
|
|
175
|
+
output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_agenerate_structured_output_enforces_pydantic_strict_validation(
|
|
181
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
182
|
+
) -> None:
|
|
183
|
+
client = _client()
|
|
184
|
+
|
|
185
|
+
async def fake_agenerate(**kwargs):
|
|
186
|
+
_ = kwargs
|
|
187
|
+
return GenerateResult(
|
|
188
|
+
text='{"answer":"ok","count":"1"}',
|
|
189
|
+
tool_calls=[],
|
|
190
|
+
finish_reason="stop",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr(client._engine, "agenerate", fake_agenerate)
|
|
194
|
+
|
|
195
|
+
with pytest.raises(ValidationError):
|
|
196
|
+
await client.agenerate("return json", output_schema=StructuredOutput)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_astream_accepts_output_schema_and_keeps_text_stream(
|
|
201
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
202
|
+
) -> None:
|
|
203
|
+
client = _client()
|
|
204
|
+
calls: list[dict[str, object]] = []
|
|
205
|
+
|
|
206
|
+
async def fake_astream(**kwargs):
|
|
207
|
+
calls.append(kwargs)
|
|
208
|
+
|
|
209
|
+
async def events():
|
|
210
|
+
yield StreamEvent(type="text_delta", delta="{", response_id="resp_1")
|
|
211
|
+
yield StreamEvent(type="text_delta", delta='"ok":true}', response_id="resp_1")
|
|
212
|
+
yield StreamEvent(type="done", response_id="resp_1")
|
|
213
|
+
|
|
214
|
+
return events()
|
|
215
|
+
|
|
216
|
+
monkeypatch.setattr(client._engine, "agenerate_stream", fake_astream)
|
|
217
|
+
|
|
218
|
+
out: list[str] = []
|
|
219
|
+
async for delta in client.astream(
|
|
220
|
+
"return json",
|
|
221
|
+
output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
|
|
222
|
+
):
|
|
223
|
+
out.append(delta)
|
|
224
|
+
|
|
225
|
+
assert out == ["{", '"ok":true}']
|
|
226
|
+
assert calls[0]["strict_output"] is True
|
|
227
|
+
response_format = calls[0]["response_format"]
|
|
228
|
+
assert response_format["type"] == "json_schema"
|
|
229
|
+
assert response_format["strict"] is True
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
4
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
5
5
|
|
|
6
6
|
from conftest import InMemoryTokenStore
|
|
7
7
|
from oauth_codex import Client
|
|
@@ -16,6 +16,11 @@ class ToolInput(BaseModel):
|
|
|
16
16
|
query: str
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class StructuredOutput(BaseModel):
|
|
20
|
+
answer: str
|
|
21
|
+
count: int
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
def _client() -> Client:
|
|
20
25
|
return Client(
|
|
21
26
|
token_store=InMemoryTokenStore(
|
|
@@ -277,6 +282,108 @@ def test_generate_raises_when_tool_round_limit_exceeded(monkeypatch: pytest.Monk
|
|
|
277
282
|
client.generate("loop", tools=[loop])
|
|
278
283
|
|
|
279
284
|
|
|
285
|
+
def test_generate_supports_structured_output_with_pydantic_schema(
|
|
286
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
287
|
+
) -> None:
|
|
288
|
+
client = _client()
|
|
289
|
+
captured: dict[str, object] = {}
|
|
290
|
+
|
|
291
|
+
def fake_generate(**kwargs):
|
|
292
|
+
captured.update(kwargs)
|
|
293
|
+
return GenerateResult(
|
|
294
|
+
text='{"answer":"ok","count":1}',
|
|
295
|
+
tool_calls=[],
|
|
296
|
+
finish_reason="stop",
|
|
297
|
+
response_id="resp_1",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
monkeypatch.setattr(client._engine, "generate", fake_generate)
|
|
301
|
+
|
|
302
|
+
out = client.generate("return json", output_schema=StructuredOutput)
|
|
303
|
+
|
|
304
|
+
assert out == {"answer": "ok", "count": 1}
|
|
305
|
+
response_format = captured["response_format"]
|
|
306
|
+
assert response_format["type"] == "json_schema"
|
|
307
|
+
assert response_format["name"] == "StructuredOutput"
|
|
308
|
+
assert response_format["strict"] is True
|
|
309
|
+
assert response_format["schema"]["type"] == "object"
|
|
310
|
+
assert response_format["schema"]["additionalProperties"] is False
|
|
311
|
+
assert response_format["schema"]["required"] == ["answer", "count"]
|
|
312
|
+
assert captured["strict_output"] is True
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_generate_supports_structured_output_with_raw_schema_dict(
|
|
316
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
317
|
+
) -> None:
|
|
318
|
+
client = _client()
|
|
319
|
+
captured: dict[str, object] = {}
|
|
320
|
+
|
|
321
|
+
def fake_generate(**kwargs):
|
|
322
|
+
captured.update(kwargs)
|
|
323
|
+
return GenerateResult(
|
|
324
|
+
text='{"ok":true}',
|
|
325
|
+
tool_calls=[],
|
|
326
|
+
finish_reason="stop",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
monkeypatch.setattr(client._engine, "generate", fake_generate)
|
|
330
|
+
|
|
331
|
+
out = client.generate(
|
|
332
|
+
"return json",
|
|
333
|
+
output_schema={
|
|
334
|
+
"type": "object",
|
|
335
|
+
"properties": {"ok": {"type": "boolean"}},
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
assert out == {"ok": True}
|
|
340
|
+
response_format = captured["response_format"]
|
|
341
|
+
assert response_format["type"] == "json_schema"
|
|
342
|
+
assert response_format["name"] == "output"
|
|
343
|
+
assert response_format["strict"] is True
|
|
344
|
+
assert response_format["schema"]["required"] == ["ok"]
|
|
345
|
+
assert response_format["schema"]["additionalProperties"] is False
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_generate_structured_output_rejects_invalid_json(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
349
|
+
client = _client()
|
|
350
|
+
|
|
351
|
+
def fake_generate(**kwargs):
|
|
352
|
+
_ = kwargs
|
|
353
|
+
return GenerateResult(
|
|
354
|
+
text="not-json",
|
|
355
|
+
tool_calls=[],
|
|
356
|
+
finish_reason="stop",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
monkeypatch.setattr(client._engine, "generate", fake_generate)
|
|
360
|
+
|
|
361
|
+
with pytest.raises(ValueError, match="valid JSON"):
|
|
362
|
+
client.generate(
|
|
363
|
+
"return json",
|
|
364
|
+
output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_generate_structured_output_enforces_pydantic_strict_validation(
|
|
369
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
370
|
+
) -> None:
|
|
371
|
+
client = _client()
|
|
372
|
+
|
|
373
|
+
def fake_generate(**kwargs):
|
|
374
|
+
_ = kwargs
|
|
375
|
+
return GenerateResult(
|
|
376
|
+
text='{"answer":"ok","count":"1"}',
|
|
377
|
+
tool_calls=[],
|
|
378
|
+
finish_reason="stop",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
monkeypatch.setattr(client._engine, "generate", fake_generate)
|
|
382
|
+
|
|
383
|
+
with pytest.raises(ValidationError):
|
|
384
|
+
client.generate("return json", output_schema=StructuredOutput)
|
|
385
|
+
|
|
386
|
+
|
|
280
387
|
def test_stream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
281
388
|
client = _client()
|
|
282
389
|
calls: list[dict[str, object]] = []
|
|
@@ -308,3 +415,31 @@ def test_stream_supports_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
308
415
|
assert calls[1]["messages"] == []
|
|
309
416
|
tool_results = calls[1]["tool_results"]
|
|
310
417
|
assert tool_results[0].output == {"sum": 3}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_stream_accepts_output_schema_and_keeps_text_stream(
|
|
421
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
422
|
+
) -> None:
|
|
423
|
+
client = _client()
|
|
424
|
+
calls: list[dict[str, object]] = []
|
|
425
|
+
|
|
426
|
+
def fake_stream(**kwargs):
|
|
427
|
+
calls.append(kwargs)
|
|
428
|
+
yield StreamEvent(type="text_delta", delta="{", response_id="resp_1")
|
|
429
|
+
yield StreamEvent(type="text_delta", delta='"ok":true}', response_id="resp_1")
|
|
430
|
+
yield StreamEvent(type="done", response_id="resp_1")
|
|
431
|
+
|
|
432
|
+
monkeypatch.setattr(client._engine, "generate_stream", fake_stream)
|
|
433
|
+
|
|
434
|
+
out = list(
|
|
435
|
+
client.stream(
|
|
436
|
+
"return json",
|
|
437
|
+
output_schema={"type": "object", "properties": {"ok": {"type": "boolean"}}},
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
assert out == ["{", '"ok":true}']
|
|
442
|
+
assert calls[0]["strict_output"] is True
|
|
443
|
+
response_format = calls[0]["response_format"]
|
|
444
|
+
assert response_format["type"] == "json_schema"
|
|
445
|
+
assert response_format["strict"] is True
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from oauth_codex.tooling import _ensure_strict_json_schema, build_strict_response_format, to_responses_tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_build_strict_response_format_normalizes_recursive_object_schema() -> None:
|
|
9
|
+
response_format = build_strict_response_format(
|
|
10
|
+
{
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"nested": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"value": {"type": "string"},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"name": {"type": "string"},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"choice": {
|
|
29
|
+
"anyOf": [
|
|
30
|
+
{
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"a": {"type": "string"},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"b": {"type": "string"},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
schema = response_format["schema"]
|
|
49
|
+
assert schema["type"] == "object"
|
|
50
|
+
assert schema["additionalProperties"] is False
|
|
51
|
+
assert schema["required"] == ["nested", "items", "choice"]
|
|
52
|
+
assert schema["properties"]["nested"]["additionalProperties"] is False
|
|
53
|
+
assert schema["properties"]["items"]["items"]["additionalProperties"] is False
|
|
54
|
+
assert schema["properties"]["choice"]["anyOf"][0]["additionalProperties"] is False
|
|
55
|
+
assert schema["properties"]["choice"]["anyOf"][1]["additionalProperties"] is False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_build_strict_response_format_inlines_ref_with_sibling_keys() -> None:
|
|
59
|
+
response_format = build_strict_response_format(
|
|
60
|
+
{
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"item": {
|
|
64
|
+
"$ref": "#/$defs/Item",
|
|
65
|
+
"description": "Inline ref",
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"$defs": {
|
|
69
|
+
"Item": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"name": {"type": "string", "default": None},
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
item_schema = response_format["schema"]["properties"]["item"]
|
|
80
|
+
assert "$ref" not in item_schema
|
|
81
|
+
assert item_schema["description"] == "Inline ref"
|
|
82
|
+
assert item_schema["additionalProperties"] is False
|
|
83
|
+
assert "default" not in item_schema["properties"]["name"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_build_strict_response_format_rejects_invalid_schema_input() -> None:
|
|
87
|
+
with pytest.raises(TypeError):
|
|
88
|
+
build_strict_response_format({"type": "json_schema", "schema": "bad"})
|
|
89
|
+
|
|
90
|
+
with pytest.raises(ValueError, match="root must be a JSON object"):
|
|
91
|
+
build_strict_response_format({"type": "array", "items": {"type": "string"}})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_ensure_strict_json_schema_fails_fast_on_unresolvable_ref() -> None:
|
|
95
|
+
with pytest.raises(ValueError, match="Unresolvable JSON pointer ref"):
|
|
96
|
+
_ensure_strict_json_schema(
|
|
97
|
+
{
|
|
98
|
+
"type": "object",
|
|
99
|
+
"properties": {
|
|
100
|
+
"item": {
|
|
101
|
+
"$ref": "#/$defs/Missing",
|
|
102
|
+
"description": "broken",
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_to_responses_tools_applies_strict_schema_to_parameters() -> None:
|
|
110
|
+
tools = [
|
|
111
|
+
{
|
|
112
|
+
"type": "function",
|
|
113
|
+
"name": "demo",
|
|
114
|
+
"description": "Demo tool",
|
|
115
|
+
"parameters": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"payload": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {"value": {"type": "string"}},
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
normalized = to_responses_tools(tools, strict_output=True)
|
|
128
|
+
assert normalized[0]["strict"] is True
|
|
129
|
+
assert normalized[0]["parameters"]["additionalProperties"] is False
|
|
130
|
+
assert normalized[0]["parameters"]["properties"]["payload"]["additionalProperties"] is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/file_batches.py
RENAMED
|
File without changes
|
|
File without changes
|
{oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/resources/vector_stores/vector_stores.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/responses/response_stream_event.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_deleted.py
RENAMED
|
File without changes
|
{oauth_codex-2.1.0 → oauth_codex-2.2.0}/src/oauth_codex/types/vector_stores/vector_store_file.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|