Harden DeepSeek agent: LiteLLM adapter, DSML/reasoning/embeddings/error fixes

- LiteLLMAdapter (subclasses OpenAIAdapter via _acreate hook): routes DeepSeek
  through LiteLLM. Opt-in AGENTIC_DEFAULT_MODEL_PROVIDER=litellm. A/B beat the
  hand-rolled adapter (0 DSML, 0 parse-fails). Defensive chunk.usage getattr,
  token-estimate usage fallback for billing, quiet litellm logs.
- DSML parser: tolerate single/multi fullwidth pipes, honor string="true/false"
  typed args (openai_adapter fallback when DeepSeek leaks tool calls as text).
- Thinking mode: capture and round-trip reasoning_content across turns.
- Embeddings: dedicated AGENTIC_EMBEDDINGS_API_KEY (DeepSeek has no embeddings);
  disable cleanly when unset to avoid per-turn 401.
- claude_format: friendly generic error messages to the chat, raw only in logs.
- acai agent max_tokens 4096->16384 (whole-file writes no longer truncate);
  system.md size-based edit policy; strict tools opt-in (off).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordan Diaz
2026-06-07 14:49:48 +00:00
parent e34a39e3bf
commit 6a03fdf284
12 changed files with 396 additions and 58 deletions

View File

@@ -19,6 +19,71 @@ from .sse import EventType, SSEEmitter
logger = logging.getLogger(__name__)
_GENERIC_ERROR = (
"Ha ocurrido un error procesando tu mensaje. Vuelve a intentarlo en unos momentos."
)
# Patrones que el frontend interpreta por sí mismo (login / sesión expirada).
# No los genericamos para no romper esas detecciones.
_PASSTHROUGH_PATTERNS = (
"not logged in",
"login required",
"authentication required",
"no conversation found",
)
def friendly_error_message(raw: str, code: str = "") -> str:
"""Traduce un error crudo (proveedor/excepción) a un mensaje genérico y
localizado para el usuario final, sin filtrar detalles internos.
Devuelve el texto original sin tocar para los casos de auth/sesión que el
frontend ya gestiona por contenido.
"""
raw = raw or ""
text = "{} {}".format(code or "", raw).lower()
# Auth / sesión: dejar pasar el texto original (lo maneja el frontend)
if any(p in text for p in _PASSTHROUGH_PATTERNS):
return raw
# Timeout de ejecución
if "timeout" in text or "timed out" in text:
return (
"La tarea tardó demasiado en completarse. Prueba a dividirla en "
"pasos más pequeños o vuelve a intentarlo."
)
# Saldo insuficiente / facturación del proveedor (402)
if (
"402" in text
or "insufficient balance" in text
or "insufficient_quota" in text
or "billing" in text
):
return (
"El asistente no está disponible en este momento. Inténtalo de "
"nuevo en unos minutos."
)
# Credenciales del proveedor inválidas (401)
if (
"401" in text
or "invalid_api_key" in text
or "incorrect api key" in text
or "invalid api key" in text
):
return (
"El asistente no está disponible temporalmente por un problema de "
"configuración. Estamos trabajando en ello."
)
# Límite de peticiones (429)
if "429" in text or "rate limit" in text or "rate_limit" in text:
return (
"Hay mucha demanda en este momento. Espera unos segundos y vuelve "
"a intentarlo."
)
return _GENERIC_ERROR
class ClaudeFormatEmitter:
"""Emits events in Claude Code CLI SSE format.
@@ -304,7 +369,10 @@ class ClaudeFormatEmitter:
self._push(session_id, {"type": "done"})
elif event_type == EventType.ERROR:
error_msg = data.get("message", str(data.get("error", "Unknown error")))
raw_msg = data.get("message", str(data.get("error", "Unknown error")))
user_msg = friendly_error_message(raw_msg, str(data.get("error", "")))
# El error real (detalles del proveedor) solo va al log, nunca al cliente.
logger.warning("Session %s error (raw): %s", session_id, raw_msg)
# Close any open block
self._close_text_block(session_id)
@@ -312,7 +380,7 @@ class ClaudeFormatEmitter:
self._push(session_id, {
"type": "result",
"is_error": True,
"result": error_msg,
"result": user_msg,
"usage": {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0},
"total_cost_usd": 0,
})