codex-backend-sdk 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/PKG-INFO +25 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/README.md +24 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/__init__.py +5 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_client.py +10 -40
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_models.py +53 -1
- codex_backend_sdk-0.3.3/codex_backend_sdk/_transport.py +71 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_utils.py +3 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/codex_client.py +4 -0
- codex_backend_sdk-0.3.2/codex_backend_sdk/resources/responses.py → codex_backend_sdk-0.3.3/codex_backend_sdk/resources/_responses_payloads.py +114 -176
- codex_backend_sdk-0.3.3/codex_backend_sdk/resources/responses.py +224 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/docs/backend-api.md +4 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/pyproject.toml +1 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_client_retry.py +5 -5
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_responses_resource.py +102 -1
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/.gitignore +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/LICENSE +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/_streaming.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/oauth.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/pkce.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/__init__.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/codex.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/files.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/models.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/openai_oauth.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/realtime.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/storage.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/examples/agent.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/conftest.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_basic.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_codex_resources.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_conversation.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_files_resource.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_openai_oauth_resources.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_realtime_resource.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_reasoning.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_structured_output.py +0 -0
- {codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-backend-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Unofficial Python SDK for the ChatGPT Codex backend API
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -240,6 +240,30 @@ response = client.responses.create(
|
|
|
240
240
|
)
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
For structured output, `client.responses.parse(...)` accepts a Pydantic model,
|
|
244
|
+
sends it as a strict JSON schema, and returns `ParsedResponse`:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pydantic import BaseModel
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Person(BaseModel):
|
|
251
|
+
name: str
|
|
252
|
+
age: int
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
parsed = client.responses.parse(
|
|
256
|
+
model="gpt-5.4",
|
|
257
|
+
input="Extract: Ada is 37 years old.",
|
|
258
|
+
text_format=Person,
|
|
259
|
+
)
|
|
260
|
+
print(parsed.output_parsed.name)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Collected responses expose convenience properties for common output items:
|
|
264
|
+
`response.output_text`, `response.reasoning_summary`, and
|
|
265
|
+
`response.tool_calls`.
|
|
266
|
+
|
|
243
267
|
Unsupported official Responses parameters are rejected explicitly with
|
|
244
268
|
`CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
|
|
245
269
|
`max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
|
|
@@ -214,6 +214,30 @@ response = client.responses.create(
|
|
|
214
214
|
)
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
For structured output, `client.responses.parse(...)` accepts a Pydantic model,
|
|
218
|
+
sends it as a strict JSON schema, and returns `ParsedResponse`:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pydantic import BaseModel
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Person(BaseModel):
|
|
225
|
+
name: str
|
|
226
|
+
age: int
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
parsed = client.responses.parse(
|
|
230
|
+
model="gpt-5.4",
|
|
231
|
+
input="Extract: Ada is 37 years old.",
|
|
232
|
+
text_format=Person,
|
|
233
|
+
)
|
|
234
|
+
print(parsed.output_parsed.name)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Collected responses expose convenience properties for common output items:
|
|
238
|
+
`response.output_text`, `response.reasoning_summary`, and
|
|
239
|
+
`response.tool_calls`.
|
|
240
|
+
|
|
217
241
|
Unsupported official Responses parameters are rejected explicitly with
|
|
218
242
|
`CodexBackendUnsupportedParameterError`, including `temperature`, `top_p`,
|
|
219
243
|
`max_output_tokens`, `metadata`, `user`, `safety_identifier`, `truncation`,
|
|
@@ -17,7 +17,7 @@ Quickstart:
|
|
|
17
17
|
print(response.output_text)
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.3.
|
|
20
|
+
__version__ = "0.3.3"
|
|
21
21
|
|
|
22
22
|
from .oauth import run_oauth_flow, refresh_access_token, obtain_api_key
|
|
23
23
|
from .storage import load_tokens, save_tokens, TokenStore
|
|
@@ -38,7 +38,9 @@ from .codex_client import (
|
|
|
38
38
|
Model,
|
|
39
39
|
RawMemory,
|
|
40
40
|
RawMemoryMetadata,
|
|
41
|
+
ParsedResponse,
|
|
41
42
|
Response,
|
|
43
|
+
ResponseFormatJsonSchema,
|
|
42
44
|
ResponseStreamEvent,
|
|
43
45
|
ResponseUsage,
|
|
44
46
|
RealtimeCallResponse,
|
|
@@ -61,9 +63,11 @@ __all__ = [
|
|
|
61
63
|
"MemorySummarizeOutput",
|
|
62
64
|
"MemorySummarizeResponse",
|
|
63
65
|
"Model",
|
|
66
|
+
"ParsedResponse",
|
|
64
67
|
"RawMemory",
|
|
65
68
|
"RawMemoryMetadata",
|
|
66
69
|
"Response",
|
|
70
|
+
"ResponseFormatJsonSchema",
|
|
67
71
|
"ResponseStreamEvent",
|
|
68
72
|
"ResponseUsage",
|
|
69
73
|
"RealtimeCallResponse",
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import urllib.parse
|
|
6
|
-
import time
|
|
7
6
|
from typing import Any, Optional
|
|
8
7
|
|
|
9
8
|
import requests
|
|
10
9
|
|
|
10
|
+
from ._transport import request_with_retries
|
|
11
11
|
from ._utils import _UNSET, _is_given
|
|
12
12
|
from .storage import TokenStore, load_tokens, save_tokens, token_needs_refresh
|
|
13
13
|
|
|
@@ -271,37 +271,16 @@ class CodexClient:
|
|
|
271
271
|
return response.json()
|
|
272
272
|
|
|
273
273
|
def _request_with_retries(self, method: str, url: str, **kwargs: Any) -> requests.Response:
|
|
274
|
-
last_error: requests.RequestException | None = None
|
|
275
274
|
use_session = kwargs.pop("_use_session", True)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
except (requests.Timeout, requests.ConnectionError) as exc:
|
|
286
|
-
last_error = exc
|
|
287
|
-
if attempt >= self._max_retries:
|
|
288
|
-
raise
|
|
289
|
-
self._sleep_before_retry(None, attempt)
|
|
290
|
-
if last_error is not None:
|
|
291
|
-
raise last_error
|
|
292
|
-
raise RuntimeError("Request retry loop exhausted")
|
|
293
|
-
|
|
294
|
-
def _should_retry_response(self, response: requests.Response, attempt: int) -> bool:
|
|
295
|
-
if attempt >= self._max_retries:
|
|
296
|
-
return False
|
|
297
|
-
return response.status_code == 429 or 500 <= response.status_code < 600
|
|
298
|
-
|
|
299
|
-
def _sleep_before_retry(self, response: requests.Response | None, attempt: int) -> None:
|
|
300
|
-
retry_after = response.headers.get("Retry-After") if response is not None else None
|
|
301
|
-
delay = _parse_retry_after(retry_after)
|
|
302
|
-
if delay is None:
|
|
303
|
-
delay = self._retry_base_delay * (2 ** attempt)
|
|
304
|
-
time.sleep(delay)
|
|
275
|
+
return request_with_retries(
|
|
276
|
+
self._session,
|
|
277
|
+
method,
|
|
278
|
+
url,
|
|
279
|
+
max_retries=self._max_retries,
|
|
280
|
+
retry_base_delay=self._retry_base_delay,
|
|
281
|
+
use_session=use_session,
|
|
282
|
+
**kwargs,
|
|
283
|
+
)
|
|
305
284
|
|
|
306
285
|
def realtime_websocket_url(self, *, model: str) -> str:
|
|
307
286
|
"""Return the official OpenAI Realtime WebSocket URL for Codex plugins."""
|
|
@@ -324,12 +303,3 @@ class CodexClient:
|
|
|
324
303
|
|
|
325
304
|
|
|
326
305
|
OpenAI = CodexClient
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def _parse_retry_after(value: str | None) -> float | None:
|
|
330
|
-
if not value:
|
|
331
|
-
return None
|
|
332
|
-
try:
|
|
333
|
-
return max(0.0, float(value))
|
|
334
|
-
except ValueError:
|
|
335
|
-
return None
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
from collections.abc import Iterator
|
|
7
|
-
from typing import Any, Literal, Optional
|
|
7
|
+
from typing import Any, Generic, Literal, Optional, TypeVar
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -13,6 +13,7 @@ ReasoningEffort = Literal["minimal", "low", "medium", "high", "xhigh"]
|
|
|
13
13
|
ReasoningSummary = Literal["concise", "detailed", "auto"]
|
|
14
14
|
Verbosity = Literal["low", "medium", "high"]
|
|
15
15
|
ServiceTier = Literal["flex", "priority"]
|
|
16
|
+
ParsedT = TypeVar("ParsedT")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class CodexBaseModel(BaseModel):
|
|
@@ -73,6 +74,14 @@ class ResponseUsage(CodexBaseModel):
|
|
|
73
74
|
output_tokens_details: TokenDetails = Field(default_factory=TokenDetails)
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
class ResponseFormatJsonSchema(CodexBaseModel):
|
|
78
|
+
type: Literal["json_schema"] = "json_schema"
|
|
79
|
+
name: str
|
|
80
|
+
schema_: dict[str, Any] = Field(alias="schema")
|
|
81
|
+
strict: Optional[bool] = None
|
|
82
|
+
description: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
|
|
76
85
|
class Response(CodexBaseModel):
|
|
77
86
|
id: str
|
|
78
87
|
created_at: float = Field(default_factory=time.time)
|
|
@@ -117,6 +126,49 @@ class Response(CodexBaseModel):
|
|
|
117
126
|
texts.append(content.get("text", ""))
|
|
118
127
|
return "".join(texts)
|
|
119
128
|
|
|
129
|
+
@property
|
|
130
|
+
def reasoning_summary(self) -> str | None:
|
|
131
|
+
texts: list[str] = []
|
|
132
|
+
for output in self.output:
|
|
133
|
+
if output.get("type") == "reasoning":
|
|
134
|
+
for summary in output.get("summary", []):
|
|
135
|
+
if isinstance(summary, dict):
|
|
136
|
+
texts.append(summary.get("text", ""))
|
|
137
|
+
return "\n".join(text for text in texts if text) or None
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def tool_calls(self) -> list[dict[str, Any]]:
|
|
141
|
+
return [output for output in self.output if output.get("type") == "function_call"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ParsedResponse(CodexBaseModel, Generic[ParsedT]):
|
|
145
|
+
response: Response
|
|
146
|
+
output_parsed: ParsedT
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def id(self) -> str:
|
|
150
|
+
return self.response.id
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def model(self) -> Optional[str]:
|
|
154
|
+
return self.response.model
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def output(self) -> list[dict[str, Any]]:
|
|
158
|
+
return self.response.output
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def output_text(self) -> str:
|
|
162
|
+
return self.response.output_text
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def status(self) -> Optional[str]:
|
|
166
|
+
return self.response.status
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def usage(self) -> Optional[ResponseUsage]:
|
|
170
|
+
return self.response.usage
|
|
171
|
+
|
|
120
172
|
|
|
121
173
|
class ResponseStreamEvent(CodexBaseModel):
|
|
122
174
|
type: str
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""HTTP transport helpers shared by client resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def request_with_retries(
|
|
12
|
+
session: requests.Session,
|
|
13
|
+
method: str,
|
|
14
|
+
url: str,
|
|
15
|
+
*,
|
|
16
|
+
max_retries: int,
|
|
17
|
+
retry_base_delay: float,
|
|
18
|
+
use_session: bool = True,
|
|
19
|
+
**kwargs: Any,
|
|
20
|
+
) -> requests.Response:
|
|
21
|
+
last_error: requests.RequestException | None = None
|
|
22
|
+
for attempt in range(max_retries + 1):
|
|
23
|
+
try:
|
|
24
|
+
request = session.request if use_session else requests.request
|
|
25
|
+
response = request(method, url, **kwargs)
|
|
26
|
+
if should_retry_response(response, attempt, max_retries=max_retries):
|
|
27
|
+
sleep_before_retry(response, attempt, retry_base_delay=retry_base_delay)
|
|
28
|
+
continue
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
return response
|
|
31
|
+
except (requests.Timeout, requests.ConnectionError) as exc:
|
|
32
|
+
last_error = exc
|
|
33
|
+
if attempt >= max_retries:
|
|
34
|
+
raise
|
|
35
|
+
sleep_before_retry(None, attempt, retry_base_delay=retry_base_delay)
|
|
36
|
+
if last_error is not None:
|
|
37
|
+
raise last_error
|
|
38
|
+
raise RuntimeError("Request retry loop exhausted")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def should_retry_response(
|
|
42
|
+
response: requests.Response,
|
|
43
|
+
attempt: int,
|
|
44
|
+
*,
|
|
45
|
+
max_retries: int,
|
|
46
|
+
) -> bool:
|
|
47
|
+
if attempt >= max_retries:
|
|
48
|
+
return False
|
|
49
|
+
return response.status_code == 429 or 500 <= response.status_code < 600
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def sleep_before_retry(
|
|
53
|
+
response: requests.Response | None,
|
|
54
|
+
attempt: int,
|
|
55
|
+
*,
|
|
56
|
+
retry_base_delay: float,
|
|
57
|
+
) -> None:
|
|
58
|
+
retry_after = response.headers.get("Retry-After") if response is not None else None
|
|
59
|
+
delay = parse_retry_after(retry_after)
|
|
60
|
+
if delay is None:
|
|
61
|
+
delay = retry_base_delay * (2 ** attempt)
|
|
62
|
+
time.sleep(delay)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_retry_after(value: str | None) -> float | None:
|
|
66
|
+
if not value:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return max(0.0, float(value))
|
|
70
|
+
except ValueError:
|
|
71
|
+
return None
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
|
+
from dataclasses import asdict, is_dataclass
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from pydantic import BaseModel
|
|
@@ -26,6 +27,8 @@ def image_b64(data: str, media_type: str = "image/jpeg") -> dict[str, str]:
|
|
|
26
27
|
def _jsonable(value: Any) -> Any:
|
|
27
28
|
if isinstance(value, BaseModel):
|
|
28
29
|
return value.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
|
30
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
31
|
+
return _jsonable(asdict(value))
|
|
29
32
|
if isinstance(value, dict):
|
|
30
33
|
return {key: _jsonable(item) for key, item in value.items()}
|
|
31
34
|
if isinstance(value, list):
|
|
@@ -16,12 +16,14 @@ from ._models import (
|
|
|
16
16
|
Model,
|
|
17
17
|
MemorySummarizeOutput,
|
|
18
18
|
MemorySummarizeResponse,
|
|
19
|
+
ParsedResponse,
|
|
19
20
|
RawMemory,
|
|
20
21
|
RawMemoryMetadata,
|
|
21
22
|
ReasoningEffort,
|
|
22
23
|
ReasoningSummary,
|
|
23
24
|
RealtimeCallResponse,
|
|
24
25
|
Response,
|
|
26
|
+
ResponseFormatJsonSchema,
|
|
25
27
|
ResponseStreamEvent,
|
|
26
28
|
ResponseUsage,
|
|
27
29
|
ServiceTier,
|
|
@@ -45,11 +47,13 @@ __all__ = [
|
|
|
45
47
|
"MemorySummarizeResponse",
|
|
46
48
|
"Model",
|
|
47
49
|
"OpenAI",
|
|
50
|
+
"ParsedResponse",
|
|
48
51
|
"RawMemory",
|
|
49
52
|
"RawMemoryMetadata",
|
|
50
53
|
"ReasoningEffort",
|
|
51
54
|
"ReasoningSummary",
|
|
52
55
|
"Response",
|
|
56
|
+
"ResponseFormatJsonSchema",
|
|
53
57
|
"ResponseStreamEvent",
|
|
54
58
|
"ResponseUsage",
|
|
55
59
|
"RealtimeCallResponse",
|
|
@@ -1,150 +1,25 @@
|
|
|
1
|
-
"""Responses resource."""
|
|
1
|
+
"""Internal builders and collectors for the Codex Responses resource."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from typing import Any, Optional
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import Any, Optional
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
10
|
|
|
11
11
|
from .._models import (
|
|
12
12
|
CodexBaseModel,
|
|
13
|
-
CompactedResponse,
|
|
14
13
|
Response,
|
|
14
|
+
ResponseFormatJsonSchema,
|
|
15
15
|
ResponseStreamEvent,
|
|
16
16
|
ResponseUsage,
|
|
17
17
|
TokenDetails,
|
|
18
18
|
)
|
|
19
|
-
from ..
|
|
20
|
-
from .._utils import _UNSET, _default, _is_given, _reject_backend_unsupported
|
|
19
|
+
from .._utils import _UNSET, _default, _is_given, _jsonable
|
|
21
20
|
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from .._client import CodexClient
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
class Responses:
|
|
27
|
-
def __init__(self, client: CodexClient) -> None:
|
|
28
|
-
self._client = client
|
|
29
|
-
|
|
30
|
-
def create(
|
|
31
|
-
self,
|
|
32
|
-
*,
|
|
33
|
-
background: Any = _UNSET,
|
|
34
|
-
context_management: Any = _UNSET,
|
|
35
|
-
conversation: Any = _UNSET,
|
|
36
|
-
include: Any = _UNSET,
|
|
37
|
-
input: Any = _UNSET,
|
|
38
|
-
instructions: Any = _UNSET,
|
|
39
|
-
max_output_tokens: Any = _UNSET,
|
|
40
|
-
max_tool_calls: Any = _UNSET,
|
|
41
|
-
metadata: Any = _UNSET,
|
|
42
|
-
model: Any = _UNSET,
|
|
43
|
-
parallel_tool_calls: Any = _UNSET,
|
|
44
|
-
previous_response_id: Any = _UNSET,
|
|
45
|
-
prompt: Any = _UNSET,
|
|
46
|
-
prompt_cache_key: Any = _UNSET,
|
|
47
|
-
prompt_cache_retention: Any = _UNSET,
|
|
48
|
-
reasoning: Any = _UNSET,
|
|
49
|
-
safety_identifier: Any = _UNSET,
|
|
50
|
-
service_tier: Any = _UNSET,
|
|
51
|
-
store: Any = _UNSET,
|
|
52
|
-
stream: Any = _UNSET,
|
|
53
|
-
stream_options: Any = _UNSET,
|
|
54
|
-
temperature: Any = _UNSET,
|
|
55
|
-
text: Any = _UNSET,
|
|
56
|
-
tool_choice: Any = _UNSET,
|
|
57
|
-
tools: Any = _UNSET,
|
|
58
|
-
top_logprobs: Any = _UNSET,
|
|
59
|
-
top_p: Any = _UNSET,
|
|
60
|
-
truncation: Any = _UNSET,
|
|
61
|
-
user: Any = _UNSET,
|
|
62
|
-
extra_headers: Any = None,
|
|
63
|
-
extra_query: Any = None,
|
|
64
|
-
extra_body: Any = None,
|
|
65
|
-
timeout: Any = _UNSET,
|
|
66
|
-
) -> Response | Iterator[ResponseStreamEvent]:
|
|
67
|
-
_reject_backend_unsupported(
|
|
68
|
-
background=background,
|
|
69
|
-
context_management=context_management,
|
|
70
|
-
conversation=conversation,
|
|
71
|
-
max_output_tokens=max_output_tokens,
|
|
72
|
-
max_tool_calls=max_tool_calls,
|
|
73
|
-
metadata=metadata,
|
|
74
|
-
previous_response_id=previous_response_id,
|
|
75
|
-
prompt=prompt,
|
|
76
|
-
prompt_cache_retention=prompt_cache_retention,
|
|
77
|
-
safety_identifier=safety_identifier,
|
|
78
|
-
stream_options=stream_options,
|
|
79
|
-
temperature=temperature,
|
|
80
|
-
top_logprobs=top_logprobs,
|
|
81
|
-
top_p=top_p,
|
|
82
|
-
truncation=truncation,
|
|
83
|
-
user=user,
|
|
84
|
-
extra_body=extra_body,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
if _is_given(store) and store is not False:
|
|
88
|
-
raise NotImplementedError("The Codex backend only accepts store=False.")
|
|
89
|
-
|
|
90
|
-
request = _ResponsesCreateRequest.from_openai_params(
|
|
91
|
-
client_defaults=self._client._defaults,
|
|
92
|
-
input=input,
|
|
93
|
-
include=include,
|
|
94
|
-
instructions=instructions,
|
|
95
|
-
model=model,
|
|
96
|
-
parallel_tool_calls=parallel_tool_calls,
|
|
97
|
-
prompt_cache_key=prompt_cache_key,
|
|
98
|
-
reasoning=reasoning,
|
|
99
|
-
service_tier=service_tier,
|
|
100
|
-
text=text,
|
|
101
|
-
tool_choice=tool_choice,
|
|
102
|
-
tools=tools,
|
|
103
|
-
)
|
|
104
|
-
response = self._client._post("/responses", body=request.payload, stream=True)
|
|
105
|
-
events = stream_response_events(response)
|
|
106
|
-
stream_enabled = bool(stream) if _is_given(stream) else False
|
|
107
|
-
|
|
108
|
-
if stream_enabled:
|
|
109
|
-
return events
|
|
110
|
-
return _collect_response(events, request=request)
|
|
111
|
-
|
|
112
|
-
def compact(
|
|
113
|
-
self,
|
|
114
|
-
*,
|
|
115
|
-
input: list[dict[str, Any]],
|
|
116
|
-
model: Any = _UNSET,
|
|
117
|
-
instructions: Any = _UNSET,
|
|
118
|
-
tools: Any = _UNSET,
|
|
119
|
-
parallel_tool_calls: Any = _UNSET,
|
|
120
|
-
reasoning: Any = _UNSET,
|
|
121
|
-
service_tier: Any = _UNSET,
|
|
122
|
-
prompt_cache_key: Any = _UNSET,
|
|
123
|
-
text: Any = _UNSET,
|
|
124
|
-
) -> CompactedResponse:
|
|
125
|
-
normalized_tools = _normalize_tools(tools)
|
|
126
|
-
payload = {
|
|
127
|
-
"model": _default(model, self._client._defaults["model"]),
|
|
128
|
-
"instructions": _default(instructions, self._client._defaults["instructions"]) or "",
|
|
129
|
-
"input": [_normalize_input_item(item) for item in input],
|
|
130
|
-
"tools": normalized_tools,
|
|
131
|
-
"parallel_tool_calls": (
|
|
132
|
-
bool(_default(parallel_tool_calls, False)) if normalized_tools else False
|
|
133
|
-
),
|
|
134
|
-
}
|
|
135
|
-
if _is_given(reasoning) and reasoning is not None:
|
|
136
|
-
payload["reasoning"] = _normalize_reasoning(reasoning)
|
|
137
|
-
if _is_given(service_tier) and service_tier is not None:
|
|
138
|
-
payload["service_tier"] = service_tier
|
|
139
|
-
if _is_given(prompt_cache_key) and prompt_cache_key is not None:
|
|
140
|
-
payload["prompt_cache_key"] = prompt_cache_key
|
|
141
|
-
if _is_given(text) and text is not None:
|
|
142
|
-
payload["text"] = _normalize_text(text)
|
|
143
|
-
data = self._client._post("/responses/compact", body=payload).json()
|
|
144
|
-
return CompactedResponse(id=data.get("id", ""), output=data.get("output", []))
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class _ResponsesCreateRequest(CodexBaseModel):
|
|
22
|
+
class ResponsesCreateRequest(CodexBaseModel):
|
|
148
23
|
model: str
|
|
149
24
|
instructions: Optional[str]
|
|
150
25
|
input: list[dict[str, Any]]
|
|
@@ -164,9 +39,9 @@ class _ResponsesCreateRequest(CodexBaseModel):
|
|
|
164
39
|
*,
|
|
165
40
|
client_defaults: dict[str, Any],
|
|
166
41
|
**params: Any,
|
|
167
|
-
) -> "
|
|
168
|
-
input_items =
|
|
169
|
-
tools =
|
|
42
|
+
) -> "ResponsesCreateRequest":
|
|
43
|
+
input_items = normalize_input(params["input"])
|
|
44
|
+
tools = normalize_tools(params["tools"])
|
|
170
45
|
include = (
|
|
171
46
|
[]
|
|
172
47
|
if not _is_given(params["include"]) or params["include"] is None
|
|
@@ -198,9 +73,9 @@ class _ResponsesCreateRequest(CodexBaseModel):
|
|
|
198
73
|
if service_tier is not None:
|
|
199
74
|
payload["service_tier"] = service_tier
|
|
200
75
|
if reasoning is not None:
|
|
201
|
-
payload["reasoning"] =
|
|
76
|
+
payload["reasoning"] = normalize_reasoning(reasoning)
|
|
202
77
|
if text is not None:
|
|
203
|
-
payload["text"] =
|
|
78
|
+
payload["text"] = normalize_text(text)
|
|
204
79
|
|
|
205
80
|
return cls(
|
|
206
81
|
model=payload["model"],
|
|
@@ -218,10 +93,10 @@ class _ResponsesCreateRequest(CodexBaseModel):
|
|
|
218
93
|
)
|
|
219
94
|
|
|
220
95
|
|
|
221
|
-
def
|
|
96
|
+
def collect_response(
|
|
222
97
|
events: Iterable[ResponseStreamEvent],
|
|
223
98
|
*,
|
|
224
|
-
request:
|
|
99
|
+
request: ResponsesCreateRequest,
|
|
225
100
|
) -> Response:
|
|
226
101
|
output: list[dict[str, Any]] = []
|
|
227
102
|
text_parts: list[str] = []
|
|
@@ -267,42 +142,18 @@ def _collect_response(
|
|
|
267
142
|
)
|
|
268
143
|
|
|
269
144
|
|
|
270
|
-
def
|
|
271
|
-
delta = getattr(event, "delta", None)
|
|
272
|
-
if isinstance(delta, str):
|
|
273
|
-
return delta
|
|
274
|
-
if isinstance(delta, dict):
|
|
275
|
-
return delta.get("text", "")
|
|
276
|
-
return ""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _event_response_dict(event: ResponseStreamEvent) -> dict[str, Any]:
|
|
280
|
-
response = getattr(event, "response", None)
|
|
281
|
-
if isinstance(response, BaseModel):
|
|
282
|
-
return response.model_dump()
|
|
283
|
-
return response if isinstance(response, dict) else {}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _event_error_message(event: ResponseStreamEvent) -> str:
|
|
287
|
-
response = _event_response_dict(event)
|
|
288
|
-
error = response.get("error") or getattr(event, "error", None) or {}
|
|
289
|
-
if isinstance(error, dict):
|
|
290
|
-
return error.get("message", "Response failed")
|
|
291
|
-
return str(error)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def _normalize_input(input_value: Any) -> list[dict[str, Any]]:
|
|
145
|
+
def normalize_input(input_value: Any) -> list[dict[str, Any]]:
|
|
295
146
|
if not _is_given(input_value) or input_value is None:
|
|
296
147
|
return []
|
|
297
148
|
if isinstance(input_value, str):
|
|
298
149
|
return [_message("user", [{"type": "input_text", "text": input_value}])]
|
|
299
150
|
if isinstance(input_value, list):
|
|
300
|
-
return [
|
|
301
|
-
return [
|
|
151
|
+
return [normalize_input_item(item) for item in input_value]
|
|
152
|
+
return [normalize_input_item(input_value)]
|
|
302
153
|
|
|
303
154
|
|
|
304
|
-
def
|
|
305
|
-
raw =
|
|
155
|
+
def normalize_input_item(item: Any) -> dict[str, Any]:
|
|
156
|
+
raw = _as_dict(item)
|
|
306
157
|
if raw.get("type") and raw.get("type") != "message":
|
|
307
158
|
return raw
|
|
308
159
|
if "role" not in raw:
|
|
@@ -321,23 +172,20 @@ def _normalize_input_item(item: Any) -> dict[str, Any]:
|
|
|
321
172
|
return _message(role, content)
|
|
322
173
|
|
|
323
174
|
|
|
324
|
-
def
|
|
325
|
-
return {"type": "message", "role": role, "content": content}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _normalize_tools(tools: Any) -> list[dict[str, Any]]:
|
|
175
|
+
def normalize_tools(tools: Any) -> list[dict[str, Any]]:
|
|
329
176
|
if not _is_given(tools) or tools is None:
|
|
330
177
|
return []
|
|
331
178
|
normalized = []
|
|
332
179
|
for tool in tools:
|
|
333
|
-
item =
|
|
180
|
+
item = _as_dict(tool)
|
|
334
181
|
if item.get("type") == "web_search":
|
|
335
182
|
item["type"] = "web_search_preview"
|
|
336
183
|
normalized.append(item)
|
|
337
184
|
return normalized
|
|
338
185
|
|
|
339
186
|
|
|
340
|
-
def
|
|
187
|
+
def normalize_reasoning(reasoning: Any) -> dict[str, Any]:
|
|
188
|
+
reasoning = _jsonable(reasoning)
|
|
341
189
|
if isinstance(reasoning, dict):
|
|
342
190
|
return {key: value for key, value in reasoning.items() if value is not None}
|
|
343
191
|
return {
|
|
@@ -347,8 +195,8 @@ def _normalize_reasoning(reasoning: Any) -> dict[str, Any]:
|
|
|
347
195
|
}
|
|
348
196
|
|
|
349
197
|
|
|
350
|
-
def
|
|
351
|
-
text_dict =
|
|
198
|
+
def normalize_text(text: Any) -> dict[str, Any]:
|
|
199
|
+
text_dict = _as_dict(text)
|
|
352
200
|
fmt = text_dict.get("format")
|
|
353
201
|
if isinstance(fmt, dict) and fmt.get("type") == "json_schema":
|
|
354
202
|
schema = fmt.get("schema")
|
|
@@ -363,6 +211,96 @@ def _normalize_text(text: Any) -> dict[str, Any]:
|
|
|
363
211
|
return text_dict
|
|
364
212
|
|
|
365
213
|
|
|
214
|
+
def merge_text_format(text: Any, fmt: ResponseFormatJsonSchema) -> dict[str, Any]:
|
|
215
|
+
if not _is_given(text) or text is None:
|
|
216
|
+
text_dict: dict[str, Any] = {}
|
|
217
|
+
else:
|
|
218
|
+
text_dict = normalize_text(text)
|
|
219
|
+
if text_dict.get("format") is not None:
|
|
220
|
+
raise TypeError("Cannot pass both text_format and text.format.")
|
|
221
|
+
text_dict["format"] = fmt.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
222
|
+
return text_dict
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def pydantic_to_format(model_class: type[Any]) -> ResponseFormatJsonSchema:
|
|
226
|
+
try:
|
|
227
|
+
schema = model_class.model_json_schema()
|
|
228
|
+
model_class.model_validate_json
|
|
229
|
+
except AttributeError:
|
|
230
|
+
raise TypeError("responses.parse() requires a Pydantic BaseModel class.") from None
|
|
231
|
+
|
|
232
|
+
schema = _ensure_strict_schema(schema)
|
|
233
|
+
return ResponseFormatJsonSchema(
|
|
234
|
+
name=getattr(model_class, "__name__", "output"),
|
|
235
|
+
schema=schema,
|
|
236
|
+
strict=True,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _event_delta_text(event: ResponseStreamEvent) -> str:
|
|
241
|
+
delta = getattr(event, "delta", None)
|
|
242
|
+
if isinstance(delta, str):
|
|
243
|
+
return delta
|
|
244
|
+
if isinstance(delta, dict):
|
|
245
|
+
return delta.get("text", "")
|
|
246
|
+
return ""
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _event_response_dict(event: ResponseStreamEvent) -> dict[str, Any]:
|
|
250
|
+
response = getattr(event, "response", None)
|
|
251
|
+
if isinstance(response, BaseModel):
|
|
252
|
+
return response.model_dump()
|
|
253
|
+
return response if isinstance(response, dict) else {}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _event_error_message(event: ResponseStreamEvent) -> str:
|
|
257
|
+
response = _event_response_dict(event)
|
|
258
|
+
error = response.get("error") or getattr(event, "error", None) or {}
|
|
259
|
+
if isinstance(error, dict):
|
|
260
|
+
return error.get("message", "Response failed")
|
|
261
|
+
return str(error)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _message(role: str, content: list[dict[str, Any]]) -> dict[str, Any]:
|
|
265
|
+
return {"type": "message", "role": role, "content": content}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
269
|
+
value = _jsonable(value)
|
|
270
|
+
if isinstance(value, dict):
|
|
271
|
+
return value
|
|
272
|
+
return dict(value)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _ensure_strict_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
276
|
+
schema = dict(schema)
|
|
277
|
+
if schema.get("type") == "object":
|
|
278
|
+
schema.setdefault("additionalProperties", False)
|
|
279
|
+
props = schema.get("properties")
|
|
280
|
+
if isinstance(props, dict):
|
|
281
|
+
schema["properties"] = {
|
|
282
|
+
key: _ensure_strict_schema(value) if isinstance(value, dict) else value
|
|
283
|
+
for key, value in props.items()
|
|
284
|
+
}
|
|
285
|
+
items = schema.get("items")
|
|
286
|
+
if isinstance(items, dict):
|
|
287
|
+
schema["items"] = _ensure_strict_schema(items)
|
|
288
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
289
|
+
variants = schema.get(key)
|
|
290
|
+
if isinstance(variants, list):
|
|
291
|
+
schema[key] = [
|
|
292
|
+
_ensure_strict_schema(value) if isinstance(value, dict) else value
|
|
293
|
+
for value in variants
|
|
294
|
+
]
|
|
295
|
+
defs = schema.get("$defs")
|
|
296
|
+
if isinstance(defs, dict):
|
|
297
|
+
schema["$defs"] = {
|
|
298
|
+
key: _ensure_strict_schema(value) if isinstance(value, dict) else value
|
|
299
|
+
for key, value in defs.items()
|
|
300
|
+
}
|
|
301
|
+
return schema
|
|
302
|
+
|
|
303
|
+
|
|
366
304
|
def _usage_from_backend(raw: Any) -> ResponseUsage:
|
|
367
305
|
raw = raw or {}
|
|
368
306
|
return ResponseUsage(
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Responses resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .._models import CompactedResponse, ParsedResponse, Response, ResponseStreamEvent
|
|
9
|
+
from .._streaming import stream_response_events
|
|
10
|
+
from .._utils import _UNSET, _default, _is_given, _reject_backend_unsupported
|
|
11
|
+
from ._responses_payloads import (
|
|
12
|
+
ResponsesCreateRequest,
|
|
13
|
+
collect_response,
|
|
14
|
+
merge_text_format,
|
|
15
|
+
normalize_input_item,
|
|
16
|
+
normalize_reasoning,
|
|
17
|
+
normalize_text,
|
|
18
|
+
normalize_tools,
|
|
19
|
+
pydantic_to_format,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .._client import CodexClient
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Responses:
|
|
27
|
+
def __init__(self, client: CodexClient) -> None:
|
|
28
|
+
self._client = client
|
|
29
|
+
|
|
30
|
+
def create(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
background: Any = _UNSET,
|
|
34
|
+
context_management: Any = _UNSET,
|
|
35
|
+
conversation: Any = _UNSET,
|
|
36
|
+
include: Any = _UNSET,
|
|
37
|
+
input: Any = _UNSET,
|
|
38
|
+
instructions: Any = _UNSET,
|
|
39
|
+
max_output_tokens: Any = _UNSET,
|
|
40
|
+
max_tool_calls: Any = _UNSET,
|
|
41
|
+
metadata: Any = _UNSET,
|
|
42
|
+
model: Any = _UNSET,
|
|
43
|
+
parallel_tool_calls: Any = _UNSET,
|
|
44
|
+
previous_response_id: Any = _UNSET,
|
|
45
|
+
prompt: Any = _UNSET,
|
|
46
|
+
prompt_cache_key: Any = _UNSET,
|
|
47
|
+
prompt_cache_retention: Any = _UNSET,
|
|
48
|
+
reasoning: Any = _UNSET,
|
|
49
|
+
safety_identifier: Any = _UNSET,
|
|
50
|
+
service_tier: Any = _UNSET,
|
|
51
|
+
store: Any = _UNSET,
|
|
52
|
+
stream: Any = _UNSET,
|
|
53
|
+
stream_options: Any = _UNSET,
|
|
54
|
+
temperature: Any = _UNSET,
|
|
55
|
+
text: Any = _UNSET,
|
|
56
|
+
tool_choice: Any = _UNSET,
|
|
57
|
+
tools: Any = _UNSET,
|
|
58
|
+
top_logprobs: Any = _UNSET,
|
|
59
|
+
top_p: Any = _UNSET,
|
|
60
|
+
truncation: Any = _UNSET,
|
|
61
|
+
user: Any = _UNSET,
|
|
62
|
+
extra_headers: Any = None,
|
|
63
|
+
extra_query: Any = None,
|
|
64
|
+
extra_body: Any = None,
|
|
65
|
+
timeout: Any = _UNSET,
|
|
66
|
+
) -> Response | Iterator[ResponseStreamEvent]:
|
|
67
|
+
_reject_backend_unsupported(
|
|
68
|
+
background=background,
|
|
69
|
+
context_management=context_management,
|
|
70
|
+
conversation=conversation,
|
|
71
|
+
max_output_tokens=max_output_tokens,
|
|
72
|
+
max_tool_calls=max_tool_calls,
|
|
73
|
+
metadata=metadata,
|
|
74
|
+
previous_response_id=previous_response_id,
|
|
75
|
+
prompt=prompt,
|
|
76
|
+
prompt_cache_retention=prompt_cache_retention,
|
|
77
|
+
safety_identifier=safety_identifier,
|
|
78
|
+
stream_options=stream_options,
|
|
79
|
+
temperature=temperature,
|
|
80
|
+
top_logprobs=top_logprobs,
|
|
81
|
+
top_p=top_p,
|
|
82
|
+
truncation=truncation,
|
|
83
|
+
user=user,
|
|
84
|
+
extra_body=extra_body,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if _is_given(store) and store is not False:
|
|
88
|
+
raise NotImplementedError("The Codex backend only accepts store=False.")
|
|
89
|
+
|
|
90
|
+
request = ResponsesCreateRequest.from_openai_params(
|
|
91
|
+
client_defaults=self._client._defaults,
|
|
92
|
+
input=input,
|
|
93
|
+
include=include,
|
|
94
|
+
instructions=instructions,
|
|
95
|
+
model=model,
|
|
96
|
+
parallel_tool_calls=parallel_tool_calls,
|
|
97
|
+
prompt_cache_key=prompt_cache_key,
|
|
98
|
+
reasoning=reasoning,
|
|
99
|
+
service_tier=service_tier,
|
|
100
|
+
text=text,
|
|
101
|
+
tool_choice=tool_choice,
|
|
102
|
+
tools=tools,
|
|
103
|
+
)
|
|
104
|
+
response = self._client._post("/responses", body=request.payload, stream=True)
|
|
105
|
+
events = stream_response_events(response)
|
|
106
|
+
stream_enabled = bool(stream) if _is_given(stream) else False
|
|
107
|
+
|
|
108
|
+
if stream_enabled:
|
|
109
|
+
return events
|
|
110
|
+
return collect_response(events, request=request)
|
|
111
|
+
|
|
112
|
+
def parse(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
text_format: type[Any],
|
|
116
|
+
background: Any = _UNSET,
|
|
117
|
+
context_management: Any = _UNSET,
|
|
118
|
+
conversation: Any = _UNSET,
|
|
119
|
+
include: Any = _UNSET,
|
|
120
|
+
input: Any = _UNSET,
|
|
121
|
+
instructions: Any = _UNSET,
|
|
122
|
+
max_output_tokens: Any = _UNSET,
|
|
123
|
+
max_tool_calls: Any = _UNSET,
|
|
124
|
+
metadata: Any = _UNSET,
|
|
125
|
+
model: Any = _UNSET,
|
|
126
|
+
parallel_tool_calls: Any = _UNSET,
|
|
127
|
+
previous_response_id: Any = _UNSET,
|
|
128
|
+
prompt: Any = _UNSET,
|
|
129
|
+
prompt_cache_key: Any = _UNSET,
|
|
130
|
+
prompt_cache_retention: Any = _UNSET,
|
|
131
|
+
reasoning: Any = _UNSET,
|
|
132
|
+
safety_identifier: Any = _UNSET,
|
|
133
|
+
service_tier: Any = _UNSET,
|
|
134
|
+
store: Any = _UNSET,
|
|
135
|
+
stream_options: Any = _UNSET,
|
|
136
|
+
temperature: Any = _UNSET,
|
|
137
|
+
text: Any = _UNSET,
|
|
138
|
+
tool_choice: Any = _UNSET,
|
|
139
|
+
tools: Any = _UNSET,
|
|
140
|
+
top_logprobs: Any = _UNSET,
|
|
141
|
+
top_p: Any = _UNSET,
|
|
142
|
+
truncation: Any = _UNSET,
|
|
143
|
+
user: Any = _UNSET,
|
|
144
|
+
extra_headers: Any = None,
|
|
145
|
+
extra_query: Any = None,
|
|
146
|
+
extra_body: Any = None,
|
|
147
|
+
timeout: Any = _UNSET,
|
|
148
|
+
) -> ParsedResponse[Any]:
|
|
149
|
+
fmt = pydantic_to_format(text_format)
|
|
150
|
+
response = self.create(
|
|
151
|
+
background=background,
|
|
152
|
+
context_management=context_management,
|
|
153
|
+
conversation=conversation,
|
|
154
|
+
include=include,
|
|
155
|
+
input=input,
|
|
156
|
+
instructions=instructions,
|
|
157
|
+
max_output_tokens=max_output_tokens,
|
|
158
|
+
max_tool_calls=max_tool_calls,
|
|
159
|
+
metadata=metadata,
|
|
160
|
+
model=model,
|
|
161
|
+
parallel_tool_calls=parallel_tool_calls,
|
|
162
|
+
previous_response_id=previous_response_id,
|
|
163
|
+
prompt=prompt,
|
|
164
|
+
prompt_cache_key=prompt_cache_key,
|
|
165
|
+
prompt_cache_retention=prompt_cache_retention,
|
|
166
|
+
reasoning=reasoning,
|
|
167
|
+
safety_identifier=safety_identifier,
|
|
168
|
+
service_tier=service_tier,
|
|
169
|
+
store=store,
|
|
170
|
+
stream=False,
|
|
171
|
+
stream_options=stream_options,
|
|
172
|
+
temperature=temperature,
|
|
173
|
+
text=merge_text_format(text, fmt),
|
|
174
|
+
tool_choice=tool_choice,
|
|
175
|
+
tools=tools,
|
|
176
|
+
top_logprobs=top_logprobs,
|
|
177
|
+
top_p=top_p,
|
|
178
|
+
truncation=truncation,
|
|
179
|
+
user=user,
|
|
180
|
+
extra_headers=extra_headers,
|
|
181
|
+
extra_query=extra_query,
|
|
182
|
+
extra_body=extra_body,
|
|
183
|
+
timeout=timeout,
|
|
184
|
+
)
|
|
185
|
+
if not isinstance(response, Response):
|
|
186
|
+
raise TypeError("responses.parse() expected a non-streaming Response")
|
|
187
|
+
return ParsedResponse(
|
|
188
|
+
response=response,
|
|
189
|
+
output_parsed=text_format.model_validate_json(response.output_text),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def compact(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
input: list[dict[str, Any]],
|
|
196
|
+
model: Any = _UNSET,
|
|
197
|
+
instructions: Any = _UNSET,
|
|
198
|
+
tools: Any = _UNSET,
|
|
199
|
+
parallel_tool_calls: Any = _UNSET,
|
|
200
|
+
reasoning: Any = _UNSET,
|
|
201
|
+
service_tier: Any = _UNSET,
|
|
202
|
+
prompt_cache_key: Any = _UNSET,
|
|
203
|
+
text: Any = _UNSET,
|
|
204
|
+
) -> CompactedResponse:
|
|
205
|
+
normalized_tools = normalize_tools(tools)
|
|
206
|
+
payload = {
|
|
207
|
+
"model": _default(model, self._client._defaults["model"]),
|
|
208
|
+
"instructions": _default(instructions, self._client._defaults["instructions"]) or "",
|
|
209
|
+
"input": [normalize_input_item(item) for item in input],
|
|
210
|
+
"tools": normalized_tools,
|
|
211
|
+
"parallel_tool_calls": (
|
|
212
|
+
bool(_default(parallel_tool_calls, False)) if normalized_tools else False
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
if _is_given(reasoning) and reasoning is not None:
|
|
216
|
+
payload["reasoning"] = normalize_reasoning(reasoning)
|
|
217
|
+
if _is_given(service_tier) and service_tier is not None:
|
|
218
|
+
payload["service_tier"] = service_tier
|
|
219
|
+
if _is_given(prompt_cache_key) and prompt_cache_key is not None:
|
|
220
|
+
payload["prompt_cache_key"] = prompt_cache_key
|
|
221
|
+
if _is_given(text) and text is not None:
|
|
222
|
+
payload["text"] = normalize_text(text)
|
|
223
|
+
data = self._client._post("/responses/compact", body=payload).json()
|
|
224
|
+
return CompactedResponse(id=data.get("id", ""), output=data.get("output", []))
|
|
@@ -84,6 +84,10 @@ backend never returns a non-streaming HTTP response. In SDK calls,
|
|
|
84
84
|
|
|
85
85
|
**SDK method**: `client.responses.create(...)`
|
|
86
86
|
|
|
87
|
+
`client.responses.parse(..., text_format=MyPydanticModel)` is a convenience
|
|
88
|
+
wrapper over the same endpoint. It populates `text.format` with a strict JSON
|
|
89
|
+
schema and returns `ParsedResponse`.
|
|
90
|
+
|
|
87
91
|
**Request body** (JSON):
|
|
88
92
|
|
|
89
93
|
```json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
|
|
3
|
-
import codex_backend_sdk.
|
|
3
|
+
import codex_backend_sdk._transport as transport_module
|
|
4
4
|
from codex_backend_sdk import OpenAI
|
|
5
5
|
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ def _response(status_code, *, body=b"{}", headers=None):
|
|
|
37
37
|
|
|
38
38
|
def test_retry_retries_5xx_then_returns_success(monkeypatch):
|
|
39
39
|
sleeps = []
|
|
40
|
-
monkeypatch.setattr(
|
|
40
|
+
monkeypatch.setattr(transport_module.time, "sleep", sleeps.append)
|
|
41
41
|
client = RetryClient([
|
|
42
42
|
_response(503),
|
|
43
43
|
_response(200, body=b'{"ok": true}'),
|
|
@@ -52,7 +52,7 @@ def test_retry_retries_5xx_then_returns_success(monkeypatch):
|
|
|
52
52
|
|
|
53
53
|
def test_retry_honors_retry_after_header(monkeypatch):
|
|
54
54
|
sleeps = []
|
|
55
|
-
monkeypatch.setattr(
|
|
55
|
+
monkeypatch.setattr(transport_module.time, "sleep", sleeps.append)
|
|
56
56
|
client = RetryClient([
|
|
57
57
|
_response(429, headers={"Retry-After": "1.5"}),
|
|
58
58
|
_response(200),
|
|
@@ -66,7 +66,7 @@ def test_retry_honors_retry_after_header(monkeypatch):
|
|
|
66
66
|
|
|
67
67
|
def test_retry_does_not_retry_client_errors(monkeypatch):
|
|
68
68
|
sleeps = []
|
|
69
|
-
monkeypatch.setattr(
|
|
69
|
+
monkeypatch.setattr(transport_module.time, "sleep", sleeps.append)
|
|
70
70
|
client = RetryClient([
|
|
71
71
|
_response(400, body=b'{"error": "bad request"}'),
|
|
72
72
|
])
|
|
@@ -84,7 +84,7 @@ def test_retry_does_not_retry_client_errors(monkeypatch):
|
|
|
84
84
|
|
|
85
85
|
def test_retry_retries_transport_timeout(monkeypatch):
|
|
86
86
|
sleeps = []
|
|
87
|
-
monkeypatch.setattr(
|
|
87
|
+
monkeypatch.setattr(transport_module.time, "sleep", sleeps.append)
|
|
88
88
|
client = RetryClient([
|
|
89
89
|
requests.Timeout("temporary timeout"),
|
|
90
90
|
_response(200, body=b'{"ok": true}'),
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from codex_backend_sdk import OpenAI, ParsedResponse, Response
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
class FakeSSE:
|
|
@@ -75,6 +79,34 @@ class FakeClient(OpenAI):
|
|
|
75
79
|
)
|
|
76
80
|
|
|
77
81
|
|
|
82
|
+
class ParsedPerson(BaseModel):
|
|
83
|
+
name: str
|
|
84
|
+
age: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class TextOptions:
|
|
89
|
+
verbosity: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ParseFakeClient(FakeClient):
|
|
93
|
+
def _post(self, path, *, body, stream=False):
|
|
94
|
+
self.posts.append((path, body, stream))
|
|
95
|
+
return FakeSSE([
|
|
96
|
+
(
|
|
97
|
+
'data: {"type":"response.output_text.delta","delta":'
|
|
98
|
+
'"{\\"name\\":\\"Ada\\",\\"age\\":37}"}'
|
|
99
|
+
),
|
|
100
|
+
"",
|
|
101
|
+
(
|
|
102
|
+
'data: {"type":"response.completed","response":'
|
|
103
|
+
'{"id":"resp_parse","model":"gpt-test","output":[{"type":"message",'
|
|
104
|
+
'"role":"assistant","content":[{"type":"output_text",'
|
|
105
|
+
'"text":"{\\"name\\":\\"Ada\\",\\"age\\":37}"}]}]}}'
|
|
106
|
+
),
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
|
|
78
110
|
def test_responses_create_collects_to_pydantic_response():
|
|
79
111
|
client = FakeClient()
|
|
80
112
|
|
|
@@ -107,6 +139,75 @@ def test_responses_create_collects_to_pydantic_response():
|
|
|
107
139
|
assert payload["text"] == {"verbosity": "low"}
|
|
108
140
|
|
|
109
141
|
|
|
142
|
+
def test_response_exposes_tool_calls_and_reasoning_summary():
|
|
143
|
+
response = Response(
|
|
144
|
+
id="resp_helpers",
|
|
145
|
+
output=[
|
|
146
|
+
{
|
|
147
|
+
"type": "reasoning",
|
|
148
|
+
"summary": [
|
|
149
|
+
{"type": "summary_text", "text": "First"},
|
|
150
|
+
{"type": "summary_text", "text": "Second"},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"type": "function_call",
|
|
155
|
+
"call_id": "call_123",
|
|
156
|
+
"name": "lookup",
|
|
157
|
+
"arguments": "{}",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert response.reasoning_summary == "First\nSecond"
|
|
163
|
+
assert response.tool_calls == [
|
|
164
|
+
{
|
|
165
|
+
"type": "function_call",
|
|
166
|
+
"call_id": "call_123",
|
|
167
|
+
"name": "lookup",
|
|
168
|
+
"arguments": "{}",
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_responses_parse_sends_strict_schema_and_returns_parsed_response():
|
|
174
|
+
client = ParseFakeClient()
|
|
175
|
+
|
|
176
|
+
parsed = client.responses.parse(
|
|
177
|
+
model="gpt-test",
|
|
178
|
+
input="Extract the person",
|
|
179
|
+
text_format=ParsedPerson,
|
|
180
|
+
text=TextOptions(verbosity="low"),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert isinstance(parsed, ParsedResponse)
|
|
184
|
+
assert parsed.id == "resp_parse"
|
|
185
|
+
assert parsed.output_parsed == ParsedPerson(name="Ada", age=37)
|
|
186
|
+
path, payload, stream = client.posts[0]
|
|
187
|
+
assert path == "/responses"
|
|
188
|
+
assert stream is True
|
|
189
|
+
assert payload["text"]["verbosity"] == "low"
|
|
190
|
+
assert payload["text"]["format"]["type"] == "json_schema"
|
|
191
|
+
assert payload["text"]["format"]["name"] == "ParsedPerson"
|
|
192
|
+
assert payload["text"]["format"]["strict"] is True
|
|
193
|
+
assert payload["text"]["format"]["schema"]["additionalProperties"] is False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_responses_parse_rejects_existing_text_format():
|
|
197
|
+
client = ParseFakeClient()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
client.responses.parse(
|
|
201
|
+
input="Extract",
|
|
202
|
+
text_format=ParsedPerson,
|
|
203
|
+
text={"format": {"type": "json_object"}},
|
|
204
|
+
)
|
|
205
|
+
except TypeError as exc:
|
|
206
|
+
assert "text_format" in str(exc)
|
|
207
|
+
else:
|
|
208
|
+
raise AssertionError("Expected TypeError")
|
|
209
|
+
|
|
210
|
+
|
|
110
211
|
def test_responses_create_normalizes_official_input_items():
|
|
111
212
|
client = FakeClient()
|
|
112
213
|
|
|
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
|
{codex_backend_sdk-0.3.2 → codex_backend_sdk-0.3.3}/codex_backend_sdk/resources/openai_oauth.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|