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,258 @@
1
+ """Convert OpenAI Responses requests into Anthropic Messages payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .errors import ResponsesConversionError
9
+ from .reasoning import (
10
+ combine_reasoning,
11
+ reasoning_text_from_item,
12
+ responses_reasoning_to_thinking,
13
+ )
14
+ from .tools import (
15
+ call_id_from_item,
16
+ convert_tool_choice,
17
+ convert_tools,
18
+ custom_tool_input_to_anthropic,
19
+ optional_str,
20
+ parse_arguments,
21
+ required_str,
22
+ responses_tool_name_to_anthropic_name,
23
+ )
24
+
25
+
26
+ def convert_request_to_anthropic_payload(
27
+ request: Mapping[str, Any],
28
+ ) -> dict[str, Any]:
29
+ """Convert an OpenAI Responses request into an Anthropic Messages payload."""
30
+
31
+ system_parts: list[str] = []
32
+ if instructions := optional_str(request.get("instructions")):
33
+ system_parts.append(instructions)
34
+
35
+ messages: list[dict[str, Any]] = []
36
+ pending_reasoning: str | None = None
37
+ for item in _iter_input_items(request.get("input")):
38
+ pending_reasoning = _append_input_item(
39
+ item,
40
+ messages=messages,
41
+ system_parts=system_parts,
42
+ pending_reasoning=pending_reasoning,
43
+ )
44
+ _append_pending_reasoning(messages, pending_reasoning)
45
+
46
+ if not messages:
47
+ raise ResponsesConversionError("Responses request input must contain a message")
48
+
49
+ payload: dict[str, Any] = {
50
+ "model": required_str(request.get("model"), "model"),
51
+ "messages": messages,
52
+ "stream": True,
53
+ }
54
+ if system_parts:
55
+ payload["system"] = "\n\n".join(system_parts)
56
+ _copy_if_present(request, payload, "temperature")
57
+ _copy_if_present(request, payload, "top_p")
58
+ if request.get("max_output_tokens") is not None:
59
+ payload["max_tokens"] = request["max_output_tokens"]
60
+ if isinstance(request.get("metadata"), dict):
61
+ payload["metadata"] = request["metadata"]
62
+
63
+ if thinking := responses_reasoning_to_thinking(request.get("reasoning")):
64
+ payload["thinking"] = thinking
65
+
66
+ raw_tool_choice = request.get("tool_choice")
67
+ tools = convert_tools(request.get("tools"))
68
+ if tools and raw_tool_choice != "none":
69
+ payload["tools"] = tools
70
+ tool_choice = convert_tool_choice(raw_tool_choice)
71
+ if tool_choice is not None:
72
+ payload["tool_choice"] = tool_choice
73
+
74
+ return payload
75
+
76
+
77
+ def _append_input_item(
78
+ item: Any,
79
+ *,
80
+ messages: list[dict[str, Any]],
81
+ system_parts: list[str],
82
+ pending_reasoning: str | None,
83
+ ) -> str | None:
84
+ if isinstance(item, str):
85
+ _append_pending_reasoning(messages, pending_reasoning)
86
+ messages.append({"role": "user", "content": item})
87
+ return None
88
+ if not isinstance(item, dict):
89
+ raise ResponsesConversionError(
90
+ f"Unsupported Responses input item: {type(item).__name__}"
91
+ )
92
+
93
+ item_type = item.get("type")
94
+ if item_type in (None, "message") or "role" in item:
95
+ role = required_str(item.get("role", "user"), "input.role")
96
+ if role == "assistant":
97
+ _append_message_item(
98
+ role,
99
+ item.get("content", ""),
100
+ messages,
101
+ system_parts,
102
+ reasoning_content=pending_reasoning,
103
+ )
104
+ return None
105
+ _append_pending_reasoning(messages, pending_reasoning)
106
+ _append_message_item(role, item.get("content", ""), messages, system_parts)
107
+ return None
108
+ if item_type in {"function_call", "custom_tool_call"}:
109
+ namespace = optional_str(item.get("namespace"))
110
+ field_name = f"{item_type}.name"
111
+ name = required_str(item.get("name"), field_name)
112
+ if item_type == "custom_tool_call":
113
+ tool_input = custom_tool_input_to_anthropic(item.get("input"))
114
+ else:
115
+ tool_input = parse_arguments(item.get("arguments"))
116
+ message = {
117
+ "role": "assistant",
118
+ "content": [
119
+ {
120
+ "type": "tool_use",
121
+ "id": call_id_from_item(item),
122
+ "name": responses_tool_name_to_anthropic_name(
123
+ name, namespace=namespace
124
+ ),
125
+ "input": tool_input,
126
+ }
127
+ ],
128
+ }
129
+ if pending_reasoning:
130
+ message["reasoning_content"] = pending_reasoning
131
+ messages.append(message)
132
+ return None
133
+ if item_type in {"function_call_output", "custom_tool_call_output"}:
134
+ _append_pending_reasoning(messages, pending_reasoning)
135
+ messages.append(
136
+ {
137
+ "role": "user",
138
+ "content": [
139
+ {
140
+ "type": "tool_result",
141
+ "tool_use_id": call_id_from_item(item),
142
+ "content": item.get("output", ""),
143
+ }
144
+ ],
145
+ }
146
+ )
147
+ return None
148
+ if item_type == "reasoning":
149
+ return combine_reasoning(pending_reasoning, reasoning_text_from_item(item))
150
+ if item_type in {"input_text", "output_text", "text"}:
151
+ _append_pending_reasoning(messages, pending_reasoning)
152
+ messages.append({"role": "user", "content": _text_from_part(item)})
153
+ return None
154
+
155
+ raise ResponsesConversionError(
156
+ f"Unsupported Responses input item type: {item_type!r}"
157
+ )
158
+
159
+
160
+ def _append_message_item(
161
+ role: str,
162
+ content: Any,
163
+ messages: list[dict[str, Any]],
164
+ system_parts: list[str],
165
+ *,
166
+ reasoning_content: str | None = None,
167
+ ) -> None:
168
+ normalized_role = "system" if role == "developer" else role
169
+ if normalized_role == "system":
170
+ text = _content_as_text(content)
171
+ if text:
172
+ system_parts.append(text)
173
+ return
174
+ if normalized_role not in {"user", "assistant"}:
175
+ raise ResponsesConversionError(f"Unsupported Responses message role: {role!r}")
176
+ message = {
177
+ "role": normalized_role,
178
+ "content": _convert_message_content(content),
179
+ }
180
+ if normalized_role == "assistant" and reasoning_content:
181
+ message["reasoning_content"] = reasoning_content
182
+ messages.append(message)
183
+
184
+
185
+ def _append_pending_reasoning(
186
+ messages: list[dict[str, Any]], pending_reasoning: str | None
187
+ ) -> None:
188
+ if pending_reasoning:
189
+ messages.append(
190
+ {
191
+ "role": "assistant",
192
+ "content": "",
193
+ "reasoning_content": pending_reasoning,
194
+ }
195
+ )
196
+
197
+
198
+ def _iter_input_items(value: Any) -> list[Any]:
199
+ if value is None:
200
+ return []
201
+ if isinstance(value, list):
202
+ return value
203
+ return [value]
204
+
205
+
206
+ def _convert_message_content(content: Any) -> str | list[dict[str, Any]]:
207
+ if isinstance(content, str):
208
+ return content
209
+ if isinstance(content, list):
210
+ blocks: list[dict[str, Any]] = []
211
+ for part in content:
212
+ if isinstance(part, str):
213
+ blocks.append({"type": "text", "text": part})
214
+ continue
215
+ if not isinstance(part, dict):
216
+ raise ResponsesConversionError(
217
+ f"Unsupported Responses content part: {type(part).__name__}"
218
+ )
219
+ part_type = part.get("type")
220
+ if part_type in {"input_text", "output_text", "text"} or "text" in part:
221
+ blocks.append({"type": "text", "text": _text_from_part(part)})
222
+ continue
223
+ if part_type == "refusal":
224
+ blocks.append({"type": "text", "text": str(part.get("refusal", ""))})
225
+ continue
226
+ raise ResponsesConversionError(
227
+ f"Unsupported Responses content part type: {part_type!r}"
228
+ )
229
+ return blocks
230
+ if isinstance(content, dict):
231
+ return [{"type": "text", "text": _text_from_part(content)}]
232
+ raise ResponsesConversionError(
233
+ f"Unsupported Responses message content: {type(content).__name__}"
234
+ )
235
+
236
+
237
+ def _content_as_text(content: Any) -> str:
238
+ converted = _convert_message_content(content)
239
+ if isinstance(converted, str):
240
+ return converted
241
+ return "\n".join(str(block.get("text", "")) for block in converted)
242
+
243
+
244
+ def _text_from_part(part: Mapping[str, Any]) -> str:
245
+ if text := optional_str(part.get("text")):
246
+ return text
247
+ if text := optional_str(part.get("input_text")):
248
+ return text
249
+ if text := optional_str(part.get("output_text")):
250
+ return text
251
+ return ""
252
+
253
+
254
+ def _copy_if_present(
255
+ source: Mapping[str, Any], target: dict[str, Any], field_name: str
256
+ ) -> None:
257
+ if source.get(field_name) is not None:
258
+ target[field_name] = source[field_name]
@@ -0,0 +1,37 @@
1
+ """Responses object and output item builders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def message_item(item_id: str, text: str, status: str) -> dict[str, Any]:
9
+ return {
10
+ "id": item_id,
11
+ "type": "message",
12
+ "status": status,
13
+ "role": "assistant",
14
+ "content": [{"type": "output_text", "text": text, "annotations": []}],
15
+ }
16
+
17
+
18
+ def reasoning_item(item_id: str, text: str, status: str) -> dict[str, Any]:
19
+ return {
20
+ "id": item_id,
21
+ "type": "reasoning",
22
+ "status": status,
23
+ "summary": [],
24
+ "content": [{"type": "reasoning_text", "text": text}],
25
+ }
26
+
27
+
28
+ def encrypted_reasoning_item(
29
+ item_id: str, encrypted_content: str, status: str
30
+ ) -> dict[str, Any]:
31
+ return {
32
+ "id": item_id,
33
+ "type": "reasoning",
34
+ "status": status,
35
+ "summary": [],
36
+ "encrypted_content": encrypted_content,
37
+ }
@@ -0,0 +1,52 @@
1
+ """Reasoning and thinking conversion helpers for OpenAI Responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .tools import optional_str
9
+
10
+
11
+ def reasoning_text_from_item(item: Mapping[str, Any]) -> str | None:
12
+ content_parts = _text_parts_from_items(
13
+ item.get("content"), item_type="reasoning_text"
14
+ )
15
+ if content_parts:
16
+ return "\n".join(content_parts)
17
+ summary_parts = _text_parts_from_items(
18
+ item.get("summary"), item_type="summary_text"
19
+ )
20
+ if summary_parts:
21
+ return "\n".join(summary_parts)
22
+ return None
23
+
24
+
25
+ def combine_reasoning(existing: str | None, addition: str | None) -> str | None:
26
+ if not addition:
27
+ return existing
28
+ if not existing:
29
+ return addition
30
+ return f"{existing}\n{addition}"
31
+
32
+
33
+ def responses_reasoning_to_thinking(value: Any) -> dict[str, Any] | None:
34
+ if not isinstance(value, Mapping):
35
+ return None
36
+ if value.get("effort") == "none":
37
+ return {"type": "disabled", "enabled": False}
38
+ if any(item is not None for item in value.values()):
39
+ return {"type": "enabled", "enabled": True}
40
+ return None
41
+
42
+
43
+ def _text_parts_from_items(value: Any, *, item_type: str) -> list[str]:
44
+ if not isinstance(value, list):
45
+ return []
46
+ parts: list[str] = []
47
+ for item in value:
48
+ if isinstance(item, dict) and item.get("type") == item_type:
49
+ text = optional_str(item.get("text"))
50
+ if text:
51
+ parts.append(text)
52
+ return parts
@@ -0,0 +1,25 @@
1
+ """Translate Anthropic SSE streams into OpenAI Responses SSE streams."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterable, AsyncIterator, Mapping
6
+ from typing import Any
7
+
8
+ from .anthropic_sse import iter_sse_events
9
+ from .stream_state import ResponsesStreamAssembler
10
+
11
+
12
+ async def iter_responses_sse_from_anthropic(
13
+ chunks: AsyncIterable[Any],
14
+ request: Mapping[str, Any],
15
+ ) -> AsyncIterator[str]:
16
+ """Yield Responses SSE events translated from an Anthropic SSE stream."""
17
+
18
+ assembler = ResponsesStreamAssembler(request)
19
+ async for event in iter_sse_events(chunks):
20
+ for chunk in assembler.process_anthropic_event(event):
21
+ yield chunk
22
+ if assembler.terminal:
23
+ return
24
+ for chunk in assembler.finish_if_needed():
25
+ yield chunk