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.
- openresponses_client-0.0.1/.gitignore +19 -0
- openresponses_client-0.0.1/LICENSE +21 -0
- openresponses_client-0.0.1/PKG-INFO +314 -0
- openresponses_client-0.0.1/README.md +288 -0
- openresponses_client-0.0.1/examples/basic.py +42 -0
- openresponses_client-0.0.1/examples/streaming.py +56 -0
- openresponses_client-0.0.1/examples/tools.py +100 -0
- openresponses_client-0.0.1/examples/websocket.py +88 -0
- openresponses_client-0.0.1/openresponses_client/__init__.py +133 -0
- openresponses_client-0.0.1/openresponses_client/_serialization.py +43 -0
- openresponses_client-0.0.1/openresponses_client/accumulator.py +219 -0
- openresponses_client-0.0.1/openresponses_client/client.py +448 -0
- openresponses_client-0.0.1/openresponses_client/const.py +183 -0
- openresponses_client-0.0.1/openresponses_client/exceptions.py +241 -0
- openresponses_client-0.0.1/openresponses_client/models/__init__.py +167 -0
- openresponses_client-0.0.1/openresponses_client/models/base.py +181 -0
- openresponses_client-0.0.1/openresponses_client/models/content.py +213 -0
- openresponses_client-0.0.1/openresponses_client/models/events.py +432 -0
- openresponses_client-0.0.1/openresponses_client/models/items.py +169 -0
- openresponses_client-0.0.1/openresponses_client/models/response.py +228 -0
- openresponses_client-0.0.1/openresponses_client/models/tools.py +80 -0
- openresponses_client-0.0.1/openresponses_client/params.py +353 -0
- openresponses_client-0.0.1/openresponses_client/py.typed +0 -0
- openresponses_client-0.0.1/openresponses_client/sse.py +74 -0
- openresponses_client-0.0.1/openresponses_client/streaming.py +227 -0
- openresponses_client-0.0.1/openresponses_client/websocket.py +521 -0
- openresponses_client-0.0.1/pyproject.toml +139 -0
- openresponses_client-0.0.1/tests/__init__.py +1 -0
- openresponses_client-0.0.1/tests/conftest.py +39 -0
- openresponses_client-0.0.1/tests/fake_server.py +402 -0
- openresponses_client-0.0.1/tests/fixtures/LICENSE-openresponses +201 -0
- openresponses_client-0.0.1/tests/fixtures/openapi.json +4230 -0
- openresponses_client-0.0.1/tests/test_accumulator.py +492 -0
- openresponses_client-0.0.1/tests/test_annotations.py +56 -0
- openresponses_client-0.0.1/tests/test_client.py +621 -0
- openresponses_client-0.0.1/tests/test_exceptions.py +96 -0
- openresponses_client-0.0.1/tests/test_models.py +602 -0
- openresponses_client-0.0.1/tests/test_spec_conformance.py +413 -0
- openresponses_client-0.0.1/tests/test_sse.py +99 -0
- openresponses_client-0.0.1/tests/test_streaming.py +406 -0
- 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())
|