openresponses-client 0.0.1__py3-none-any.whl
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/__init__.py +133 -0
- openresponses_client/_serialization.py +43 -0
- openresponses_client/accumulator.py +219 -0
- openresponses_client/client.py +448 -0
- openresponses_client/const.py +183 -0
- openresponses_client/exceptions.py +241 -0
- openresponses_client/models/__init__.py +167 -0
- openresponses_client/models/base.py +181 -0
- openresponses_client/models/content.py +213 -0
- openresponses_client/models/events.py +432 -0
- openresponses_client/models/items.py +169 -0
- openresponses_client/models/response.py +228 -0
- openresponses_client/models/tools.py +80 -0
- openresponses_client/params.py +353 -0
- openresponses_client/py.typed +0 -0
- openresponses_client/sse.py +74 -0
- openresponses_client/streaming.py +227 -0
- openresponses_client/websocket.py +521 -0
- openresponses_client-0.0.1.dist-info/METADATA +314 -0
- openresponses_client-0.0.1.dist-info/RECORD +22 -0
- openresponses_client-0.0.1.dist-info/WHEEL +4 -0
- openresponses_client-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Async client for the Open Responses API."""
|
|
2
|
+
|
|
3
|
+
from .accumulator import ResponseAccumulator
|
|
4
|
+
from .client import OpenResponsesClient
|
|
5
|
+
from .const import (
|
|
6
|
+
ContentType,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
ErrorType,
|
|
9
|
+
EventType,
|
|
10
|
+
ImageDetail,
|
|
11
|
+
Include,
|
|
12
|
+
ItemStatus,
|
|
13
|
+
ItemType,
|
|
14
|
+
MessagePhase,
|
|
15
|
+
MessageRole,
|
|
16
|
+
ReasoningEffort,
|
|
17
|
+
ReasoningSummary,
|
|
18
|
+
ResponseStatus,
|
|
19
|
+
ServiceTier,
|
|
20
|
+
ToolChoiceMode,
|
|
21
|
+
Truncation,
|
|
22
|
+
Verbosity,
|
|
23
|
+
)
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
APIConnectionError,
|
|
26
|
+
APIError,
|
|
27
|
+
APIResponseValidationError,
|
|
28
|
+
APIStatusError,
|
|
29
|
+
APITimeoutError,
|
|
30
|
+
AuthenticationError,
|
|
31
|
+
BadRequestError,
|
|
32
|
+
ConflictError,
|
|
33
|
+
InternalServerError,
|
|
34
|
+
NotFoundError,
|
|
35
|
+
OpenResponsesError,
|
|
36
|
+
PermissionDeniedError,
|
|
37
|
+
PreviousResponseNotFoundError,
|
|
38
|
+
RateLimitError,
|
|
39
|
+
ResponseStreamError,
|
|
40
|
+
UnprocessableEntityError,
|
|
41
|
+
WebSocketClosedError,
|
|
42
|
+
WebSocketConnectionLimitReachedError,
|
|
43
|
+
)
|
|
44
|
+
from .models import (
|
|
45
|
+
CompactionItem,
|
|
46
|
+
CompactResponse,
|
|
47
|
+
ErrorEvent,
|
|
48
|
+
FunctionCall,
|
|
49
|
+
FunctionCallOutput,
|
|
50
|
+
Item,
|
|
51
|
+
Message,
|
|
52
|
+
ReasoningItem,
|
|
53
|
+
Response,
|
|
54
|
+
StreamingEvent,
|
|
55
|
+
UnknownEvent,
|
|
56
|
+
UnknownItem,
|
|
57
|
+
parse_event,
|
|
58
|
+
)
|
|
59
|
+
from .params import (
|
|
60
|
+
CreateResponseParams,
|
|
61
|
+
FunctionCallOutputItemParam,
|
|
62
|
+
FunctionToolParam,
|
|
63
|
+
InputItem,
|
|
64
|
+
ResponseInput,
|
|
65
|
+
ResponseParams,
|
|
66
|
+
StreamResponseParams,
|
|
67
|
+
)
|
|
68
|
+
from .streaming import BaseResponseStream, ResponseStream
|
|
69
|
+
from .websocket import ResponsesWebSocket, WebSocketResponseStream
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"APIConnectionError",
|
|
73
|
+
"APIError",
|
|
74
|
+
"APIResponseValidationError",
|
|
75
|
+
"APIStatusError",
|
|
76
|
+
"APITimeoutError",
|
|
77
|
+
"AuthenticationError",
|
|
78
|
+
"BadRequestError",
|
|
79
|
+
"BaseResponseStream",
|
|
80
|
+
"CompactResponse",
|
|
81
|
+
"CompactionItem",
|
|
82
|
+
"ConflictError",
|
|
83
|
+
"ContentType",
|
|
84
|
+
"CreateResponseParams",
|
|
85
|
+
"ErrorCode",
|
|
86
|
+
"ErrorEvent",
|
|
87
|
+
"ErrorType",
|
|
88
|
+
"EventType",
|
|
89
|
+
"FunctionCall",
|
|
90
|
+
"FunctionCallOutput",
|
|
91
|
+
"FunctionCallOutputItemParam",
|
|
92
|
+
"FunctionToolParam",
|
|
93
|
+
"ImageDetail",
|
|
94
|
+
"Include",
|
|
95
|
+
"InputItem",
|
|
96
|
+
"InternalServerError",
|
|
97
|
+
"Item",
|
|
98
|
+
"ItemStatus",
|
|
99
|
+
"ItemType",
|
|
100
|
+
"Message",
|
|
101
|
+
"MessagePhase",
|
|
102
|
+
"MessageRole",
|
|
103
|
+
"NotFoundError",
|
|
104
|
+
"OpenResponsesClient",
|
|
105
|
+
"OpenResponsesError",
|
|
106
|
+
"PermissionDeniedError",
|
|
107
|
+
"PreviousResponseNotFoundError",
|
|
108
|
+
"RateLimitError",
|
|
109
|
+
"ReasoningEffort",
|
|
110
|
+
"ReasoningItem",
|
|
111
|
+
"ReasoningSummary",
|
|
112
|
+
"Response",
|
|
113
|
+
"ResponseAccumulator",
|
|
114
|
+
"ResponseInput",
|
|
115
|
+
"ResponseParams",
|
|
116
|
+
"ResponseStatus",
|
|
117
|
+
"ResponseStream",
|
|
118
|
+
"ResponseStreamError",
|
|
119
|
+
"ResponsesWebSocket",
|
|
120
|
+
"ServiceTier",
|
|
121
|
+
"StreamResponseParams",
|
|
122
|
+
"StreamingEvent",
|
|
123
|
+
"ToolChoiceMode",
|
|
124
|
+
"Truncation",
|
|
125
|
+
"UnknownEvent",
|
|
126
|
+
"UnknownItem",
|
|
127
|
+
"UnprocessableEntityError",
|
|
128
|
+
"Verbosity",
|
|
129
|
+
"WebSocketClosedError",
|
|
130
|
+
"WebSocketConnectionLimitReachedError",
|
|
131
|
+
"WebSocketResponseStream",
|
|
132
|
+
"parse_event",
|
|
133
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Build JSON request bodies."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_body(
|
|
9
|
+
params: Mapping[str, Any], extra_body: Mapping[str, Any] | None = None
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
"""Drop `None` values, add `type: message` to messages and merge `extra_body`."""
|
|
12
|
+
body = {key: value for key, value in params.items() if value is not None}
|
|
13
|
+
match body:
|
|
14
|
+
case {"input": [*items]}:
|
|
15
|
+
body["input"] = [_with_type(item) for item in items]
|
|
16
|
+
return {**body, **(extra_body or {})}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _with_type(item: Any) -> Any:
|
|
20
|
+
"""Add `type: message` to messages given as role-only mappings."""
|
|
21
|
+
match item:
|
|
22
|
+
case {"type": _}:
|
|
23
|
+
return item
|
|
24
|
+
case {"role": _}:
|
|
25
|
+
return {"type": "message", **item}
|
|
26
|
+
return item
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dumps(body: Any) -> str:
|
|
30
|
+
"""Encode a body as JSON, serializing models with `to_dict()`."""
|
|
31
|
+
return json.dumps(body, default=_to_dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _to_dict(value: Any) -> Any:
|
|
35
|
+
try:
|
|
36
|
+
return value.to_dict()
|
|
37
|
+
except AttributeError as err:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"Object of type {type(value).__name__} is not JSON serializable"
|
|
40
|
+
) from err
|
|
41
|
+
except (TypeError, ValueError) as err:
|
|
42
|
+
# Models check their fields, for example that `str` fields hold strings.
|
|
43
|
+
raise TypeError(f"Invalid {type(value).__name__}: {err}") from err
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Rebuild a response from its streaming events."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Literal, Protocol
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
ContentPart,
|
|
8
|
+
FunctionCall,
|
|
9
|
+
Item,
|
|
10
|
+
Message,
|
|
11
|
+
OutputTextContent,
|
|
12
|
+
ReasoningItem,
|
|
13
|
+
ReasoningTextContent,
|
|
14
|
+
RefusalContent,
|
|
15
|
+
Response,
|
|
16
|
+
ResponseContentPartAddedEvent,
|
|
17
|
+
ResponseContentPartDoneEvent,
|
|
18
|
+
ResponseFunctionCallArgumentsDeltaEvent,
|
|
19
|
+
ResponseFunctionCallArgumentsDoneEvent,
|
|
20
|
+
ResponseLifecycleEvent,
|
|
21
|
+
ResponseOutputItemAddedEvent,
|
|
22
|
+
ResponseOutputItemDoneEvent,
|
|
23
|
+
ResponseOutputTextAnnotationAddedEvent,
|
|
24
|
+
ResponseOutputTextDeltaEvent,
|
|
25
|
+
ResponseOutputTextDoneEvent,
|
|
26
|
+
ResponseReasoningDeltaEvent,
|
|
27
|
+
ResponseReasoningDoneEvent,
|
|
28
|
+
ResponseReasoningSummaryPartAddedEvent,
|
|
29
|
+
ResponseReasoningSummaryPartDoneEvent,
|
|
30
|
+
ResponseReasoningSummaryTextDeltaEvent,
|
|
31
|
+
ResponseReasoningSummaryTextDoneEvent,
|
|
32
|
+
ResponseRefusalDeltaEvent,
|
|
33
|
+
ResponseRefusalDoneEvent,
|
|
34
|
+
StreamingEvent,
|
|
35
|
+
SummaryTextContent,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = ["ResponseAccumulator"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _ItemEvent(Protocol):
|
|
42
|
+
"""Event that addresses an output item."""
|
|
43
|
+
|
|
44
|
+
item_id: str
|
|
45
|
+
output_index: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
type _TextEvent = (
|
|
49
|
+
ResponseOutputTextDeltaEvent
|
|
50
|
+
| ResponseOutputTextDoneEvent
|
|
51
|
+
| ResponseRefusalDeltaEvent
|
|
52
|
+
| ResponseRefusalDoneEvent
|
|
53
|
+
| ResponseReasoningDeltaEvent
|
|
54
|
+
| ResponseReasoningDoneEvent
|
|
55
|
+
| ResponseReasoningSummaryTextDeltaEvent
|
|
56
|
+
| ResponseReasoningSummaryTextDoneEvent
|
|
57
|
+
| ResponseFunctionCallArgumentsDeltaEvent
|
|
58
|
+
| ResponseFunctionCallArgumentsDoneEvent
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _set_at[T](items: list[T], index: int, value: T) -> None:
|
|
63
|
+
"""Replace the item at `index`, or append it."""
|
|
64
|
+
if 0 <= index < len(items):
|
|
65
|
+
items[index] = value
|
|
66
|
+
else:
|
|
67
|
+
items.append(value)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _updated(text: str, event: _TextEvent) -> str:
|
|
71
|
+
"""Append the delta of an event, or take the final text of a done event."""
|
|
72
|
+
match event:
|
|
73
|
+
case (
|
|
74
|
+
ResponseOutputTextDoneEvent(text=final)
|
|
75
|
+
| ResponseRefusalDoneEvent(refusal=final)
|
|
76
|
+
| ResponseReasoningDoneEvent(text=final)
|
|
77
|
+
| ResponseReasoningSummaryTextDoneEvent(text=final)
|
|
78
|
+
| ResponseFunctionCallArgumentsDoneEvent(arguments=final)
|
|
79
|
+
):
|
|
80
|
+
return final
|
|
81
|
+
case _:
|
|
82
|
+
return text + event.delta
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ResponseAccumulator:
|
|
86
|
+
"""Rebuild a response snapshot from streaming events."""
|
|
87
|
+
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._response = Response(status="in_progress")
|
|
90
|
+
self._started = False
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def response(self) -> Response | None:
|
|
94
|
+
"""The current snapshot, or `None` before the response or an item arrived."""
|
|
95
|
+
return self._response if self._started else None
|
|
96
|
+
|
|
97
|
+
def add(self, event: StreamingEvent) -> None:
|
|
98
|
+
"""Apply an event to the snapshot."""
|
|
99
|
+
match event:
|
|
100
|
+
case ResponseLifecycleEvent():
|
|
101
|
+
self._apply_response(event.response)
|
|
102
|
+
case (
|
|
103
|
+
ResponseOutputItemAddedEvent(item=item)
|
|
104
|
+
| ResponseOutputItemDoneEvent(item=item)
|
|
105
|
+
) if item is not None:
|
|
106
|
+
self._started = True
|
|
107
|
+
_set_at(self._response.output, event.output_index, deepcopy(item))
|
|
108
|
+
case (
|
|
109
|
+
ResponseContentPartAddedEvent(part=part)
|
|
110
|
+
| ResponseContentPartDoneEvent(part=part)
|
|
111
|
+
) if part is not None:
|
|
112
|
+
parts = self._parts(event, "content")
|
|
113
|
+
_set_at(parts, event.content_index, deepcopy(part))
|
|
114
|
+
case (
|
|
115
|
+
ResponseReasoningSummaryPartAddedEvent(part=part)
|
|
116
|
+
| ResponseReasoningSummaryPartDoneEvent(part=part)
|
|
117
|
+
) if part is not None:
|
|
118
|
+
parts = self._parts(event, "summary")
|
|
119
|
+
_set_at(parts, event.summary_index, deepcopy(part))
|
|
120
|
+
case ResponseOutputTextDeltaEvent() | ResponseOutputTextDoneEvent():
|
|
121
|
+
self._apply_output_text(event)
|
|
122
|
+
case ResponseOutputTextAnnotationAddedEvent(annotation=annotation) if (
|
|
123
|
+
annotation is not None
|
|
124
|
+
):
|
|
125
|
+
text = self._part(
|
|
126
|
+
event, "content", event.content_index, OutputTextContent
|
|
127
|
+
)
|
|
128
|
+
_set_at(text.annotations, event.annotation_index, deepcopy(annotation))
|
|
129
|
+
case ResponseRefusalDeltaEvent() | ResponseRefusalDoneEvent():
|
|
130
|
+
refusal = self._part(
|
|
131
|
+
event, "content", event.content_index, RefusalContent
|
|
132
|
+
)
|
|
133
|
+
refusal.refusal = _updated(refusal.refusal, event)
|
|
134
|
+
case ResponseReasoningDeltaEvent() | ResponseReasoningDoneEvent():
|
|
135
|
+
reasoning = self._part(
|
|
136
|
+
event, "content", event.content_index, ReasoningTextContent
|
|
137
|
+
)
|
|
138
|
+
reasoning.text = _updated(reasoning.text, event)
|
|
139
|
+
case (
|
|
140
|
+
ResponseReasoningSummaryTextDeltaEvent()
|
|
141
|
+
| ResponseReasoningSummaryTextDoneEvent()
|
|
142
|
+
):
|
|
143
|
+
summary = self._part(
|
|
144
|
+
event, "summary", event.summary_index, SummaryTextContent
|
|
145
|
+
)
|
|
146
|
+
summary.text = _updated(summary.text, event)
|
|
147
|
+
case (
|
|
148
|
+
ResponseFunctionCallArgumentsDeltaEvent()
|
|
149
|
+
| ResponseFunctionCallArgumentsDoneEvent()
|
|
150
|
+
):
|
|
151
|
+
self._apply_arguments(event)
|
|
152
|
+
|
|
153
|
+
def _apply_response(self, response: Response) -> None:
|
|
154
|
+
"""Take the response of a lifecycle event, keeping streamed output."""
|
|
155
|
+
snapshot = deepcopy(response)
|
|
156
|
+
snapshot.output = snapshot.output or self._response.output
|
|
157
|
+
self._response = snapshot
|
|
158
|
+
self._started = True
|
|
159
|
+
|
|
160
|
+
def _apply_output_text(
|
|
161
|
+
self, event: ResponseOutputTextDeltaEvent | ResponseOutputTextDoneEvent
|
|
162
|
+
) -> None:
|
|
163
|
+
part = self._part(event, "content", event.content_index, OutputTextContent)
|
|
164
|
+
part.text = _updated(part.text, event)
|
|
165
|
+
match event:
|
|
166
|
+
case ResponseOutputTextDeltaEvent(logprobs=logprobs) if logprobs:
|
|
167
|
+
part.logprobs = [*(part.logprobs or []), *deepcopy(logprobs)]
|
|
168
|
+
case ResponseOutputTextDoneEvent(logprobs=logprobs) if logprobs is not None:
|
|
169
|
+
part.logprobs = deepcopy(logprobs)
|
|
170
|
+
|
|
171
|
+
def _apply_arguments(
|
|
172
|
+
self,
|
|
173
|
+
event: ResponseFunctionCallArgumentsDeltaEvent
|
|
174
|
+
| ResponseFunctionCallArgumentsDoneEvent,
|
|
175
|
+
) -> None:
|
|
176
|
+
match self._item(event):
|
|
177
|
+
case FunctionCall() as call:
|
|
178
|
+
call.arguments = _updated(call.arguments, event)
|
|
179
|
+
|
|
180
|
+
def _item(self, event: _ItemEvent) -> Item | None:
|
|
181
|
+
"""Find the item of an event by output index, falling back to its id."""
|
|
182
|
+
output = self._response.output
|
|
183
|
+
if 0 <= event.output_index < len(output):
|
|
184
|
+
item = output[event.output_index]
|
|
185
|
+
if not event.item_id or item.id in (None, event.item_id):
|
|
186
|
+
return item
|
|
187
|
+
return next(
|
|
188
|
+
(item for item in output if event.item_id and item.id == event.item_id),
|
|
189
|
+
None,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _parts(
|
|
193
|
+
self, event: _ItemEvent, attribute: Literal["content", "summary"]
|
|
194
|
+
) -> list[ContentPart]:
|
|
195
|
+
"""Return the parts of the event's item; unknown targets give a new list."""
|
|
196
|
+
match self._item(event), attribute:
|
|
197
|
+
case Message(content=parts), "content":
|
|
198
|
+
return parts
|
|
199
|
+
case ReasoningItem(summary=parts), "summary":
|
|
200
|
+
return parts
|
|
201
|
+
case ReasoningItem() as item, "content":
|
|
202
|
+
item.content = item.content or []
|
|
203
|
+
return item.content
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
def _part[P: ContentPart](
|
|
207
|
+
self,
|
|
208
|
+
event: _ItemEvent,
|
|
209
|
+
attribute: Literal["content", "summary"],
|
|
210
|
+
index: int,
|
|
211
|
+
kind: type[P],
|
|
212
|
+
) -> P:
|
|
213
|
+
"""Return the part of `kind` at `index`; unknown targets give a new part."""
|
|
214
|
+
parts = self._parts(event, attribute)
|
|
215
|
+
if index == len(parts):
|
|
216
|
+
# The part was not announced; create it from the delta.
|
|
217
|
+
parts.append(kind())
|
|
218
|
+
part = parts[index] if 0 <= index < len(parts) else None
|
|
219
|
+
return part if isinstance(part, kind) else kind()
|