Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/arcopante_asistente-local.json
T

1 line
11 KiB
JSON

{"repo": "arcopante/asistente-local", "files": {"core/llm_client.py": {"b8f6c18d4354bc869c8099b4a7485cfa79b7a8c0": {"t": 1785674524.680117, "n": 9653, "h": "e38f33edeac821c7", "c": "\"\"\"\nllm_client.py — Cliente LLM generico compatible con LM Studio, Ollama y OpenRouter.\n\nLos tres exponen una API compatible con OpenAI en /v1.\n\nVariables de entorno relevantes (configuradas en start.sh):\n BACKEND lmstudio | ollama | openrouter (por defecto: lmstudio)\n LMSTUDIO_HOST http://localhost:1234\n OLLAMA_HOST http://localhost:11434\n OPENROUTER_API_KEY sk-or-...\n OPENROUTER_MODEL mistralai/mistral-7b-instruct (obligatorio con openrouter)\n\"\"\"\n\nimport json\nimport os\nimport httpx\nfrom typing import Iterator, Optional, List, Tuple\n\n\n# ── Helpers internos ──────────────────────────────────────────────────────────\n\ndef _strip_thinking(text: str) -> str:\n \"\"\"Elimina bloques de razonamiento interno (<think>...</think>) de la respuesta.\"\"\"\n import re\n text = re.sub(r\"<think>.*?</think>\", \"\", text, flags=re.DOTALL)\n return text.strip()\n\n\ndef _backend() -> str:\n return os.environ.get(\"BACKEND\", \"lmstudio\").lower()\n\n\ndef _base_url() -> str:\n b = _backend()\n if b == \"ollama\":\n host = os.environ.get(\"OLLAMA_HOST\", \"http://localhost:11434\").rstrip(\"/\")\n return host + \"/v1\"\n if b == \"openrouter\":\n return \"https://openrouter.ai/api/v1\"\n host = os.environ.get(\"LMSTUDIO_HOST\", \"http://localhost:1234\").rstrip(\"/\")\n return host + \"/v1\"\n\n\ndef _headers() -> dict:\n \"\"\"Cabeceras HTTP. OpenRouter requiere Authorization y cabeceras opcionales.\"\"\"\n b = _backend()\n if b == \"openrouter\":\n key = os.environ.get(\"OPENROUTER_API_KEY\", \"\")\n if not key:\n raise RuntimeError(\"OPENROUTER_API_KEY no configurado en start.sh\")\n return {\n \"Authorization\": \"Bearer \" + key,\n \"HTTP-Referer\": \"https://github.com/arcopante/asistente-local\",\n \"X-Title\": \"Asistente Local\",\n }\n return {}\n\n\ndef _client() -> httpx.Client:\n return httpx.Client(base_url=_base_url(), headers=_headers(), timeout=120.0)\n\n\ndef _resolve_model(model: Optional[str]) -> str:\n \"\"\"\n Resuelve el modelo a usar.\n En OpenRouter el modelo es obligatorio y se puede fijar en start.sh.\n En LM Studio y Ollama se usa el que venga del estado del agente.\n \"\"\"\n if _backend() == \"openrouter\":\n return (\n model\n or os.environ.get(\"OPENROUTER_MODEL\", \"\")\n or \"mistralai/mistral-7b-instruct\"\n )\n return model or \"\"\n\n\n# ── Modelos ───────────────────────────────────────────────────────────────────\n\ndef list_models() -> List[dict]:\n \"\"\"Lista los modelos disponibles en el backend activo.\"\"\"\n with _client() as c:\n r = c.get(\"/models\")\n r.raise_for_status()\n return r.json().get(\"data\", [])\n\n\ndef get_loaded_model() -> Optional[str]:\n \"\"\"Devuelve el modelo activo. En OpenRouter devuelve el configurado en start.sh.\"\"\"\n if _backend() == \"openrouter\":\n return os.environ.get(\"OPENROUTER_MODEL\", \"mistralai/mistral-7b-instruct\")\n models = list_models()\n return models[0].get(\"id\") if models else None\n\n\ndef load_model(model_id: str) -> bool:\n \"\"\"\n Carga un modelo.\n - LM Studio: inferencia minima para forzar la carga.\n - Ollama: pull automatico si no esta descargado.\n - OpenRouter: no aplica, el modelo se selecciona por parametro.\n \"\"\"\n if _backend() == \"openrouter\":\n # En OpenRouter no hay carga local; simplemente actualizamos la variable\n os.environ[\"OPENROUTER_MODEL\"] = model_id\n return True\n if _backend() == \"ollama\":\n return _ollama_pull(model_id)\n with _client() as c:\n try:\n r = c.post(\"/chat/completions\", json={\n \"model\": model_id,\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"max_tokens\": 1,\n })\n r.raise_for_status()\n return True\n except Exception:\n return False\n\n\ndef unload_model(model_id: Optional[str] = None) -> Tuple[bool, str]:\n \"\"\"\n Descarga un modelo de memoria.\n - LM Studio: endpoint DELETE /api/v0/models/<id>.\n - Ollama / OpenRouter: no aplica.\n \"\"\"\n b = _backend()\n if b == \"ollama\":\n return False, \"Ollama gestiona la memoria automaticamente.\"\n if b == \"openrouter\":\n return False, \"OpenRouter es un servicio en la nube, no hay modelo local que descargar.\"\n\n if not model_id:\n model_id = get_loaded_model()\n if not model_id:\n return False, \"No hay ningun modelo cargado actualmente.\"\n\n base = os.environ.get(\"LMSTUDIO_HOST\", \"http://localhost:1234\").rstrip(\"/\")\n try:\n with httpx.Client(base_url=base, timeout=30.0) as c:\n r = c.delete(\"/api/v0/models/\" + model_id)\n if r.status_code in (200, 204):\n return True, model_id\n r2 = c.post(\"/api/v0/models/unload\", json={\"identifier\": model_id})\n if r2.status_code in (200, 204):\n return True, model_id\n return False, \"LM Studio respondio \" + str(r.status_code)\n except Exception as e:\n return False, str(e)\n\n\n# ── Inferencia ────────────────────────────────────────────────────────────────\n\ndef chat_stream(\n model: str,\n messages: List[dict],\n temperature: float = 0.7,\n max_tokens: int = 2048,\n) -> Iterator[str]:\n \"\"\"Genera respuesta en streaming. Filtra bloques <think>...</think>.\"\"\"\n payload = {\n \"model\": _resolve_model(model),\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": max_tokens,\n \"stream\": True,\n }\n in_think = False\n buf = \"\"\n\n with httpx.Client(base_url=_base_url(), headers=_headers(), timeout=120.0) as c:\n with c.stream(\"POST\", \"/chat/completions\", json=payload) as r:\n r.raise_for_status()\n for line in r.iter_lines():\n if line.startswith(\"data: \"):\n chunk = line[6:]\n if chunk.strip() == \"[DONE]\":\n break\n try:\n data = json.loads(chunk)\n delta = data[\"choices\"][0][\"delta\"].get(\"content\", \"\")\n if not delta:\n continue\n\n buf += delta\n out = \"\"\n\n while buf:\n if in_think:\n end = buf.find(\"</think>\")\n if end != -1:\n buf = buf[end + 8:]\n in_think = False\n else:\n buf = \"\"\n else:\n start = buf.find(\"<think>\")\n if start != -1:\n out += buf[:start]\n buf = buf[start + 7:]\n in_think = True\n else:\n if buf.endswith(\"<\"):\n out += buf[:-1]\n buf = \"<\"\n break\n else:\n out += buf\n buf = \"\"\n if out:\n yield out\n\n except Exception:\n continue\n\n\ndef chat(\n model: str,\n messages: List[dict],\n temperature: float = 0.7,\n max_tokens: int = 2048,\n) -> Tuple[str, int, list]:\n \"\"\"\n Genera respuesta completa.\n Devuelve (texto, tokens_usados, ficheros_generados).\n \"\"\"\n payload = {\n \"model\": _resolve_model(model),\n \"messages\": messages,\n \"temperature\": temperature,\n \"max_tokens\": max_tokens,\n \"stream\": False,\n }\n with _client() as c:\n r = c.post(\"/chat/completions\", json=payload)\n r.raise_for_status()\n data = r.json()\n\n raw_content = data[\"choices\"][0][\"message\"].get(\"content\") or \"\"\n if isinstance(raw_content, list):\n text = \" \".join(b.get(\"text\", \"\") for b in raw_content if b.get(\"type\") == \"text\")\n else:\n text = raw_content\n text = _strip_thinking(text)\n\n tokens = data.get(\"usage\", {}).get(\"total_tokens\", 0)\n\n from core.downloads import extract_generated_files\n files = extract_generated_files(data)\n\n return text, tokens, files\n\n\n# ── Helpers especificos de Ollama ─────────────────────────────────────────────\n\ndef _ollama_pull(model_id: str) -> bool:\n \"\"\"Descarga un modelo en Ollama si no esta disponible localmente.\"\"\"\n host = os.environ.get(\"OLLAMA_HOST\", \"http://localhost:11434\").rstrip(\"/\")\n try:\n with httpx.Client(base_url=host, timeout=300.0) as c:\n r = c.post(\"/api/pull\", json={\"name\": model_id, \"stream\": False})\n return r.status_code == 200\n except Exception:\n return False\n\n\n# ── Info del backend ──────────────────────────────────────────────────────────\n\ndef backend_info() -> str:\n \"\"\"Devuelve una cadena descriptiva del backend activo.\"\"\"\n b = _backend()\n if b == \"ollama\":\n host = os.environ.get(\"OLLAMA_HOST\", \"http://localhost:11434\")\n return \"Ollama (\" + host + \")\"\n if b == \"openrouter\":\n model = os.environ.get(\"OPENROUTER_MODEL\", \"no configurado\")\n return \"OpenRouter (modelo: \" + model + \")\"\n host = os.environ.get(\"LMSTUDIO_HOST\", \"http://localhost:1234\")\n return \"LM Studio (\" + host + \")\"\n"}}}}