Runtime IA: modelo dinámico, razonamiento, coste por modelo y visión nativa
- Resolución dinámica del modelo por sesión (model_resolver): override de usuario (metadata) → default global (Redis db 0 acai:config:ai:*) → fallback. Mapea a string litellm; LiteLLMAdapter respeta el modelo por request y enruta openrouter/* con OPENROUTER_API_KEY del entorno. - Razonamiento: reasoning_effort por sesión en ModelConfig/AgentProfile, aplicado al agente y al planner. - Coste: cost.py calcula por modelo (catálogo OpenRouter/DeepSeek en Redis → litellm → fijo) y emite tarifas + modelo usado en EXECUTION_COMPLETED. - Visión nativa: imágenes como bloques image_url en el turno del usuario (TaskState.image_attachments → Context Engine → adapter), con persistencia en recent_messages y conteo de tokens de imagen (~1500). - El turno no se pierde al cancelar: se persiste el mensaje del usuario + marca de interrupción para que un "vuelve a intentarlo" tenga contexto. - Fix analyze_image: preservar el subdirectorio de usuario del chat-upload (basename descartaba "<user>/" → not found). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,8 @@ class OpenAIAdapter(ModelAdapter):
|
||||
"stream": True,
|
||||
"stream_options": {"include_usage": True},
|
||||
}
|
||||
if getattr(config, "reasoning_effort", ""):
|
||||
kwargs["reasoning_effort"] = config.reasoning_effort
|
||||
if tools:
|
||||
kwargs["tools"] = self._format_tools(tools)
|
||||
|
||||
@@ -266,6 +268,8 @@ class OpenAIAdapter(ModelAdapter):
|
||||
"temperature": config.temperature,
|
||||
"messages": self._to_openai_messages(messages),
|
||||
}
|
||||
if getattr(config, "reasoning_effort", ""):
|
||||
kwargs["reasoning_effort"] = config.reasoning_effort
|
||||
if tools:
|
||||
kwargs["tools"] = self._format_tools(tools)
|
||||
# Fuerza al modelo a usar un tool concreto para garantizar JSON por schema
|
||||
@@ -428,8 +432,9 @@ class OpenAIAdapter(ModelAdapter):
|
||||
if tool_calls:
|
||||
m["tool_calls"] = tool_calls
|
||||
out.append(m)
|
||||
else: # user (puede traer tool_result blocks)
|
||||
else: # user (puede traer tool_result blocks, texto e imágenes)
|
||||
text_parts = []
|
||||
image_blocks: list[dict[str, Any]] = []
|
||||
for b in content:
|
||||
if not isinstance(b, dict):
|
||||
continue
|
||||
@@ -442,7 +447,18 @@ class OpenAIAdapter(ModelAdapter):
|
||||
})
|
||||
elif t == "text":
|
||||
text_parts.append(b.get("text", ""))
|
||||
if text_parts:
|
||||
elif t == "image_url":
|
||||
# Visión nativa: preservar el bloque en formato multimodal OpenAI.
|
||||
image_blocks.append({"type": "image_url", "image_url": b.get("image_url") or {}})
|
||||
if image_blocks:
|
||||
# Content como lista de bloques (texto + imágenes).
|
||||
parts: list[dict[str, Any]] = []
|
||||
joined = "\n".join(p for p in text_parts if p)
|
||||
if joined:
|
||||
parts.append({"type": "text", "text": joined})
|
||||
parts.extend(image_blocks)
|
||||
out.append({"role": "user", "content": parts})
|
||||
elif text_parts:
|
||||
out.append({"role": "user", "content": "\n".join(text_parts)})
|
||||
# Guard defensivo: el compactor ya garantiza el invariante tool_use ↔
|
||||
# tool_result (`_enforce_tool_pairing`), pero si algo se escapa el
|
||||
|
||||
Reference in New Issue
Block a user