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,156 @@
1
+ """OpenAI-compatible chat transport base."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import abstractmethod
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from typing import Any
8
+
9
+ import httpx
10
+ from openai import AsyncOpenAI
11
+
12
+ from core.anthropic import SSEBuilder
13
+ from providers.base import BaseProvider, ProviderConfig
14
+ from providers.error_mapping import (
15
+ extract_provider_error_detail,
16
+ map_error,
17
+ user_visible_message_for_mapped_provider_error,
18
+ )
19
+ from providers.model_listing import extract_openai_model_ids
20
+ from providers.rate_limit import GlobalRateLimiter
21
+
22
+ from .stream import OpenAIChatStreamRunner
23
+
24
+
25
+ class OpenAIChatTransport(BaseProvider):
26
+ """Base for OpenAI-compatible ``/chat/completions`` adapters."""
27
+
28
+ def __init__(
29
+ self,
30
+ config: ProviderConfig,
31
+ *,
32
+ provider_name: str,
33
+ base_url: str,
34
+ api_key: str,
35
+ ):
36
+ super().__init__(config)
37
+ self._provider_name = provider_name
38
+ self._api_key = api_key
39
+ self._base_url = base_url.rstrip("/")
40
+ self._global_rate_limiter = GlobalRateLimiter.get_scoped_instance(
41
+ provider_name.lower(),
42
+ rate_limit=config.rate_limit,
43
+ rate_window=config.rate_window,
44
+ max_concurrency=config.max_concurrency,
45
+ )
46
+ http_client = None
47
+ if config.proxy:
48
+ http_client = httpx.AsyncClient(
49
+ proxy=config.proxy,
50
+ timeout=httpx.Timeout(
51
+ config.http_read_timeout,
52
+ connect=config.http_connect_timeout,
53
+ read=config.http_read_timeout,
54
+ write=config.http_write_timeout,
55
+ ),
56
+ )
57
+ self._client = AsyncOpenAI(
58
+ api_key=self._api_key,
59
+ base_url=self._base_url,
60
+ max_retries=0,
61
+ timeout=httpx.Timeout(
62
+ config.http_read_timeout,
63
+ connect=config.http_connect_timeout,
64
+ read=config.http_read_timeout,
65
+ write=config.http_write_timeout,
66
+ ),
67
+ http_client=http_client,
68
+ )
69
+
70
+ async def cleanup(self) -> None:
71
+ """Release HTTP client resources."""
72
+ client = getattr(self, "_client", None)
73
+ if client is not None:
74
+ await client.close()
75
+
76
+ async def list_model_ids(self) -> frozenset[str]:
77
+ """Return model ids from the provider's OpenAI-compatible models endpoint."""
78
+ payload = await self._client.models.list()
79
+ return extract_openai_model_ids(payload, provider_name=self._provider_name)
80
+
81
+ @abstractmethod
82
+ def _build_request_body(
83
+ self, request: Any, thinking_enabled: bool | None = None
84
+ ) -> dict:
85
+ """Build request body. Must be implemented by subclasses."""
86
+
87
+ def _handle_extra_reasoning(
88
+ self, delta: Any, sse: SSEBuilder, *, thinking_enabled: bool
89
+ ) -> Iterator[str]:
90
+ """Hook for provider-specific reasoning."""
91
+ return iter(())
92
+
93
+ def _get_retry_request_body(self, error: Exception, body: dict) -> dict | None:
94
+ """Return a modified request body for one retry, or None."""
95
+ return None
96
+
97
+ def _prepare_create_body(self, body: dict[str, Any]) -> dict[str, Any]:
98
+ """Return the body passed to the upstream OpenAI-compatible client."""
99
+ return body
100
+
101
+ def _record_tool_call_extra_content(
102
+ self, tool_call_id: str, extra_content: dict[str, Any]
103
+ ) -> None:
104
+ """Hook for providers that must replay OpenAI tool-call metadata later."""
105
+
106
+ def _tool_argument_aliases(self, body: dict[str, Any]) -> dict[str, dict[str, str]]:
107
+ """Return provider-specific per-tool argument aliases for this request."""
108
+ return {}
109
+
110
+ async def _create_stream(self, body: dict) -> tuple[Any, dict]:
111
+ """Create a streaming chat completion, optionally retrying once."""
112
+ try:
113
+ create_body = self._prepare_create_body(body)
114
+ stream = await self._global_rate_limiter.execute_with_retry(
115
+ self._client.chat.completions.create, **create_body, stream=True
116
+ )
117
+ return stream, body
118
+ except Exception as error:
119
+ retry_body = self._get_retry_request_body(error, body)
120
+ if retry_body is None:
121
+ raise
122
+
123
+ create_retry_body = self._prepare_create_body(retry_body)
124
+ stream = await self._global_rate_limiter.execute_with_retry(
125
+ self._client.chat.completions.create, **create_retry_body, stream=True
126
+ )
127
+ return stream, retry_body
128
+
129
+ def _openai_error_message(self, error: Exception, request_id: str | None) -> str:
130
+ mapped_error = map_error(error, rate_limiter=self._global_rate_limiter)
131
+ return user_visible_message_for_mapped_provider_error(
132
+ mapped_error,
133
+ provider_name=self._provider_name,
134
+ read_timeout_s=self._config.http_read_timeout,
135
+ detail=extract_provider_error_detail(error),
136
+ request_id=request_id,
137
+ )
138
+
139
+ async def stream_response(
140
+ self,
141
+ request: Any,
142
+ input_tokens: int = 0,
143
+ *,
144
+ request_id: str | None = None,
145
+ thinking_enabled: bool | None = None,
146
+ ) -> AsyncIterator[str]:
147
+ """Stream response in Anthropic SSE format."""
148
+ runner = OpenAIChatStreamRunner(
149
+ self,
150
+ request=request,
151
+ input_tokens=input_tokens,
152
+ request_id=request_id,
153
+ thinking_enabled=thinking_enabled,
154
+ )
155
+ async for event in runner.run():
156
+ yield event
@@ -0,0 +1,10 @@
1
+ """Wafer provider exports."""
2
+
3
+ from providers.defaults import WAFER_DEFAULT_BASE
4
+
5
+ from .client import WaferProvider
6
+
7
+ __all__ = [
8
+ "WAFER_DEFAULT_BASE",
9
+ "WaferProvider",
10
+ ]
@@ -0,0 +1,50 @@
1
+ """Wafer provider implementation (native Anthropic-compatible Messages)."""
2
+
3
+ from typing import Any
4
+
5
+ from providers.base import ProviderConfig
6
+ from providers.defaults import WAFER_DEFAULT_BASE
7
+ from providers.transports.anthropic_messages import AnthropicMessagesTransport
8
+
9
+ _ANTHROPIC_VERSION = "2023-06-01"
10
+
11
+
12
+ class WaferProvider(AnthropicMessagesTransport):
13
+ """Wafer using ``https://pass.wafer.ai/v1/messages``."""
14
+
15
+ def __init__(self, config: ProviderConfig):
16
+ super().__init__(
17
+ config,
18
+ provider_name="WAFER",
19
+ default_base_url=WAFER_DEFAULT_BASE,
20
+ )
21
+
22
+ def _build_request_body(
23
+ self, request: Any, thinking_enabled: bool | None = None
24
+ ) -> dict:
25
+ """Build native body; Wafer rejects omitted thinking as ``reasoning_effort=none``."""
26
+ effective_thinking_enabled = self._is_thinking_enabled(
27
+ request, thinking_enabled
28
+ )
29
+ body = self._build_request_body_with_resolved_thinking(
30
+ request,
31
+ thinking_enabled=effective_thinking_enabled,
32
+ )
33
+ if "thinking" not in body:
34
+ body["thinking"] = (
35
+ {"type": "enabled"}
36
+ if effective_thinking_enabled
37
+ else {"type": "disabled"}
38
+ )
39
+ return body
40
+
41
+ def _request_headers(self) -> dict[str, str]:
42
+ return {
43
+ "Accept": "text/event-stream",
44
+ "Authorization": f"Bearer {self._api_key}",
45
+ "Content-Type": "application/json",
46
+ "anthropic-version": _ANTHROPIC_VERSION,
47
+ }
48
+
49
+ def _model_list_headers(self) -> dict[str, str]:
50
+ return {"Authorization": f"Bearer {self._api_key}"}
@@ -0,0 +1,10 @@
1
+ """Z.ai provider exports."""
2
+
3
+ from providers.defaults import ZAI_DEFAULT_BASE
4
+
5
+ from .client import ZaiProvider
6
+
7
+ __all__ = [
8
+ "ZAI_DEFAULT_BASE",
9
+ "ZaiProvider",
10
+ ]
@@ -0,0 +1,46 @@
1
+ """Z.ai provider implementation (Anthropic-compatible Messages API)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from providers.base import ProviderConfig
8
+ from providers.defaults import ZAI_DEFAULT_BASE
9
+ from providers.transports.anthropic_messages import AnthropicMessagesTransport
10
+
11
+ from .request import build_request_body
12
+
13
+ _ANTHROPIC_VERSION = "2023-06-01"
14
+
15
+
16
+ class ZaiProvider(AnthropicMessagesTransport):
17
+ """Z.ai using Anthropic-compatible Messages at api.z.ai/api/anthropic/v1."""
18
+
19
+ def __init__(self, config: ProviderConfig):
20
+ super().__init__(
21
+ config,
22
+ provider_name="ZAI",
23
+ default_base_url=ZAI_DEFAULT_BASE,
24
+ )
25
+
26
+ def _build_request_body(
27
+ self, request: Any, thinking_enabled: bool | None = None
28
+ ) -> dict:
29
+ return build_request_body(
30
+ request,
31
+ thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
32
+ )
33
+
34
+ def _request_headers(self) -> dict[str, str]:
35
+ return {
36
+ "Accept": "text/event-stream",
37
+ "Content-Type": "application/json",
38
+ "x-api-key": self._api_key,
39
+ "anthropic-version": _ANTHROPIC_VERSION,
40
+ }
41
+
42
+ def _model_list_headers(self) -> dict[str, str]:
43
+ return {
44
+ "x-api-key": self._api_key,
45
+ "anthropic-version": _ANTHROPIC_VERSION,
46
+ }
@@ -0,0 +1,42 @@
1
+ """Native Anthropic Messages request builder for Z.ai."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from loguru import logger
8
+
9
+ from config.constants import ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS
10
+ from core.anthropic.native_messages_request import (
11
+ build_base_native_anthropic_request_body,
12
+ )
13
+ from providers.exceptions import InvalidRequestError
14
+
15
+
16
+ def build_request_body(request_data: Any, *, thinking_enabled: bool) -> dict:
17
+ """Build JSON for Z.ai Anthropic-compat ``POST …/messages``."""
18
+ logger.debug(
19
+ "ZAI_REQUEST: native build model={} msgs={}",
20
+ getattr(request_data, "model", "?"),
21
+ len(getattr(request_data, "messages", [])),
22
+ )
23
+
24
+ body = build_base_native_anthropic_request_body(
25
+ request_data,
26
+ default_max_tokens=ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
27
+ thinking_enabled=thinking_enabled,
28
+ )
29
+ extra = getattr(request_data, "extra_body", None)
30
+ if extra:
31
+ raise InvalidRequestError(
32
+ "Z.ai native Messages API does not support extra_body on requests."
33
+ )
34
+ body["stream"] = True
35
+
36
+ logger.debug(
37
+ "ZAI_REQUEST: build done model={} msgs={} tools={}",
38
+ body.get("model"),
39
+ len(body.get("messages", [])),
40
+ len(body.get("tools", [])),
41
+ )
42
+ return body