"""Interfaz común de los clientes de modelo. Abstrae la ruta híbrida: medGemma auto-alojado (privado por defecto) y Claude (opcional, mayor precisión). Ambos deben devolver una InterpretacionClinica validada; la validación de esquema vive en cada cliente para poder reintentar ante salida malformada. """ from __future__ import annotations from typing import Protocol from ..schemas import InterpretacionClinica class ErrorModelo(Exception): """Fallo al invocar un modelo o validar su salida. `reintentable` decide si el servicio vuelve a muestrear. La distinción importa de verdad: el reintento existe para la salida malformada de medGemma (razonamiento filtrado o bucle de repetición), donde volver a muestrear suele funcionar. Reintentar un 429 hace lo contrario — duplica la presión sobre la cuota de ZeroGPU, que es justo el recurso agotado— y reintentar un rechazo por seguridad o una clave mal configurada no puede cambiar nada. `saturado` marca los casos de límite de tasa/cuota, para poder responder 503 + Retry-After en vez de un 502 genérico. `truncado` marca la respuesta cortada a mitad de frase. Es reintentable, pero repetir la misma petición no sirve: se ha medido que el corte es determinista y lo provoca el contexto RAG comiéndose el presupuesto de generación del Space. El servicio lo usa para reintentar con MENOS literatura en vez de con el mismo prompt. """ def __init__( self, mensaje: str, *, reintentable: bool = True, saturado: bool = False, truncado: bool = False, ) -> None: super().__init__(mensaje) self.reintentable = reintentable self.saturado = saturado self.truncado = truncado class ClienteModelo(Protocol): nombre: str modelo: str # True si el cliente devuelve TEXTO libre envuelto en `interpretacion` en vez de rellenar # los campos estructurados. El servicio lo consulta para elegir el system prompt, para no # exigir campos que esta ruta no puede rellenar y para suplir `requiere_derivacion` desde # el motor determinista. Antes se deducía del nombre del cliente, que dejó de bastar en # cuanto la prosa pudo venir también de un modelo local. prosa: bool async def interpretar( self, sistema: str, mensaje_usuario: str, imagenes: list[str], ) -> InterpretacionClinica: """Devuelve una interpretación validada o lanza ErrorModelo.""" ...