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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- 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,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,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}"}
|