# core/llm_client.py import asyncio import httpx from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from agents.registry import PROVIDERS, AGENT_REGISTRY class LLMClient: def __init__(self): self.semaphore = asyncio.Semaphore(6) # máximo 6 llamadas simultáneas async def call(self, agent_key: str, messages: list, temperature: float = 0.7): agent = AGENT_REGISTRY.get(agent_key) if not agent: raise ValueError(f"Agente {agent_key} no encontrado") provider = PROVIDERS[agent["provider"]] system_prompt = agent["role"] full_messages = [{"role": "system", "content": system_prompt}] + messages if provider["type"] == "gemini": # ── Soporte para Google Gemini ─────────────────────────────────────── try: import google.generativeai as genai genai.configure(api_key=provider["key"]) model_name = agent["models"][0] print(f"Intentando Gemini con modelo: {model_name}") try: model = genai.GenerativeModel(model_name) except Exception as e: print(f"Modelo {model_name} falló: {str(e)}") model_name = "gemini-2.5-flash" # fallback seguro print(f"Usando fallback: {model_name}") model = genai.GenerativeModel(model_name) # Convertir formato OpenAI → Gemini gemini_messages = [] for msg in full_messages: role = "user" if msg["role"] in ("system", "user") else "model" gemini_messages.append({ "role": role, "parts": [{"text": msg["content"]}] }) response = await asyncio.to_thread( model.generate_content, gemini_messages, generation_config={"temperature": temperature, "max_output_tokens": 4096} ) print(f"Gemini respondió exitosamente con {model_name}") return response.text except ImportError: raise ImportError("Instala google-generativeai: pip install google-generativeai") except Exception as e: print(f"Error completo en Gemini ({model_name}): {str(e)}") if hasattr(e, 'response'): print(f"Status: {e.response.status_code}") print(f"Body: {e.response.text[:500]}") raise elif provider["type"] == "openai_compat": # ── Soporte para OpenRouter, Groq, etc. ─────────────────────────────── @retry( stop=stop_after_attempt(6), # un intento más wait=wait_exponential(multiplier=2, min=5, max=120), # espera progresiva: 5s → 10s → 20s → 40s → 80s → 120s retry=retry_if_exception_type((httpx.HTTPStatusError, httpx.TimeoutException)), before_sleep=lambda retry_state: print(f"Reintento {retry_state.attempt_number} después de {retry_state.outcome.exception()}") ) async def try_provider(base_url, model, api_key, headers): async with self.semaphore: payload = { "model": model, "messages": full_messages, "temperature": temperature, "max_tokens": 4096, "top_p": 0.92 } print(f"Intentando {provider['name']} con modelo {model}") async with httpx.AsyncClient(timeout=120) as client: # timeout más largo url = f"{base_url}/chat/completions" # ← ¡esto es lo que falta! print(f"URL completa: {url}") resp = await client.post( url, headers={"Authorization": f"Bearer {api_key}", **headers}, json=payload ) try: resp.raise_for_status() except httpx.HTTPStatusError as exc: print(f"HTTP Error {resp.status_code} con {model}") print(f"Response body: {resp.text[:500]}") raise exc content = resp.json()["choices"][0]["message"]["content"] print(f"Éxito con {model}") return content last_err = None for model in agent.get("models", []): try: return await try_provider( provider["base_url"], model, provider["key"], provider.get("headers", {}) ) except Exception as e: print(f"Error completo con {model}: {str(e)}") if hasattr(e, 'response') and e.response is not None: print(f" → Status: {e.response.status_code}") print(f" → Body: {e.response.text[:500]}") print(f" → Headers: {dict(e.response.headers)}") last_err = e # Fallback a OpenRouter if agent["provider"] != "openrouter" and PROVIDERS["openrouter"]["key"]: or_p = PROVIDERS["openrouter"] print("Probando fallback a OpenRouter...") for model in or_p["models"][:3]: # solo los primeros 3 para no saturar try: return await try_provider( or_p["base_url"], model, or_p["key"], or_p.get("headers", {}) ) except Exception as e: print(f"Fallback falló con {model}: {str(e)}") last_err = e error_msg = f"Todos los proveedores fallaron. Último error: {last_err}" print(error_msg) raise Exception(error_msg) else: raise ValueError(f"Tipo de proveedor desconocido: {provider['type']}") # Instancia global llm = LLMClient()