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
messaging/workflow.py ADDED
@@ -0,0 +1,200 @@
1
+ """Messaging workflow coordinator for Discord and Telegram prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from loguru import logger
6
+
7
+ from core.trace import trace_event
8
+
9
+ from .models import IncomingMessage
10
+ from .node_runner import MessagingNodeRunner
11
+ from .platforms.base import ManagedClaudeSessionManagerProtocol, MessagingPlatform
12
+ from .rendering.profiles import build_rendering_profile
13
+ from .safe_diagnostics import format_exception_for_log
14
+ from .session import SessionStore
15
+ from .transcript import RenderCtx
16
+ from .trees import MessageNode, MessageState, MessageTree, TreeQueueManager
17
+ from .turn_intake import MessagingTurnIntake
18
+
19
+
20
+ class MessagingWorkflow:
21
+ """
22
+ Platform-agnostic messaging workflow.
23
+
24
+ It coordinates dependencies and owns stop/clear side effects. Inbound turn
25
+ intake and queued node execution live in dedicated collaborators.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ platform: MessagingPlatform,
31
+ cli_manager: ManagedClaudeSessionManagerProtocol,
32
+ session_store: SessionStore,
33
+ *,
34
+ debug_platform_edits: bool = False,
35
+ debug_subagent_stack: bool = False,
36
+ log_raw_messaging_content: bool = False,
37
+ log_raw_cli_diagnostics: bool = False,
38
+ log_messaging_error_details: bool = False,
39
+ ):
40
+ self.platform = platform
41
+ self.cli_manager = cli_manager
42
+ self.session_store = session_store
43
+ self._log_messaging_error_details = log_messaging_error_details
44
+ self._tree_queue = TreeQueueManager()
45
+ self._rendering_profile = build_rendering_profile(platform.name)
46
+
47
+ self.node_runner = MessagingNodeRunner(
48
+ platform=platform,
49
+ cli_manager=cli_manager,
50
+ session_store=session_store,
51
+ get_tree_queue=lambda: self._tree_queue,
52
+ format_status=self.format_status,
53
+ get_parse_mode=self._parse_mode,
54
+ get_render_ctx=self.get_render_ctx,
55
+ get_limit_chars=self._get_limit_chars,
56
+ debug_platform_edits=debug_platform_edits,
57
+ debug_subagent_stack=debug_subagent_stack,
58
+ log_raw_cli_diagnostics=log_raw_cli_diagnostics,
59
+ log_messaging_error_details=log_messaging_error_details,
60
+ )
61
+ self.turn_intake = MessagingTurnIntake(
62
+ platform=platform,
63
+ session_store=session_store,
64
+ command_context=self,
65
+ get_tree_queue=lambda: self._tree_queue,
66
+ process_node=self.node_runner.process_node,
67
+ format_status=self.format_status,
68
+ get_parse_mode=self._parse_mode,
69
+ record_outgoing_message=self.record_outgoing_message,
70
+ log_messaging_error_details=log_messaging_error_details,
71
+ )
72
+ self._wire_tree_callbacks()
73
+
74
+ def _wire_tree_callbacks(self) -> None:
75
+ self._tree_queue.set_queue_update_callback(
76
+ self.turn_intake.update_queue_positions
77
+ )
78
+ self._tree_queue.set_node_started_callback(
79
+ self.turn_intake.mark_node_processing
80
+ )
81
+
82
+ def format_status(self, emoji: str, label: str, suffix: str | None = None) -> str:
83
+ return self._rendering_profile.format_status(emoji, label, suffix)
84
+
85
+ def _parse_mode(self) -> str | None:
86
+ return self._rendering_profile.parse_mode
87
+
88
+ def get_render_ctx(self) -> RenderCtx:
89
+ return self._rendering_profile.render_ctx
90
+
91
+ def _get_limit_chars(self) -> int:
92
+ return self._rendering_profile.limit_chars
93
+
94
+ @property
95
+ def tree_queue(self) -> TreeQueueManager:
96
+ """Accessor for the current tree queue manager."""
97
+ return self._tree_queue
98
+
99
+ def replace_tree_queue(self, tree_queue: TreeQueueManager) -> None:
100
+ """Replace tree queue manager via explicit API."""
101
+ self._tree_queue = tree_queue
102
+ self._wire_tree_callbacks()
103
+
104
+ async def handle_message(self, incoming: IncomingMessage) -> None:
105
+ """
106
+ Main entry point for handling an incoming platform message.
107
+ """
108
+ platform_name = getattr(self.platform, "name", "messaging")
109
+ trace_event(
110
+ stage="ingress",
111
+ event="turn.received",
112
+ source=platform_name,
113
+ chat_id=incoming.chat_id,
114
+ platform_message_id=incoming.message_id,
115
+ reply_to_message_id=incoming.reply_to_message_id,
116
+ thread_id=getattr(incoming, "message_thread_id", None),
117
+ message_text=incoming.text or "",
118
+ )
119
+
120
+ with logger.contextualize(
121
+ chat_id=incoming.chat_id, node_id=incoming.message_id
122
+ ):
123
+ await self.turn_intake.handle_message(incoming)
124
+
125
+ async def update_queue_positions(self, tree: MessageTree) -> None:
126
+ """Refresh queued status messages after a dequeue."""
127
+ await self.turn_intake.update_queue_positions(tree)
128
+
129
+ async def mark_node_processing(self, tree: MessageTree, node_id: str) -> None:
130
+ """Update the dequeued node's status to processing immediately."""
131
+ await self.turn_intake.mark_node_processing(tree, node_id)
132
+
133
+ async def stop_all_tasks(self) -> int:
134
+ """
135
+ Stop all pending and in-progress messaging tasks.
136
+ """
137
+ logger.info("Cancelling tree queue tasks...")
138
+ cancelled_nodes = await self.tree_queue.cancel_all()
139
+ logger.info(f"Cancelled {len(cancelled_nodes)} nodes")
140
+
141
+ logger.info("Stopping all CLI sessions...")
142
+ await self.cli_manager.stop_all()
143
+
144
+ self.update_cancelled_nodes_ui(cancelled_nodes)
145
+ return len(cancelled_nodes)
146
+
147
+ async def stop_task(self, node_id: str) -> int:
148
+ """Stop a single queued or in-progress task node."""
149
+ tree = self.tree_queue.get_tree_for_node(node_id)
150
+ if tree:
151
+ node = tree.get_node(node_id)
152
+ if node and node.state not in (MessageState.COMPLETED, MessageState.ERROR):
153
+ node.set_context({"cancel_reason": "stop"})
154
+
155
+ cancelled_nodes = await self.tree_queue.cancel_node(node_id)
156
+ self.update_cancelled_nodes_ui(cancelled_nodes)
157
+ return len(cancelled_nodes)
158
+
159
+ def record_outgoing_message(
160
+ self,
161
+ platform: str,
162
+ chat_id: str,
163
+ msg_id: str | None,
164
+ kind: str,
165
+ ) -> None:
166
+ """Record outgoing message ID for /clear. Best-effort, never raises."""
167
+ if not msg_id:
168
+ return
169
+ try:
170
+ self.session_store.record_message_id(
171
+ platform, chat_id, str(msg_id), direction="out", kind=kind
172
+ )
173
+ except Exception as e:
174
+ logger.debug(
175
+ "Failed to record message_id: {}",
176
+ format_exception_for_log(
177
+ e, log_full_message=self._log_messaging_error_details
178
+ ),
179
+ )
180
+
181
+ def update_cancelled_nodes_ui(self, nodes: list[MessageNode]) -> None:
182
+ """Update status messages and persist tree state for cancelled nodes."""
183
+ trees_to_save: dict[str, MessageTree] = {}
184
+ for node in nodes:
185
+ self.platform.fire_and_forget(
186
+ self.platform.queue_edit_message(
187
+ node.incoming.chat_id,
188
+ node.status_message_id,
189
+ self.format_status("⏹", "Stopped."),
190
+ parse_mode=self._parse_mode(),
191
+ )
192
+ )
193
+ tree = self.tree_queue.get_tree_for_node(node.node_id)
194
+ if tree:
195
+ trees_to_save[tree.root_id] = tree
196
+ for root_id, tree in trees_to_save.items():
197
+ self.session_store.save_tree(root_id, tree.to_dict())
198
+
199
+
200
+ __all__ = ["MessagingWorkflow"]
providers/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Providers package - implement your own provider by extending BaseProvider.
2
+
3
+ Concrete adapters (e.g. ``NvidiaNimProvider``) live in subpackages; import them
4
+ from ``providers.nvidia_nim`` etc. to avoid loading every adapter when the
5
+ ``providers`` package is imported.
6
+ """
7
+
8
+ from .base import BaseProvider, ProviderConfig
9
+ from .exceptions import (
10
+ APIError,
11
+ AuthenticationError,
12
+ InvalidRequestError,
13
+ ModelListResponseError,
14
+ OverloadedError,
15
+ ProviderError,
16
+ RateLimitError,
17
+ UnknownProviderTypeError,
18
+ )
19
+
20
+ __all__ = [
21
+ "APIError",
22
+ "AuthenticationError",
23
+ "BaseProvider",
24
+ "InvalidRequestError",
25
+ "ModelListResponseError",
26
+ "OverloadedError",
27
+ "ProviderConfig",
28
+ "ProviderError",
29
+ "RateLimitError",
30
+ "UnknownProviderTypeError",
31
+ ]
providers/base.py ADDED
@@ -0,0 +1,152 @@
1
+ """Base provider interface - extend this to implement your own provider."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import AsyncIterator
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from config.constants import HTTP_CONNECT_TIMEOUT_DEFAULT
10
+ from providers.model_listing import ProviderModelInfo, model_infos_from_ids
11
+
12
+
13
+ class ProviderConfig(BaseModel):
14
+ """Configuration for a provider.
15
+
16
+ Base fields apply to all providers. Provider-specific parameters
17
+ (e.g. NIM temperature, top_p) are passed by the provider constructor.
18
+ """
19
+
20
+ api_key: str
21
+ base_url: str | None = None
22
+ rate_limit: int | None = None
23
+ rate_window: int = 60
24
+ max_concurrency: int = 5
25
+ http_read_timeout: float = 300.0
26
+ http_write_timeout: float = 10.0
27
+ http_connect_timeout: float = HTTP_CONNECT_TIMEOUT_DEFAULT
28
+ enable_thinking: bool = True
29
+ proxy: str = ""
30
+ log_raw_sse_events: bool = False
31
+ log_api_error_tracebacks: bool = False
32
+
33
+
34
+ class BaseProvider(ABC):
35
+ """Base class for all providers. Extend this to add your own."""
36
+
37
+ def __init__(self, config: ProviderConfig):
38
+ self._config = config
39
+
40
+ def _is_thinking_enabled(
41
+ self, request: Any, thinking_enabled: bool | None = None
42
+ ) -> bool:
43
+ """Return whether thinking should be enabled for this request."""
44
+ thinking = getattr(request, "thinking", None)
45
+ config_enabled = (
46
+ self._config.enable_thinking
47
+ if thinking_enabled is None
48
+ else thinking_enabled
49
+ )
50
+ request_enabled = True
51
+ if thinking is not None:
52
+ thinking_type = (
53
+ thinking.get("type")
54
+ if isinstance(thinking, dict)
55
+ else getattr(thinking, "type", None)
56
+ )
57
+ if isinstance(thinking, dict):
58
+ enabled = thinking.get("enabled")
59
+ enabled_supplied = "enabled" in thinking
60
+ else:
61
+ enabled = getattr(thinking, "enabled", None)
62
+ fields_set = getattr(thinking, "model_fields_set", None)
63
+ enabled_supplied = (
64
+ "enabled" in fields_set
65
+ if isinstance(fields_set, set | frozenset)
66
+ else enabled is not None
67
+ )
68
+ if enabled_supplied and enabled is not None:
69
+ request_enabled = bool(enabled)
70
+ if thinking_type == "disabled":
71
+ request_enabled = False
72
+ return config_enabled and request_enabled
73
+
74
+ def preflight_stream(
75
+ self, request: Any, *, thinking_enabled: bool | None = None
76
+ ) -> None:
77
+ """Eagerly validate/build the upstream request before opening an SSE stream.
78
+
79
+ Subclasses with ``_build_request_body`` (OpenAI and native) raise
80
+ :class:`providers.exceptions.InvalidRequestError` on conversion failures.
81
+ """
82
+ build = getattr(self, "_build_request_body", None)
83
+ if build is None:
84
+ return
85
+ build(request, thinking_enabled=thinking_enabled)
86
+
87
+ def _log_stream_transport_error(
88
+ self,
89
+ tag: str,
90
+ req_tag: str,
91
+ error: Exception,
92
+ *,
93
+ request_id: str | None = None,
94
+ ) -> None:
95
+ """Log streaming transport failures (metadata-only unless verbose is enabled)."""
96
+ from loguru import logger
97
+
98
+ from core.trace import trace_event
99
+
100
+ response = getattr(error, "response", None)
101
+ http_status = (
102
+ getattr(response, "status_code", None) if response is not None else None
103
+ )
104
+ trace_event(
105
+ stage="provider",
106
+ event="provider.response.transport_error",
107
+ source="provider",
108
+ provider=tag,
109
+ request_id=request_id,
110
+ exc_type=type(error).__name__,
111
+ http_status=http_status,
112
+ )
113
+
114
+ if self._config.log_api_error_tracebacks:
115
+ logger.error(
116
+ "{}_ERROR:{} {}: {}", tag, req_tag, type(error).__name__, error
117
+ )
118
+ return
119
+ logger.error(
120
+ "{}_ERROR:{} exc_type={} http_status={}",
121
+ tag,
122
+ req_tag,
123
+ type(error).__name__,
124
+ http_status,
125
+ )
126
+
127
+ @abstractmethod
128
+ async def cleanup(self) -> None:
129
+ """Release any resources held by this provider."""
130
+
131
+ @abstractmethod
132
+ async def list_model_ids(self) -> frozenset[str]:
133
+ """Return the model ids currently advertised by this provider."""
134
+
135
+ async def list_model_infos(self) -> frozenset[ProviderModelInfo]:
136
+ """Return advertised model ids with optional provider capability metadata."""
137
+ return model_infos_from_ids(await self.list_model_ids())
138
+
139
+ @abstractmethod
140
+ async def stream_response(
141
+ self,
142
+ request: Any,
143
+ input_tokens: int = 0,
144
+ *,
145
+ request_id: str | None = None,
146
+ thinking_enabled: bool | None = None,
147
+ ) -> AsyncIterator[str]:
148
+ """Stream response in Anthropic SSE format."""
149
+ # Typing: abstract async generators need a yield for AsyncIterator[str]
150
+ # inference; this branch is never executed.
151
+ if False:
152
+ yield ""
@@ -0,0 +1,7 @@
1
+ """Cerebras Inference (OpenAI-compatible) adapter."""
2
+
3
+ from providers.defaults import CEREBRAS_DEFAULT_BASE
4
+
5
+ from .client import CerebrasProvider
6
+
7
+ __all__ = ["CEREBRAS_DEFAULT_BASE", "CerebrasProvider"]
@@ -0,0 +1,31 @@
1
+ """Cerebras Inference provider (OpenAI-compatible chat completions)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from providers.base import ProviderConfig
8
+ from providers.defaults import CEREBRAS_DEFAULT_BASE
9
+ from providers.transports.openai_chat import OpenAIChatTransport
10
+
11
+ from .request import build_request_body
12
+
13
+
14
+ class CerebrasProvider(OpenAIChatTransport):
15
+ """Cerebras API at ``https://api.cerebras.ai/v1/chat/completions``."""
16
+
17
+ def __init__(self, config: ProviderConfig):
18
+ super().__init__(
19
+ config,
20
+ provider_name="CEREBRAS",
21
+ base_url=config.base_url or CEREBRAS_DEFAULT_BASE,
22
+ api_key=config.api_key,
23
+ )
24
+
25
+ def _build_request_body(
26
+ self, request: Any, thinking_enabled: bool | None = None
27
+ ) -> dict:
28
+ return build_request_body(
29
+ request,
30
+ thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
31
+ )
@@ -0,0 +1,55 @@
1
+ """Request builder for Cerebras Inference (OpenAI-compatible chat completions).
2
+
3
+ Docs: https://inference-docs.cerebras.ai/resources/openai — use ``max_completion_tokens``
4
+ in API examples; non-standard fields via ``extra_body`` with the OpenAI client.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from loguru import logger
12
+
13
+ from core.anthropic import ReasoningReplayMode, build_base_request_body
14
+ from core.anthropic.conversion import OpenAIConversionError
15
+ from providers.exceptions import InvalidRequestError
16
+
17
+
18
+ def _normalize_max_completion_tokens(body: dict[str, Any]) -> None:
19
+ if "max_completion_tokens" in body:
20
+ body.pop("max_tokens", None)
21
+ return
22
+ if "max_tokens" in body and body["max_tokens"] is not None:
23
+ body["max_completion_tokens"] = body.pop("max_tokens")
24
+
25
+
26
+ def build_request_body(request_data: Any, *, thinking_enabled: bool) -> dict:
27
+ """Build OpenAI-format request body from an Anthropic request for Cerebras."""
28
+ logger.debug(
29
+ "CEREBRAS_REQUEST: conversion start model={} msgs={}",
30
+ getattr(request_data, "model", "?"),
31
+ len(getattr(request_data, "messages", [])),
32
+ )
33
+ try:
34
+ body = build_base_request_body(
35
+ request_data,
36
+ reasoning_replay=ReasoningReplayMode.REASONING_CONTENT
37
+ if thinking_enabled
38
+ else ReasoningReplayMode.DISABLED,
39
+ )
40
+ except OpenAIConversionError as exc:
41
+ raise InvalidRequestError(str(exc)) from exc
42
+
43
+ request_extra = getattr(request_data, "extra_body", None)
44
+ if isinstance(request_extra, dict) and request_extra:
45
+ body["extra_body"] = dict(request_extra)
46
+
47
+ _normalize_max_completion_tokens(body)
48
+
49
+ logger.debug(
50
+ "CEREBRAS_REQUEST: conversion done model={} msgs={} tools={}",
51
+ body.get("model"),
52
+ len(body.get("messages", [])),
53
+ len(body.get("tools", [])),
54
+ )
55
+ return body
@@ -0,0 +1,7 @@
1
+ """Mistral Codestral provider (codestral.mistral.ai) exports."""
2
+
3
+ from providers.defaults import CODESTRAL_DEFAULT_BASE
4
+
5
+ from .client import CodestralProvider
6
+
7
+ __all__ = ["CODESTRAL_DEFAULT_BASE", "CodestralProvider"]
@@ -0,0 +1,34 @@
1
+ """Mistral Codestral provider (OpenAI-compatible chat on codestral.mistral.ai)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from providers.base import ProviderConfig
8
+ from providers.defaults import CODESTRAL_DEFAULT_BASE
9
+ from providers.mistral.request import build_request_body
10
+ from providers.transports.openai_chat import OpenAIChatTransport
11
+
12
+
13
+ class CodestralProvider(OpenAIChatTransport):
14
+ """Codestral host using ``https://codestral.mistral.ai/v1/chat/completions``.
15
+
16
+ Uses a separate Codestral API key from La Plateforme (``MISTRAL_API_KEY``).
17
+ Request shaping matches Mistral La Plateforme (shared ``build_request_body``).
18
+ """
19
+
20
+ def __init__(self, config: ProviderConfig):
21
+ super().__init__(
22
+ config,
23
+ provider_name="CODESTRAL",
24
+ base_url=config.base_url or CODESTRAL_DEFAULT_BASE,
25
+ api_key=config.api_key,
26
+ )
27
+
28
+ def _build_request_body(
29
+ self, request: Any, thinking_enabled: bool | None = None
30
+ ) -> dict:
31
+ return build_request_body(
32
+ request,
33
+ thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
34
+ )
@@ -0,0 +1,11 @@
1
+ """DeepSeek provider exports."""
2
+
3
+ from providers.defaults import DEEPSEEK_ANTHROPIC_DEFAULT_BASE, DEEPSEEK_DEFAULT_BASE
4
+
5
+ from .client import DeepSeekProvider
6
+
7
+ __all__ = [
8
+ "DEEPSEEK_ANTHROPIC_DEFAULT_BASE",
9
+ "DEEPSEEK_DEFAULT_BASE",
10
+ "DeepSeekProvider",
11
+ ]
@@ -0,0 +1,51 @@
1
+ """DeepSeek provider implementation (native Anthropic-compatible Messages)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from providers.base import ProviderConfig
10
+ from providers.defaults import DEEPSEEK_ANTHROPIC_DEFAULT_BASE
11
+ from providers.transports.anthropic_messages import AnthropicMessagesTransport
12
+
13
+ from .request import build_request_body
14
+
15
+
16
+ class DeepSeekProvider(AnthropicMessagesTransport):
17
+ """DeepSeek using ``https://api.deepseek.com/anthropic`` (Anthropic Messages API)."""
18
+
19
+ def __init__(self, config: ProviderConfig):
20
+ super().__init__(
21
+ config,
22
+ provider_name="DEEPSEEK",
23
+ default_base_url=DEEPSEEK_ANTHROPIC_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
+ }
40
+
41
+ async def _send_model_list_request(self) -> httpx.Response:
42
+ """DeepSeek lists models from the OpenAI-format root, not /anthropic."""
43
+ url = str(
44
+ httpx.URL(self._base_url).copy_with(
45
+ path="/models", query=None, fragment=None
46
+ )
47
+ )
48
+ return await self._client.get(url, headers=self._model_list_headers())
49
+
50
+ def _model_list_headers(self) -> dict[str, str]:
51
+ return {"Authorization": f"Bearer {self._api_key}"}