P0 contexto: ventana por modelo + recuperación ante overflow + self-heal del catálogo
Que las conversaciones largas no se rompan ni gasten de más: Ventana de contexto por modelo (antes: budget estático 120k/200k para todos): - cost.resolve_context_window: lee context_length del catálogo OpenRouter/DeepSeek en Redis, con fallback a litellm. config.budget_for_window deriva el budget de la ventana real (window - max_output - reserve). build_context lo aplica por turno (param model_id) en vez del fijo de settings. - Self-heal del catálogo OpenRouter: el admin panel lo cachea con TTL 1h y solo lo repuebla al abrir su ventana de IA → en runtime caducaba y se perdían ventana y precio. Ahora cost._get_catalog lo refresca solo (fetch público, mismo shape, cooldown 5min, TTL 24h). Arregla también el coste (caía al fijo). Recuperación ante overflow: - adapters.base.ContextOverflowError; openai_adapter traduce el error de context-length del proveedor (init e iteración del stream). - base.py: retry proactivo que recompacta hasta caber en la ventana ANTES de llamar al LLM; si ni así cabe → error accionable (no rompe la sesión). - engine.py: mensaje user-facing claro (modelo + ventana). Tests: ventana/budget, self-heal (mockeado), overflow, y sesión REAL de Redis. 106 verdes. evals/: harness para evaluar al agente acai-code (driver + README + resultados). Comparativa kimi vs deepseek vs glm (deepseek-v4-pro high = mejor calidad/precio). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,36 @@ from typing import Any, AsyncIterator
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from ..config import settings
|
||||
from .base import ModelAdapter, ModelConfig, ModelResponse, StreamChunk
|
||||
from .base import (
|
||||
ContextOverflowError,
|
||||
ModelAdapter,
|
||||
ModelConfig,
|
||||
ModelResponse,
|
||||
StreamChunk,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Señales de que el proveedor rechazó por ventana de contexto. Detectamos por
|
||||
# tipo (litellm.ContextWindowExceededError) y por mensaje (openai.BadRequestError
|
||||
# u otros 400), sin acoplar el adapter a litellm con un import duro.
|
||||
_CONTEXT_OVERFLOW_MARKERS = (
|
||||
"context_length_exceeded",
|
||||
"maximum context length",
|
||||
"context window",
|
||||
"context length",
|
||||
"too many tokens",
|
||||
"reduce the length",
|
||||
"prompt is too long",
|
||||
)
|
||||
|
||||
|
||||
def _is_context_overflow(exc: Exception) -> bool:
|
||||
if type(exc).__name__ in ("ContextWindowExceededError",):
|
||||
return True
|
||||
msg = str(getattr(exc, "message", "") or exc).lower()
|
||||
return any(marker in msg for marker in _CONTEXT_OVERFLOW_MARKERS)
|
||||
|
||||
|
||||
def _estimate_usage(messages: list[dict[str, Any]], output_text: str) -> dict[str, int]:
|
||||
"""Estimacion de tokens cuando el proveedor no entrega usage (p.ej. LiteLLM
|
||||
@@ -62,6 +88,26 @@ class OpenAIAdapter(ModelAdapter):
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
config: ModelConfig | None = None,
|
||||
) -> AsyncIterator[StreamChunk]:
|
||||
"""Envoltorio que traduce errores de ventana de contexto del proveedor a
|
||||
`ContextOverflowError` (dominio), tanto si saltan al iniciar el stream
|
||||
como durante la primera iteración. El loop del agente lo usa para
|
||||
reintentar con compactación agresiva si aún no emitió nada."""
|
||||
try:
|
||||
async for chunk in self._stream_impl(messages, tools, config):
|
||||
yield chunk
|
||||
except ContextOverflowError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if _is_context_overflow(e):
|
||||
raise ContextOverflowError(str(getattr(e, "message", "") or e)) from e
|
||||
raise
|
||||
|
||||
async def _stream_impl(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
config: ModelConfig | None = None,
|
||||
) -> AsyncIterator[StreamChunk]:
|
||||
config = config or ModelConfig(
|
||||
model_id=settings.default_model_id,
|
||||
@@ -281,7 +327,14 @@ class OpenAIAdapter(ModelAdapter):
|
||||
"function": {"name": force_tool},
|
||||
}
|
||||
|
||||
response = await self._acreate(kwargs)
|
||||
try:
|
||||
response = await self._acreate(kwargs)
|
||||
except ContextOverflowError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if _is_context_overflow(e):
|
||||
raise ContextOverflowError(str(getattr(e, "message", "") or e)) from e
|
||||
raise
|
||||
choice = response.choices[0]
|
||||
|
||||
content = choice.message.content or ""
|
||||
|
||||
Reference in New Issue
Block a user