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,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
+ )