fix(router): guard DSML tool-call flows

Prevent DeepSeek DSML from leaking to users and avoid returning raw memory_search/web results when DSML is detected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Apple
2026-02-10 04:19:57 -08:00
parent c41c68dc08
commit 7f3ee700a4
2 changed files with 80 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ from typing import Literal, Optional, Dict, Any, List
import asyncio
import json
import os
import re
import yaml
import httpx
import logging
@@ -39,6 +40,35 @@ except ImportError:
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _strip_dsml_keep_text_before(text: str) -> str:
"""If response contains DSML, return only the part before the first DSML-like tag. Otherwise return empty (caller will use fallback)."""
if not text or len(text.strip()) < 10:
return ""
# Find first occurrence of DSML-like patterns (tag or keyword that starts markup)
dsml_start_patterns = [
r"<function_calls",
r"<invoke\s",
r"<parameter\s",
r"<think>",
# DSML variants (ASCII and Unicode separators, e.g. <DSMLinvoke ...>)
r"<\s*(?:\||)?\s*DSML",
r"DSML\s*(?:\||)",
r"DSML\s*>\s*",
]
earliest = len(text)
for pat in dsml_start_patterns:
m = re.search(pat, text, re.IGNORECASE | re.DOTALL)
if m:
earliest = min(earliest, m.start())
if earliest == 0:
return ""
prefix = text[:earliest].strip()
# Remove trailing incomplete tags
prefix = re.sub(r"<[^>]*$", "", prefix).strip()
return prefix if len(prefix) > 30 else ""
app = FastAPI(title="DAARION Router", version="2.0.0")
# Configuration
@@ -1054,7 +1084,12 @@ async def agent_infer(agent_id: str, request: InferRequest):
response_text = final_data.get("choices", [{}])[0].get("message", {}).get("content", "")
# CRITICAL: Check for DSML in second response too!
if response_text and "DSML" in response_text:
if response_text and ("DSML" in response_text or "invoke name=" in response_text or "function_calls>" in response_text):
prefix_before_dsml = _strip_dsml_keep_text_before(response_text)
if prefix_before_dsml:
logger.warning(f"🧹 DSML in 2nd response: keeping text before DSML ({len(prefix_before_dsml)} chars), discarding {len(response_text) - len(prefix_before_dsml)} chars")
response_text = prefix_before_dsml
else:
logger.warning(f"🧹 DSML detected in 2nd LLM response, trying 3rd call ({len(response_text)} chars)")
# Third LLM call: explicitly ask to synthesize tool results
tool_summary_parts = []
@@ -1104,8 +1139,12 @@ async def agent_infer(agent_id: str, request: InferRequest):
if response_text:
# FINAL DSML check before returning - never show DSML to user
if "DSML" in response_text or "invoke name=" in response_text or "function_calls>" in response_text:
prefix_before_dsml = _strip_dsml_keep_text_before(response_text)
if prefix_before_dsml:
logger.warning(f"🧹 DSML in final response: keeping text before DSML ({len(prefix_before_dsml)} chars)")
response_text = prefix_before_dsml
else:
logger.warning(f"🧹 DSML in final response! Replacing with fallback ({len(response_text)} chars)")
# Use dsml_detected mode - LLM confused, just acknowledge presence
response_text = format_tool_calls_for_response(tool_results, fallback_mode="dsml_detected")
# Check if any tool generated an image

View File

@@ -854,6 +854,11 @@ def format_tool_calls_for_response(tool_results: List[Dict], fallback_mode: str
if tool_results:
for tr in tool_results:
if tr.get("success") and tr.get("result"):
# Avoid dumping raw retrieval/search payloads to the user.
# These often look like "memory dumps" and are perceived as incorrect answers.
tool_name = (tr.get("name") or "").strip()
if tool_name in {"memory_search", "web_search", "web_extract", "web_read"}:
continue
result = str(tr.get("result", ""))
if result and len(result) > 10 and "error" not in result.lower():
# We have a useful tool result - use it!
@@ -861,7 +866,7 @@ def format_tool_calls_for_response(tool_results: List[Dict], fallback_mode: str
return result[:600] + "..."
return result
# No useful tool results - give presence acknowledgment
return "Я тут. Чим можу допомогти?"
return "Вибач, відповідь згенерувалась некоректно. Спробуй ще раз (коротше/конкретніше) або повтори питання одним реченням."
if not tool_results:
if fallback_mode == "empty_response":