Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7527,6 +7527,17 @@ def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> No
)
if kimi_requires_reasoning and source_msg.get("tool_calls"):
api_msg["reasoning_content"] = ""
return

# То же самое для DeepSeek thinking-моделей через kilo gateway:
# kilo срезает reasoning_content/reasoning/reasoning_details при
# форварде в upstream DeepSeek (truthy-check в removeChatCompletionsReasoning).
# DeepSeek thinking-модели (v3.2+) отдают 400, если в истории есть
# tool-результат и любой последующий assistant-ход без reasoning_content —
# как с tool_calls, так и без (обычный assistant-text).
# Пустая строка "" gateway дропает (falsy), поэтому подставляем один символ.
if self._needs_deepseek_thinking_tool_merge():
api_msg["reasoning_content"] = "."

@staticmethod
def _sanitize_tool_calls_for_strict_api(api_msg: dict) -> dict:
Expand Down Expand Up @@ -7569,6 +7580,96 @@ def _should_sanitize_tool_calls(self) -> bool:
"""
return self.api_mode != "codex_responses"

def _needs_deepseek_thinking_tool_merge(self) -> bool:
"""Определяет, нужен ли обход бага kilo gateway для DeepSeek thinking-моделей.

Kilo gateway срезает `reasoning_content` / `reasoning` / `reasoning_details`
при форварде в upstream-провайдер DeepSeek (см.
Kilo-Org/cloud/apps/web/src/lib/ai-gateway/providers/openrouter/request-helpers.ts:
`removeChatCompletionsReasoning`). В результате DeepSeek thinking-моделям
(v3.2+, включая v4-pro/flash, deepseek-reasoner) в истории диалога после
пары [assistant+tool_calls → tool_result] не хватает `reasoning_content`,
и любое последующее user-сообщение вызывает 400
"reasoning_content in the thinking mode must be passed back to the API".

Обход (как в Roo Code, опция `mergeToolResultText`): сливать любой текст,
идущий после последнего tool-результата, прямо в его content, чтобы
user-сообщение после tool не возникало.
"""
try:
if not base_url_host_matches(self.base_url, "api.kilo.ai"):
return False
except Exception:
return False
model = (self.model or "").lower()
if "deepseek" not in model:
return False
# deepseek-chat — non-thinking, проблема не проявляется
if "deepseek-chat" in model:
return False
return True

@staticmethod
def _extract_text_from_content(content) -> str:
"""Достаёт плоский текст из content (строки или списка blocks)."""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for p in content:
if isinstance(p, dict):
if p.get("type") == "text" and isinstance(p.get("text"), str):
parts.append(p["text"])
return "\n".join(parts)
return ""

@classmethod
def _merge_post_tool_text_into_tool(cls, api_messages: list) -> list:
"""Сливает КАЖДОЕ user-сообщение, идущее после tool-результата,
внутрь content ближайшего предыдущего tool-сообщения — и удаляет
его из списка.

Покрывает и trailing user после последнего tool, и user-сообщения
между tool-циклами (новый пользовательский ввод в продолжающейся
сессии), включая случай, когда между tool и user есть assistant-text
(kilo всё равно ломается, если user идёт после tool в истории).

E2E-тесты через kilo показали, что DeepSeek thinking-модели отдают
400 "reasoning_content in the thinking mode must be passed back
to the API" при ЛЮБОМ user-сообщении, идущем после tool в истории.

assistant-text-сообщения между tool и user остаются на своих местах —
они kilo не мешают.

Первый user в диалоге (до появления любого tool) НЕ трогается.

Возвращает новый список (копию). Входной список не мутирует.
"""
if not api_messages:
return api_messages

result = []
last_tool_idx_in_result = None # индекс последнего tool в формируемом result
for msg in api_messages:
role = msg.get("role")
if role == "tool":
result.append(msg.copy() if isinstance(msg, dict) else msg)
last_tool_idx_in_result = len(result) - 1
elif role == "user" and last_tool_idx_in_result is not None:
# Слить этот user в ближайший предыдущий tool-результат.
prev = result[last_tool_idx_in_result]
prev_text = cls._extract_text_from_content(prev.get("content")) or ""
user_text = cls._extract_text_from_content(msg.get("content"))
if user_text:
joined = prev_text + ("\n\n" if prev_text else "") + "[user]\n" + user_text
prev_copy = prev.copy()
prev_copy["content"] = joined
result[last_tool_idx_in_result] = prev_copy
# иначе — пустой user, просто выкидываем
else:
result.append(msg.copy() if isinstance(msg, dict) else msg)
return result

def flush_memories(self, messages: list = None, min_turns: int = None):
"""Give the model one turn to persist memories before context is lost.

Expand Down Expand Up @@ -7620,6 +7721,10 @@ def flush_memories(self, messages: list = None, min_turns: int = None):
self._sanitize_tool_calls_for_strict_api(api_msg)
api_messages.append(api_msg)

# Тот же обход, что и в основном пути — для flush-вызова через kilo+DeepSeek thinking.
if self._needs_deepseek_thinking_tool_merge():
api_messages = self._merge_post_tool_text_into_tool(api_messages)

if self._cached_system_prompt:
api_messages = [{"role": "system", "content": self._cached_system_prompt}] + api_messages

Expand Down Expand Up @@ -9398,6 +9503,13 @@ def run_conversation(
# The signature field helps maintain reasoning continuity
api_messages.append(api_msg)

# Обход бага kilo gateway для DeepSeek thinking-моделей (v3.2+).
# Сливаем любой user/assistant-text, идущий после последнего tool,
# прямо в его content — иначе DeepSeek отдаёт 400
# "reasoning_content in the thinking mode must be passed back to the API".
if self._needs_deepseek_thinking_tool_merge():
api_messages = self._merge_post_tool_text_into_tool(api_messages)

# Build the final system message: cached prompt + ephemeral system prompt.
# Ephemeral additions are API-call-time only (not persisted to session DB).
# External recall context is injected into the user message, not the system
Expand Down
Loading