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,313 @@
1
+ """Shared native Anthropic SSE thinking policy, block remapping, and overlap repair.
2
+
3
+ Used by :class:`OpenRouterProvider` and line-mode
4
+ :class:`providers.transports.anthropic_messages.AnthropicMessagesTransport` providers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+ __all__ = [
15
+ "NativeSseBlockPolicyState",
16
+ "format_native_sse_event",
17
+ "is_terminal_openrouter_done_event",
18
+ "parse_native_sse_event",
19
+ "transform_native_sse_block_event",
20
+ ]
21
+
22
+
23
+ @dataclass
24
+ class _UpstreamBlockState:
25
+ """Per-upstream content block: segment index and liveness in the model stream."""
26
+
27
+ block_type: str
28
+ down_index: int
29
+ open: bool
30
+ last_start_block: dict[str, Any] | None = None
31
+
32
+
33
+ @dataclass
34
+ class NativeSseBlockPolicyState:
35
+ """Track per-upstream content blocks and remapped Anthropic ``index`` field."""
36
+
37
+ next_index: int = 0
38
+ by_upstream: dict[int, _UpstreamBlockState] = field(default_factory=dict)
39
+ dropped_indexes: set[int] = field(default_factory=set)
40
+ pending_suppressed_stops: set[int] = field(default_factory=set)
41
+ message_stopped: bool = False
42
+
43
+
44
+ def format_native_sse_event(event_name: str | None, data_text: str) -> str:
45
+ """Format an SSE event from its event name and data payload."""
46
+ lines: list[str] = []
47
+ if event_name:
48
+ lines.append(f"event: {event_name}")
49
+ lines.extend(f"data: {line}" for line in data_text.splitlines())
50
+ return "\n".join(lines) + "\n\n"
51
+
52
+
53
+ def parse_native_sse_event(event: str) -> tuple[str | None, str]:
54
+ """Extract the event name and raw data payload from an SSE event."""
55
+ event_name = None
56
+ data_lines: list[str] = []
57
+ for line in event.strip().splitlines():
58
+ if line.startswith("event:"):
59
+ event_name = line[6:].strip()
60
+ elif line.startswith("data:"):
61
+ data_lines.append(line[5:].lstrip())
62
+ return event_name, "\n".join(data_lines)
63
+
64
+
65
+ def is_terminal_openrouter_done_event(event_name: str | None, data_text: str) -> bool:
66
+ """Return whether an event is OpenAI-style terminal noise (``[DONE]``)."""
67
+ return (event_name is None or event_name in {"data", "done"}) and (
68
+ data_text.strip().upper() == "[DONE]"
69
+ )
70
+
71
+
72
+ def _delta_type_to_block_kind(delta_type: Any) -> str | None:
73
+ """Map a content_block_delta type to a content block kind (text/thinking/tool_use)."""
74
+ if not isinstance(delta_type, str):
75
+ return None
76
+ if delta_type in {"thinking_delta", "signature_delta"}:
77
+ return "thinking"
78
+ if delta_type == "text_delta":
79
+ return "text"
80
+ if delta_type == "input_json_delta":
81
+ return "tool_use"
82
+ return None
83
+
84
+
85
+ def _synthetic_start_content_block(
86
+ block_kind: str,
87
+ *,
88
+ upstream_index: int,
89
+ stored_tool_block: dict[str, Any] | None = None,
90
+ ) -> dict[str, Any]:
91
+ """Build a `content_block` for a `content_block_start` with empty streaming fields."""
92
+ if block_kind == "tool_use":
93
+ if (
94
+ isinstance(stored_tool_block, dict)
95
+ and stored_tool_block.get("type") == "tool_use"
96
+ ):
97
+ tool_id = stored_tool_block.get("id")
98
+ name = stored_tool_block.get("name")
99
+ inp = stored_tool_block.get("input")
100
+ return {
101
+ "type": "tool_use",
102
+ "id": tool_id
103
+ if isinstance(tool_id, str) and tool_id
104
+ else f"toolu_or_{upstream_index}",
105
+ "name": name if isinstance(name, str) else "",
106
+ "input": inp if isinstance(inp, dict) else {},
107
+ }
108
+ return {
109
+ "type": "tool_use",
110
+ "id": f"toolu_or_{upstream_index}",
111
+ "name": "",
112
+ "input": {},
113
+ }
114
+ if block_kind == "thinking":
115
+ return {"type": "thinking", "thinking": ""}
116
+ if block_kind == "text":
117
+ return {"type": "text", "text": ""}
118
+ return {"type": "text", "text": ""}
119
+
120
+
121
+ def _should_drop_block_type(block_type: Any, *, thinking_enabled: bool) -> bool:
122
+ if not isinstance(block_type, str):
123
+ return False
124
+ if block_type.startswith("redacted_thinking"):
125
+ return not thinking_enabled
126
+ return not thinking_enabled and "thinking" in block_type
127
+
128
+
129
+ def _synthetic_close_other_open_blocks(
130
+ state: NativeSseBlockPolicyState, current_upstream: int
131
+ ) -> str:
132
+ """Close every open block except `current_upstream` and track duplicate upstream stops."""
133
+ out: list[str] = []
134
+ for upstream, seg in list(state.by_upstream.items()):
135
+ if upstream == current_upstream or not seg.open:
136
+ continue
137
+ out.append(
138
+ format_native_sse_event(
139
+ "content_block_stop",
140
+ json.dumps(
141
+ {
142
+ "type": "content_block_stop",
143
+ "index": seg.down_index,
144
+ }
145
+ ),
146
+ )
147
+ )
148
+ seg.open = False
149
+ state.pending_suppressed_stops.add(upstream)
150
+ return "".join(out)
151
+
152
+
153
+ def _allocate_new_segment(
154
+ state: NativeSseBlockPolicyState,
155
+ upstream_index: int,
156
+ block_type: str,
157
+ *,
158
+ last_start_block: dict[str, Any] | None = None,
159
+ ) -> int:
160
+ """Assign a new downstream `index` for a segment and record upstream state."""
161
+ new_idx = state.next_index
162
+ state.next_index += 1
163
+ state.by_upstream[upstream_index] = _UpstreamBlockState(
164
+ block_type=block_type,
165
+ down_index=new_idx,
166
+ open=True,
167
+ last_start_block=last_start_block,
168
+ )
169
+ return new_idx
170
+
171
+
172
+ def transform_native_sse_block_event(
173
+ event: str,
174
+ state: NativeSseBlockPolicyState,
175
+ *,
176
+ thinking_enabled: bool,
177
+ ) -> str | None:
178
+ """Normalize native Anthropic SSE events and enforce local thinking policy."""
179
+ event_name, data_text = parse_native_sse_event(event)
180
+ if not event_name or not data_text:
181
+ return event
182
+
183
+ try:
184
+ payload = json.loads(data_text)
185
+ except json.JSONDecodeError:
186
+ return event
187
+
188
+ if event_name == "content_block_start":
189
+ block = payload.get("content_block")
190
+ if not isinstance(block, dict):
191
+ return event
192
+ block_type = block.get("type")
193
+ upstream_index = payload.get("index")
194
+ if not isinstance(upstream_index, int):
195
+ return event
196
+ if _should_drop_block_type(block_type, thinking_enabled=thinking_enabled):
197
+ state.dropped_indexes.add(upstream_index)
198
+ return None
199
+
200
+ if not isinstance(block_type, str):
201
+ return event
202
+ prefix = _synthetic_close_other_open_blocks(state, upstream_index)
203
+ stored = copy.deepcopy(block)
204
+ new_idx = _allocate_new_segment(
205
+ state,
206
+ upstream_index,
207
+ block_type=block_type,
208
+ last_start_block=stored,
209
+ )
210
+ payload["index"] = new_idx
211
+ return prefix + format_native_sse_event(event_name, json.dumps(payload))
212
+
213
+ if event_name == "content_block_delta":
214
+ delta = payload.get("delta")
215
+ if not isinstance(delta, dict):
216
+ return event
217
+ delta_type = delta.get("type")
218
+ upstream_index = payload.get("index")
219
+ if not isinstance(upstream_index, int):
220
+ return event
221
+ if upstream_index in state.dropped_indexes:
222
+ return None
223
+ if _should_drop_block_type(delta_type, thinking_enabled=thinking_enabled):
224
+ return None
225
+
226
+ block_kind = _delta_type_to_block_kind(delta_type)
227
+ if block_kind is None:
228
+ return event
229
+
230
+ seg = state.by_upstream.get(upstream_index)
231
+ if seg and seg.open:
232
+ payload["index"] = seg.down_index
233
+ return format_native_sse_event(event_name, json.dumps(payload))
234
+
235
+ if seg is not None and not seg.open:
236
+ # More deltas for an upstream block after a synthetic (or other) close:
237
+ # reopen with a new downstream `index` and emit a synthetic `content_block_start` first.
238
+ state.pending_suppressed_stops.discard(upstream_index)
239
+ carry = seg.last_start_block
240
+ new_idx = _allocate_new_segment(
241
+ state,
242
+ upstream_index,
243
+ block_type=block_kind,
244
+ last_start_block=carry,
245
+ )
246
+ stored_tool = (
247
+ carry
248
+ if isinstance(carry, dict) and carry.get("type") == "tool_use"
249
+ else None
250
+ )
251
+ start_payload = {
252
+ "type": "content_block_start",
253
+ "index": new_idx,
254
+ "content_block": _synthetic_start_content_block(
255
+ block_kind,
256
+ upstream_index=upstream_index,
257
+ stored_tool_block=stored_tool,
258
+ ),
259
+ }
260
+ prefix = format_native_sse_event(
261
+ "content_block_start", json.dumps(start_payload)
262
+ )
263
+ payload["index"] = new_idx
264
+ return prefix + format_native_sse_event(event_name, json.dumps(payload))
265
+
266
+ # Delta with no prior `content_block_start` in this stream
267
+ if block_kind in ("text", "tool_use"):
268
+ synthetic_block = _synthetic_start_content_block(
269
+ block_kind,
270
+ upstream_index=upstream_index,
271
+ )
272
+ new_idx = _allocate_new_segment(
273
+ state,
274
+ upstream_index,
275
+ block_type=block_kind,
276
+ last_start_block=copy.deepcopy(synthetic_block),
277
+ )
278
+ start_payload = {
279
+ "type": "content_block_start",
280
+ "index": new_idx,
281
+ "content_block": synthetic_block,
282
+ }
283
+ prefix = format_native_sse_event(
284
+ "content_block_start", json.dumps(start_payload)
285
+ )
286
+ payload["index"] = new_idx
287
+ return prefix + format_native_sse_event(event_name, json.dumps(payload))
288
+ # thinking: pass through raw (unusual upstream shape)
289
+ return event
290
+
291
+ if event_name == "content_block_stop":
292
+ upstream_index = payload.get("index")
293
+ if not isinstance(upstream_index, int):
294
+ return event
295
+ if upstream_index in state.dropped_indexes:
296
+ return None
297
+ if upstream_index in state.pending_suppressed_stops:
298
+ state.pending_suppressed_stops.discard(upstream_index)
299
+ return None
300
+
301
+ seg = state.by_upstream.get(upstream_index)
302
+ if seg is not None and seg.open:
303
+ payload["index"] = seg.down_index
304
+ seg.open = False
305
+ return format_native_sse_event(event_name, json.dumps(payload))
306
+ if seg is not None:
307
+ # Spurious or duplicate `content_block_stop` for a closed block.
308
+ return None
309
+ if not thinking_enabled:
310
+ return None
311
+ return event
312
+
313
+ return event
@@ -0,0 +1,34 @@
1
+ """Canonical Anthropic-style SSE sequence for provider-side streaming errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from core.anthropic.sse import SSEBuilder
10
+
11
+
12
+ def iter_provider_stream_error_sse_events(
13
+ *,
14
+ request: Any,
15
+ input_tokens: int,
16
+ error_message: str,
17
+ sent_any_event: bool,
18
+ log_raw_sse_events: bool,
19
+ message_id: str | None = None,
20
+ ) -> Iterator[str]:
21
+ """Yield message_start (if needed), a text block with the error, then message_delta/stop."""
22
+ mid = message_id or f"msg_{uuid.uuid4()}"
23
+ model = getattr(request, "model", "") or ""
24
+ sse = SSEBuilder(
25
+ mid,
26
+ model,
27
+ input_tokens,
28
+ log_raw_events=log_raw_sse_events,
29
+ )
30
+ if not sent_any_event:
31
+ yield sse.message_start()
32
+ yield from sse.emit_error(error_message)
33
+ yield sse.message_delta("end_turn", 1)
34
+ yield sse.message_stop()
@@ -0,0 +1,14 @@
1
+ """SSE content_block ``type`` values for Anthropic web server tools (local handlers).
2
+
3
+ Shared by :mod:`api.web_tools` and stream contract tests to avoid drift.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Final
9
+
10
+ SERVER_TOOL_USE: Final = "server_tool_use"
11
+ WEB_SEARCH_TOOL_RESULT: Final = "web_search_tool_result"
12
+ WEB_FETCH_TOOL_RESULT: Final = "web_fetch_tool_result"
13
+ WEB_SEARCH_TOOL_RESULT_ERROR: Final = "web_search_tool_result_error"
14
+ WEB_FETCH_TOOL_ERROR: Final = "web_fetch_tool_error"