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:
Jordan
2026-06-19 14:47:55 +01:00
parent 4543300101
commit 5883473e92
15 changed files with 408 additions and 29 deletions

View File

@@ -57,11 +57,16 @@ function resolveChatPreviewPath(imageUrl) {
const fileParam = qs.get("file");
if (!fileParam) return null;
// Sanitizar: evitar traversal — solo nombre base permitido
const safeName = path.basename(fileParam);
if (!safeName || safeName === "." || safeName === "..") return null;
// Sanitizar contra traversal PRESERVANDO el subdirectorio de usuario
// (el file= es "<username>/<archivo>"; basename lo perdía → not found).
if (fileParam.includes("..") || fileParam.startsWith("/") || fileParam.includes("\\")) return null;
return path.join(CHAT_UPLOADS_DIR, safeName);
const fullPath = path.join(CHAT_UPLOADS_DIR, fileParam);
// Asegurar que queda dentro de CHAT_UPLOADS_DIR.
const base = path.resolve(CHAT_UPLOADS_DIR);
if (!path.resolve(fullPath).startsWith(base + path.sep)) return null;
return fullPath;
}
/**