openresponses-client 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. openresponses_client-0.0.1/.gitignore +19 -0
  2. openresponses_client-0.0.1/LICENSE +21 -0
  3. openresponses_client-0.0.1/PKG-INFO +314 -0
  4. openresponses_client-0.0.1/README.md +288 -0
  5. openresponses_client-0.0.1/examples/basic.py +42 -0
  6. openresponses_client-0.0.1/examples/streaming.py +56 -0
  7. openresponses_client-0.0.1/examples/tools.py +100 -0
  8. openresponses_client-0.0.1/examples/websocket.py +88 -0
  9. openresponses_client-0.0.1/openresponses_client/__init__.py +133 -0
  10. openresponses_client-0.0.1/openresponses_client/_serialization.py +43 -0
  11. openresponses_client-0.0.1/openresponses_client/accumulator.py +219 -0
  12. openresponses_client-0.0.1/openresponses_client/client.py +448 -0
  13. openresponses_client-0.0.1/openresponses_client/const.py +183 -0
  14. openresponses_client-0.0.1/openresponses_client/exceptions.py +241 -0
  15. openresponses_client-0.0.1/openresponses_client/models/__init__.py +167 -0
  16. openresponses_client-0.0.1/openresponses_client/models/base.py +181 -0
  17. openresponses_client-0.0.1/openresponses_client/models/content.py +213 -0
  18. openresponses_client-0.0.1/openresponses_client/models/events.py +432 -0
  19. openresponses_client-0.0.1/openresponses_client/models/items.py +169 -0
  20. openresponses_client-0.0.1/openresponses_client/models/response.py +228 -0
  21. openresponses_client-0.0.1/openresponses_client/models/tools.py +80 -0
  22. openresponses_client-0.0.1/openresponses_client/params.py +353 -0
  23. openresponses_client-0.0.1/openresponses_client/py.typed +0 -0
  24. openresponses_client-0.0.1/openresponses_client/sse.py +74 -0
  25. openresponses_client-0.0.1/openresponses_client/streaming.py +227 -0
  26. openresponses_client-0.0.1/openresponses_client/websocket.py +521 -0
  27. openresponses_client-0.0.1/pyproject.toml +139 -0
  28. openresponses_client-0.0.1/tests/__init__.py +1 -0
  29. openresponses_client-0.0.1/tests/conftest.py +39 -0
  30. openresponses_client-0.0.1/tests/fake_server.py +402 -0
  31. openresponses_client-0.0.1/tests/fixtures/LICENSE-openresponses +201 -0
  32. openresponses_client-0.0.1/tests/fixtures/openapi.json +4230 -0
  33. openresponses_client-0.0.1/tests/test_accumulator.py +492 -0
  34. openresponses_client-0.0.1/tests/test_annotations.py +56 -0
  35. openresponses_client-0.0.1/tests/test_client.py +621 -0
  36. openresponses_client-0.0.1/tests/test_exceptions.py +96 -0
  37. openresponses_client-0.0.1/tests/test_models.py +602 -0
  38. openresponses_client-0.0.1/tests/test_spec_conformance.py +413 -0
  39. openresponses_client-0.0.1/tests/test_sse.py +99 -0
  40. openresponses_client-0.0.1/tests/test_streaming.py +406 -0
  41. openresponses_client-0.0.1/tests/test_websocket.py +872 -0
@@ -0,0 +1,19 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Tooling caches and reports
13
+ .coverage
14
+ .coverage.*
15
+ coverage.xml
16
+ htmlcov/
17
+ .mypy_cache/
18
+ .pytest_cache/
19
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Josef Zweck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.5
2
+ Name: openresponses-client
3
+ Version: 0.0.1
4
+ Summary: Asynchronous Python client for the Open Responses API specification, built on aiohttp.
5
+ Project-URL: Homepage, https://github.com/zweckj/openresponses-client
6
+ Project-URL: Repository, https://github.com/zweckj/openresponses-client
7
+ Project-URL: Documentation, https://github.com/zweckj/openresponses-client#readme
8
+ Project-URL: Specification, https://www.openresponses.org/specification
9
+ Author-email: Josef Zweck <josef@zweck.dev>
10
+ Maintainer-email: Josef Zweck <josef@zweck.dev>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: aiohttp,async,client,llm,open responses,openresponses,responses api
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Framework :: aiohttp
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Natural Language :: English
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.14
27
+ Requires-Dist: aiohttp>=3.13
28
+ Requires-Dist: mashumaro>=3.20
29
+ Requires-Dist: yarl>=1.21
30
+ Description-Content-Type: text/markdown
31
+
32
+ ## openresponses-client
33
+
34
+ Async Python client for [Open Responses](https://www.openresponses.org), the open,
35
+ multi-provider API spec based on the Responses API. Built on [aiohttp](https://docs.aiohttp.org).
36
+
37
+ * Works with any server that implements the [spec](https://www.openresponses.org/specification)
38
+ * Typed dataclasses ([mashumaro](https://github.com/Fatal1ty/mashumaro)) for all items and 24 streaming events
39
+ * HTTP streaming, WebSocket mode and compaction
40
+ * Keeps provider extensions instead of failing on them
41
+ * Python 3.14+, targets spec release `2026-04-24`
42
+
43
+ [Quick start](#quick-start) | [Streaming](#streaming) | [Tool calling](#tool-calling) |
44
+ [WebSocket](#websocket-mode) | [Errors](#errors) | [Configuration](#configuration)
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install openresponses-client
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import asyncio
56
+
57
+ from openresponses_client import OpenResponsesClient
58
+
59
+
60
+ async def main() -> None:
61
+ client = OpenResponsesClient("http://localhost:8080/v1", api_key="sk-...")
62
+ async with client:
63
+ response = await client.create(model="gpt-oss:20b", input="Hi!")
64
+ print(response.output_text)
65
+
66
+
67
+ asyncio.run(main())
68
+ ```
69
+
70
+ * `base_url` includes the version prefix. Requests go to `{base_url}/responses`.
71
+ * `api_key` is sent as a Bearer token. Leave it out for local servers.
72
+ * Every request field of the spec is a keyword argument, for example `tools` or `reasoning`.
73
+ * Pass `session=` to reuse your own `aiohttp.ClientSession`.
74
+
75
+ ## Input
76
+
77
+ Input is a string or a list of items. Items are plain dicts or output items of a response.
78
+
79
+ ```python
80
+ response = await client.create(
81
+ model="gpt-oss:20b",
82
+ input=[
83
+ {"role": "developer", "content": "You are a pirate."},
84
+ {
85
+ "role": "user",
86
+ "content": [
87
+ {"type": "input_text", "text": "What do you see?"},
88
+ {"type": "input_image", "image_url": "data:image/png;base64,..."},
89
+ ],
90
+ },
91
+ ],
92
+ )
93
+ ```
94
+
95
+ * Messages without `type` get `"type": "message"` added.
96
+ * Files use `{"type": "input_file", "filename": "a.pdf", "file_data": "data:..."}`.
97
+ * All item shapes are typed as `TypedDict`s in `openresponses_client.params`.
98
+ * Models in `input` are checked when sent: a non-string in a `str` field raises `TypeError`.
99
+
100
+ ## Streaming
101
+
102
+ ```python
103
+ from openresponses_client.models import ResponseOutputTextDeltaEvent
104
+
105
+ async with client.stream(model="gpt-oss:20b", input="Write a haiku.") as stream:
106
+ async for event in stream:
107
+ match event:
108
+ case ResponseOutputTextDeltaEvent(delta=delta):
109
+ print(delta, end="", flush=True)
110
+ response = await stream.get_final_response()
111
+ ```
112
+
113
+ | Member | Gives you |
114
+ |------------------------------|---------------------------------------------------|
115
+ | `await get_final_response()` | The final response, after consuming the stream |
116
+ | `text_deltas()` | An async iterator over the text deltas |
117
+ | `snapshot` | The response rebuilt from the events so far |
118
+ | `final_response` | The final response, once a terminal event arrived |
119
+ | `error_event` | The last `error` event, if any |
120
+
121
+ * Failed and incomplete responses are returned, not raised. Check `response.status`.
122
+ * `get_final_response()` raises `ResponseStreamError` if no final response arrives.
123
+ * `ResponseAccumulator` rebuilds a response from events you received elsewhere.
124
+
125
+ ## Tool calling
126
+
127
+ Run the function calls of a response and send the results back:
128
+
129
+ ```python
130
+ import json
131
+
132
+ tools = [
133
+ {
134
+ "type": "function",
135
+ "name": "get_weather",
136
+ "description": "Get the current weather for a city.",
137
+ "parameters": {
138
+ "type": "object",
139
+ "properties": {"city": {"type": "string"}},
140
+ "required": ["city"],
141
+ },
142
+ }
143
+ ]
144
+
145
+ history = [{"role": "user", "content": "What's the weather in Paris?"}]
146
+ while True:
147
+ response = await client.create(
148
+ model="gpt-oss:20b", input=history, tools=tools, store=False
149
+ )
150
+ history.extend(response.output)
151
+ if not response.function_calls:
152
+ break
153
+ for call in response.function_calls:
154
+ result = get_weather(**call.parse_arguments())
155
+ history.append(
156
+ {
157
+ "type": "function_call_output",
158
+ "call_id": call.call_id,
159
+ "output": json.dumps(result),
160
+ }
161
+ )
162
+
163
+ print(response.output_text)
164
+ ```
165
+
166
+ * Output items go back as input unchanged, including reasoning and provider items.
167
+ * Servers that store responses can continue with `previous_response_id` instead.
168
+ * `tool_choice` takes `"auto"`, `"required"`, `"none"`, a function, `allowed_tools` or a
169
+ hosted tool such as `{"type": "image_generation"}`.
170
+
171
+ ## WebSocket mode
172
+
173
+ One connection for many turns, with the same events as HTTP streaming:
174
+
175
+ ```python
176
+ async with client.websocket() as ws:
177
+ first = await ws.create(model="gpt-oss:20b", store=False, input="Remember: cobalt.")
178
+ second = await ws.create(
179
+ model="gpt-oss:20b",
180
+ store=False,
181
+ previous_response_id=first.id,
182
+ input="What was the code word?",
183
+ )
184
+ ```
185
+
186
+ * `ws.stream()` streams a turn, like `client.stream()`.
187
+ * One turn runs at a time. Concurrent calls are queued. Open more connections for parallel turns.
188
+ * `stream`, `stream_options` and `background` are not accepted.
189
+ * A closed connection reopens for the next turn. A turn the server never started is resent once.
190
+ * Options: `heartbeat`, `receive_timeout`, `max_msg_size`, `extra_headers`, `auto_reconnect`.
191
+
192
+ > [!IMPORTANT]
193
+ > With `store=False` only the current connection remembers the last response. After a
194
+ > reconnect, continuing raises `PreviousResponseNotFoundError`. Resend the full history:
195
+
196
+ ```python
197
+ from openresponses_client import PreviousResponseNotFoundError
198
+
199
+ try:
200
+ response = await ws.create(
201
+ model="gpt-oss:20b",
202
+ store=False,
203
+ previous_response_id=previous.id,
204
+ input=[message],
205
+ )
206
+ except PreviousResponseNotFoundError:
207
+ response = await ws.create(
208
+ model="gpt-oss:20b", store=False, input=[*history, message]
209
+ )
210
+ ```
211
+
212
+ ## Compaction
213
+
214
+ Shrink a long conversation, then start a new chain from the result:
215
+
216
+ ```python
217
+ compacted = await client.compact(model="gpt-oss:20b", input=history)
218
+ response = await client.create(
219
+ model="gpt-oss:20b",
220
+ input=[*compacted.output, {"role": "user", "content": "Continue."}],
221
+ )
222
+ ```
223
+
224
+ ## Errors
225
+
226
+ All exceptions derive from `OpenResponsesError`.
227
+
228
+ | Exception | Raised when |
229
+ |----------------------------------------|-------------------------------------------|
230
+ | `APIStatusError` | The server returned an error (base class) |
231
+ | `BadRequestError` | Status 400 |
232
+ | `AuthenticationError` | Status 401 |
233
+ | `PermissionDeniedError` | Status 403 |
234
+ | `NotFoundError` | Status 404 |
235
+ | `ConflictError` | Status 409 |
236
+ | `UnprocessableEntityError` | Status 422 |
237
+ | `RateLimitError` | Status 429 |
238
+ | `InternalServerError` | Status 5xx |
239
+ | `PreviousResponseNotFoundError` | Code `previous_response_not_found` |
240
+ | `WebSocketConnectionLimitReachedError` | Code `websocket_connection_limit_reached` |
241
+ | `APIConnectionError` | The server is unreachable or disconnected |
242
+ | `APITimeoutError` | A request or stream timed out |
243
+ | `WebSocketClosedError` | The WebSocket closed during a turn |
244
+ | `APIResponseValidationError` | The server sent invalid data |
245
+ | `ResponseStreamError` | A stream ended without a final response |
246
+
247
+ ```python
248
+ from openresponses_client import APIStatusError, RateLimitError
249
+
250
+ try:
251
+ response = await client.create(model="gpt-oss:20b", input="Hi")
252
+ except RateLimitError:
253
+ ...
254
+ except APIStatusError as err:
255
+ print(err.status, err.code, err.message)
256
+ ```
257
+
258
+ * `APIStatusError` has `status`, `code`, `type`, `param`, `message`, `body` and `headers`.
259
+ * `APITimeoutError` and `WebSocketClosedError` are subclasses of `APIConnectionError`.
260
+ * Error messages strip query strings and credentials from URLs, so they are safe to log.
261
+
262
+ ## Extensions
263
+
264
+ Providers can add their own items, tools, events and fields. Nothing is lost:
265
+
266
+ * Unknown types become `UnknownItem`, `UnknownContent`, `UnknownTool` or `UnknownEvent`.
267
+ * Their fields are attributes, also for type checkers, for example `item.code`.
268
+ * Extra fields of known types are in `extra`.
269
+ * `to_dict()` returns the data unchanged, ready to send back.
270
+ * OpenAI style `error` events and `response.reasoning_text.*` events are mapped to the spec.
271
+
272
+ Send your own request fields and headers with `extra_body` and `extra_headers`:
273
+
274
+ ```python
275
+ response = await client.create(
276
+ model="my-model",
277
+ input="Find documents about climate change.",
278
+ tools=[{"type": "acme:document_search", "index": "papers"}],
279
+ extra_body={"acme_priority": "high"},
280
+ )
281
+ ```
282
+
283
+ ## Configuration
284
+
285
+ | Argument | Default | Description |
286
+ |-----------------|----------|---------------------------------------------------------------|
287
+ | `base_url` | required | API URL including the version prefix, such as `.../v1` |
288
+ | `api_key` | `None` | Sent as `Authorization: Bearer <api_key>` |
289
+ | `session` | `None` | Your own `aiohttp.ClientSession` |
290
+ | `headers` | `None` | Headers for every request |
291
+ | `timeout` | `600` | Idle timeout in seconds, an `aiohttp.ClientTimeout` or `None` |
292
+ | `max_retries` | `2` | Retries for connection errors and status 408, 409, 429, 5xx |
293
+ | `websocket_url` | derived | `ws://` or `wss://` version of `{base_url}/responses` |
294
+
295
+ * `create()`, `stream()` and `compact()` also take `timeout`, `extra_headers` and `extra_body`.
296
+ * Retries respect the `Retry-After` header.
297
+
298
+ ## Examples
299
+
300
+ Runnable scripts in [examples](examples): basic request, streaming, tool loop and WebSocket.
301
+ Set `OPENRESPONSES_BASE_URL`, `OPENRESPONSES_API_KEY` and `OPENRESPONSES_MODEL` to run them.
302
+
303
+ ## Development
304
+
305
+ ```bash
306
+ uv sync
307
+ uv run ruff check . && uv run ruff format --check .
308
+ uv run mypy
309
+ uv run pytest --cov
310
+ ```
311
+
312
+ Tests use an in-process server and validate every request against the official OpenAPI
313
+ document in `tests/fixtures`.
314
+
@@ -0,0 +1,288 @@
1
+ ---
2
+ title: openresponses-client
3
+ description: Async Python client for the Open Responses API, built on aiohttp
4
+ ---
5
+
6
+ ## openresponses-client
7
+
8
+ Async Python client for [Open Responses](https://www.openresponses.org), the open,
9
+ multi-provider API spec based on the Responses API. Built on [aiohttp](https://docs.aiohttp.org).
10
+
11
+ * Works with any server that implements the [spec](https://www.openresponses.org/specification)
12
+ * Typed dataclasses ([mashumaro](https://github.com/Fatal1ty/mashumaro)) for all items and 24 streaming events
13
+ * HTTP streaming, WebSocket mode and compaction
14
+ * Keeps provider extensions instead of failing on them
15
+ * Python 3.14+, targets spec release `2026-04-24`
16
+
17
+ [Quick start](#quick-start) | [Streaming](#streaming) | [Tool calling](#tool-calling) |
18
+ [WebSocket](#websocket-mode) | [Errors](#errors) | [Configuration](#configuration)
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install openresponses-client
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ import asyncio
30
+
31
+ from openresponses_client import OpenResponsesClient
32
+
33
+
34
+ async def main() -> None:
35
+ client = OpenResponsesClient("http://localhost:8080/v1", api_key="sk-...")
36
+ async with client:
37
+ response = await client.create(model="gpt-oss:20b", input="Hi!")
38
+ print(response.output_text)
39
+
40
+
41
+ asyncio.run(main())
42
+ ```
43
+
44
+ * `base_url` includes the version prefix. Requests go to `{base_url}/responses`.
45
+ * `api_key` is sent as a Bearer token. Leave it out for local servers.
46
+ * Every request field of the spec is a keyword argument, for example `tools` or `reasoning`.
47
+ * Pass `session=` to reuse your own `aiohttp.ClientSession`.
48
+
49
+ ## Input
50
+
51
+ Input is a string or a list of items. Items are plain dicts or output items of a response.
52
+
53
+ ```python
54
+ response = await client.create(
55
+ model="gpt-oss:20b",
56
+ input=[
57
+ {"role": "developer", "content": "You are a pirate."},
58
+ {
59
+ "role": "user",
60
+ "content": [
61
+ {"type": "input_text", "text": "What do you see?"},
62
+ {"type": "input_image", "image_url": "data:image/png;base64,..."},
63
+ ],
64
+ },
65
+ ],
66
+ )
67
+ ```
68
+
69
+ * Messages without `type` get `"type": "message"` added.
70
+ * Files use `{"type": "input_file", "filename": "a.pdf", "file_data": "data:..."}`.
71
+ * All item shapes are typed as `TypedDict`s in `openresponses_client.params`.
72
+ * Models in `input` are checked when sent: a non-string in a `str` field raises `TypeError`.
73
+
74
+ ## Streaming
75
+
76
+ ```python
77
+ from openresponses_client.models import ResponseOutputTextDeltaEvent
78
+
79
+ async with client.stream(model="gpt-oss:20b", input="Write a haiku.") as stream:
80
+ async for event in stream:
81
+ match event:
82
+ case ResponseOutputTextDeltaEvent(delta=delta):
83
+ print(delta, end="", flush=True)
84
+ response = await stream.get_final_response()
85
+ ```
86
+
87
+ | Member | Gives you |
88
+ |------------------------------|---------------------------------------------------|
89
+ | `await get_final_response()` | The final response, after consuming the stream |
90
+ | `text_deltas()` | An async iterator over the text deltas |
91
+ | `snapshot` | The response rebuilt from the events so far |
92
+ | `final_response` | The final response, once a terminal event arrived |
93
+ | `error_event` | The last `error` event, if any |
94
+
95
+ * Failed and incomplete responses are returned, not raised. Check `response.status`.
96
+ * `get_final_response()` raises `ResponseStreamError` if no final response arrives.
97
+ * `ResponseAccumulator` rebuilds a response from events you received elsewhere.
98
+
99
+ ## Tool calling
100
+
101
+ Run the function calls of a response and send the results back:
102
+
103
+ ```python
104
+ import json
105
+
106
+ tools = [
107
+ {
108
+ "type": "function",
109
+ "name": "get_weather",
110
+ "description": "Get the current weather for a city.",
111
+ "parameters": {
112
+ "type": "object",
113
+ "properties": {"city": {"type": "string"}},
114
+ "required": ["city"],
115
+ },
116
+ }
117
+ ]
118
+
119
+ history = [{"role": "user", "content": "What's the weather in Paris?"}]
120
+ while True:
121
+ response = await client.create(
122
+ model="gpt-oss:20b", input=history, tools=tools, store=False
123
+ )
124
+ history.extend(response.output)
125
+ if not response.function_calls:
126
+ break
127
+ for call in response.function_calls:
128
+ result = get_weather(**call.parse_arguments())
129
+ history.append(
130
+ {
131
+ "type": "function_call_output",
132
+ "call_id": call.call_id,
133
+ "output": json.dumps(result),
134
+ }
135
+ )
136
+
137
+ print(response.output_text)
138
+ ```
139
+
140
+ * Output items go back as input unchanged, including reasoning and provider items.
141
+ * Servers that store responses can continue with `previous_response_id` instead.
142
+ * `tool_choice` takes `"auto"`, `"required"`, `"none"`, a function, `allowed_tools` or a
143
+ hosted tool such as `{"type": "image_generation"}`.
144
+
145
+ ## WebSocket mode
146
+
147
+ One connection for many turns, with the same events as HTTP streaming:
148
+
149
+ ```python
150
+ async with client.websocket() as ws:
151
+ first = await ws.create(model="gpt-oss:20b", store=False, input="Remember: cobalt.")
152
+ second = await ws.create(
153
+ model="gpt-oss:20b",
154
+ store=False,
155
+ previous_response_id=first.id,
156
+ input="What was the code word?",
157
+ )
158
+ ```
159
+
160
+ * `ws.stream()` streams a turn, like `client.stream()`.
161
+ * One turn runs at a time. Concurrent calls are queued. Open more connections for parallel turns.
162
+ * `stream`, `stream_options` and `background` are not accepted.
163
+ * A closed connection reopens for the next turn. A turn the server never started is resent once.
164
+ * Options: `heartbeat`, `receive_timeout`, `max_msg_size`, `extra_headers`, `auto_reconnect`.
165
+
166
+ > [!IMPORTANT]
167
+ > With `store=False` only the current connection remembers the last response. After a
168
+ > reconnect, continuing raises `PreviousResponseNotFoundError`. Resend the full history:
169
+
170
+ ```python
171
+ from openresponses_client import PreviousResponseNotFoundError
172
+
173
+ try:
174
+ response = await ws.create(
175
+ model="gpt-oss:20b",
176
+ store=False,
177
+ previous_response_id=previous.id,
178
+ input=[message],
179
+ )
180
+ except PreviousResponseNotFoundError:
181
+ response = await ws.create(
182
+ model="gpt-oss:20b", store=False, input=[*history, message]
183
+ )
184
+ ```
185
+
186
+ ## Compaction
187
+
188
+ Shrink a long conversation, then start a new chain from the result:
189
+
190
+ ```python
191
+ compacted = await client.compact(model="gpt-oss:20b", input=history)
192
+ response = await client.create(
193
+ model="gpt-oss:20b",
194
+ input=[*compacted.output, {"role": "user", "content": "Continue."}],
195
+ )
196
+ ```
197
+
198
+ ## Errors
199
+
200
+ All exceptions derive from `OpenResponsesError`.
201
+
202
+ | Exception | Raised when |
203
+ |----------------------------------------|-------------------------------------------|
204
+ | `APIStatusError` | The server returned an error (base class) |
205
+ | `BadRequestError` | Status 400 |
206
+ | `AuthenticationError` | Status 401 |
207
+ | `PermissionDeniedError` | Status 403 |
208
+ | `NotFoundError` | Status 404 |
209
+ | `ConflictError` | Status 409 |
210
+ | `UnprocessableEntityError` | Status 422 |
211
+ | `RateLimitError` | Status 429 |
212
+ | `InternalServerError` | Status 5xx |
213
+ | `PreviousResponseNotFoundError` | Code `previous_response_not_found` |
214
+ | `WebSocketConnectionLimitReachedError` | Code `websocket_connection_limit_reached` |
215
+ | `APIConnectionError` | The server is unreachable or disconnected |
216
+ | `APITimeoutError` | A request or stream timed out |
217
+ | `WebSocketClosedError` | The WebSocket closed during a turn |
218
+ | `APIResponseValidationError` | The server sent invalid data |
219
+ | `ResponseStreamError` | A stream ended without a final response |
220
+
221
+ ```python
222
+ from openresponses_client import APIStatusError, RateLimitError
223
+
224
+ try:
225
+ response = await client.create(model="gpt-oss:20b", input="Hi")
226
+ except RateLimitError:
227
+ ...
228
+ except APIStatusError as err:
229
+ print(err.status, err.code, err.message)
230
+ ```
231
+
232
+ * `APIStatusError` has `status`, `code`, `type`, `param`, `message`, `body` and `headers`.
233
+ * `APITimeoutError` and `WebSocketClosedError` are subclasses of `APIConnectionError`.
234
+ * Error messages strip query strings and credentials from URLs, so they are safe to log.
235
+
236
+ ## Extensions
237
+
238
+ Providers can add their own items, tools, events and fields. Nothing is lost:
239
+
240
+ * Unknown types become `UnknownItem`, `UnknownContent`, `UnknownTool` or `UnknownEvent`.
241
+ * Their fields are attributes, also for type checkers, for example `item.code`.
242
+ * Extra fields of known types are in `extra`.
243
+ * `to_dict()` returns the data unchanged, ready to send back.
244
+ * OpenAI style `error` events and `response.reasoning_text.*` events are mapped to the spec.
245
+
246
+ Send your own request fields and headers with `extra_body` and `extra_headers`:
247
+
248
+ ```python
249
+ response = await client.create(
250
+ model="my-model",
251
+ input="Find documents about climate change.",
252
+ tools=[{"type": "acme:document_search", "index": "papers"}],
253
+ extra_body={"acme_priority": "high"},
254
+ )
255
+ ```
256
+
257
+ ## Configuration
258
+
259
+ | Argument | Default | Description |
260
+ |-----------------|----------|---------------------------------------------------------------|
261
+ | `base_url` | required | API URL including the version prefix, such as `.../v1` |
262
+ | `api_key` | `None` | Sent as `Authorization: Bearer <api_key>` |
263
+ | `session` | `None` | Your own `aiohttp.ClientSession` |
264
+ | `headers` | `None` | Headers for every request |
265
+ | `timeout` | `600` | Idle timeout in seconds, an `aiohttp.ClientTimeout` or `None` |
266
+ | `max_retries` | `2` | Retries for connection errors and status 408, 409, 429, 5xx |
267
+ | `websocket_url` | derived | `ws://` or `wss://` version of `{base_url}/responses` |
268
+
269
+ * `create()`, `stream()` and `compact()` also take `timeout`, `extra_headers` and `extra_body`.
270
+ * Retries respect the `Retry-After` header.
271
+
272
+ ## Examples
273
+
274
+ Runnable scripts in [examples](examples): basic request, streaming, tool loop and WebSocket.
275
+ Set `OPENRESPONSES_BASE_URL`, `OPENRESPONSES_API_KEY` and `OPENRESPONSES_MODEL` to run them.
276
+
277
+ ## Development
278
+
279
+ ```bash
280
+ uv sync
281
+ uv run ruff check . && uv run ruff format --check .
282
+ uv run mypy
283
+ uv run pytest --cov
284
+ ```
285
+
286
+ Tests use an in-process server and validate every request against the official OpenAPI
287
+ document in `tests/fixtures`.
288
+
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """Create a response and print its text.
3
+
4
+ Usage: python examples/basic.py "What is the capital of France?"
5
+ Configure with OPENRESPONSES_BASE_URL, OPENRESPONSES_API_KEY, OPENRESPONSES_MODEL.
6
+ """
7
+
8
+ import asyncio
9
+ import os
10
+ import sys
11
+
12
+ from openresponses_client import APIError, OpenResponsesClient
13
+
14
+
15
+ async def run(question: str) -> int:
16
+ async with OpenResponsesClient(
17
+ os.environ.get("OPENRESPONSES_BASE_URL", "http://localhost:8080/v1"),
18
+ api_key=os.environ.get("OPENRESPONSES_API_KEY"),
19
+ ) as client:
20
+ try:
21
+ response = await client.create(
22
+ model=os.environ.get("OPENRESPONSES_MODEL", "gpt-oss:20b"),
23
+ instructions="Answer in one short sentence.",
24
+ input=question,
25
+ )
26
+ except APIError as err:
27
+ print(f"Request failed: {err}", file=sys.stderr)
28
+ return 1
29
+
30
+ print(response.output_text)
31
+ if response.usage is not None:
32
+ print(f"\n[{response.usage.total_tokens} tokens, status {response.status}]")
33
+ return 0
34
+
35
+
36
+ def main() -> int:
37
+ question = " ".join(sys.argv[1:]) or "Say hello in exactly three words."
38
+ return asyncio.run(run(question))
39
+
40
+
41
+ if __name__ == "__main__":
42
+ sys.exit(main())