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.
@@ -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()