devcopilot 0.2.0__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.
Files changed (189) hide show
  1. api/__init__.py +17 -0
  2. api/admin_config.py +1303 -0
  3. api/admin_routes.py +287 -0
  4. api/admin_static/admin.css +459 -0
  5. api/admin_static/admin.js +497 -0
  6. api/admin_static/index.html +77 -0
  7. api/admin_urls.py +34 -0
  8. api/app.py +194 -0
  9. api/command_utils.py +164 -0
  10. api/dependencies.py +144 -0
  11. api/detection.py +152 -0
  12. api/gateway_model_ids.py +54 -0
  13. api/model_catalog.py +133 -0
  14. api/model_router.py +125 -0
  15. api/models/__init__.py +45 -0
  16. api/models/anthropic.py +234 -0
  17. api/models/openai_responses.py +28 -0
  18. api/models/responses.py +60 -0
  19. api/optimization_handlers.py +154 -0
  20. api/request_pipeline.py +424 -0
  21. api/routes.py +156 -0
  22. api/runtime.py +334 -0
  23. api/validation_log.py +48 -0
  24. api/web_server_tools.py +22 -0
  25. api/web_tools/__init__.py +17 -0
  26. api/web_tools/constants.py +15 -0
  27. api/web_tools/egress.py +99 -0
  28. api/web_tools/outbound.py +278 -0
  29. api/web_tools/parsers.py +104 -0
  30. api/web_tools/request.py +87 -0
  31. api/web_tools/streaming.py +206 -0
  32. cli/__init__.py +5 -0
  33. cli/claude_env.py +12 -0
  34. cli/entrypoints.py +166 -0
  35. cli/env.example +209 -0
  36. cli/launchers/__init__.py +1 -0
  37. cli/launchers/claude.py +84 -0
  38. cli/launchers/codex.py +204 -0
  39. cli/launchers/codex_model_catalog.py +186 -0
  40. cli/launchers/common.py +93 -0
  41. cli/managed/__init__.py +6 -0
  42. cli/managed/claude.py +215 -0
  43. cli/managed/manager.py +157 -0
  44. cli/managed/session.py +260 -0
  45. cli/process_registry.py +78 -0
  46. config/__init__.py +5 -0
  47. config/constants.py +13 -0
  48. config/logging_config.py +159 -0
  49. config/nim.py +118 -0
  50. config/paths.py +91 -0
  51. config/provider_catalog.py +259 -0
  52. config/provider_ids.py +7 -0
  53. config/settings.py +538 -0
  54. core/__init__.py +1 -0
  55. core/anthropic/__init__.py +46 -0
  56. core/anthropic/content.py +31 -0
  57. core/anthropic/conversion.py +587 -0
  58. core/anthropic/emitted_sse_tracker.py +346 -0
  59. core/anthropic/errors.py +70 -0
  60. core/anthropic/native_messages_request.py +280 -0
  61. core/anthropic/native_sse_block_policy.py +313 -0
  62. core/anthropic/provider_stream_error.py +34 -0
  63. core/anthropic/server_tool_sse.py +14 -0
  64. core/anthropic/sse.py +440 -0
  65. core/anthropic/stream_contracts.py +205 -0
  66. core/anthropic/stream_recovery.py +346 -0
  67. core/anthropic/stream_recovery_session.py +133 -0
  68. core/anthropic/thinking.py +140 -0
  69. core/anthropic/tokens.py +117 -0
  70. core/anthropic/tools.py +212 -0
  71. core/anthropic/utils.py +9 -0
  72. core/openai_responses/__init__.py +5 -0
  73. core/openai_responses/adapter.py +31 -0
  74. core/openai_responses/anthropic_sse.py +59 -0
  75. core/openai_responses/errors.py +22 -0
  76. core/openai_responses/events.py +19 -0
  77. core/openai_responses/ids.py +21 -0
  78. core/openai_responses/input.py +258 -0
  79. core/openai_responses/items.py +37 -0
  80. core/openai_responses/reasoning.py +52 -0
  81. core/openai_responses/stream.py +25 -0
  82. core/openai_responses/stream_state.py +654 -0
  83. core/openai_responses/tools.py +374 -0
  84. core/openai_responses/usage.py +37 -0
  85. core/rate_limit.py +60 -0
  86. core/trace.py +216 -0
  87. devcopilot-0.2.0.dist-info/METADATA +687 -0
  88. devcopilot-0.2.0.dist-info/RECORD +189 -0
  89. devcopilot-0.2.0.dist-info/WHEEL +4 -0
  90. devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
  91. devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
  92. messaging/__init__.py +26 -0
  93. messaging/cli_event_constants.py +67 -0
  94. messaging/command_context.py +66 -0
  95. messaging/command_dispatcher.py +37 -0
  96. messaging/commands.py +275 -0
  97. messaging/event_parser.py +181 -0
  98. messaging/limiter.py +300 -0
  99. messaging/models.py +36 -0
  100. messaging/node_event_pipeline.py +127 -0
  101. messaging/node_runner.py +342 -0
  102. messaging/platforms/__init__.py +15 -0
  103. messaging/platforms/base.py +228 -0
  104. messaging/platforms/discord.py +567 -0
  105. messaging/platforms/factory.py +103 -0
  106. messaging/platforms/outbox.py +144 -0
  107. messaging/platforms/telegram.py +688 -0
  108. messaging/platforms/voice_flow.py +295 -0
  109. messaging/rendering/__init__.py +3 -0
  110. messaging/rendering/discord_markdown.py +318 -0
  111. messaging/rendering/markdown_tables.py +49 -0
  112. messaging/rendering/profiles.py +55 -0
  113. messaging/rendering/telegram_markdown.py +327 -0
  114. messaging/safe_diagnostics.py +17 -0
  115. messaging/session.py +334 -0
  116. messaging/transcript.py +581 -0
  117. messaging/transcription.py +164 -0
  118. messaging/trees/__init__.py +15 -0
  119. messaging/trees/data.py +482 -0
  120. messaging/trees/manager.py +433 -0
  121. messaging/trees/processor.py +179 -0
  122. messaging/trees/repository.py +177 -0
  123. messaging/turn_intake.py +235 -0
  124. messaging/ui_updates.py +101 -0
  125. messaging/voice.py +76 -0
  126. messaging/workflow.py +200 -0
  127. providers/__init__.py +31 -0
  128. providers/base.py +152 -0
  129. providers/cerebras/__init__.py +7 -0
  130. providers/cerebras/client.py +31 -0
  131. providers/cerebras/request.py +55 -0
  132. providers/codestral/__init__.py +7 -0
  133. providers/codestral/client.py +34 -0
  134. providers/deepseek/__init__.py +11 -0
  135. providers/deepseek/client.py +51 -0
  136. providers/deepseek/request.py +475 -0
  137. providers/defaults.py +41 -0
  138. providers/error_mapping.py +309 -0
  139. providers/exceptions.py +113 -0
  140. providers/fireworks/__init__.py +5 -0
  141. providers/fireworks/client.py +45 -0
  142. providers/fireworks/request.py +48 -0
  143. providers/gemini/__init__.py +7 -0
  144. providers/gemini/client.py +49 -0
  145. providers/gemini/request.py +199 -0
  146. providers/groq/__init__.py +7 -0
  147. providers/groq/client.py +31 -0
  148. providers/groq/request.py +83 -0
  149. providers/kimi/__init__.py +10 -0
  150. providers/kimi/client.py +53 -0
  151. providers/kimi/request.py +42 -0
  152. providers/llamacpp/__init__.py +3 -0
  153. providers/llamacpp/client.py +16 -0
  154. providers/lmstudio/__init__.py +5 -0
  155. providers/lmstudio/client.py +16 -0
  156. providers/mistral/__init__.py +7 -0
  157. providers/mistral/client.py +31 -0
  158. providers/mistral/request.py +37 -0
  159. providers/model_listing.py +133 -0
  160. providers/nvidia_nim/__init__.py +7 -0
  161. providers/nvidia_nim/client.py +91 -0
  162. providers/nvidia_nim/request.py +430 -0
  163. providers/nvidia_nim/voice.py +95 -0
  164. providers/ollama/__init__.py +7 -0
  165. providers/ollama/client.py +39 -0
  166. providers/open_router/__init__.py +7 -0
  167. providers/open_router/client.py +124 -0
  168. providers/open_router/request.py +42 -0
  169. providers/opencode/__init__.py +11 -0
  170. providers/opencode/client.py +31 -0
  171. providers/opencode/request.py +35 -0
  172. providers/rate_limit.py +300 -0
  173. providers/registry.py +527 -0
  174. providers/transports/__init__.py +1 -0
  175. providers/transports/anthropic_messages/__init__.py +5 -0
  176. providers/transports/anthropic_messages/http.py +118 -0
  177. providers/transports/anthropic_messages/recovery.py +206 -0
  178. providers/transports/anthropic_messages/stream.py +295 -0
  179. providers/transports/anthropic_messages/transport.py +236 -0
  180. providers/transports/openai_chat/__init__.py +5 -0
  181. providers/transports/openai_chat/recovery.py +217 -0
  182. providers/transports/openai_chat/stream.py +384 -0
  183. providers/transports/openai_chat/tool_calls.py +293 -0
  184. providers/transports/openai_chat/transport.py +156 -0
  185. providers/wafer/__init__.py +10 -0
  186. providers/wafer/client.py +50 -0
  187. providers/zai/__init__.py +10 -0
  188. providers/zai/client.py +46 -0
  189. providers/zai/request.py +42 -0
@@ -0,0 +1,654 @@
1
+ """Block-indexed OpenAI Responses stream assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ import uuid
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Literal
11
+
12
+ from .anthropic_sse import AnthropicSseEvent
13
+ from .events import format_response_sse_event
14
+ from .ids import (
15
+ new_call_id,
16
+ new_message_item_id,
17
+ new_reasoning_item_id,
18
+ new_response_id,
19
+ )
20
+ from .items import (
21
+ encrypted_reasoning_item,
22
+ message_item,
23
+ reasoning_item,
24
+ )
25
+ from .tools import (
26
+ custom_tool_input_text_from_arguments,
27
+ responses_tool_identity_from_anthropic_name,
28
+ )
29
+ from .usage import estimate_text_tokens
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class _TextBlockState:
34
+ index: int
35
+ output_index: int
36
+ item_id: str
37
+ text_parts: list[str] = field(default_factory=list)
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class _ReasoningBlockState:
42
+ index: int
43
+ output_index: int
44
+ item_id: str
45
+ text_parts: list[str] = field(default_factory=list)
46
+ encrypted_content: str | None = None
47
+
48
+
49
+ @dataclass(slots=True)
50
+ class _ToolBlockState:
51
+ index: int
52
+ output_index: int
53
+ item_id: str
54
+ call_id: str
55
+ kind: Literal["function", "custom"]
56
+ name: str
57
+ namespace: str | None = None
58
+ argument_parts: list[str] = field(default_factory=list)
59
+
60
+
61
+ _BlockState = _TextBlockState | _ReasoningBlockState | _ToolBlockState
62
+
63
+
64
+ class ResponsesStreamAssembler:
65
+ """Assemble Responses SSE events from indexed Anthropic content blocks."""
66
+
67
+ def __init__(self, request: Mapping[str, Any]) -> None:
68
+ self._request = request
69
+ self._response_id = new_response_id()
70
+ self._created_at = int(time.time())
71
+ self._output_slots: list[dict[str, Any] | None] = []
72
+ self._active_blocks: dict[int, _BlockState] = {}
73
+ self._fallback_text_index = -1
74
+ self._input_tokens: int | None = None
75
+ self._output_tokens: int | None = None
76
+ self._reasoning_tokens_estimate = 0
77
+ self._started = False
78
+ self.terminal = False
79
+ self.final_response: dict[str, Any] | None = None
80
+
81
+ def process_anthropic_event(self, event: AnthropicSseEvent) -> list[str]:
82
+ if self.terminal:
83
+ return []
84
+
85
+ chunks = self._ensure_started()
86
+ if event.event == "content_block_start":
87
+ chunks.extend(self._handle_content_block_start(event.data))
88
+ elif event.event == "content_block_delta":
89
+ chunks.extend(self._handle_content_block_delta(event.data))
90
+ elif event.event == "content_block_stop":
91
+ chunks.extend(self._handle_content_block_stop(event.data))
92
+ elif event.event == "message_delta":
93
+ self._handle_message_delta(event.data)
94
+ elif event.event == "message_stop":
95
+ chunks.extend(self.complete_response())
96
+ elif event.event == "error":
97
+ chunks.extend(self.fail_response(event.data))
98
+ return chunks
99
+
100
+ def finish_if_needed(self) -> list[str]:
101
+ if self.terminal:
102
+ return []
103
+ chunks = self._ensure_started()
104
+ chunks.extend(self.complete_response())
105
+ return chunks
106
+
107
+ def response_payload(
108
+ self, *, status: str, error: dict[str, Any] | None = None
109
+ ) -> dict[str, Any]:
110
+ return {
111
+ "id": self._response_id,
112
+ "object": "response",
113
+ "created_at": self._created_at,
114
+ "status": status,
115
+ "model": str(self._request.get("model", "")),
116
+ "output": self._output(),
117
+ "parallel_tool_calls": bool(self._request.get("parallel_tool_calls", True)),
118
+ "tool_choice": self._request.get("tool_choice", "auto"),
119
+ "temperature": self._request.get("temperature"),
120
+ "top_p": self._request.get("top_p"),
121
+ "max_output_tokens": self._request.get("max_output_tokens"),
122
+ "usage": self._usage(),
123
+ "error": error,
124
+ }
125
+
126
+ def complete_response(self) -> list[str]:
127
+ chunks = self._flush_active_blocks()
128
+ self.final_response = self.response_payload(status="completed")
129
+ chunks.append(
130
+ format_response_sse_event(
131
+ "response.completed",
132
+ {"type": "response.completed", "response": self.final_response},
133
+ )
134
+ )
135
+ self.terminal = True
136
+ return chunks
137
+
138
+ def fail_response(self, data: Mapping[str, Any]) -> list[str]:
139
+ chunks = self._flush_active_blocks()
140
+ error = _openai_error_from_anthropic_error(data)
141
+ self.final_response = self.response_payload(status="failed", error=error)
142
+ chunks.append(
143
+ format_response_sse_event(
144
+ "response.failed",
145
+ {"type": "response.failed", "response": self.final_response},
146
+ )
147
+ )
148
+ self.terminal = True
149
+ return chunks
150
+
151
+ def _ensure_started(self) -> list[str]:
152
+ if self._started:
153
+ return []
154
+ self._started = True
155
+ return [
156
+ format_response_sse_event(
157
+ "response.created",
158
+ {
159
+ "type": "response.created",
160
+ "response": self.response_payload(status="in_progress"),
161
+ },
162
+ )
163
+ ]
164
+
165
+ def _handle_content_block_start(self, data: Mapping[str, Any]) -> list[str]:
166
+ block = data.get("content_block")
167
+ if not isinstance(block, dict):
168
+ return []
169
+ block_type = block.get("type")
170
+ index = _event_index(data)
171
+ if block_type == "text":
172
+ index = self._safe_index(index)
173
+ chunks, state = self._start_text_block(index)
174
+ if text := _string_value(block.get("text")):
175
+ chunks.extend(self._emit_text_delta(state, text))
176
+ return chunks
177
+ if block_type == "thinking":
178
+ if index is None:
179
+ return []
180
+ chunks, state = self._start_reasoning_block(index)
181
+ if text := _string_value(block.get("thinking")):
182
+ chunks.extend(self._emit_reasoning_delta(state, text))
183
+ return chunks
184
+ if block_type == "redacted_thinking":
185
+ if index is None:
186
+ return []
187
+ chunks, _state = self._start_reasoning_block(
188
+ index, encrypted_content=_string_value(block.get("data"))
189
+ )
190
+ return chunks
191
+ if block_type == "tool_use":
192
+ if index is None:
193
+ return []
194
+ return self._start_tool_block(index, block)
195
+ return []
196
+
197
+ def _handle_content_block_delta(self, data: Mapping[str, Any]) -> list[str]:
198
+ delta = data.get("delta")
199
+ if not isinstance(delta, dict):
200
+ return []
201
+ delta_type = delta.get("type")
202
+ index = _event_index(data)
203
+ if delta_type == "text_delta":
204
+ index = self._safe_index(index)
205
+ state = self._active_blocks.get(index)
206
+ chunks: list[str] = []
207
+ if not isinstance(state, _TextBlockState):
208
+ chunks, state = self._start_text_block(index)
209
+ chunks.extend(
210
+ self._emit_text_delta(state, _string_value(delta.get("text")))
211
+ )
212
+ return chunks
213
+ if delta_type == "thinking_delta":
214
+ if index is None:
215
+ return []
216
+ state = self._active_blocks.get(index)
217
+ chunks = []
218
+ if not isinstance(state, _ReasoningBlockState):
219
+ chunks, state = self._start_reasoning_block(index)
220
+ chunks.extend(
221
+ self._emit_reasoning_delta(state, _string_value(delta.get("thinking")))
222
+ )
223
+ return chunks
224
+ if delta_type == "input_json_delta":
225
+ state = self._active_blocks.get(index) if index is not None else None
226
+ if isinstance(state, _ToolBlockState):
227
+ state.argument_parts.append(_string_value(delta.get("partial_json")))
228
+ return []
229
+
230
+ def _handle_content_block_stop(self, data: Mapping[str, Any]) -> list[str]:
231
+ index = _event_index(data)
232
+ if index is None:
233
+ return []
234
+ state = self._active_blocks.pop(index, None)
235
+ if state is None:
236
+ return []
237
+ return self._complete_block(state)
238
+
239
+ def _handle_message_delta(self, data: Mapping[str, Any]) -> None:
240
+ usage = data.get("usage")
241
+ if not isinstance(usage, dict):
242
+ return
243
+ if isinstance(usage.get("input_tokens"), int):
244
+ self._input_tokens = usage["input_tokens"]
245
+ if isinstance(usage.get("output_tokens"), int):
246
+ self._output_tokens = usage["output_tokens"]
247
+
248
+ def _start_text_block(self, index: int) -> tuple[list[str], _TextBlockState]:
249
+ chunks = self._complete_existing_block(index)
250
+ output_index = self._reserve_output_slot()
251
+ state = _TextBlockState(
252
+ index=index,
253
+ output_index=output_index,
254
+ item_id=new_message_item_id(),
255
+ )
256
+ self._active_blocks[index] = state
257
+ item = {
258
+ "id": state.item_id,
259
+ "type": "message",
260
+ "status": "in_progress",
261
+ "role": "assistant",
262
+ "content": [],
263
+ }
264
+ chunks.extend(
265
+ [
266
+ format_response_sse_event(
267
+ "response.output_item.added",
268
+ {
269
+ "type": "response.output_item.added",
270
+ "output_index": output_index,
271
+ "item": item,
272
+ },
273
+ ),
274
+ format_response_sse_event(
275
+ "response.content_part.added",
276
+ {
277
+ "type": "response.content_part.added",
278
+ "item_id": state.item_id,
279
+ "output_index": output_index,
280
+ "content_index": 0,
281
+ "part": {
282
+ "type": "output_text",
283
+ "text": "",
284
+ "annotations": [],
285
+ },
286
+ },
287
+ ),
288
+ ]
289
+ )
290
+ return chunks, state
291
+
292
+ def _start_reasoning_block(
293
+ self, index: int, *, encrypted_content: str | None = None
294
+ ) -> tuple[list[str], _ReasoningBlockState]:
295
+ chunks = self._complete_existing_block(index)
296
+ output_index = self._reserve_output_slot()
297
+ state = _ReasoningBlockState(
298
+ index=index,
299
+ output_index=output_index,
300
+ item_id=new_reasoning_item_id(),
301
+ encrypted_content=encrypted_content,
302
+ )
303
+ self._active_blocks[index] = state
304
+ chunks.append(
305
+ format_response_sse_event(
306
+ "response.output_item.added",
307
+ {
308
+ "type": "response.output_item.added",
309
+ "output_index": output_index,
310
+ "item": _reasoning_output_item(state, status="in_progress"),
311
+ },
312
+ )
313
+ )
314
+ return chunks, state
315
+
316
+ def _start_tool_block(self, index: int, block: Mapping[str, Any]) -> list[str]:
317
+ chunks = self._complete_existing_block(index)
318
+ identity = responses_tool_identity_from_anthropic_name(
319
+ self._request, _string_value(block.get("name"))
320
+ )
321
+ state = _ToolBlockState(
322
+ index=index,
323
+ output_index=self._reserve_output_slot(),
324
+ item_id=f"{'ctc' if identity.kind == 'custom' else 'fc'}_"
325
+ f"{uuid.uuid4().hex[:24]}",
326
+ call_id=_string_value(block.get("id")) or new_call_id(),
327
+ kind=identity.kind,
328
+ name=identity.name,
329
+ namespace=identity.namespace,
330
+ )
331
+ initial_input = block.get("input")
332
+ if (identity.kind == "custom" and initial_input not in (None, {}, "")) or (
333
+ isinstance(initial_input, dict) and initial_input
334
+ ):
335
+ state.argument_parts.append(json.dumps(initial_input))
336
+ self._active_blocks[index] = state
337
+ chunks.append(
338
+ format_response_sse_event(
339
+ "response.output_item.added",
340
+ {
341
+ "type": "response.output_item.added",
342
+ "output_index": state.output_index,
343
+ "item": self._tool_item(state, status="in_progress"),
344
+ },
345
+ )
346
+ )
347
+ return chunks
348
+
349
+ def _emit_text_delta(self, state: _TextBlockState, text: str) -> list[str]:
350
+ if not text:
351
+ return []
352
+ state.text_parts.append(text)
353
+ return [
354
+ format_response_sse_event(
355
+ "response.output_text.delta",
356
+ {
357
+ "type": "response.output_text.delta",
358
+ "item_id": state.item_id,
359
+ "output_index": state.output_index,
360
+ "content_index": 0,
361
+ "delta": text,
362
+ },
363
+ )
364
+ ]
365
+
366
+ def _emit_reasoning_delta(
367
+ self, state: _ReasoningBlockState, text: str
368
+ ) -> list[str]:
369
+ if not text:
370
+ return []
371
+ state.text_parts.append(text)
372
+ return [
373
+ format_response_sse_event(
374
+ "response.reasoning_text.delta",
375
+ {
376
+ "type": "response.reasoning_text.delta",
377
+ "item_id": state.item_id,
378
+ "output_index": state.output_index,
379
+ "content_index": 0,
380
+ "delta": text,
381
+ },
382
+ )
383
+ ]
384
+
385
+ def _complete_existing_block(self, index: int) -> list[str]:
386
+ existing = self._active_blocks.pop(index, None)
387
+ if existing is None:
388
+ return []
389
+ return self._complete_block(existing)
390
+
391
+ def _complete_block(self, state: _BlockState) -> list[str]:
392
+ if isinstance(state, _TextBlockState):
393
+ return self._complete_text_block(state)
394
+ if isinstance(state, _ReasoningBlockState):
395
+ return self._complete_reasoning_block(state)
396
+ return self._complete_tool_block(state)
397
+
398
+ def _complete_text_block(self, state: _TextBlockState) -> list[str]:
399
+ text = "".join(state.text_parts)
400
+ item = message_item(state.item_id, text, "completed")
401
+ self._commit_output(state.output_index, item)
402
+ return [
403
+ format_response_sse_event(
404
+ "response.output_text.done",
405
+ {
406
+ "type": "response.output_text.done",
407
+ "item_id": state.item_id,
408
+ "output_index": state.output_index,
409
+ "content_index": 0,
410
+ "text": text,
411
+ },
412
+ ),
413
+ format_response_sse_event(
414
+ "response.content_part.done",
415
+ {
416
+ "type": "response.content_part.done",
417
+ "item_id": state.item_id,
418
+ "output_index": state.output_index,
419
+ "content_index": 0,
420
+ "part": {"type": "output_text", "text": text, "annotations": []},
421
+ },
422
+ ),
423
+ format_response_sse_event(
424
+ "response.output_item.done",
425
+ {
426
+ "type": "response.output_item.done",
427
+ "output_index": state.output_index,
428
+ "item": item,
429
+ },
430
+ ),
431
+ ]
432
+
433
+ def _complete_reasoning_block(self, state: _ReasoningBlockState) -> list[str]:
434
+ item = _reasoning_output_item(state, status="completed")
435
+ self._commit_output(state.output_index, item)
436
+ chunks: list[str] = []
437
+ text = "".join(state.text_parts)
438
+ if text:
439
+ self._reasoning_tokens_estimate += estimate_text_tokens(text)
440
+ chunks.append(
441
+ format_response_sse_event(
442
+ "response.reasoning_text.done",
443
+ {
444
+ "type": "response.reasoning_text.done",
445
+ "item_id": state.item_id,
446
+ "output_index": state.output_index,
447
+ "content_index": 0,
448
+ "text": text,
449
+ },
450
+ )
451
+ )
452
+ chunks.append(
453
+ format_response_sse_event(
454
+ "response.output_item.done",
455
+ {
456
+ "type": "response.output_item.done",
457
+ "output_index": state.output_index,
458
+ "item": item,
459
+ },
460
+ )
461
+ )
462
+ return chunks
463
+
464
+ def _complete_tool_block(self, state: _ToolBlockState) -> list[str]:
465
+ if state.kind == "custom":
466
+ return self._complete_custom_tool_block(state)
467
+ arguments = "".join(state.argument_parts) or "{}"
468
+ item = self._tool_item(state, status="completed", arguments=arguments)
469
+ self._commit_output(state.output_index, item)
470
+ chunks: list[str] = []
471
+ if arguments:
472
+ chunks.append(
473
+ format_response_sse_event(
474
+ "response.function_call_arguments.delta",
475
+ {
476
+ "type": "response.function_call_arguments.delta",
477
+ "item_id": state.item_id,
478
+ "output_index": state.output_index,
479
+ "delta": arguments,
480
+ },
481
+ )
482
+ )
483
+ chunks.extend(
484
+ [
485
+ format_response_sse_event(
486
+ "response.function_call_arguments.done",
487
+ {
488
+ "type": "response.function_call_arguments.done",
489
+ "item_id": state.item_id,
490
+ "output_index": state.output_index,
491
+ "arguments": arguments,
492
+ },
493
+ ),
494
+ format_response_sse_event(
495
+ "response.output_item.done",
496
+ {
497
+ "type": "response.output_item.done",
498
+ "output_index": state.output_index,
499
+ "item": item,
500
+ },
501
+ ),
502
+ ]
503
+ )
504
+ return chunks
505
+
506
+ def _complete_custom_tool_block(self, state: _ToolBlockState) -> list[str]:
507
+ input_text = custom_tool_input_text_from_arguments(
508
+ "".join(state.argument_parts)
509
+ )
510
+ item = self._tool_item(state, status="completed", input_text=input_text)
511
+ self._commit_output(state.output_index, item)
512
+ chunks: list[str] = []
513
+ if input_text:
514
+ chunks.append(
515
+ format_response_sse_event(
516
+ "response.custom_tool_call_input.delta",
517
+ {
518
+ "type": "response.custom_tool_call_input.delta",
519
+ "item_id": state.item_id,
520
+ "output_index": state.output_index,
521
+ "delta": input_text,
522
+ },
523
+ )
524
+ )
525
+ chunks.extend(
526
+ [
527
+ format_response_sse_event(
528
+ "response.custom_tool_call_input.done",
529
+ {
530
+ "type": "response.custom_tool_call_input.done",
531
+ "item_id": state.item_id,
532
+ "output_index": state.output_index,
533
+ "input": input_text,
534
+ },
535
+ ),
536
+ format_response_sse_event(
537
+ "response.output_item.done",
538
+ {
539
+ "type": "response.output_item.done",
540
+ "output_index": state.output_index,
541
+ "item": item,
542
+ },
543
+ ),
544
+ ]
545
+ )
546
+ return chunks
547
+
548
+ def _tool_item(
549
+ self,
550
+ state: _ToolBlockState,
551
+ *,
552
+ status: str,
553
+ arguments: str = "",
554
+ input_text: str = "",
555
+ ) -> dict[str, Any]:
556
+ if state.kind == "custom":
557
+ item = {
558
+ "id": state.item_id,
559
+ "type": "custom_tool_call",
560
+ "status": status,
561
+ "call_id": state.call_id,
562
+ "name": state.name,
563
+ "input": input_text,
564
+ }
565
+ else:
566
+ item = {
567
+ "id": state.item_id,
568
+ "type": "function_call",
569
+ "status": status,
570
+ "call_id": state.call_id,
571
+ "name": state.name,
572
+ "arguments": arguments,
573
+ }
574
+ if state.namespace:
575
+ item["namespace"] = state.namespace
576
+ return item
577
+
578
+ def _flush_active_blocks(self) -> list[str]:
579
+ chunks: list[str] = []
580
+ states = sorted(
581
+ self._active_blocks.values(), key=lambda state: state.output_index
582
+ )
583
+ self._active_blocks.clear()
584
+ for state in states:
585
+ chunks.extend(self._complete_block(state))
586
+ return chunks
587
+
588
+ def _reserve_output_slot(self) -> int:
589
+ output_index = len(self._output_slots)
590
+ self._output_slots.append(None)
591
+ return output_index
592
+
593
+ def _commit_output(self, output_index: int, item: dict[str, Any]) -> None:
594
+ while output_index >= len(self._output_slots):
595
+ self._output_slots.append(None)
596
+ self._output_slots[output_index] = item
597
+
598
+ def _output(self) -> list[dict[str, Any]]:
599
+ return [item for item in self._output_slots if item is not None]
600
+
601
+ def _usage(self) -> dict[str, Any] | None:
602
+ if self._input_tokens is None and self._output_tokens is None:
603
+ return None
604
+ input_tokens = self._input_tokens or 0
605
+ output_tokens = self._output_tokens or 0
606
+ usage: dict[str, Any] = {
607
+ "input_tokens": input_tokens,
608
+ "output_tokens": output_tokens,
609
+ "total_tokens": input_tokens + output_tokens,
610
+ }
611
+ capped_reasoning_tokens = min(self._reasoning_tokens_estimate, output_tokens)
612
+ if capped_reasoning_tokens:
613
+ usage["output_tokens_details"] = {
614
+ "reasoning_tokens": capped_reasoning_tokens
615
+ }
616
+ return usage
617
+
618
+ def _safe_index(self, index: int | None) -> int:
619
+ if index is not None:
620
+ return index
621
+ value = self._fallback_text_index
622
+ self._fallback_text_index -= 1
623
+ return value
624
+
625
+
626
+ def _event_index(data: Mapping[str, Any]) -> int | None:
627
+ value = data.get("index")
628
+ return value if isinstance(value, int) else None
629
+
630
+
631
+ def _reasoning_output_item(
632
+ state: _ReasoningBlockState, *, status: str
633
+ ) -> dict[str, Any]:
634
+ if state.encrypted_content is not None:
635
+ return encrypted_reasoning_item(state.item_id, state.encrypted_content, status)
636
+ return reasoning_item(state.item_id, "".join(state.text_parts), status)
637
+
638
+
639
+ def _openai_error_from_anthropic_error(data: Mapping[str, Any]) -> dict[str, Any]:
640
+ error = data.get("error")
641
+ if not isinstance(error, dict):
642
+ error = {"type": "api_error", "message": str(data)}
643
+ return {
644
+ "message": str(error.get("message", "")),
645
+ "type": str(error.get("type", "api_error")),
646
+ "param": None,
647
+ "code": None,
648
+ }
649
+
650
+
651
+ def _string_value(value: Any) -> str:
652
+ if value is None:
653
+ return ""
654
+ return value if isinstance(value, str) else str(value)