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
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""Request builder and DeepSeek native Anthropic compatibility sanitizer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from config.constants import ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS
|
|
12
|
+
from core.anthropic.native_messages_request import dump_raw_messages_request
|
|
13
|
+
from providers.exceptions import InvalidRequestError
|
|
14
|
+
|
|
15
|
+
# Block types not supported on DeepSeek partial Anthropic-compatible API.
|
|
16
|
+
_UNSUPPORTED_MESSAGE_BLOCK_TYPES = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"image",
|
|
19
|
+
"document",
|
|
20
|
+
"server_tool_use",
|
|
21
|
+
"web_search_tool_result",
|
|
22
|
+
"web_fetch_tool_result",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Block types silently stripped for DeepSeek since the content is typically
|
|
27
|
+
# also provided via tool_result (e.g. Claude Code attaches PDFs as document
|
|
28
|
+
# blocks alongside a Read tool_result containing the text).
|
|
29
|
+
_STRIPPABLE_MESSAGE_BLOCK_TYPES = frozenset({"image", "document"})
|
|
30
|
+
_OMITTED_ATTACHMENT_TEXT = (
|
|
31
|
+
"[attachment omitted: DeepSeek does not support image or document inputs]"
|
|
32
|
+
)
|
|
33
|
+
_OMITTED_ATTACHMENT_BLOCK = {"type": "text", "text": _OMITTED_ATTACHMENT_TEXT}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _strip_unsupported_attachment_blocks(messages: Any) -> Any:
|
|
37
|
+
"""Remove image/document blocks that DeepSeek cannot process.
|
|
38
|
+
|
|
39
|
+
Claude Code sends PDFs as ``document`` blocks alongside a Read ``tool_result``
|
|
40
|
+
that already contains the extracted text. Stripping preserves the request
|
|
41
|
+
instead of failing with an unsupported block error.
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(messages, list):
|
|
44
|
+
return messages
|
|
45
|
+
|
|
46
|
+
stripped: list[Any] = []
|
|
47
|
+
top_level_dropped: dict[str, int] = {}
|
|
48
|
+
nested_dropped: dict[str, int] = {}
|
|
49
|
+
placeholder_replacements = 0
|
|
50
|
+
|
|
51
|
+
for message in messages:
|
|
52
|
+
if not isinstance(message, dict):
|
|
53
|
+
stripped.append(message)
|
|
54
|
+
continue
|
|
55
|
+
content = message.get("content")
|
|
56
|
+
if not isinstance(content, list):
|
|
57
|
+
stripped.append(message)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
new_content: list[Any] = []
|
|
61
|
+
message_dropped_attachment = False
|
|
62
|
+
for block in content:
|
|
63
|
+
if isinstance(block, dict):
|
|
64
|
+
btype = block.get("type")
|
|
65
|
+
if btype in _STRIPPABLE_MESSAGE_BLOCK_TYPES:
|
|
66
|
+
top_level_dropped[btype] = top_level_dropped.get(btype, 0) + 1
|
|
67
|
+
message_dropped_attachment = True
|
|
68
|
+
continue
|
|
69
|
+
if btype == "tool_result":
|
|
70
|
+
inner = block.get("content")
|
|
71
|
+
if isinstance(inner, list):
|
|
72
|
+
filtered_inner: list[Any] = []
|
|
73
|
+
for sub in inner:
|
|
74
|
+
if (
|
|
75
|
+
isinstance(sub, dict)
|
|
76
|
+
and sub.get("type") in _STRIPPABLE_MESSAGE_BLOCK_TYPES
|
|
77
|
+
):
|
|
78
|
+
sub_type = sub["type"]
|
|
79
|
+
nested_dropped[sub_type] = (
|
|
80
|
+
nested_dropped.get(sub_type, 0) + 1
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
filtered_inner.append(sub)
|
|
84
|
+
if not filtered_inner:
|
|
85
|
+
filtered_inner = [_OMITTED_ATTACHMENT_BLOCK]
|
|
86
|
+
placeholder_replacements += 1
|
|
87
|
+
new_block = dict(block)
|
|
88
|
+
new_block["content"] = filtered_inner
|
|
89
|
+
new_content.append(new_block)
|
|
90
|
+
continue
|
|
91
|
+
new_content.append(block)
|
|
92
|
+
if not new_content and message_dropped_attachment:
|
|
93
|
+
new_content = [_OMITTED_ATTACHMENT_BLOCK]
|
|
94
|
+
placeholder_replacements += 1
|
|
95
|
+
new_msg = dict(message)
|
|
96
|
+
new_msg["content"] = new_content
|
|
97
|
+
stripped.append(new_msg)
|
|
98
|
+
|
|
99
|
+
if top_level_dropped or nested_dropped:
|
|
100
|
+
logger.warning(
|
|
101
|
+
"DEEPSEEK_REQUEST: stripped unsupported attachment blocks "
|
|
102
|
+
"(top_level={} nested_in_tool_result={} placeholder_tool_results={}). "
|
|
103
|
+
"DeepSeek has no vision/document support; the model will not see this content.",
|
|
104
|
+
dict(top_level_dropped),
|
|
105
|
+
dict(nested_dropped),
|
|
106
|
+
placeholder_replacements,
|
|
107
|
+
)
|
|
108
|
+
return stripped
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_server_listed_tool(tool: Mapping[str, Any]) -> bool:
|
|
112
|
+
"""True for Anthropic web_search / web_fetch-style tool definitions (listed tools)."""
|
|
113
|
+
name = (tool.get("name") or "").strip()
|
|
114
|
+
if name in ("web_search", "web_fetch"):
|
|
115
|
+
return True
|
|
116
|
+
typ = tool.get("type")
|
|
117
|
+
if isinstance(typ, str):
|
|
118
|
+
return typ.startswith("web_search") or typ.startswith("web_fetch")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _walk_block_list_for_unsupported(blocks: Any, *, where: str) -> None:
|
|
123
|
+
if not isinstance(blocks, list):
|
|
124
|
+
return
|
|
125
|
+
for block in blocks:
|
|
126
|
+
if not isinstance(block, dict):
|
|
127
|
+
continue
|
|
128
|
+
btype = block.get("type")
|
|
129
|
+
if btype in _UNSUPPORTED_MESSAGE_BLOCK_TYPES:
|
|
130
|
+
raise InvalidRequestError(
|
|
131
|
+
f"DeepSeek native does not support {btype!r} blocks ({where})."
|
|
132
|
+
)
|
|
133
|
+
if btype == "tool_result" and "content" in block:
|
|
134
|
+
_walk_block_list_for_unsupported(
|
|
135
|
+
block["content"], where=f"{where} (tool_result content)"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _validate_deepseek_native_request_dict(data: dict[str, Any]) -> None:
|
|
140
|
+
mcp = data.get("mcp_servers")
|
|
141
|
+
if mcp:
|
|
142
|
+
raise InvalidRequestError(
|
|
143
|
+
"DeepSeek native does not support mcp_servers on requests."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
for tool in data.get("tools") or ():
|
|
147
|
+
if not isinstance(tool, dict):
|
|
148
|
+
continue
|
|
149
|
+
if _is_server_listed_tool(tool):
|
|
150
|
+
raise InvalidRequestError(
|
|
151
|
+
"DeepSeek native does not support listed Anthropic server tools "
|
|
152
|
+
"(web_search / web_fetch). Remove them or use a different provider."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for i, message in enumerate(data.get("messages") or ()):
|
|
156
|
+
if not isinstance(message, dict):
|
|
157
|
+
continue
|
|
158
|
+
c = message.get("content")
|
|
159
|
+
if isinstance(c, list):
|
|
160
|
+
_walk_block_list_for_unsupported(c, where=f"messages[{i}].content")
|
|
161
|
+
if isinstance(c, str) and "<think>" in c:
|
|
162
|
+
# Unusual, but block encoded redacted content — treat as unsafe for DeepSeek.
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
system = data.get("system")
|
|
166
|
+
if isinstance(system, list):
|
|
167
|
+
_walk_block_list_for_unsupported(system, where="system")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _has_tool_history_blocks(message: Mapping[str, Any]) -> bool:
|
|
171
|
+
role = message.get("role")
|
|
172
|
+
content = message.get("content")
|
|
173
|
+
if not isinstance(content, list):
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
for block in content:
|
|
177
|
+
if not isinstance(block, dict):
|
|
178
|
+
continue
|
|
179
|
+
btype = block.get("type")
|
|
180
|
+
if role == "assistant" and btype == "tool_use":
|
|
181
|
+
return True
|
|
182
|
+
if role == "user" and btype == "tool_result":
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _has_replayable_thinking_before_tool_use(message: Mapping[str, Any]) -> bool:
|
|
188
|
+
if message.get("role") != "assistant":
|
|
189
|
+
return False
|
|
190
|
+
content = message.get("content")
|
|
191
|
+
if not isinstance(content, list):
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
has_thinking = False
|
|
195
|
+
for block in content:
|
|
196
|
+
if not isinstance(block, dict):
|
|
197
|
+
continue
|
|
198
|
+
btype = block.get("type")
|
|
199
|
+
if btype == "thinking" and isinstance(block.get("thinking"), str):
|
|
200
|
+
has_thinking = bool(block["thinking"])
|
|
201
|
+
continue
|
|
202
|
+
if btype == "tool_use":
|
|
203
|
+
return has_thinking
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _has_tool_history(data: dict[str, Any]) -> bool:
|
|
208
|
+
for message in data.get("messages") or ():
|
|
209
|
+
if isinstance(message, Mapping) and _has_tool_history_blocks(message):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _has_replayable_tool_thinking(data: dict[str, Any]) -> bool:
|
|
215
|
+
for message in data.get("messages") or ():
|
|
216
|
+
if isinstance(message, Mapping) and _has_replayable_thinking_before_tool_use(
|
|
217
|
+
message
|
|
218
|
+
):
|
|
219
|
+
return True
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _remove_deepseek_thinking_hints(data: dict[str, Any]) -> None:
|
|
224
|
+
"""Remove request hints that can keep DeepSeek in thinking mode after fallback."""
|
|
225
|
+
output_config = data.get("output_config")
|
|
226
|
+
if isinstance(output_config, dict) and "effort" in output_config:
|
|
227
|
+
cleaned_output_config = dict(output_config)
|
|
228
|
+
cleaned_output_config.pop("effort", None)
|
|
229
|
+
if cleaned_output_config:
|
|
230
|
+
data["output_config"] = cleaned_output_config
|
|
231
|
+
else:
|
|
232
|
+
data.pop("output_config", None)
|
|
233
|
+
|
|
234
|
+
context_management = data.get("context_management")
|
|
235
|
+
if not isinstance(context_management, dict):
|
|
236
|
+
return
|
|
237
|
+
edits = context_management.get("edits")
|
|
238
|
+
if not isinstance(edits, list):
|
|
239
|
+
return
|
|
240
|
+
filtered_edits = [
|
|
241
|
+
edit
|
|
242
|
+
for edit in edits
|
|
243
|
+
if not (
|
|
244
|
+
isinstance(edit, dict)
|
|
245
|
+
and isinstance(edit.get("type"), str)
|
|
246
|
+
and edit["type"].startswith("clear_thinking_")
|
|
247
|
+
)
|
|
248
|
+
]
|
|
249
|
+
if len(filtered_edits) == len(edits):
|
|
250
|
+
return
|
|
251
|
+
cleaned_context_management = dict(context_management)
|
|
252
|
+
if filtered_edits:
|
|
253
|
+
cleaned_context_management["edits"] = filtered_edits
|
|
254
|
+
data["context_management"] = cleaned_context_management
|
|
255
|
+
else:
|
|
256
|
+
cleaned_context_management.pop("edits", None)
|
|
257
|
+
if cleaned_context_management:
|
|
258
|
+
data["context_management"] = cleaned_context_management
|
|
259
|
+
else:
|
|
260
|
+
data.pop("context_management", None)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def sanitize_deepseek_messages_for_native(
|
|
264
|
+
messages: Any, *, thinking_enabled: bool
|
|
265
|
+
) -> Any:
|
|
266
|
+
"""Filter assistant content for DeepSeek: unsigned ``thinking`` is allowed; no ``redacted_thinking``."""
|
|
267
|
+
if not isinstance(messages, list):
|
|
268
|
+
return messages
|
|
269
|
+
|
|
270
|
+
sanitized: list[Any] = []
|
|
271
|
+
for message in messages:
|
|
272
|
+
if not isinstance(message, dict):
|
|
273
|
+
sanitized.append(message)
|
|
274
|
+
continue
|
|
275
|
+
if message.get("role") != "assistant":
|
|
276
|
+
sanitized.append(message)
|
|
277
|
+
continue
|
|
278
|
+
content = message.get("content")
|
|
279
|
+
if not isinstance(content, list):
|
|
280
|
+
sanitized.append(message)
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
if not thinking_enabled:
|
|
284
|
+
filtered = [
|
|
285
|
+
block
|
|
286
|
+
for block in content
|
|
287
|
+
if not (
|
|
288
|
+
isinstance(block, dict)
|
|
289
|
+
and block.get("type") in ("thinking", "redacted_thinking")
|
|
290
|
+
)
|
|
291
|
+
]
|
|
292
|
+
else:
|
|
293
|
+
filtered = [
|
|
294
|
+
block
|
|
295
|
+
for block in content
|
|
296
|
+
if not (
|
|
297
|
+
isinstance(block, dict) and block.get("type") == "redacted_thinking"
|
|
298
|
+
)
|
|
299
|
+
]
|
|
300
|
+
new_msg = dict(message)
|
|
301
|
+
new_msg["content"] = filtered or ""
|
|
302
|
+
sanitized.append(new_msg)
|
|
303
|
+
return sanitized
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _serialize_tool_result_content(content: Any) -> str:
|
|
307
|
+
"""Serialize tool_result content to string for DeepSeek API.
|
|
308
|
+
|
|
309
|
+
DeepSeek's Anthropic-compatible API expects tool_result.content to be a string,
|
|
310
|
+
not an array of content blocks.
|
|
311
|
+
"""
|
|
312
|
+
if content is None:
|
|
313
|
+
return ""
|
|
314
|
+
if isinstance(content, str):
|
|
315
|
+
return content
|
|
316
|
+
if isinstance(content, dict):
|
|
317
|
+
return json.dumps(content, ensure_ascii=False)
|
|
318
|
+
if isinstance(content, list):
|
|
319
|
+
parts: list[str] = []
|
|
320
|
+
for item in content:
|
|
321
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
322
|
+
parts.append(str(item.get("text", "")))
|
|
323
|
+
elif isinstance(item, dict):
|
|
324
|
+
parts.append(json.dumps(item, ensure_ascii=False))
|
|
325
|
+
else:
|
|
326
|
+
parts.append(str(item))
|
|
327
|
+
return "\n".join(parts)
|
|
328
|
+
return str(content)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _normalize_tool_result_content(messages: Any) -> Any:
|
|
332
|
+
"""Normalize tool_result content to strings for DeepSeek API compatibility."""
|
|
333
|
+
if not isinstance(messages, list):
|
|
334
|
+
return messages
|
|
335
|
+
|
|
336
|
+
normalized: list[Any] = []
|
|
337
|
+
for message in messages:
|
|
338
|
+
if not isinstance(message, dict):
|
|
339
|
+
normalized.append(message)
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
content = message.get("content")
|
|
343
|
+
if not isinstance(content, list):
|
|
344
|
+
normalized.append(message)
|
|
345
|
+
continue
|
|
346
|
+
|
|
347
|
+
# Process content blocks
|
|
348
|
+
new_content: list[Any] = []
|
|
349
|
+
for block in content:
|
|
350
|
+
if not isinstance(block, dict):
|
|
351
|
+
new_content.append(block)
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
if block.get("type") == "tool_result":
|
|
355
|
+
# Normalize tool_result content to string
|
|
356
|
+
normalized_block = dict(block)
|
|
357
|
+
normalized_block["content"] = _serialize_tool_result_content(
|
|
358
|
+
block.get("content")
|
|
359
|
+
)
|
|
360
|
+
new_content.append(normalized_block)
|
|
361
|
+
else:
|
|
362
|
+
new_content.append(block)
|
|
363
|
+
|
|
364
|
+
new_msg = dict(message)
|
|
365
|
+
new_msg["content"] = new_content
|
|
366
|
+
normalized.append(new_msg)
|
|
367
|
+
|
|
368
|
+
return normalized
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _strip_reasoning_content_when_native(messages: Any) -> Any:
|
|
372
|
+
"""``reasoning_content`` is OpenAI-helper metadata; not part of native Anthropic body."""
|
|
373
|
+
if not isinstance(messages, list):
|
|
374
|
+
return messages
|
|
375
|
+
out: list[Any] = []
|
|
376
|
+
for m in messages:
|
|
377
|
+
if not isinstance(m, dict):
|
|
378
|
+
out.append(m)
|
|
379
|
+
continue
|
|
380
|
+
msg = {k: v for k, v in m.items() if k != "reasoning_content"}
|
|
381
|
+
out.append(msg)
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def build_request_body(request_data: Any, *, thinking_enabled: bool) -> dict:
|
|
386
|
+
"""Build a DeepSeek ``/v1/messages`` JSON body (Anthropic format)."""
|
|
387
|
+
logger.debug(
|
|
388
|
+
"DEEPSEEK_REQUEST: native build model={} msgs={}",
|
|
389
|
+
getattr(request_data, "model", "?"),
|
|
390
|
+
len(getattr(request_data, "messages", [])),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
data = dump_raw_messages_request(request_data)
|
|
394
|
+
if "messages" in data:
|
|
395
|
+
data["messages"] = _strip_unsupported_attachment_blocks(data["messages"])
|
|
396
|
+
_validate_deepseek_native_request_dict(data)
|
|
397
|
+
data.pop("extra_body", None)
|
|
398
|
+
_downgrade_forced_tool_choice(data)
|
|
399
|
+
|
|
400
|
+
has_tool_history = _has_tool_history(data)
|
|
401
|
+
has_replayable_tool_thinking = _has_replayable_tool_thinking(data)
|
|
402
|
+
unsafe_tool_followup = has_tool_history and not has_replayable_tool_thinking
|
|
403
|
+
effective_thinking_enabled = thinking_enabled and not unsafe_tool_followup
|
|
404
|
+
if thinking_enabled:
|
|
405
|
+
if unsafe_tool_followup:
|
|
406
|
+
logger.debug(
|
|
407
|
+
"DEEPSEEK_REQUEST: disabling thinking for tool follow-up without "
|
|
408
|
+
"replayable thinking model={} msgs={} tools={}",
|
|
409
|
+
data.get("model"),
|
|
410
|
+
len(data.get("messages", [])),
|
|
411
|
+
len(data.get("tools", [])),
|
|
412
|
+
)
|
|
413
|
+
_remove_deepseek_thinking_hints(data)
|
|
414
|
+
elif has_tool_history:
|
|
415
|
+
logger.debug(
|
|
416
|
+
"DEEPSEEK_REQUEST: keeping thinking for tool follow-up with "
|
|
417
|
+
"replayable thinking model={} msgs={} tools={}",
|
|
418
|
+
data.get("model"),
|
|
419
|
+
len(data.get("messages", [])),
|
|
420
|
+
len(data.get("tools", [])),
|
|
421
|
+
)
|
|
422
|
+
elif data.get("tools") or data.get("tool_choice"):
|
|
423
|
+
logger.debug(
|
|
424
|
+
"DEEPSEEK_REQUEST: keeping thinking for initial tool request "
|
|
425
|
+
"model={} msgs={} tools={}",
|
|
426
|
+
data.get("model"),
|
|
427
|
+
len(data.get("messages", [])),
|
|
428
|
+
len(data.get("tools", [])),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
thinking_cfg = data.pop("thinking", None)
|
|
432
|
+
if effective_thinking_enabled and isinstance(thinking_cfg, dict):
|
|
433
|
+
thinking_payload: dict[str, Any] = {"type": "enabled"}
|
|
434
|
+
budget_tokens = thinking_cfg.get("budget_tokens")
|
|
435
|
+
if isinstance(budget_tokens, int):
|
|
436
|
+
thinking_payload["budget_tokens"] = budget_tokens
|
|
437
|
+
data["thinking"] = thinking_payload
|
|
438
|
+
|
|
439
|
+
if "messages" in data:
|
|
440
|
+
data["messages"] = _strip_reasoning_content_when_native(
|
|
441
|
+
_normalize_tool_result_content(
|
|
442
|
+
sanitize_deepseek_messages_for_native(
|
|
443
|
+
data["messages"],
|
|
444
|
+
thinking_enabled=effective_thinking_enabled,
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
if "max_tokens" not in data or data.get("max_tokens") is None:
|
|
449
|
+
data["max_tokens"] = ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS
|
|
450
|
+
|
|
451
|
+
data["stream"] = True
|
|
452
|
+
|
|
453
|
+
logger.debug(
|
|
454
|
+
"DEEPSEEK_REQUEST: build done model={} msgs={} tools={}",
|
|
455
|
+
data.get("model"),
|
|
456
|
+
len(data.get("messages", [])),
|
|
457
|
+
len(data.get("tools", [])),
|
|
458
|
+
)
|
|
459
|
+
return data
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _downgrade_forced_tool_choice(data: dict[str, Any]) -> None:
|
|
463
|
+
tool_choice = data.get("tool_choice")
|
|
464
|
+
if not isinstance(tool_choice, dict):
|
|
465
|
+
return
|
|
466
|
+
if tool_choice.get("type") != "tool" or not isinstance(
|
|
467
|
+
tool_choice.get("name"), str
|
|
468
|
+
):
|
|
469
|
+
return
|
|
470
|
+
logger.debug(
|
|
471
|
+
"DEEPSEEK_REQUEST: downgrading forced tool_choice to auto for unsupported "
|
|
472
|
+
"native request shape tool={}",
|
|
473
|
+
tool_choice["name"],
|
|
474
|
+
)
|
|
475
|
+
data["tool_choice"] = {"type": "auto"}
|
providers/defaults.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Re-exports default upstream base URLs from the config provider catalog."""
|
|
2
|
+
|
|
3
|
+
from config.provider_catalog import (
|
|
4
|
+
CEREBRAS_DEFAULT_BASE,
|
|
5
|
+
CODESTRAL_DEFAULT_BASE,
|
|
6
|
+
DEEPSEEK_ANTHROPIC_DEFAULT_BASE,
|
|
7
|
+
DEEPSEEK_DEFAULT_BASE,
|
|
8
|
+
GEMINI_DEFAULT_BASE,
|
|
9
|
+
GROQ_DEFAULT_BASE,
|
|
10
|
+
KIMI_DEFAULT_BASE,
|
|
11
|
+
LLAMACPP_DEFAULT_BASE,
|
|
12
|
+
LMSTUDIO_DEFAULT_BASE,
|
|
13
|
+
MISTRAL_DEFAULT_BASE,
|
|
14
|
+
NVIDIA_NIM_DEFAULT_BASE,
|
|
15
|
+
OLLAMA_DEFAULT_BASE,
|
|
16
|
+
OPENCODE_DEFAULT_BASE,
|
|
17
|
+
OPENCODE_GO_DEFAULT_BASE,
|
|
18
|
+
OPENROUTER_DEFAULT_BASE,
|
|
19
|
+
WAFER_DEFAULT_BASE,
|
|
20
|
+
ZAI_DEFAULT_BASE,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
"CEREBRAS_DEFAULT_BASE",
|
|
25
|
+
"CODESTRAL_DEFAULT_BASE",
|
|
26
|
+
"DEEPSEEK_ANTHROPIC_DEFAULT_BASE",
|
|
27
|
+
"DEEPSEEK_DEFAULT_BASE",
|
|
28
|
+
"GEMINI_DEFAULT_BASE",
|
|
29
|
+
"GROQ_DEFAULT_BASE",
|
|
30
|
+
"KIMI_DEFAULT_BASE",
|
|
31
|
+
"LLAMACPP_DEFAULT_BASE",
|
|
32
|
+
"LMSTUDIO_DEFAULT_BASE",
|
|
33
|
+
"MISTRAL_DEFAULT_BASE",
|
|
34
|
+
"NVIDIA_NIM_DEFAULT_BASE",
|
|
35
|
+
"OLLAMA_DEFAULT_BASE",
|
|
36
|
+
"OPENCODE_DEFAULT_BASE",
|
|
37
|
+
"OPENCODE_GO_DEFAULT_BASE",
|
|
38
|
+
"OPENROUTER_DEFAULT_BASE",
|
|
39
|
+
"WAFER_DEFAULT_BASE",
|
|
40
|
+
"ZAI_DEFAULT_BASE",
|
|
41
|
+
)
|