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,117 @@
1
+ """Token estimation for Anthropic-compatible requests."""
2
+
3
+ import json
4
+
5
+ import tiktoken
6
+ from loguru import logger
7
+
8
+ from .content import get_block_attr
9
+
10
+ ENCODER = tiktoken.get_encoding("cl100k_base")
11
+
12
+ _DISALLOWED_SPECIAL: tuple[str, ...] = ()
13
+
14
+
15
+ def _count_text_tokens(text: str) -> int:
16
+ return len(ENCODER.encode(text, disallowed_special=_DISALLOWED_SPECIAL))
17
+
18
+
19
+ def get_token_count(
20
+ messages: list,
21
+ system: str | list | None = None,
22
+ tools: list | None = None,
23
+ ) -> int:
24
+ """Estimate token count for a request."""
25
+ total_tokens = 0
26
+
27
+ if system:
28
+ if isinstance(system, str):
29
+ total_tokens += _count_text_tokens(system)
30
+ elif isinstance(system, list):
31
+ for block in system:
32
+ text = get_block_attr(block, "text", "")
33
+ if text:
34
+ total_tokens += _count_text_tokens(str(text))
35
+ total_tokens += 4
36
+
37
+ for msg in messages:
38
+ if isinstance(msg.content, str):
39
+ total_tokens += _count_text_tokens(msg.content)
40
+ elif isinstance(msg.content, list):
41
+ for block in msg.content:
42
+ b_type = get_block_attr(block, "type") or None
43
+
44
+ if b_type == "text":
45
+ text = get_block_attr(block, "text", "")
46
+ total_tokens += _count_text_tokens(str(text))
47
+ elif b_type == "thinking":
48
+ thinking = get_block_attr(block, "thinking", "")
49
+ total_tokens += _count_text_tokens(str(thinking))
50
+ elif b_type == "tool_use":
51
+ name = get_block_attr(block, "name", "")
52
+ inp = get_block_attr(block, "input", {})
53
+ block_id = get_block_attr(block, "id", "")
54
+ total_tokens += _count_text_tokens(str(name))
55
+ total_tokens += _count_text_tokens(json.dumps(inp))
56
+ total_tokens += _count_text_tokens(str(block_id))
57
+ total_tokens += 15
58
+ elif b_type == "image":
59
+ source = get_block_attr(block, "source")
60
+ if isinstance(source, dict):
61
+ data = source.get("data") or source.get("base64") or ""
62
+ if data:
63
+ total_tokens += max(85, len(data) // 3000)
64
+ else:
65
+ total_tokens += 765
66
+ else:
67
+ total_tokens += 765
68
+ elif b_type == "tool_result":
69
+ content = get_block_attr(block, "content", "")
70
+ tool_use_id = get_block_attr(block, "tool_use_id", "")
71
+ if isinstance(content, str):
72
+ total_tokens += _count_text_tokens(content)
73
+ else:
74
+ total_tokens += _count_text_tokens(json.dumps(content))
75
+ total_tokens += _count_text_tokens(str(tool_use_id))
76
+ total_tokens += 8
77
+ elif b_type in (
78
+ "server_tool_use",
79
+ "web_search_tool_result",
80
+ "web_fetch_tool_result",
81
+ ):
82
+ if hasattr(block, "model_dump"):
83
+ blob: object = block.model_dump()
84
+ else:
85
+ blob = block
86
+ try:
87
+ total_tokens += _count_text_tokens(
88
+ json.dumps(blob, default=str, ensure_ascii=False)
89
+ )
90
+ except (TypeError, ValueError, OverflowError) as e:
91
+ logger.debug(
92
+ "Block encode fallback b_type={} err={}", b_type, e
93
+ )
94
+ total_tokens += _count_text_tokens(str(blob))
95
+ total_tokens += 12
96
+ else:
97
+ logger.debug(
98
+ "Unexpected block type %r, falling back to json/str encoding",
99
+ b_type,
100
+ )
101
+ try:
102
+ total_tokens += _count_text_tokens(json.dumps(block))
103
+ except TypeError, ValueError:
104
+ total_tokens += _count_text_tokens(str(block))
105
+
106
+ if tools:
107
+ for tool in tools:
108
+ tool_str = (
109
+ tool.name + (tool.description or "") + json.dumps(tool.input_schema)
110
+ )
111
+ total_tokens += _count_text_tokens(tool_str)
112
+
113
+ total_tokens += len(messages) * 4
114
+ if tools:
115
+ total_tokens += len(tools) * 5
116
+
117
+ return max(1, total_tokens)
@@ -0,0 +1,212 @@
1
+ """Heuristic parser for text-emitted tool calls."""
2
+
3
+ import json
4
+ import re
5
+ import uuid
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from loguru import logger
10
+
11
+ _CONTROL_TOKEN_RE = re.compile(r"<\|[^|>]{1,80}\|>")
12
+ _CONTROL_TOKEN_START = "<|"
13
+ _CONTROL_TOKEN_END = "|>"
14
+
15
+
16
+ class ParserState(Enum):
17
+ TEXT = 1
18
+ MATCHING_FUNCTION = 2
19
+ PARSING_PARAMETERS = 3
20
+
21
+
22
+ class HeuristicToolParser:
23
+ """
24
+ Stateful parser for raw text tool calls.
25
+
26
+ Some OpenAI-compatible models emit tool calls as text rather than structured
27
+ chunks. This parser converts the common ``● <function=...>`` form into
28
+ Anthropic-style ``tool_use`` blocks.
29
+ """
30
+
31
+ _FUNC_START_PATTERN = re.compile(r"●\s*<function=([^>]+)>")
32
+ _PARAM_PATTERN = re.compile(
33
+ r"<parameter=([^>]+)>(.*?)(?:</parameter>|$)", re.DOTALL
34
+ )
35
+ _WEB_TOOL_JSON_PATTERN = re.compile(
36
+ r"(?is)\b(?:use\s+)?(?P<tool>WebFetch|WebSearch)\b.*?(?P<json>\{.*?\})"
37
+ )
38
+
39
+ def __init__(self):
40
+ self._state = ParserState.TEXT
41
+ self._buffer = ""
42
+ self._current_tool_id = None
43
+ self._current_function_name = None
44
+ self._current_parameters = {}
45
+
46
+ def _extract_web_tool_json_calls(self) -> tuple[str, list[dict[str, Any]]]:
47
+ detected_tools: list[dict[str, Any]] = []
48
+
49
+ for match in self._WEB_TOOL_JSON_PATTERN.finditer(self._buffer):
50
+ try:
51
+ tool_input = json.loads(match.group("json"))
52
+ except json.JSONDecodeError:
53
+ continue
54
+ if not isinstance(tool_input, dict):
55
+ continue
56
+
57
+ tool_name = match.group("tool")
58
+ if tool_name == "WebFetch" and "url" not in tool_input:
59
+ continue
60
+ if tool_name == "WebSearch" and "query" not in tool_input:
61
+ continue
62
+
63
+ detected_tools.append(
64
+ {
65
+ "type": "tool_use",
66
+ "id": f"toolu_heuristic_{uuid.uuid4().hex[:8]}",
67
+ "name": tool_name,
68
+ "input": tool_input,
69
+ }
70
+ )
71
+ logger.debug(
72
+ "Heuristic bypass: Detected JSON-style tool call '{}'",
73
+ tool_name,
74
+ )
75
+
76
+ if not detected_tools:
77
+ return self._buffer, []
78
+
79
+ return "", detected_tools
80
+
81
+ def _strip_control_tokens(self, text: str) -> str:
82
+ return _CONTROL_TOKEN_RE.sub("", text)
83
+
84
+ def _split_incomplete_control_token_tail(self) -> str:
85
+ start = self._buffer.rfind(_CONTROL_TOKEN_START)
86
+ if start == -1:
87
+ return ""
88
+ end = self._buffer.find(_CONTROL_TOKEN_END, start)
89
+ if end != -1:
90
+ return ""
91
+
92
+ prefix = self._buffer[:start]
93
+ self._buffer = self._buffer[start:]
94
+ return prefix
95
+
96
+ def feed(self, text: str) -> tuple[str, list[dict[str, Any]]]:
97
+ """Feed text and return safe text plus detected tool calls."""
98
+ self._buffer += text
99
+ self._buffer = self._strip_control_tokens(self._buffer)
100
+ self._buffer, detected_tools = self._extract_web_tool_json_calls()
101
+ filtered_output_parts: list[str] = []
102
+
103
+ while True:
104
+ if self._state == ParserState.TEXT:
105
+ if "●" in self._buffer:
106
+ idx = self._buffer.find("●")
107
+ filtered_output_parts.append(self._buffer[:idx])
108
+ self._buffer = self._buffer[idx:]
109
+ self._state = ParserState.MATCHING_FUNCTION
110
+ else:
111
+ safe_prefix = self._split_incomplete_control_token_tail()
112
+ if safe_prefix:
113
+ filtered_output_parts.append(safe_prefix)
114
+ break
115
+
116
+ filtered_output_parts.append(self._buffer)
117
+ self._buffer = ""
118
+ break
119
+
120
+ if self._state == ParserState.MATCHING_FUNCTION:
121
+ match = self._FUNC_START_PATTERN.search(self._buffer)
122
+ if match:
123
+ self._current_function_name = match.group(1).strip()
124
+ self._current_tool_id = f"toolu_heuristic_{uuid.uuid4().hex[:8]}"
125
+ self._current_parameters = {}
126
+ self._buffer = self._buffer[match.end() :]
127
+ self._state = ParserState.PARSING_PARAMETERS
128
+ logger.debug(
129
+ "Heuristic bypass: Detected start of tool call '{}'",
130
+ self._current_function_name,
131
+ )
132
+ elif len(self._buffer) > 100:
133
+ filtered_output_parts.append(self._buffer[0])
134
+ self._buffer = self._buffer[1:]
135
+ self._state = ParserState.TEXT
136
+ else:
137
+ break
138
+
139
+ if self._state == ParserState.PARSING_PARAMETERS:
140
+ finished_tool_call = False
141
+
142
+ while True:
143
+ param_match = self._PARAM_PATTERN.search(self._buffer)
144
+ if param_match and "</parameter>" in param_match.group(0):
145
+ pre_match_text = self._buffer[: param_match.start()]
146
+ if pre_match_text:
147
+ filtered_output_parts.append(pre_match_text)
148
+
149
+ key = param_match.group(1).strip()
150
+ val = param_match.group(2).strip()
151
+ self._current_parameters[key] = val
152
+ self._buffer = self._buffer[param_match.end() :]
153
+ else:
154
+ break
155
+
156
+ if "●" in self._buffer:
157
+ idx = self._buffer.find("●")
158
+ if idx > 0:
159
+ filtered_output_parts.append(self._buffer[:idx])
160
+ self._buffer = self._buffer[idx:]
161
+ finished_tool_call = True
162
+ elif len(self._buffer) > 0 and not self._buffer.strip().startswith("<"):
163
+ if "<parameter=" not in self._buffer:
164
+ filtered_output_parts.append(self._buffer)
165
+ self._buffer = ""
166
+ finished_tool_call = True
167
+
168
+ if finished_tool_call:
169
+ detected_tools.append(
170
+ {
171
+ "type": "tool_use",
172
+ "id": self._current_tool_id,
173
+ "name": self._current_function_name,
174
+ "input": self._current_parameters,
175
+ }
176
+ )
177
+ logger.debug(
178
+ "Heuristic bypass: Emitting tool call '{}' with {} params",
179
+ self._current_function_name,
180
+ len(self._current_parameters),
181
+ )
182
+ self._state = ParserState.TEXT
183
+ else:
184
+ break
185
+
186
+ return "".join(filtered_output_parts), detected_tools
187
+
188
+ def flush(self) -> list[dict[str, Any]]:
189
+ """Flush any remaining tool call in the buffer."""
190
+ self._buffer = self._strip_control_tokens(self._buffer)
191
+ detected_tools = []
192
+ if self._state == ParserState.PARSING_PARAMETERS:
193
+ partial_matches = re.finditer(
194
+ r"<parameter=([^>]+)>(.*)$", self._buffer, re.DOTALL
195
+ )
196
+ for match in partial_matches:
197
+ key = match.group(1).strip()
198
+ val = match.group(2).strip()
199
+ self._current_parameters[key] = val
200
+
201
+ detected_tools.append(
202
+ {
203
+ "type": "tool_use",
204
+ "id": self._current_tool_id,
205
+ "name": self._current_function_name,
206
+ "input": self._current_parameters,
207
+ }
208
+ )
209
+ self._state = ParserState.TEXT
210
+ self._buffer = ""
211
+
212
+ return detected_tools
@@ -0,0 +1,9 @@
1
+ """Small shared protocol utility helpers."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def set_if_not_none(body: dict[str, Any], key: str, value: Any) -> None:
7
+ """Set ``body[key]`` only when value is not None."""
8
+ if value is not None:
9
+ body[key] = value
@@ -0,0 +1,5 @@
1
+ """OpenAI Responses protocol adapter."""
2
+
3
+ from .adapter import OpenAIResponsesAdapter
4
+
5
+ __all__ = ["OpenAIResponsesAdapter"]
@@ -0,0 +1,31 @@
1
+ """Facade for OpenAI Responses protocol adaptation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterable, AsyncIterator, Mapping
6
+ from typing import Any, ClassVar
7
+
8
+ from .errors import ResponsesConversionError, openai_error_payload
9
+ from .events import OPENAI_RESPONSES_SSE_HEADERS
10
+ from .input import convert_request_to_anthropic_payload
11
+ from .stream import iter_responses_sse_from_anthropic
12
+
13
+
14
+ class OpenAIResponsesAdapter:
15
+ """Convert between OpenAI Responses and the proxy's Anthropic core path."""
16
+
17
+ ConversionError: ClassVar[type[ResponsesConversionError]] = ResponsesConversionError
18
+ sse_headers: ClassVar[dict[str, str]] = OPENAI_RESPONSES_SSE_HEADERS
19
+
20
+ def to_anthropic_payload(self, request: Mapping[str, Any]) -> dict[str, Any]:
21
+ return convert_request_to_anthropic_payload(request)
22
+
23
+ def iter_sse_from_anthropic(
24
+ self,
25
+ chunks: AsyncIterable[Any],
26
+ request: Mapping[str, Any],
27
+ ) -> AsyncIterator[str]:
28
+ return iter_responses_sse_from_anthropic(chunks, request)
29
+
30
+ def error_payload(self, *, message: str, error_type: str) -> dict[str, Any]:
31
+ return openai_error_payload(message=message, error_type=error_type)
@@ -0,0 +1,59 @@
1
+ """Anthropic SSE parsing used by the Responses stream adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import AsyncIterable, AsyncIterator
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class AnthropicSseEvent:
13
+ event: str
14
+ data: dict[str, Any]
15
+
16
+
17
+ async def iter_sse_events(
18
+ chunks: AsyncIterable[Any],
19
+ ) -> AsyncIterator[AnthropicSseEvent]:
20
+ buffer = ""
21
+ async for chunk in chunks:
22
+ if isinstance(chunk, bytes):
23
+ buffer += chunk.decode("utf-8", errors="replace")
24
+ else:
25
+ buffer += str(chunk)
26
+
27
+ while "\n\n" in buffer:
28
+ raw, buffer = buffer.split("\n\n", 1)
29
+ event = parse_sse_event(raw)
30
+ if event is not None:
31
+ yield event
32
+
33
+ if buffer.strip():
34
+ event = parse_sse_event(buffer)
35
+ if event is not None:
36
+ yield event
37
+
38
+
39
+ def parse_sse_event(raw: str) -> AnthropicSseEvent | None:
40
+ event_type = ""
41
+ data_parts: list[str] = []
42
+ for line in raw.splitlines():
43
+ stripped = line.rstrip("\r")
44
+ if stripped.startswith("event:"):
45
+ event_type = stripped.split(":", 1)[1].strip()
46
+ elif stripped.startswith("data:"):
47
+ data_parts.append(stripped.split(":", 1)[1].strip())
48
+ if not event_type and not data_parts:
49
+ return None
50
+ data_text = "\n".join(data_parts)
51
+ if data_text == "[DONE]":
52
+ return None
53
+ try:
54
+ parsed = json.loads(data_text) if data_text else {}
55
+ except json.JSONDecodeError:
56
+ parsed = {"raw": data_text}
57
+ if not isinstance(parsed, dict):
58
+ parsed = {"value": parsed}
59
+ return AnthropicSseEvent(event=event_type, data=parsed)
@@ -0,0 +1,22 @@
1
+ """Errors and error envelopes for OpenAI Responses compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class ResponsesConversionError(ValueError):
9
+ """Raised when a Responses request cannot be converted deterministically."""
10
+
11
+
12
+ def openai_error_payload(*, message: str, error_type: str) -> dict[str, Any]:
13
+ """Return an OpenAI-compatible error envelope."""
14
+
15
+ return {
16
+ "error": {
17
+ "message": message,
18
+ "type": error_type,
19
+ "param": None,
20
+ "code": None,
21
+ }
22
+ }
@@ -0,0 +1,19 @@
1
+ """OpenAI Responses SSE event formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ OPENAI_RESPONSES_SSE_HEADERS: dict[str, str] = {
10
+ "X-Accel-Buffering": "no",
11
+ "Cache-Control": "no-cache",
12
+ "Connection": "keep-alive",
13
+ }
14
+
15
+
16
+ def format_response_sse_event(event_type: str, data: Mapping[str, Any]) -> str:
17
+ """Format one OpenAI Responses SSE event."""
18
+
19
+ return f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
@@ -0,0 +1,21 @@
1
+ """Identifier helpers for OpenAI Responses payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+
8
+ def new_response_id() -> str:
9
+ return f"resp_{uuid.uuid4().hex}"
10
+
11
+
12
+ def new_message_item_id() -> str:
13
+ return f"msg_{uuid.uuid4().hex}"
14
+
15
+
16
+ def new_reasoning_item_id() -> str:
17
+ return f"rs_{uuid.uuid4().hex}"
18
+
19
+
20
+ def new_call_id() -> str:
21
+ return f"call_{uuid.uuid4().hex[:24]}"