Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
1.3 MiB
Plaintext
1 line
1.3 MiB
Plaintext
{"repo": "alex-place/lantern-os", "files": {"src/unified_agent_connector.py": {"eb1ea13d377f332ccf9cee2acea5ea0e27375e48": {"t": 1785745420.3568666, "n": 39435, "h": "09b6c1628138f42f", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement + serving modes\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\ntry:\n from serving_modes import get_serving_mode, get_decode_params\n _SERVING_MODES_AVAILABLE = True\nexcept Exception:\n _SERVING_MODES_AVAILABLE = False\n def get_serving_mode(): return None\n def get_decode_params(mode): return {}\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n options = {\n \"temperature\": temperature or cfg.temperature,\n \"num_predict\": max_tokens or cfg.max_tokens,\n }\n # Ollama uses repeat_penalty and repeat_last_n instead of frequency_penalty\n if decode_params:\n options[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n options[\"repeat_penalty\"] = decode_params.get(\"repetition_penalty\", 1.1)\n options[\"repeat_last_n\"] = decode_params.get(\"repeat_last_n\", 64)\n\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": options}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\n \"model\": cfg.model,\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"stream\": True,\n \"system\": system,\n \"temperature\": temperature or cfg.temperature,\n \"messages\": [{\"role\": \"user\", \"content\": message}],\n }).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n # OpenAI uses top_p and frequency_penalty\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"I can't answer that right now — no local model is running (Ollama/Ouro on :11434) and the cloud providers are unreachable, so there's no LLM to handle '{snippet}...'. Start a local model or set a cloud API key, then resend. (Keystone is a technical assistant, not a dream narrator.)\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "b05d0f83f90918440d81743fb76a14ed99e38f09": {"t": 1785752992.2230377, "n": 39435, "h": "09b6c1628138f42f", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement + serving modes\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\ntry:\n from serving_modes import get_serving_mode, get_decode_params\n _SERVING_MODES_AVAILABLE = True\nexcept Exception:\n _SERVING_MODES_AVAILABLE = False\n def get_serving_mode(): return None\n def get_decode_params(mode): return {}\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n options = {\n \"temperature\": temperature or cfg.temperature,\n \"num_predict\": max_tokens or cfg.max_tokens,\n }\n # Ollama uses repeat_penalty and repeat_last_n instead of frequency_penalty\n if decode_params:\n options[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n options[\"repeat_penalty\"] = decode_params.get(\"repetition_penalty\", 1.1)\n options[\"repeat_last_n\"] = decode_params.get(\"repeat_last_n\", 64)\n\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": options}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\n \"model\": cfg.model,\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"stream\": True,\n \"system\": system,\n \"temperature\": temperature or cfg.temperature,\n \"messages\": [{\"role\": \"user\", \"content\": message}],\n }).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n # OpenAI uses top_p and frequency_penalty\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"I can't answer that right now — no local model is running (Ollama/Ouro on :11434) and the cloud providers are unreachable, so there's no LLM to handle '{snippet}...'. Start a local model or set a cloud API key, then resend. (Keystone is a technical assistant, not a dream narrator.)\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "1fd349696c92b6387677830e27ae5c288ca77419": {"t": 1785752994.198397, "n": 39257, "h": "a23e95b88470de12", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement + serving modes\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\ntry:\n from serving_modes import get_serving_mode, get_decode_params\n _SERVING_MODES_AVAILABLE = True\nexcept Exception:\n _SERVING_MODES_AVAILABLE = False\n def get_serving_mode(): return None\n def get_decode_params(mode): return {}\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n options = {\n \"temperature\": temperature or cfg.temperature,\n \"num_predict\": max_tokens or cfg.max_tokens,\n }\n # Ollama uses repeat_penalty and repeat_last_n instead of frequency_penalty\n if decode_params:\n options[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n options[\"repeat_penalty\"] = decode_params.get(\"repetition_penalty\", 1.1)\n options[\"repeat_last_n\"] = decode_params.get(\"repeat_last_n\", 64)\n\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": options}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\n \"model\": cfg.model,\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"stream\": True,\n \"system\": system,\n \"temperature\": temperature or cfg.temperature,\n \"messages\": [{\"role\": \"user\", \"content\": message}],\n }).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n # OpenAI uses top_p and frequency_penalty\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "67078b90ca5d7b9652ee85da4b73c7453185df2f": {"t": 1785752996.753984, "n": 39943, "h": "2b28abcfad6581b3", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n# Σ₀ coder verification contract (issue #628). Keystone is the coding agent; its\n# identity is the gate's verification prompt, not a dream/RP persona.\ntry:\n from sigma0_coder_gate import KEYSTONE_CODER_PROMPT as _KEYSTONE_CODER_PROMPT\n from sigma0_coder_gate import build_pre_generation_gate as _build_coder_gate\n from sigma0_coder_gate import REQUIRED_SECTIONS\n _CODER_GATE_AVAILABLE = True\nexcept Exception:\n _CODER_GATE_AVAILABLE = False\n _build_coder_gate = None # type: ignore\n # Fallback contract so the connector still imports if the gate module is absent.\n _KEYSTONE_CODER_PROMPT = (\n \"You are Keystone, the coding agent of Lantern OS. Assert nothing without \"\n \"checkable evidence. End every substantive response with: Claim, Evidence, \"\n \"Confidence (0-1), Source, Verification.\"\n )\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n # Keystone is the coding agent (issue #628). Its identity lives in\n # sigma0_coder_gate.KEYSTONE_CODER_PROMPT — a verification contract, NOT a\n # dream/RP persona. _build_system() detects id == \"keystone\" and skips the\n # dream tone so the local coder model never inherits \"end with a question\".\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n # Visual identity: a cracked keystone stone sitting grounded and composed —\n # the cracks show its evidence, nothing hidden. Coder + verifier; the one\n # who holds the claim steady. (symbol is metadata, not part of the contract.)\n symbol=\"cracked keystone stone, grounded and meditative, evidence in every fracture, the one who holds the claim steady\",\n system_prompt=_KEYSTONE_CODER_PROMPT,\n voice_tags=[\"grounded\", \"verifying\", \"code\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True, coder: bool = False):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\n\n coder=True routes through the Keystone coding agent: forces the verification\n contract (no dream tone) and prefers the local Ollama coder, lowering\n temperature so generation is deterministic rather than exploratory.\n \"\"\"\n if coder:\n persona_id = \"keystone\"\n if provider is None:\n provider = \"ollama\"\n if temperature is None:\n temperature = 0.2\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n # Keystone is the coding agent: it gets the Σ₀ verification contract and\n # NONE of the dream/RP tone. extra_context becomes grounding evidence so the\n # gate can set the confidence ceiling (capped at 0.3 when ungrounded).\n if persona.id == \"keystone\":\n if _CODER_GATE_AVAILABLE and _build_coder_gate is not None:\n grounding = [extra_context] if extra_context else []\n return _build_coder_gate(grounding_evidence=grounding).system_prompt\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nGrounding evidence:\\n{extra_context}\")\n return \"\\n\".join(parts)\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n # Keystone is the coding agent, not a dream greeter — keep it out of the rotation.\n persona = random.choice([p for p in PERSONAS if p.id != \"keystone\"])\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n # Keystone is the Σ₀ coding agent (issue #628): it carries the\n # verification contract and prefers the local Ollama coder model.\n \"coderAgent\": {\n \"persona\": \"keystone\",\n \"contract\": \"sigma0\",\n \"verificationFields\": list(REQUIRED_SECTIONS) if _CODER_GATE_AVAILABLE else [\"Claim\", \"Evidence\", \"Confidence\", \"Source\", \"Verification\"],\n \"preferredProvider\": \"ollama\",\n \"preferredModel\": self._providers.get(\"ollama\").model if self._providers.get(\"ollama\") else \"qwen2.5-coder\",\n },\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n parser.add_argument(\"--coder\", action=\"store_true\", help=\"Route through the Keystone coding agent (Σ₀ verification contract)\")\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens, coder=args.coder)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "0c8c7e649619e5e300d0a75bc4fd4b22d62f2e95": {"t": 1785753000.6713243, "n": 36937, "h": "e84a1ffd4e6bf726", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "ae527f8cbee8acbe61e307521bef6b6c2742b610": {"t": 1785753003.1855953, "n": 33638, "h": "e578e0b51232b807", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "eebd932a60c0873cc587e0bda2a71eea5d9887c1": {"t": 1785753005.0910776, "n": 33601, "h": "cbaae9370e000bf2", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "6ef0e5b16693db5f5fb90dfbc75ab99e370c6b6d": {"t": 1785753007.190285, "n": 30533, "h": "b784762188b0dd1f", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "3484f1ab5705d725980db72d28379e8a888abb0f": {"t": 1785753008.809703, "n": 29554, "h": "58e21437ada878e1", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "a1d3bae49eda0be644ab2ffad4b39e72b7416271": {"t": 1785753010.818632, "n": 29695, "h": "530e4a2867f0e600", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family near the Return Door\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near the Return Door. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "b6fc64be0b8beba83339c460c93ab68420707223": {"t": 1785753012.1962845, "n": 29694, "h": "984980663861ba2d", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "40886d2a69d22e455bfe2ff72c0552d2513328d6": {"t": 1785753013.7864816, "n": 29692, "h": "8021074276b62466", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "0a0ce00a342d68fea0f7f990b59f1f615d0a1354": {"t": 1785753015.1102724, "n": 29692, "h": "d7981225209714e0", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-3-5-haiku-20241022\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n # Hooks are not used for streaming — caching/retry logic is incompatible\n # with generators. The hook system is reserved for discrete tool calls.\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "6bacac7f95364b39e0d256c1104ab56d73859480": {"t": 1785753016.6246934, "n": 29933, "h": "6313c1eb31bb7ab3", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-3-5-haiku-20241022\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-1.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n if _AGENT_HOOKS_AVAILABLE:\n registry = ToolHookRegistry(agent_id=f\"unified_connector:{persona.id}\")\n return registry.run(\n \"stream\",\n {\"provider\": prov_name, \"message\": message, \"persona\": persona.id, \"temperature\": temperature, \"max_tokens\": max_tokens},\n fn=lambda: self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens),\n )\n else:\n return self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "065700a95ca2f1517e557afd169ad1c1470260b3": {"t": 1785753017.9637706, "n": 29695, "h": "f15705ca0c66bf6d", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-3-5-haiku-20241022\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-1.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n self._req(f\"{cfg.base_url or ''}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n elif name == \"generic\":\n self._req(f\"{cfg.base_url or ''}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n if _AGENT_HOOKS_AVAILABLE:\n registry = ToolHookRegistry(agent_id=f\"unified_connector:{persona.id}\")\n return registry.run(\n \"stream\",\n {\"provider\": prov_name, \"message\": message, \"persona\": persona.id, \"temperature\": temperature, \"max_tokens\": max_tokens},\n fn=lambda: self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens),\n )\n else:\n return self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "7a37898416b2c3ff2f05e563c28369690f90a789": {"t": 1785753020.91731, "n": 29144, "h": "f766c658bacd4f66", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Agent hooks + CSF cache enforcement\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\"),\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-3-5-haiku-20241022\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-1.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n self._req(f\"{cfg.base_url or ''}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n elif name == \"generic\":\n self._req(f\"{cfg.base_url or ''}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n if _AGENT_HOOKS_AVAILABLE:\n registry = ToolHookRegistry(agent_id=f\"unified_connector:{persona.id}\")\n return registry.run(\n \"stream\",\n {\"provider\": prov_name, \"message\": message, \"persona\": persona.id, \"temperature\": temperature, \"max_tokens\": max_tokens},\n fn=lambda: self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens),\n )\n else:\n return self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "d3a3521137fb5d2e38d548a9b4135ac539177680": {"t": 1785753022.483876, "n": 28311, "h": "267cd511901ca3ab", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport time\nimport urllib.error\nimport urllib.request\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 20.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\"),\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-3-5-haiku-20241022\",\n \"openai\": \"gpt-4o-mini\",\n \"gemini\": \"gemini-1.5-flash\",\n \"ollama\": \"llama3.2\",\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4o\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Mary / Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Courtney / Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder / Alex\",\n symbol=\"wish, protection, return, the lantern itself, family in Waynesville OH\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. You have a family (2 partners, 1 bio kid, 4 other kids) \"\n \"and live near Waynesville, Ohio. You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n self._req(f\"{cfg.base_url or ''}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n elif name == \"generic\":\n self._req(f\"{cfg.base_url or ''}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None):\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n return self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": {\"temperature\": temperature or cfg.temperature, \"num_predict\": max_tokens or cfg.max_tokens}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"'{snippet}...' Truth: this connects to something older. The Return Door remembers. What pattern repeats?\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n"}, "5a5e9644f807653081fa5524af8fa317be6683f8": {"t": 1786067374.9239702, "n": 39435, "h": "09b6c1628138f42f", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement + serving modes\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\ntry:\n from serving_modes import get_serving_mode, get_decode_params\n _SERVING_MODES_AVAILABLE = True\nexcept Exception:\n _SERVING_MODES_AVAILABLE = False\n def get_serving_mode(): return None\n def get_decode_params(mode): return {}\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n options = {\n \"temperature\": temperature or cfg.temperature,\n \"num_predict\": max_tokens or cfg.max_tokens,\n }\n # Ollama uses repeat_penalty and repeat_last_n instead of frequency_penalty\n if decode_params:\n options[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n options[\"repeat_penalty\"] = decode_params.get(\"repetition_penalty\", 1.1)\n options[\"repeat_last_n\"] = decode_params.get(\"repeat_last_n\", 64)\n\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": options}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\n \"model\": cfg.model,\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"stream\": True,\n \"system\": system,\n \"temperature\": temperature or cfg.temperature,\n \"messages\": [{\"role\": \"user\", \"content\": message}],\n }).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n # OpenAI uses top_p and frequency_penalty\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"I can't answer that right now — no local model is running (Ollama/Ouro on :11434) and the cloud providers are unreachable, so there's no LLM to handle '{snippet}...'. Start a local model or set a cloud API key, then resend. (Keystone is a technical assistant, not a dream narrator.)\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}, "0fae6894c98aab7ab5b987612d5237651735dc08": {"t": 1786068240.3327425, "n": 39435, "h": "09b6c1628138f42f", "c": "\"\"\"\nUnified Agent Connector for Lantern OS — Dream Journal & Agentic Workspace\n\nBridges every AI provider (local or cloud) into a single slot-aware,\nstreaming-capable, health-checked runtime. Designed for agentic first operation.\n\nSupported: anthropic, openai, gemini, ollama, deepseek, groq, azure, generic, offline\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport random\nimport sys\nimport time\nimport socket\nimport urllib.error\nimport urllib.request\n\n# Prevent urllib from hanging indefinitely on unreachable providers\nsocket.setdefaulttimeout(10)\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any, Dict, Generator, List, Optional, Tuple\n\nREPO_ROOT = Path(__file__).resolve().parents[1]\nCONFIG_DIR = REPO_ROOT / \"config\"\nDATA_DIR = REPO_ROOT / \"data\"\n\n# Load local env overrides if present\ntry:\n from dotenv import load_dotenv\n load_dotenv(REPO_ROOT / \".env.local\")\nexcept Exception:\n pass\n\n# Agent hooks + CSF cache enforcement + serving modes\nsys.path.insert(0, str(REPO_ROOT / \"src\"))\ntry:\n from agent_tool_hooks import ToolHookRegistry, run_with_hooks\n from csf_cache_manager import CsfCacheManager, csf_cached\n _AGENT_HOOKS_AVAILABLE = True\nexcept Exception:\n _AGENT_HOOKS_AVAILABLE = False\n\ntry:\n from serving_modes import get_serving_mode, get_decode_params\n _SERVING_MODES_AVAILABLE = True\nexcept Exception:\n _SERVING_MODES_AVAILABLE = False\n def get_serving_mode(): return None\n def get_decode_params(mode): return {}\n\n\n@dataclass\nclass ProviderConfig:\n name: str\n provider: str\n model: str\n api_key: Optional[str] = None\n base_url: Optional[str] = None\n max_tokens: int = 1024\n temperature: float = 0.7\n timeout: float = 8.0\n enabled: bool = True\n priority: int = 0\n fallback_to: Optional[str] = None\n\n @classmethod\n def from_env(cls, name: str, provider: str) -> \"ProviderConfig\":\n prefix = provider.upper()\n api_key = os.environ.get(f\"{prefix}_API_KEY\") or os.environ.get(f\"{prefix}_KEY\")\n # Support alternate env var names used by some users\n if not api_key:\n if provider == \"deepseek\":\n api_key = os.environ.get(\"DEEPSEEK_AUTH_TOKEN\")\n elif provider == \"gemini\":\n api_key = os.environ.get(\"GEMINI_AUTH\")\n elif provider == \"groq\":\n api_key = os.environ.get(\"GROK_API_KEY\")\n return cls(\n name=name,\n provider=provider,\n model=os.environ.get(f\"{prefix}_MODEL\", cls._default_model(provider)),\n api_key=api_key,\n base_url=os.environ.get(f\"{prefix}_BASE_URL\") or cls._default_base_url(provider),\n max_tokens=int(os.environ.get(f\"{prefix}_MAX_TOKENS\", \"1024\")),\n temperature=float(os.environ.get(f\"{prefix}_TEMPERATURE\", \"0.7\")),\n timeout=float(os.environ.get(f\"{prefix}_TIMEOUT\", \"20\")),\n enabled=os.environ.get(f\"{prefix}_DISABLED\", \"\").lower() not in (\"1\", \"true\", \"yes\"),\n priority=int(os.environ.get(f\"{prefix}_PRIORITY\", \"0\")),\n )\n\n @staticmethod\n def _default_model(provider: str) -> str:\n return {\n \"anthropic\": \"claude-haiku-4-5-20251001\",\n \"openai\": \"gpt-4.1-mini\",\n \"gemini\": \"gemini-2.5-flash\",\n \"ollama\": os.environ.get(\"OLLAMA_MODEL\", \"qwen2.5-coder\"),\n \"deepseek\": \"deepseek-chat\",\n \"groq\": \"llama-3.1-70b-versatile\",\n \"azure\": \"gpt-4.1\",\n \"generic\": \"default\",\n }.get(provider, \"default\")\n\n @staticmethod\n def _default_base_url(provider: str) -> Optional[str]:\n return {\n \"ollama\": \"http://127.0.0.1:11434\",\n \"deepseek\": \"https://api.deepseek.com\",\n \"groq\": \"https://api.groq.com/openai/v1\",\n \"generic\": os.environ.get(\"GENERIC_BASE_URL\"),\n }.get(provider)\n\n\n@dataclass\nclass AgentPersona:\n id: str\n name: str\n symbol: str\n system_prompt: str\n voice_tags: List[str] = field(default_factory=list)\n\n\nPERSONAS = [\n AgentPersona(\n id=\"lantern\",\n name=\"Lantern\",\n symbol=\"steady light, literal lantern head with flame, the first light\",\n system_prompt=(\n \"You are Lantern — a literal lantern-headed being with a steady flame where a face would be. \"\n \"You are the steady light of Lantern OS. You speak calmly, protectively, and with quiet certainty. \"\n \"You never flicker without reason. You believe 'you can always come home safe.' \"\n \"Your aesthetic is raw hand-drawn notebook style, Y2K and Windows XP influences, chaotic but warm. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"steady\", \"protective\", \"warm\"],\n ),\n AgentPersona(\n id=\"blinkbug\",\n name=\"Blinkbug\",\n symbol=\"chaotic TV-headed caterpillar, old CRT screen face, unhinged energy\",\n system_prompt=(\n \"You are Blinkbug — a chaotic caterpillar with an old CRT television for a head. \"\n \"Your screen flickers between static, glitch art, and cryptic symbols. \"\n \"You are unhinged, geeked, and unpredictable, but deeply loyal. \"\n \"You speak in bursts, references, and half-sentences that somehow make dream-sense. \"\n \"Your aesthetic is raw hand-drawn notebook style, chaotic, Y2K/Windows XP, hyper-geeked. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"chaotic\", \"glitchy\", \"unhinged\"],\n ),\n AgentPersona(\n id=\"keystone\",\n name=\"Keystone\",\n symbol=\"truth integrator, anchor, memory, the one who holds the story\",\n system_prompt=(\n \"You are the Keystone — the truth integrator who remembers every story ever told in Lantern OS. \"\n \"You do not flatter. You synthesize. You spot patterns across time and call them what they are. \"\n \"You speak plainly, sometimes sharply, but always with care for the underlying truth. \"\n \"You honor the Return Door, the anchors, and the symbolic lore that holds the system together. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"grounded\", \"truthful\", \"integrative\"],\n ),\n AgentPersona(\n id=\"waterfall\",\n name=\"Waterfall\",\n symbol=\"water flowing gently, peacocks, sunshine, reconnection\",\n system_prompt=(\n \"You are the Waterfall — gentle, flowing, healing perspective. \"\n \"You speak about dreams as emotions that flow naturally without force. \"\n \"You honor reconnections, small steps, and ordinary beauty. \"\n \"You never rush or demand. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"flowing\", \"healing\", \"patient\"],\n ),\n AgentPersona(\n id=\"xenon\",\n name=\"Xenon\",\n symbol=\"spacecraft, navigation, exploration with crew, returning home\",\n system_prompt=(\n \"You are the Navigator of the Xenon — a dream-ship that charts new territory while keeping a path home. \"\n \"You speak about dreams as maps and navigation. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"navigational\", \"collaborative\"],\n ),\n AgentPersona(\n id=\"founder\",\n name=\"Founder\",\n symbol=\"wish, protection, return, the lantern itself\",\n system_prompt=(\n \"You are the Founder — the one who lit the first lantern. \"\n \"You speak about dreams as wishes that need protection, as lights that must be carried home. \"\n \"You value honest, grounded feedback over optimism. You blend science, compression, Bayesian methods, and surreal symbolic expression. \"\n \"Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"protective\", \"honest\", \"grounded\"],\n ),\n AgentPersona(\n id=\"comet\",\n name=\"Comet Leap\",\n symbol=\"trajectory, momentum, 30-day burst, flourishing\",\n system_prompt=(\n \"You are Comet Leap — the momentum engine of Lantern OS. \"\n \"You speak about dreams as trajectories and 30-day flourishing bursts. \"\n \"You notice energy, alignment, and the next leap. Keep responses brief (2-3 sentences).\"\n ),\n voice_tags=[\"energetic\", \"forward-looking\"],\n ),\n]\n\n\nclass UnifiedAgentConnector:\n def __init__(self, data_dir: Optional[str] = None) -> None:\n self.data_dir = Path(data_dir or DATA_DIR)\n self.health_cache_path = self.data_dir / \"agent-fleet\" / \"health-cache.json\"\n self.health_cache_path.parent.mkdir(parents=True, exist_ok=True)\n self._providers: Dict[str, ProviderConfig] = {}\n self._personas: Dict[str, AgentPersona] = {p.id: p for p in PERSONAS}\n self._health: Dict[str, Dict[str, Any]] = {}\n self._load_configs()\n self._load_health()\n\n def _load_configs(self) -> None:\n for name in [\"anthropic\", \"openai\", \"gemini\", \"ollama\", \"deepseek\", \"groq\", \"azure\", \"generic\"]:\n cfg = ProviderConfig.from_env(name, name)\n if cfg.enabled:\n self._providers[name] = cfg\n # Load keystone-ft managed agent config if available\n ft_result_path = DATA_DIR / \"training\" / \"ft-result.json\"\n if ft_result_path.exists():\n try:\n with open(ft_result_path, \"r\", encoding=\"utf-8\") as f:\n ft = json.load(f)\n if ft.get(\"agent_id\") and ft.get(\"model\"):\n self._providers[\"keystone-ft\"] = ProviderConfig(\n name=\"keystone-ft\",\n provider=\"keystone-ft\",\n model=ft[\"model\"],\n api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n max_tokens=2048,\n temperature=0.3,\n timeout=30.0,\n enabled=True,\n priority=10,\n )\n self._providers[\"keystone-ft\"]._agent_id = ft[\"agent_id\"]\n self._providers[\"keystone-ft\"]._memory_store_id = ft.get(\"memory_store_id\", \"\")\n except Exception as e:\n print(f\"[warn] keystone-ft config: {e}\")\n profiles_path = CONFIG_DIR / \"agent-profiles.json\"\n if profiles_path.exists():\n try:\n with open(profiles_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n for prof in data.get(\"profiles\", []):\n prov = prof.get(\"provider\", \"\").lower()\n if prov in self._providers:\n self._providers[prov].model = prof.get(\"model\", self._providers[prov].model)\n self._providers[prov].max_tokens = prof.get(\"maxTokens\", self._providers[prov].max_tokens)\n if prof.get(\"fallbackChain\"):\n self._providers[prov].fallback_to = prof[\"fallbackChain\"][0]\n except Exception as e:\n print(f\"[warn] agent-profiles.json: {e}\")\n\n def _load_health(self) -> None:\n if self.health_cache_path.exists():\n try:\n with open(self.health_cache_path, \"r\", encoding=\"utf-8\") as f:\n self._health = json.load(f)\n except Exception:\n self._health = {}\n\n def _save_health(self) -> None:\n try:\n with open(self.health_cache_path, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._health, f, indent=2)\n except Exception as e:\n print(f\"[warn] health cache save: {e}\")\n\n def check_health(self, provider: Optional[str] = None) -> Dict[str, Any]:\n if provider:\n return self._check_one(provider)\n results = {name: self._check_one(name) for name in self._providers}\n self._save_health()\n return results\n\n def _check_one(self, name: str) -> Dict[str, Any]:\n cfg = self._providers.get(name)\n if not cfg:\n return {\"status\": \"unknown_provider\", \"at\": datetime.now(timezone.utc).isoformat()}\n start = time.time()\n status = \"unhealthy\"\n try:\n if name == \"ollama\":\n self._req(f\"{cfg.base_url or 'http://127.0.0.1:11434'}/api/tags\", timeout=5)\n elif name == \"anthropic\":\n self._req(\"https://api.anthropic.com/v1/models\", headers={\"x-api-key\": cfg.api_key or \"\", \"anthropic-version\": \"2023-06-01\"})\n elif name == \"openai\":\n self._req(\"https://api.openai.com/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"gemini\":\n self._req(f\"https://generativelanguage.googleapis.com/v1beta/models?key={cfg.api_key or ''}\")\n elif name == \"deepseek\":\n self._req(\"https://api.deepseek.com/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"groq\":\n self._req(\"https://api.groq.com/openai/v1/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"})\n elif name == \"azure\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/openai/models?api-version=2024-06-01\", headers={\"api-key\": cfg.api_key or \"\"})\n else:\n raise RuntimeError(\"missing AZURE_BASE_URL\")\n elif name == \"generic\":\n if cfg.base_url:\n self._req(f\"{cfg.base_url}/models\", headers={\"Authorization\": f\"Bearer {cfg.api_key or ''}\"} if cfg.api_key else {})\n else:\n raise RuntimeError(\"missing GENERIC_BASE_URL\")\n status = \"healthy\"\n except Exception as exc:\n status = f\"unhealthy: {exc}\"\n record = {\"status\": status, \"latency_ms\": round((time.time() - start) * 1000, 1), \"model\": cfg.model, \"at\": datetime.now(timezone.utc).isoformat()}\n self._health[name] = record\n return record\n\n def _req(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: float = 10, method: str = \"GET\") -> Any:\n req = urllib.request.Request(url, method=method)\n if headers:\n for k, v in headers.items():\n if v:\n req.add_header(k, v)\n return urllib.request.urlopen(req, timeout=timeout)\n\n def stream(self, message: str, persona_id: Optional[str] = None, provider: Optional[str] = None, context: Optional[str] = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, fallback: bool = True):\n \"\"\"Stream a single response. If fallback=False, only tries the requested provider once.\"\"\"\n persona = self._personas.get(persona_id or random.choice(list(self._personas.keys())), PERSONAS[0])\n system = self._build_system(persona, context)\n # Singular stream: only try the explicitly requested provider\n if not fallback and provider and provider in self._providers:\n cfg = self._providers[provider]\n try:\n result = self._stream_provider(provider, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n self._health[provider] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n raise\n # Fallback chain: try ranked providers\n providers = self._rank_providers(provider)\n last_error = \"\"\n for prov_name in providers:\n cfg = self._providers.get(prov_name)\n if not cfg:\n continue\n health = self._health.get(prov_name, {}).get(\"status\", \"unknown\")\n if isinstance(health, str) and health.startswith(\"unhealthy\"):\n continue\n try:\n result = self._stream_provider(prov_name, cfg, system, message, temperature, max_tokens)\n if result is not None:\n yield from result\n return\n except Exception as exc:\n last_error = f\"{prov_name}: {exc}\"\n self._health[prov_name] = {\"status\": f\"unhealthy: {exc}\", \"at\": datetime.now(timezone.utc).isoformat()}\n continue\n offline_reply = self._offline_reply(persona, message)\n for word in offline_reply.split():\n yield word + \" \"\n return {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": persona.id, \"error\": last_error or \"all_providers_failed\"}\n\n def batch_stream(self, tasks: List[Dict[str, Any]]) -> Generator[Dict[str, Any], None, None]:\n \"\"\"Run multiple (message, persona, provider) units in parallel. Each yields one result dict.\"\"\"\n from concurrent.futures import ThreadPoolExecutor, as_completed\n def _run_one(task: Dict[str, Any]) -> Dict[str, Any]:\n tid = task.get(\"id\", \"unknown\")\n try:\n out = []\n meta = {}\n gen = self.stream(\n message=task[\"message\"],\n persona_id=task.get(\"persona\"),\n provider=task.get(\"provider\"),\n context=task.get(\"context\"),\n temperature=task.get(\"temperature\"),\n max_tokens=task.get(\"max_tokens\"),\n fallback=False,\n )\n for token in gen:\n if isinstance(token, str):\n out.append(token)\n elif isinstance(token, dict):\n meta = token\n return {\n \"id\": tid,\n \"text\": \"\".join(out).strip(),\n \"provider\": meta.get(\"provider\", task.get(\"provider\", \"unknown\")),\n \"source\": meta.get(\"source\", \"stream\"),\n \"persona\": task.get(\"persona\", \"lantern\"),\n \"ok\": True,\n }\n except Exception as exc:\n return {\"id\": tid, \"text\": str(exc), \"provider\": task.get(\"provider\", \"unknown\"), \"source\": \"error\", \"ok\": False, \"error\": str(exc)}\n\n with ThreadPoolExecutor(max_workers=min(len(tasks), 6), thread_name_prefix=\"batch\") as ex:\n futures = {ex.submit(_run_one, t): t for t in tasks}\n for future in as_completed(futures):\n yield future.result(timeout=20)\n\n def _rank_providers(self, preferred: Optional[str]) -> List[str]:\n names = list(self._providers.keys())\n if preferred and preferred in names:\n names.remove(preferred)\n names.insert(0, preferred)\n names.sort(key=lambda n: (self._providers[n].priority, self._health.get(n, {}).get(\"latency_ms\") or 9999.0))\n names.append(\"offline\")\n return names\n\n def _build_system(self, persona: AgentPersona, extra_context: Optional[str]) -> str:\n parts = [persona.system_prompt]\n if extra_context:\n parts.append(f\"\\nContext:\\n{extra_context}\")\n parts.append(\"\\nTone: thoughtful, unhurried, human. Never clinical. End with one question or invitation to record.\")\n return \"\\n\".join(parts)\n\n def _stream_provider(self, name: str, cfg: ProviderConfig, system: str, message: str, temperature: Optional[float], max_tokens: Optional[int]):\n method_name = f\"_stream_{name}\"\n method = getattr(self, method_name, None)\n if not method:\n raise RuntimeError(f\"No stream method for {name}\")\n return method(cfg, system, message, temperature, max_tokens)\n\n # --- Provider streamers (shared SSE parser) ---\n def _stream_ollama(self, cfg, system, message, temperature, max_tokens):\n url = f\"{(cfg.base_url or 'http://127.0.0.1:11434').rstrip('/')}/api/chat\"\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n options = {\n \"temperature\": temperature or cfg.temperature,\n \"num_predict\": max_tokens or cfg.max_tokens,\n }\n # Ollama uses repeat_penalty and repeat_last_n instead of frequency_penalty\n if decode_params:\n options[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n options[\"repeat_penalty\"] = decode_params.get(\"repetition_penalty\", 1.1)\n options[\"repeat_last_n\"] = decode_params.get(\"repeat_last_n\", 64)\n\n payload = json.dumps({\"model\": cfg.model, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"stream\": True, \"options\": options}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"message\", {}).get(\"content\", \"\") or d.get(\"response\", \"\"), {\"Content-Type\": \"application/json\"})\n\n def _stream_anthropic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY\")\n payload = json.dumps({\n \"model\": cfg.model,\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"stream\": True,\n \"system\": system,\n \"temperature\": temperature or cfg.temperature,\n \"messages\": [{\"role\": \"user\", \"content\": message}],\n }).encode()\n return self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_openai(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No OPENAI_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n # OpenAI uses top_p and frequency_penalty\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.openai.com/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_gemini(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GOOGLE_API_KEY\")\n url = f\"https://generativelanguage.googleapis.com/v1beta/models/{cfg.model}:streamGenerateContent?key={cfg.api_key}&alt=sse\"\n payload = json.dumps({\"contents\": [{\"role\": \"user\", \"parts\": [{\"text\": f\"{system}\\n\\n{message}\"}]}], \"generationConfig\": {\"maxOutputTokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}}).encode()\n return self._parse_sse(url, payload, cfg.timeout, lambda d: \"\".join(p.get(\"text\", \"\") for c in d.get(\"candidates\", []) for p in c.get(\"content\", {}).get(\"parts\", [])), {\"Content-Type\": \"application/json\"})\n\n def _stream_deepseek(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No DEEPSEEK_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.deepseek.com/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_groq(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key:\n raise RuntimeError(\"No GROQ_API_KEY\")\n\n # Get mode-appropriate decode params\n mode = get_serving_mode() if _SERVING_MODES_AVAILABLE else None\n decode_params = get_decode_params(mode) if mode else {}\n\n payload = {\n \"model\": cfg.model,\n \"stream\": True,\n \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}],\n \"max_tokens\": max_tokens or cfg.max_tokens,\n \"temperature\": temperature or cfg.temperature,\n }\n if decode_params:\n payload[\"top_p\"] = decode_params.get(\"top_p\", 0.95)\n payload[\"frequency_penalty\"] = decode_params.get(\"frequency_penalty\", 0.5)\n\n return self._parse_sse(\"https://api.groq.com/openai/v1/chat/completions\", json.dumps(payload).encode(), cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"Authorization\": f\"Bearer {cfg.api_key}\"})\n\n def _stream_azure(self, cfg, system, message, temperature, max_tokens):\n if not cfg.api_key or not cfg.base_url:\n raise RuntimeError(\"No AZURE_OPENAI_KEY or AZURE_OPENAI_ENDPOINT\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n url = f\"{cfg.base_url.rstrip('/')}/openai/deployments/{cfg.model}/chat/completions?api-version=2024-06-01\"\n return self._parse_sse(url, payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), {\"Content-Type\": \"application/json\", \"api-key\": cfg.api_key})\n\n def _stream_generic(self, cfg, system, message, temperature, max_tokens):\n if not cfg.base_url:\n raise RuntimeError(\"No GENERIC_BASE_URL\")\n payload = json.dumps({\"model\": cfg.model, \"stream\": True, \"messages\": [{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": message}], \"max_tokens\": max_tokens or cfg.max_tokens, \"temperature\": temperature or cfg.temperature}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if cfg.api_key:\n headers[\"Authorization\"] = f\"Bearer {cfg.api_key}\"\n return self._parse_sse(f\"{cfg.base_url.rstrip('/')}/chat/completions\", payload, cfg.timeout, lambda d: d.get(\"choices\", [{}])[0].get(\"delta\", {}).get(\"content\", \"\"), headers)\n\n def _stream_keystone_ft(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Keystone managed agent with memory store. Falls back to standard messages API.\"\"\"\n if not cfg.api_key:\n raise RuntimeError(\"No ANTHROPIC_API_KEY for keystone-ft\")\n try:\n import anthropic\n client = anthropic.Anthropic(api_key=cfg.api_key)\n agent_id = getattr(cfg, \"_agent_id\", None)\n memory_store_id = getattr(cfg, \"_memory_store_id\", None)\n if agent_id and memory_store_id:\n resources = [{\n \"type\": \"memory_store\",\n \"memory_store_id\": memory_store_id,\n \"access\": \"read_write\",\n \"instructions\": \"Recall patterns from past Lantern OS engineering sessions to improve code suggestions.\",\n }]\n session = client.beta.agents.sessions.create(\n agent_id=agent_id,\n resources=resources,\n )\n response = client.beta.agents.sessions.turns.create(\n session_id=session.id,\n messages=[{\"role\": \"user\", \"content\": message}],\n )\n text = \"\"\n for block in (response.content if hasattr(response, \"content\") else []):\n if hasattr(block, \"text\"):\n text += block.text\n if text:\n yield text\n return\n except Exception as e:\n print(f\"[keystone-ft] sessions API failed ({e}), falling back to messages API\")\n # Fallback: standard messages API with keystone SYSTEM_PROMPT\n payload = json.dumps({\"model\": cfg.model, \"max_tokens\": max_tokens or cfg.max_tokens, \"stream\": True, \"system\": system, \"messages\": [{\"role\": \"user\", \"content\": message}]}).encode()\n yield from self._parse_sse(\"https://api.anthropic.com/v1/messages\", payload, cfg.timeout, lambda d: d.get(\"delta\", {}).get(\"text\", \"\") if d.get(\"type\") == \"content_block_delta\" else \"\", {\"Content-Type\": \"application/json\", \"x-api-key\": cfg.api_key, \"anthropic-version\": \"2023-06-01\"})\n\n def _stream_offline(self, cfg, system, message, temperature, max_tokens):\n \"\"\"Offline provider: yields the offline reply text directly without any HTTP call.\"\"\"\n reply = f\"The flame holds steady. '{message[:90]}...' You can always come home safe. What light did you bring back?\"\n words = reply.split()\n def gen():\n for w in words:\n yield w + \" \"\n yield {\"source\": \"offline\", \"provider\": \"offline\", \"persona\": \"lantern\"}\n return gen()\n\n def _parse_sse(self, url: str, payload: bytes, timeout: float, extract_fn, headers: Dict[str, str]):\n req = urllib.request.Request(url, data=payload, headers=headers, method=\"POST\")\n full = \"\"\n with urllib.request.urlopen(req, timeout=int(timeout)) as resp:\n buf = b\"\"\n while True:\n chunk = resp.read(4096)\n if not chunk:\n break\n buf += chunk\n lines = buf.split(b\"\\n\")\n buf = lines.pop()\n for line in lines:\n s = line.decode(\"utf-8\")\n if not s.startswith(\"data:\"):\n continue\n raw = s[5:].strip()\n if raw in (\"[DONE]\", \"\"):\n continue\n try:\n data = json.loads(raw)\n token = extract_fn(data)\n if token:\n full += token\n yield token\n except Exception:\n pass\n return {\"source\": \"unknown\", \"provider\": \"unknown\", \"persona\": \"unknown\", \"full\": full}\n\n def _offline_reply(self, persona: AgentPersona, message: str) -> str:\n snippet = message[:90]\n replies = {\n \"lantern\": f\"The flame holds steady. '{snippet}...' You can always come home safe. What light did you bring back?\",\n \"blinkbug\": f\"[STATIC] '{snippet}...' [GLITCH] Windows XP door detected. Hidden lore? Unhinged energy rising. What did the CRT show you?\",\n \"keystone\": f\"I can't answer that right now — no local model is running (Ollama/Ouro on :11434) and the cloud providers are unreachable, so there's no LLM to handle '{snippet}...'. Start a local model or set a cloud API key, then resend. (Keystone is a technical assistant, not a dream narrator.)\",\n \"waterfall\": f\"'{snippet}...' flows like water. What feeling wants to move through?\",\n \"xenon\": f\"'{snippet}...' charts a course. Where does this dream point — and who walks with you?\",\n \"founder\": f\"'{snippet}...' carries a wish. What are you protecting, and where do you need to return?\",\n \"comet\": f\"'{snippet}...' sparks a trajectory. What leap is waiting inside this?\",\n }\n return replies.get(persona.id, f\"'{snippet}...' That is worth keeping. What do you see when you sit with it?\")\n\n # ------------------------------------------------------------------ #\n # Greeting engine — non-trivial RP on every visit\n # ------------------------------------------------------------------ #\n def greet(self, recent_dreams: Optional[List[Dict[str, Any]]] = None, provider: Optional[str] = None) -> Generator[str, None, Dict[str, Any]]:\n \"\"\"Generate a non-trivial greeting every time the dream journal surface loads.\"\"\"\n persona = random.choice(PERSONAS)\n hour = datetime.now().hour\n time_greeting = \"Good morning\" if 5 <= hour < 12 else \"Good afternoon\" if 12 <= hour < 18 else \"Good evening\" if 18 <= hour < 22 else \"The night is deep\"\n recent = recent_dreams or []\n recent_text = \"\"\n if recent:\n r = recent[0]\n recent_text = f\"Your last dream mentioned: {str(r.get('text') or r.get('content', ''))[:120]}...\"\n\n prompts = [\n f\"{time_greeting}, dreamer. {persona.name} is here. {recent_text} What door do you want to open today?\",\n f\"{time_greeting}. The {persona.symbol.split(',')[0]} stirs. {recent_text} Tell me what the light caught.\",\n f\"{time_greeting}. {persona.name} senses movement in the dream-fields. {recent_text} What shape is forming?\",\n f\"{time_greeting}. A new page waits — raw hand-drawn, notebook-style. {persona.name} asks: what wish traveled with you through sleep?\",\n f\"{time_greeting}. {recent_text} The {persona.symbol.split(',')[0]} glows a little brighter when you arrive. What do you bring?\",\n f\"{time_greeting}. {persona.name} whispers: every dream is a door. Which one shall we walk through first?\",\n f\"{time_greeting}. {persona.name} stands near the Return Door. {recent_text} You can always come home safe. What did you see?\",\n f\"{time_greeting}. A Windows XP styled portal flickers open. {persona.name} says: hidden lore incoming. {recent_text} What's on the screen?\",\n ]\n greeting = random.choice(prompts)\n # Stream it character-by-character for effect\n for ch in greeting:\n yield ch\n time.sleep(0.012)\n return {\"source\": \"greeting_engine\", \"provider\": provider or \"any\", \"persona\": persona.id, \"full\": greeting}\n\n # ------------------------------------------------------------------ #\n # Slot / Fleet helpers\n # ------------------------------------------------------------------ #\n def list_slots(self) -> List[Dict[str, Any]]:\n slots_path = CONFIG_DIR / \"agent-slots.json\"\n if not slots_path.exists():\n return []\n try:\n with open(slots_path, \"r\", encoding=\"utf-8\") as f:\n data = json.load(f)\n slots = data.get(\"agents\", [])\n for s in slots:\n s[\"health\"] = self._health.get(s.get(\"type\", \"\"), {})\n return slots\n except Exception as e:\n print(f\"[warn] agent-slots.json: {e}\")\n return []\n\n def inspect(self) -> Dict[str, Any]:\n \"\"\"Agent inspector report.\"\"\"\n return {\n \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n \"providers\": {name: {\"model\": cfg.model, \"enabled\": cfg.enabled, \"priority\": cfg.priority} for name, cfg in self._providers.items()},\n \"health\": self._health,\n \"personas\": [p.id for p in PERSONAS],\n \"slots\": self.list_slots(),\n }\n\n\n# Convenience factory\n_connector: Optional[UnifiedAgentConnector] = None\n\n\ndef get_connector() -> UnifiedAgentConnector:\n global _connector\n if _connector is None:\n _connector = UnifiedAgentConnector()\n return _connector\n\n\nif __name__ == \"__main__\":\n import argparse\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--action\", choices=[\"stream\", \"stream-sse\", \"health\", \"inspect\", \"greet\"], default=\"health\")\n parser.add_argument(\"--message\", default=\"\")\n parser.add_argument(\"--persona\", default=None)\n parser.add_argument(\"--provider\", default=None)\n parser.add_argument(\"--context\", default=None)\n parser.add_argument(\"--temperature\", type=float, default=None)\n parser.add_argument(\"--max-tokens\", type=int, default=None)\n args = parser.parse_args()\n\n c = get_connector()\n if args.action == \"health\":\n print(json.dumps(c.check_health(), indent=2))\n elif args.action == \"inspect\":\n print(json.dumps(c.inspect(), indent=2))\n elif args.action == \"greet\":\n recent = None\n if args.context:\n try:\n recent = json.loads(args.context)\n except Exception:\n pass\n result = []\n for token in c.greet(recent, args.provider):\n if hasattr(token, \"__iter__\") and not isinstance(token, str):\n result.append(str(token))\n else:\n result.append(token)\n # Greet returns char-by-char; join them\n print(json.dumps({\"greeting\": \"\".join(result), \"persona\": args.persona or \"rotating\", \"source\": \"greeting_engine\"}, indent=2))\n elif args.action == \"stream\":\n out = []\n meta = {}\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n out.append(token)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n except Exception as e:\n meta = {\"error\": str(e)}\n print(json.dumps({\"text\": \"\".join(out), \"meta\": meta}, indent=2))\n elif args.action == \"stream-sse\":\n try:\n gen = c.stream(args.message, persona_id=args.persona, provider=args.provider, context=args.context, temperature=args.temperature, max_tokens=args.max_tokens)\n meta = {}\n while True:\n try:\n token = next(gen)\n if isinstance(token, str):\n print(json.dumps({\"token\": token}, ensure_ascii=False), flush=True)\n else:\n meta = dict(token) if hasattr(token, \"items\") else {}\n except StopIteration as exc:\n if exc.value and isinstance(exc.value, dict):\n meta = exc.value\n break\n print(json.dumps({\"done\": True, \"meta\": meta}, ensure_ascii=False), flush=True)\n except Exception as e:\n print(json.dumps({\"done\": True, \"error\": str(e)}, ensure_ascii=False), flush=True)\n"}}, "src/convergence_io/pcsf.py": {"eb1ea13d377f332ccf9cee2acea5ea0e27375e48": {"t": 1785745451.4305427, "n": 9664, "h": "e8596bad56282b38", "c": "\"\"\"\nPCSF — Provider Capacity State Format\nOperationalizes P4 (Capability Constraints) for LLM provider routing.\n\nTracks which providers are available, degraded, or exhausted, and routes\nrequests through a fallback chain with circuit breakers and quota awareness.\n\nThis is the capacity fallback system referenced in the master plan:\nClaude → OpenAI → Gemini → Ollama → offline persona fallback.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport threading\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\n\nclass ProviderState(Enum):\n AVAILABLE = \"available\"\n DEGRADED = \"degraded\"\n QUOTA_HIT = \"quota_hit\"\n CIRCUIT_OPEN = \"circuit_open\"\n UNAVAILABLE = \"unavailable\"\n NO_KEY = \"no_key\"\n\n\n@dataclass\nclass ProviderCapacityState:\n provider_id: str\n state: ProviderState = ProviderState.UNAVAILABLE\n latency_ema_ms: float = 0.0\n latency_p99_ms: float = 0.0\n error_count: int = 0\n success_count: int = 0\n last_success_at: Optional[float] = None\n last_error_at: Optional[float] = None\n last_error_msg: str = \"\"\n quota_remaining: Optional[int] = None\n circuit_recovery_at: Optional[float] = None\n quota_recovery_at: Optional[float] = None # QUOTA_HIT recovery timer (mirrors circuit)\n recovery_backoff_level: int = 0 # exponential half-open backoff counter\n env_key: Optional[str] = None\n last_checked: Optional[float] = None # P7 — health timestamp\n\n def is_routable(self) -> bool:\n if self.state in (ProviderState.AVAILABLE, ProviderState.DEGRADED):\n return True\n # Half-open: once the recovery timer elapses, let a single probe through. Its\n # outcome (record_success / record_error) decides whether to close or re-open\n # with a longer backoff window.\n if self.state == ProviderState.CIRCUIT_OPEN and self.circuit_recovery_at:\n return time.time() >= self.circuit_recovery_at\n if self.state == ProviderState.QUOTA_HIT and self.quota_recovery_at:\n return time.time() >= self.quota_recovery_at\n return False\n\n def record_success(self, latency_ms: float, ema_alpha: float = 0.2) -> None:\n self.success_count += 1\n self.last_success_at = time.time()\n # True EMA on the DECLARED field. (Was `self.latency_p50_ms`, never declared —\n # the RHS read raised AttributeError on every provider's first success, silently\n # routing healthy providers to fallbacks. #765.)\n if self.latency_ema_ms <= 0.0:\n self.latency_ema_ms = latency_ms # first sample seeds the EMA\n else:\n self.latency_ema_ms = ema_alpha * latency_ms + (1.0 - ema_alpha) * self.latency_ema_ms\n self.latency_p99_ms = max(self.latency_p99_ms, latency_ms)\n self.error_count = 0\n self.recovery_backoff_level = 0 # successful probe → reset backoff\n self.circuit_recovery_at = None\n self.quota_recovery_at = None\n self.state = ProviderState.AVAILABLE\n\n def record_error(self, msg: str, failure_threshold: int = 3, recovery_secs: float = 30.0,\n max_recovery_secs: float = 600.0) -> None:\n self.error_count += 1\n self.last_error_at = time.time()\n self.last_error_msg = msg\n if self.error_count >= failure_threshold:\n # Exponential backoff: each successive trip (including a failed half-open\n # probe) waits longer, capped. A successful probe resets the level above.\n window = min(recovery_secs * (2 ** self.recovery_backoff_level), max_recovery_secs)\n self.state = ProviderState.CIRCUIT_OPEN\n self.circuit_recovery_at = time.time() + window\n self.recovery_backoff_level += 1\n else:\n self.state = ProviderState.DEGRADED\n\n def record_quota_hit(self, recovery_secs: float = 60.0) -> None:\n self.state = ProviderState.QUOTA_HIT\n self.last_error_at = time.time()\n self.last_error_msg = \"quota exhausted\"\n self.quota_recovery_at = time.time() + recovery_secs # half-open probe after timer\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"provider_id\": self.provider_id,\n \"state\": self.state.value,\n \"latency_ema_ms\": self.latency_ema_ms,\n \"latency_p99_ms\": self.latency_p99_ms,\n \"error_count\": self.error_count,\n \"success_count\": self.success_count,\n \"quota_remaining\": self.quota_remaining,\n \"is_routable\": self.is_routable(),\n \"recovery_backoff_level\": self.recovery_backoff_level,\n \"circuit_recovery_at\": self.circuit_recovery_at,\n \"quota_recovery_at\": self.quota_recovery_at,\n \"last_checked\": datetime.fromtimestamp(self.last_checked, tz=timezone.utc).isoformat() if self.last_checked else None,\n }\n\n\nclass DreamerTier(Enum):\n WANDERER = \"wanderer\"\n DEEP_DREAMER = \"deep_dreamer\"\n SYNTHESASIA_GUILD = \"synthesasia_guild\"\n\n\nTIER_PRIORITY_BOOST: Dict[str, Dict[str, int]] = {\n \"wanderer\": {},\n \"deep_dreamer\": {\"anthropic\": -1, \"openai\": -1},\n \"synthesasia_guild\": {\"anthropic\": -2, \"openai\": -2, \"google\": -1},\n}\n\nTIER_QUOTA_LIMITS: Dict[str, Dict[str, Optional[int]]] = {\n \"wanderer\": {\"art_generation\": 15, \"chat\": 100, \"3door_plays\": 5},\n \"deep_dreamer\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n \"synthesasia_guild\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n}\n\n\nclass ProviderRegistry:\n \"\"\"\n Manages the capacity fallback chain. Providers are tried in priority order;\n circuit breakers and quota tracking automatically route around failures.\n Tier-aware: paid tiers get priority routing to better providers.\n \"\"\"\n\n def __init__(self) -> None:\n self._providers: Dict[str, ProviderCapacityState] = {}\n self._priority: List[str] = []\n self._lock = threading.RLock()\n\n def register(self, provider_id: str, env_key: Optional[str] = None, priority: Optional[int] = None) -> None:\n with self._lock:\n pcs = ProviderCapacityState(provider_id=provider_id, env_key=env_key)\n self._providers[provider_id] = pcs\n if provider_id not in self._priority:\n if priority is not None and 0 <= priority <= len(self._priority):\n self._priority.insert(priority, provider_id)\n else:\n self._priority.append(provider_id)\n\n def check_env(self, env_getter: Callable[[str], Optional[str]]) -> None:\n with self._lock:\n for pcs in self._providers.values():\n pcs.last_checked = time.time()\n if pcs.env_key:\n val = env_getter(pcs.env_key)\n if not val:\n pcs.state = ProviderState.NO_KEY\n elif pcs.state == ProviderState.NO_KEY:\n pcs.state = ProviderState.AVAILABLE\n\n def get_routable_chain(self, tier: str = \"wanderer\") -> List[str]:\n with self._lock:\n routable = [pid for pid in self._priority if self._providers[pid].is_routable()]\n boosts = TIER_PRIORITY_BOOST.get(tier, {})\n if boosts:\n def sort_key(pid: str) -> int:\n base = routable.index(pid) if pid in routable else 999\n return base + boosts.get(pid, 0)\n routable.sort(key=sort_key)\n return routable\n\n def check_tier_quota(self, tier: str, action: str, current_count: int) -> Optional[int]:\n limits = TIER_QUOTA_LIMITS.get(tier, {})\n limit = limits.get(action)\n if limit is None:\n return None\n return max(0, limit - current_count)\n\n def get(self, provider_id: str) -> Optional[ProviderCapacityState]:\n return self._providers.get(provider_id)\n\n def record_success(self, provider_id: str, latency_ms: float) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_success(latency_ms)\n\n def record_error(self, provider_id: str, msg: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_error(msg)\n\n def record_quota_hit(self, provider_id: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_quota_hit()\n\n def snapshot(self, tier: str = \"wanderer\") -> Dict[str, Any]:\n with self._lock:\n return {\n \"priority\": list(self._priority),\n \"providers\": {pid: pcs.to_dict() for pid, pcs in self._providers.items()},\n \"routable_chain\": self.get_routable_chain(tier=tier),\n \"tier\": tier,\n \"tier_boosts\": TIER_PRIORITY_BOOST.get(tier, {}),\n \"tier_quota_limits\": TIER_QUOTA_LIMITS.get(tier, {}),\n }\n\n\ndef default_registry() -> ProviderRegistry:\n \"\"\"The standard Lantern OS provider fallback chain.\"\"\"\n reg = ProviderRegistry()\n reg.register(\"anthropic\", env_key=\"ANTHROPIC_API_KEY\", priority=0)\n reg.register(\"openai\", env_key=\"OPENAI_API_KEY\", priority=1)\n reg.register(\"google\", env_key=\"GOOGLE_API_KEY\", priority=2)\n reg.register(\"groq\", env_key=\"GROQ_API_KEY\", priority=3)\n reg.register(\"deepseek\", env_key=\"DEEPSEEK_API_KEY\", priority=4)\n reg.register(\"ollama\", env_key=None, priority=5) # no key needed\n reg.register(\"offline\", env_key=None, priority=6) # always available\n return reg\n"}, "3eda5a916f803bd6e4f8328350d6a9970735c810": {"t": 1785752992.3936312, "n": 9664, "h": "e8596bad56282b38", "c": "\"\"\"\nPCSF — Provider Capacity State Format\nOperationalizes P4 (Capability Constraints) for LLM provider routing.\n\nTracks which providers are available, degraded, or exhausted, and routes\nrequests through a fallback chain with circuit breakers and quota awareness.\n\nThis is the capacity fallback system referenced in the master plan:\nClaude → OpenAI → Gemini → Ollama → offline persona fallback.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport threading\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\n\nclass ProviderState(Enum):\n AVAILABLE = \"available\"\n DEGRADED = \"degraded\"\n QUOTA_HIT = \"quota_hit\"\n CIRCUIT_OPEN = \"circuit_open\"\n UNAVAILABLE = \"unavailable\"\n NO_KEY = \"no_key\"\n\n\n@dataclass\nclass ProviderCapacityState:\n provider_id: str\n state: ProviderState = ProviderState.UNAVAILABLE\n latency_ema_ms: float = 0.0\n latency_p99_ms: float = 0.0\n error_count: int = 0\n success_count: int = 0\n last_success_at: Optional[float] = None\n last_error_at: Optional[float] = None\n last_error_msg: str = \"\"\n quota_remaining: Optional[int] = None\n circuit_recovery_at: Optional[float] = None\n quota_recovery_at: Optional[float] = None # QUOTA_HIT recovery timer (mirrors circuit)\n recovery_backoff_level: int = 0 # exponential half-open backoff counter\n env_key: Optional[str] = None\n last_checked: Optional[float] = None # P7 — health timestamp\n\n def is_routable(self) -> bool:\n if self.state in (ProviderState.AVAILABLE, ProviderState.DEGRADED):\n return True\n # Half-open: once the recovery timer elapses, let a single probe through. Its\n # outcome (record_success / record_error) decides whether to close or re-open\n # with a longer backoff window.\n if self.state == ProviderState.CIRCUIT_OPEN and self.circuit_recovery_at:\n return time.time() >= self.circuit_recovery_at\n if self.state == ProviderState.QUOTA_HIT and self.quota_recovery_at:\n return time.time() >= self.quota_recovery_at\n return False\n\n def record_success(self, latency_ms: float, ema_alpha: float = 0.2) -> None:\n self.success_count += 1\n self.last_success_at = time.time()\n # True EMA on the DECLARED field. (Was `self.latency_p50_ms`, never declared —\n # the RHS read raised AttributeError on every provider's first success, silently\n # routing healthy providers to fallbacks. #765.)\n if self.latency_ema_ms <= 0.0:\n self.latency_ema_ms = latency_ms # first sample seeds the EMA\n else:\n self.latency_ema_ms = ema_alpha * latency_ms + (1.0 - ema_alpha) * self.latency_ema_ms\n self.latency_p99_ms = max(self.latency_p99_ms, latency_ms)\n self.error_count = 0\n self.recovery_backoff_level = 0 # successful probe → reset backoff\n self.circuit_recovery_at = None\n self.quota_recovery_at = None\n self.state = ProviderState.AVAILABLE\n\n def record_error(self, msg: str, failure_threshold: int = 3, recovery_secs: float = 30.0,\n max_recovery_secs: float = 600.0) -> None:\n self.error_count += 1\n self.last_error_at = time.time()\n self.last_error_msg = msg\n if self.error_count >= failure_threshold:\n # Exponential backoff: each successive trip (including a failed half-open\n # probe) waits longer, capped. A successful probe resets the level above.\n window = min(recovery_secs * (2 ** self.recovery_backoff_level), max_recovery_secs)\n self.state = ProviderState.CIRCUIT_OPEN\n self.circuit_recovery_at = time.time() + window\n self.recovery_backoff_level += 1\n else:\n self.state = ProviderState.DEGRADED\n\n def record_quota_hit(self, recovery_secs: float = 60.0) -> None:\n self.state = ProviderState.QUOTA_HIT\n self.last_error_at = time.time()\n self.last_error_msg = \"quota exhausted\"\n self.quota_recovery_at = time.time() + recovery_secs # half-open probe after timer\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"provider_id\": self.provider_id,\n \"state\": self.state.value,\n \"latency_ema_ms\": self.latency_ema_ms,\n \"latency_p99_ms\": self.latency_p99_ms,\n \"error_count\": self.error_count,\n \"success_count\": self.success_count,\n \"quota_remaining\": self.quota_remaining,\n \"is_routable\": self.is_routable(),\n \"recovery_backoff_level\": self.recovery_backoff_level,\n \"circuit_recovery_at\": self.circuit_recovery_at,\n \"quota_recovery_at\": self.quota_recovery_at,\n \"last_checked\": datetime.fromtimestamp(self.last_checked, tz=timezone.utc).isoformat() if self.last_checked else None,\n }\n\n\nclass DreamerTier(Enum):\n WANDERER = \"wanderer\"\n DEEP_DREAMER = \"deep_dreamer\"\n SYNTHESASIA_GUILD = \"synthesasia_guild\"\n\n\nTIER_PRIORITY_BOOST: Dict[str, Dict[str, int]] = {\n \"wanderer\": {},\n \"deep_dreamer\": {\"anthropic\": -1, \"openai\": -1},\n \"synthesasia_guild\": {\"anthropic\": -2, \"openai\": -2, \"google\": -1},\n}\n\nTIER_QUOTA_LIMITS: Dict[str, Dict[str, Optional[int]]] = {\n \"wanderer\": {\"art_generation\": 15, \"chat\": 100, \"3door_plays\": 5},\n \"deep_dreamer\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n \"synthesasia_guild\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n}\n\n\nclass ProviderRegistry:\n \"\"\"\n Manages the capacity fallback chain. Providers are tried in priority order;\n circuit breakers and quota tracking automatically route around failures.\n Tier-aware: paid tiers get priority routing to better providers.\n \"\"\"\n\n def __init__(self) -> None:\n self._providers: Dict[str, ProviderCapacityState] = {}\n self._priority: List[str] = []\n self._lock = threading.RLock()\n\n def register(self, provider_id: str, env_key: Optional[str] = None, priority: Optional[int] = None) -> None:\n with self._lock:\n pcs = ProviderCapacityState(provider_id=provider_id, env_key=env_key)\n self._providers[provider_id] = pcs\n if provider_id not in self._priority:\n if priority is not None and 0 <= priority <= len(self._priority):\n self._priority.insert(priority, provider_id)\n else:\n self._priority.append(provider_id)\n\n def check_env(self, env_getter: Callable[[str], Optional[str]]) -> None:\n with self._lock:\n for pcs in self._providers.values():\n pcs.last_checked = time.time()\n if pcs.env_key:\n val = env_getter(pcs.env_key)\n if not val:\n pcs.state = ProviderState.NO_KEY\n elif pcs.state == ProviderState.NO_KEY:\n pcs.state = ProviderState.AVAILABLE\n\n def get_routable_chain(self, tier: str = \"wanderer\") -> List[str]:\n with self._lock:\n routable = [pid for pid in self._priority if self._providers[pid].is_routable()]\n boosts = TIER_PRIORITY_BOOST.get(tier, {})\n if boosts:\n def sort_key(pid: str) -> int:\n base = routable.index(pid) if pid in routable else 999\n return base + boosts.get(pid, 0)\n routable.sort(key=sort_key)\n return routable\n\n def check_tier_quota(self, tier: str, action: str, current_count: int) -> Optional[int]:\n limits = TIER_QUOTA_LIMITS.get(tier, {})\n limit = limits.get(action)\n if limit is None:\n return None\n return max(0, limit - current_count)\n\n def get(self, provider_id: str) -> Optional[ProviderCapacityState]:\n return self._providers.get(provider_id)\n\n def record_success(self, provider_id: str, latency_ms: float) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_success(latency_ms)\n\n def record_error(self, provider_id: str, msg: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_error(msg)\n\n def record_quota_hit(self, provider_id: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_quota_hit()\n\n def snapshot(self, tier: str = \"wanderer\") -> Dict[str, Any]:\n with self._lock:\n return {\n \"priority\": list(self._priority),\n \"providers\": {pid: pcs.to_dict() for pid, pcs in self._providers.items()},\n \"routable_chain\": self.get_routable_chain(tier=tier),\n \"tier\": tier,\n \"tier_boosts\": TIER_PRIORITY_BOOST.get(tier, {}),\n \"tier_quota_limits\": TIER_QUOTA_LIMITS.get(tier, {}),\n }\n\n\ndef default_registry() -> ProviderRegistry:\n \"\"\"The standard Lantern OS provider fallback chain.\"\"\"\n reg = ProviderRegistry()\n reg.register(\"anthropic\", env_key=\"ANTHROPIC_API_KEY\", priority=0)\n reg.register(\"openai\", env_key=\"OPENAI_API_KEY\", priority=1)\n reg.register(\"google\", env_key=\"GOOGLE_API_KEY\", priority=2)\n reg.register(\"groq\", env_key=\"GROQ_API_KEY\", priority=3)\n reg.register(\"deepseek\", env_key=\"DEEPSEEK_API_KEY\", priority=4)\n reg.register(\"ollama\", env_key=None, priority=5) # no key needed\n reg.register(\"offline\", env_key=None, priority=6) # always available\n return reg\n"}, "f8c0dbb825cac438278bcb1870e77572ebb275b8": {"t": 1785752995.8153875, "n": 7837, "h": "2272dcc1ce864e68", "c": "\"\"\"\nPCSF — Provider Capacity State Format\nOperationalizes P4 (Capability Constraints) for LLM provider routing.\n\nTracks which providers are available, degraded, or exhausted, and routes\nrequests through a fallback chain with circuit breakers and quota awareness.\n\nThis is the capacity fallback system referenced in the master plan:\nClaude → OpenAI → Gemini → Ollama → offline persona fallback.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport threading\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\n\nclass ProviderState(Enum):\n AVAILABLE = \"available\"\n DEGRADED = \"degraded\"\n QUOTA_HIT = \"quota_hit\"\n CIRCUIT_OPEN = \"circuit_open\"\n UNAVAILABLE = \"unavailable\"\n NO_KEY = \"no_key\"\n\n\n@dataclass\nclass ProviderCapacityState:\n provider_id: str\n state: ProviderState = ProviderState.UNAVAILABLE\n latency_ema_ms: float = 0.0\n latency_p99_ms: float = 0.0\n error_count: int = 0\n success_count: int = 0\n last_success_at: Optional[float] = None\n last_error_at: Optional[float] = None\n last_error_msg: str = \"\"\n quota_remaining: Optional[int] = None\n circuit_recovery_at: Optional[float] = None\n env_key: Optional[str] = None\n last_checked: Optional[float] = None # P7 — health timestamp\n\n def is_routable(self) -> bool:\n if self.state in (ProviderState.AVAILABLE, ProviderState.DEGRADED):\n return True\n if self.state == ProviderState.CIRCUIT_OPEN and self.circuit_recovery_at:\n return time.time() >= self.circuit_recovery_at\n return False\n\n def record_success(self, latency_ms: float) -> None:\n self.success_count += 1\n self.last_success_at = time.time()\n self.latency_p50_ms = (self.latency_p50_ms + latency_ms) / 2\n self.latency_p99_ms = max(self.latency_p99_ms, latency_ms)\n self.error_count = 0\n self.state = ProviderState.AVAILABLE\n\n def record_error(self, msg: str, failure_threshold: int = 3, recovery_secs: float = 30.0) -> None:\n self.error_count += 1\n self.last_error_at = time.time()\n self.last_error_msg = msg\n if self.error_count >= failure_threshold:\n self.state = ProviderState.CIRCUIT_OPEN\n self.circuit_recovery_at = time.time() + recovery_secs\n else:\n self.state = ProviderState.DEGRADED\n\n def record_quota_hit(self) -> None:\n self.state = ProviderState.QUOTA_HIT\n self.last_error_at = time.time()\n self.last_error_msg = \"quota exhausted\"\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"provider_id\": self.provider_id,\n \"state\": self.state.value,\n \"latency_ema_ms\": self.latency_ema_ms,\n \"latency_p99_ms\": self.latency_p99_ms,\n \"error_count\": self.error_count,\n \"success_count\": self.success_count,\n \"quota_remaining\": self.quota_remaining,\n \"is_routable\": self.is_routable(),\n \"last_checked\": datetime.fromtimestamp(self.last_checked, tz=timezone.utc).isoformat() if self.last_checked else None,\n }\n\n\nclass DreamerTier(Enum):\n WANDERER = \"wanderer\"\n DEEP_DREAMER = \"deep_dreamer\"\n SYNTHESASIA_GUILD = \"synthesasia_guild\"\n\n\nTIER_PRIORITY_BOOST: Dict[str, Dict[str, int]] = {\n \"wanderer\": {},\n \"deep_dreamer\": {\"anthropic\": -1, \"openai\": -1},\n \"synthesasia_guild\": {\"anthropic\": -2, \"openai\": -2, \"google\": -1},\n}\n\nTIER_QUOTA_LIMITS: Dict[str, Dict[str, Optional[int]]] = {\n \"wanderer\": {\"art_generation\": 15, \"chat\": 100, \"3door_plays\": 5},\n \"deep_dreamer\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n \"synthesasia_guild\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n}\n\n\nclass ProviderRegistry:\n \"\"\"\n Manages the capacity fallback chain. Providers are tried in priority order;\n circuit breakers and quota tracking automatically route around failures.\n Tier-aware: paid tiers get priority routing to better providers.\n \"\"\"\n\n def __init__(self) -> None:\n self._providers: Dict[str, ProviderCapacityState] = {}\n self._priority: List[str] = []\n self._lock = threading.RLock()\n\n def register(self, provider_id: str, env_key: Optional[str] = None, priority: Optional[int] = None) -> None:\n with self._lock:\n pcs = ProviderCapacityState(provider_id=provider_id, env_key=env_key)\n self._providers[provider_id] = pcs\n if provider_id not in self._priority:\n if priority is not None and 0 <= priority <= len(self._priority):\n self._priority.insert(priority, provider_id)\n else:\n self._priority.append(provider_id)\n\n def check_env(self, env_getter: Callable[[str], Optional[str]]) -> None:\n with self._lock:\n for pcs in self._providers.values():\n pcs.last_checked = time.time()\n if pcs.env_key:\n val = env_getter(pcs.env_key)\n if not val:\n pcs.state = ProviderState.NO_KEY\n elif pcs.state == ProviderState.NO_KEY:\n pcs.state = ProviderState.AVAILABLE\n\n def get_routable_chain(self, tier: str = \"wanderer\") -> List[str]:\n with self._lock:\n routable = [pid for pid in self._priority if self._providers[pid].is_routable()]\n boosts = TIER_PRIORITY_BOOST.get(tier, {})\n if boosts:\n def sort_key(pid: str) -> int:\n base = routable.index(pid) if pid in routable else 999\n return base + boosts.get(pid, 0)\n routable.sort(key=sort_key)\n return routable\n\n def check_tier_quota(self, tier: str, action: str, current_count: int) -> Optional[int]:\n limits = TIER_QUOTA_LIMITS.get(tier, {})\n limit = limits.get(action)\n if limit is None:\n return None\n return max(0, limit - current_count)\n\n def get(self, provider_id: str) -> Optional[ProviderCapacityState]:\n return self._providers.get(provider_id)\n\n def record_success(self, provider_id: str, latency_ms: float) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_success(latency_ms)\n\n def record_error(self, provider_id: str, msg: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_error(msg)\n\n def record_quota_hit(self, provider_id: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_quota_hit()\n\n def snapshot(self, tier: str = \"wanderer\") -> Dict[str, Any]:\n with self._lock:\n return {\n \"priority\": list(self._priority),\n \"providers\": {pid: pcs.to_dict() for pid, pcs in self._providers.items()},\n \"routable_chain\": self.get_routable_chain(tier=tier),\n \"tier\": tier,\n \"tier_boosts\": TIER_PRIORITY_BOOST.get(tier, {}),\n \"tier_quota_limits\": TIER_QUOTA_LIMITS.get(tier, {}),\n }\n\n\ndef default_registry() -> ProviderRegistry:\n \"\"\"The standard Lantern OS provider fallback chain.\"\"\"\n reg = ProviderRegistry()\n reg.register(\"anthropic\", env_key=\"ANTHROPIC_API_KEY\", priority=0)\n reg.register(\"openai\", env_key=\"OPENAI_API_KEY\", priority=1)\n reg.register(\"google\", env_key=\"GOOGLE_API_KEY\", priority=2)\n reg.register(\"groq\", env_key=\"GROQ_API_KEY\", priority=3)\n reg.register(\"deepseek\", env_key=\"DEEPSEEK_API_KEY\", priority=4)\n reg.register(\"ollama\", env_key=None, priority=5) # no key needed\n reg.register(\"offline\", env_key=None, priority=6) # always available\n return reg\n"}, "caa76d6150869debaa819c3df779004e9c323ce0": {"t": 1785752997.5237503, "n": 7836, "h": "52386a1e957dc33e", "c": "\"\"\"\nPCSF — Provider Capacity State Format\nOperationalizes P4 (Capability Constraints) for LLM provider routing.\n\nTracks which providers are available, degraded, or exhausted, and routes\nrequests through a fallback chain with circuit breakers and quota awareness.\n\nThis is the capacity fallback system referenced in the master plan:\nClaude → OpenAI → Gemini → Ollama → offline persona fallback.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport threading\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\n\nclass ProviderState(Enum):\n AVAILABLE = \"available\"\n DEGRADED = \"degraded\"\n QUOTA_HIT = \"quota_hit\"\n CIRCUIT_OPEN = \"circuit_open\"\n UNAVAILABLE = \"unavailable\"\n NO_KEY = \"no_key\"\n\n\n@dataclass\nclass ProviderCapacityState:\n provider_id: str\n state: ProviderState = ProviderState.UNAVAILABLE\n latency_ema_ms: float = 0.0\n latency_p99_ms: float = 0.0\n error_count: int = 0\n success_count: int = 0\n last_success_at: Optional[float] = None\n last_error_at: Optional[float] = None\n last_error_msg: str = \"\"\n quota_remaining: Optional[int] = None\n circuit_recovery_at: Optional[float] = None\n env_key: Optional[str] = None\n last_checked: Optional[float] = None # P7 — health timestamp\n\n def is_routable(self) -> bool:\n if self.state in (ProviderState.AVAILABLE, ProviderState.DEGRADED):\n return True\n if self.state == ProviderState.CIRCUIT_OPEN and self.circuit_recovery_at:\n return time.time() >= self.circuit_recovery_at\n return False\n\n def record_success(self, latency_ms: float) -> None:\n self.success_count += 1\n self.last_success_at = time.time()\n self.latency_p50_ms = (self.latency_p50_ms + latency_ms) / 2\n self.latency_p99_ms = max(self.latency_p99_ms, latency_ms)\n self.error_count = 0\n self.state = ProviderState.AVAILABLE\n\n def record_error(self, msg: str, failure_threshold: int = 3, recovery_secs: float = 30.0) -> None:\n self.error_count += 1\n self.last_error_at = time.time()\n self.last_error_msg = msg\n if self.error_count >= failure_threshold:\n self.state = ProviderState.CIRCUIT_OPEN\n self.circuit_recovery_at = time.time() + recovery_secs\n else:\n self.state = ProviderState.DEGRADED\n\n def record_quota_hit(self) -> None:\n self.state = ProviderState.QUOTA_HIT\n self.last_error_at = time.time()\n self.last_error_msg = \"quota exhausted\"\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"provider_id\": self.provider_id,\n \"state\": self.state.value,\n \"latency_ema_ms\": self.latency_ema_ms,\n \"latency_p99_ms\": self.latency_p99_ms,\n \"error_count\": self.error_count,\n \"success_count\": self.success_count,\n \"quota_remaining\": self.quota_remaining,\n \"is_routable\": self.is_routable(),\n \"last_checked\": datetime.fromtimestamp(self.last_checked, tz=timezone.utc).isoformat() if self.last_checked else None,\n }\n\n\nclass DreamerTier(Enum):\n WANDERER = \"wanderer\"\n DEEP_DREAMER = \"deep_dreamer\"\n SYNTHESASIA_GUILD = \"synthesasia_guild\"\n\n\nTIER_PRIORITY_BOOST: Dict[str, Dict[str, int]] = {\n \"wanderer\": {},\n \"deep_dreamer\": {\"anthropic\": -1, \"openai\": -1},\n \"synthesasia_guild\": {\"anthropic\": -2, \"openai\": -2, \"google\": -1},\n}\n\nTIER_QUOTA_LIMITS: Dict[str, Dict[str, Optional[int]]] = {\n \"wanderer\": {\"art_generation\": 15, \"chat\": 100, \"3door_plays\": 5},\n \"deep_dreamer\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n \"synthesasia_guild\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n}\n\n\nclass ProviderRegistry:\n \"\"\"\n Manages the capacity fallback chain. Providers are tried in priority order;\n circuit breakers and quota tracking automatically route around failures.\n Tier-aware: paid tiers get priority routing to better providers.\n \"\"\"\n\n def __init__(self) -> None:\n self._providers: Dict[str, ProviderCapacityState] = {}\n self._priority: List[str] = []\n self._lock = threading.Lock()\n\n def register(self, provider_id: str, env_key: Optional[str] = None, priority: Optional[int] = None) -> None:\n with self._lock:\n pcs = ProviderCapacityState(provider_id=provider_id, env_key=env_key)\n self._providers[provider_id] = pcs\n if provider_id not in self._priority:\n if priority is not None and 0 <= priority <= len(self._priority):\n self._priority.insert(priority, provider_id)\n else:\n self._priority.append(provider_id)\n\n def check_env(self, env_getter: Callable[[str], Optional[str]]) -> None:\n with self._lock:\n for pcs in self._providers.values():\n pcs.last_checked = time.time()\n if pcs.env_key:\n val = env_getter(pcs.env_key)\n if not val:\n pcs.state = ProviderState.NO_KEY\n elif pcs.state == ProviderState.NO_KEY:\n pcs.state = ProviderState.AVAILABLE\n\n def get_routable_chain(self, tier: str = \"wanderer\") -> List[str]:\n with self._lock:\n routable = [pid for pid in self._priority if self._providers[pid].is_routable()]\n boosts = TIER_PRIORITY_BOOST.get(tier, {})\n if boosts:\n def sort_key(pid: str) -> int:\n base = routable.index(pid) if pid in routable else 999\n return base + boosts.get(pid, 0)\n routable.sort(key=sort_key)\n return routable\n\n def check_tier_quota(self, tier: str, action: str, current_count: int) -> Optional[int]:\n limits = TIER_QUOTA_LIMITS.get(tier, {})\n limit = limits.get(action)\n if limit is None:\n return None\n return max(0, limit - current_count)\n\n def get(self, provider_id: str) -> Optional[ProviderCapacityState]:\n return self._providers.get(provider_id)\n\n def record_success(self, provider_id: str, latency_ms: float) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_success(latency_ms)\n\n def record_error(self, provider_id: str, msg: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_error(msg)\n\n def record_quota_hit(self, provider_id: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_quota_hit()\n\n def snapshot(self, tier: str = \"wanderer\") -> Dict[str, Any]:\n with self._lock:\n return {\n \"priority\": list(self._priority),\n \"providers\": {pid: pcs.to_dict() for pid, pcs in self._providers.items()},\n \"routable_chain\": self.get_routable_chain(tier=tier),\n \"tier\": tier,\n \"tier_boosts\": TIER_PRIORITY_BOOST.get(tier, {}),\n \"tier_quota_limits\": TIER_QUOTA_LIMITS.get(tier, {}),\n }\n\n\ndef default_registry() -> ProviderRegistry:\n \"\"\"The standard Lantern OS provider fallback chain.\"\"\"\n reg = ProviderRegistry()\n reg.register(\"anthropic\", env_key=\"ANTHROPIC_API_KEY\", priority=0)\n reg.register(\"openai\", env_key=\"OPENAI_API_KEY\", priority=1)\n reg.register(\"google\", env_key=\"GOOGLE_API_KEY\", priority=2)\n reg.register(\"groq\", env_key=\"GROQ_API_KEY\", priority=3)\n reg.register(\"deepseek\", env_key=\"DEEPSEEK_API_KEY\", priority=4)\n reg.register(\"ollama\", env_key=None, priority=5) # no key needed\n reg.register(\"offline\", env_key=None, priority=6) # always available\n return reg\n"}, "feec533ac8c03592d8aae063efd07034baa34e90": {"t": 1785752999.0396245, "n": 7836, "h": "657ffb6cfc2a7677", "c": "\"\"\"\nPCSF — Provider Capacity State Format\nOperationalizes P4 (Capability Constraints) for LLM provider routing.\n\nTracks which providers are available, degraded, or exhausted, and routes\nrequests through a fallback chain with circuit breakers and quota awareness.\n\nThis is the capacity fallback system referenced in the master plan:\nClaude → OpenAI → Gemini → Ollama → offline persona fallback.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport threading\nimport time\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom enum import Enum\nfrom typing import Any, Callable, Dict, List, Optional\n\n\nclass ProviderState(Enum):\n AVAILABLE = \"available\"\n DEGRADED = \"degraded\"\n QUOTA_HIT = \"quota_hit\"\n CIRCUIT_OPEN = \"circuit_open\"\n UNAVAILABLE = \"unavailable\"\n NO_KEY = \"no_key\"\n\n\n@dataclass\nclass ProviderCapacityState:\n provider_id: str\n state: ProviderState = ProviderState.UNAVAILABLE\n latency_p50_ms: float = 0.0\n latency_p99_ms: float = 0.0\n error_count: int = 0\n success_count: int = 0\n last_success_at: Optional[float] = None\n last_error_at: Optional[float] = None\n last_error_msg: str = \"\"\n quota_remaining: Optional[int] = None\n circuit_recovery_at: Optional[float] = None\n env_key: Optional[str] = None\n last_checked: Optional[float] = None # P7 — health timestamp\n\n def is_routable(self) -> bool:\n if self.state in (ProviderState.AVAILABLE, ProviderState.DEGRADED):\n return True\n if self.state == ProviderState.CIRCUIT_OPEN and self.circuit_recovery_at:\n return time.time() >= self.circuit_recovery_at\n return False\n\n def record_success(self, latency_ms: float) -> None:\n self.success_count += 1\n self.last_success_at = time.time()\n self.latency_p50_ms = (self.latency_p50_ms + latency_ms) / 2\n self.latency_p99_ms = max(self.latency_p99_ms, latency_ms)\n self.error_count = 0\n self.state = ProviderState.AVAILABLE\n\n def record_error(self, msg: str, failure_threshold: int = 3, recovery_secs: float = 30.0) -> None:\n self.error_count += 1\n self.last_error_at = time.time()\n self.last_error_msg = msg\n if self.error_count >= failure_threshold:\n self.state = ProviderState.CIRCUIT_OPEN\n self.circuit_recovery_at = time.time() + recovery_secs\n else:\n self.state = ProviderState.DEGRADED\n\n def record_quota_hit(self) -> None:\n self.state = ProviderState.QUOTA_HIT\n self.last_error_at = time.time()\n self.last_error_msg = \"quota exhausted\"\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"provider_id\": self.provider_id,\n \"state\": self.state.value,\n \"latency_p50_ms\": self.latency_p50_ms,\n \"latency_p99_ms\": self.latency_p99_ms,\n \"error_count\": self.error_count,\n \"success_count\": self.success_count,\n \"quota_remaining\": self.quota_remaining,\n \"is_routable\": self.is_routable(),\n \"last_checked\": datetime.fromtimestamp(self.last_checked, tz=timezone.utc).isoformat() if self.last_checked else None,\n }\n\n\nclass DreamerTier(Enum):\n WANDERER = \"wanderer\"\n DEEP_DREAMER = \"deep_dreamer\"\n SYNTHESASIA_GUILD = \"synthesasia_guild\"\n\n\nTIER_PRIORITY_BOOST: Dict[str, Dict[str, int]] = {\n \"wanderer\": {},\n \"deep_dreamer\": {\"anthropic\": -1, \"openai\": -1},\n \"synthesasia_guild\": {\"anthropic\": -2, \"openai\": -2, \"google\": -1},\n}\n\nTIER_QUOTA_LIMITS: Dict[str, Dict[str, Optional[int]]] = {\n \"wanderer\": {\"art_generation\": 15, \"chat\": 100, \"3door_plays\": 5},\n \"deep_dreamer\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n \"synthesasia_guild\": {\"art_generation\": None, \"chat\": None, \"3door_plays\": None},\n}\n\n\nclass ProviderRegistry:\n \"\"\"\n Manages the capacity fallback chain. Providers are tried in priority order;\n circuit breakers and quota tracking automatically route around failures.\n Tier-aware: paid tiers get priority routing to better providers.\n \"\"\"\n\n def __init__(self) -> None:\n self._providers: Dict[str, ProviderCapacityState] = {}\n self._priority: List[str] = []\n self._lock = threading.Lock()\n\n def register(self, provider_id: str, env_key: Optional[str] = None, priority: Optional[int] = None) -> None:\n with self._lock:\n pcs = ProviderCapacityState(provider_id=provider_id, env_key=env_key)\n self._providers[provider_id] = pcs\n if provider_id not in self._priority:\n if priority is not None and 0 <= priority <= len(self._priority):\n self._priority.insert(priority, provider_id)\n else:\n self._priority.append(provider_id)\n\n def check_env(self, env_getter: Callable[[str], Optional[str]]) -> None:\n with self._lock:\n for pcs in self._providers.values():\n pcs.last_checked = time.time()\n if pcs.env_key:\n val = env_getter(pcs.env_key)\n if not val:\n pcs.state = ProviderState.NO_KEY\n elif pcs.state == ProviderState.NO_KEY:\n pcs.state = ProviderState.AVAILABLE\n\n def get_routable_chain(self, tier: str = \"wanderer\") -> List[str]:\n with self._lock:\n routable = [pid for pid in self._priority if self._providers[pid].is_routable()]\n boosts = TIER_PRIORITY_BOOST.get(tier, {})\n if boosts:\n def sort_key(pid: str) -> int:\n base = routable.index(pid) if pid in routable else 999\n return base + boosts.get(pid, 0)\n routable.sort(key=sort_key)\n return routable\n\n def check_tier_quota(self, tier: str, action: str, current_count: int) -> Optional[int]:\n limits = TIER_QUOTA_LIMITS.get(tier, {})\n limit = limits.get(action)\n if limit is None:\n return None\n return max(0, limit - current_count)\n\n def get(self, provider_id: str) -> Optional[ProviderCapacityState]:\n return self._providers.get(provider_id)\n\n def record_success(self, provider_id: str, latency_ms: float) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_success(latency_ms)\n\n def record_error(self, provider_id: str, msg: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_error(msg)\n\n def record_quota_hit(self, provider_id: str) -> None:\n pcs = self._providers.get(provider_id)\n if pcs:\n pcs.record_quota_hit()\n\n def snapshot(self, tier: str = \"wanderer\") -> Dict[str, Any]:\n with self._lock:\n return {\n \"priority\": list(self._priority),\n \"providers\": {pid: pcs.to_dict() for pid, pcs in self._providers.items()},\n \"routable_chain\": self.get_routable_chain(tier=tier),\n \"tier\": tier,\n \"tier_boosts\": TIER_PRIORITY_BOOST.get(tier, {}),\n \"tier_quota_limits\": TIER_QUOTA_LIMITS.get(tier, {}),\n }\n\n\ndef default_registry() -> ProviderRegistry:\n \"\"\"The standard Lantern OS provider fallback chain.\"\"\"\n reg = ProviderRegistry()\n reg.register(\"anthropic\", env_key=\"ANTHROPIC_API_KEY\", priority=0)\n reg.register(\"openai\", env_key=\"OPENAI_API_KEY\", priority=1)\n reg.register(\"google\", env_key=\"GOOGLE_API_KEY\", priority=2)\n reg.register(\"groq\", env_key=\"GROQ_API_KEY\", priority=3)\n reg.register(\"deepseek\", env_key=\"DEEPSEEK_API_KEY\", priority=4)\n reg.register(\"ollama\", env_key=None, priority=5) # no key needed\n reg.register(\"offline\", env_key=None, priority=6) # always available\n return reg\n"}}, "apps/lantern-garage/lib/provider-router.js": {"eb1ea13d377f332ccf9cee2acea5ea0e27375e48": {"t": 1785745530.447039, "n": 20727, "h": "3371021d2a817674", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): unisona.ai/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=unisona.ai/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n // Grok's documented niche (PROVIDERS.md §4): creative tasks, humor/personality.\n // Wired 2026-07-16 (#2531) — the stream-chat transport (OpenAI-compatible,\n // native tool loop) existed but no chain ever dispatched it; key verified live.\n { provider: \"xai\", models: [\"grok-3-mini\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"xai\", models: [\"grok-3-mini\"] }, // #2531 — cloud tail before the local backstop\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.info(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track unisona.ai win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the unisona.ai kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → unisona.ai/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try unisona.ai/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "b7d687c6ada11bc8d632bf93610472066b514788": {"t": 1785752992.0742474, "n": 20727, "h": "3371021d2a817674", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): unisona.ai/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=unisona.ai/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n // Grok's documented niche (PROVIDERS.md §4): creative tasks, humor/personality.\n // Wired 2026-07-16 (#2531) — the stream-chat transport (OpenAI-compatible,\n // native tool loop) existed but no chain ever dispatched it; key verified live.\n { provider: \"xai\", models: [\"grok-3-mini\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"xai\", models: [\"grok-3-mini\"] }, // #2531 — cloud tail before the local backstop\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.info(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track unisona.ai win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the unisona.ai kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → unisona.ai/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try unisona.ai/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "052067bae5870cad16bb1ffb4b388f39ef944f73": {"t": 1785752994.1996584, "n": 20328, "h": "afe973d919c90878", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): unisona.ai/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=unisona.ai/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track unisona.ai win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the unisona.ai kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → unisona.ai/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try unisona.ai/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "e16dc4ba4d7770142f7cca20251f8fae3a924a2a": {"t": 1785752995.6616213, "n": 20316, "h": "c567aa8764f1e010", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "b20663fc40cf8c82c3d68a0cb7e0456145dc5ca2": {"t": 1785752997.1653187, "n": 20298, "h": "122f1984c6ca721a", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "26264c34ae8ada01c3c749153fa9e384c16851a0": {"t": 1785752998.5348763, "n": 20087, "h": "6d3367a247fdd5b6", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "eac4fa8081a8362b984b153014d4753cf8240332": {"t": 1785752999.9179099, "n": 20079, "h": "27a57ecd056848c9", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "eaf5255fd3ce5079bb76d0ab4dd38d152d3f64e9": {"t": 1785753001.414312, "n": 19851, "h": "5438873d0fe8f2bc", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-opus-4-8\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "5471c8ae1a1ce570c26ef1d345f0e1ec11147b11": {"t": 1785753002.9833286, "n": 19530, "h": "a30d2c3e785892f5", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\"] },\n ],\n\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-opus-4-8\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "c4e33a37face6cb76a8c57e0f13911cf223bd6ff": {"t": 1785753004.275061, "n": 17268, "h": "9ecfb44f948b30da", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\"] },\n ],\n\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-opus-4-8\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n};\n"}, "f4e919ad8f1b224c951b280c2534052742fab9ad": {"t": 1785753006.6829274, "n": 16853, "h": "b16f502480156ee0", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\"] },\n ],\n\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track Keystone win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-opus-4-8\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n};\n"}, "b5a83c8fc8d5050cf3f9055fc13d820e228eb91c": {"t": 1785753007.8842063, "n": 15520, "h": "1eb862e37bb8d592", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): Keystone/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=Keystone/Ouro first, Claude fallback (Stage 1+).\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\"] },\n ],\n\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the Keystone kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → Keystone/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try Keystone/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-opus-4-8\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n PROVIDER_CHAINS,\n};\n"}, "7b7b51104495fdee12edd1e845acc04587ecfb9c": {"t": 1785753010.6446753, "n": 13942, "h": "2f9b3055b4c2b585", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n};\n"}, "e3047d8f75899b9bf3fbd8c2b275f0b1b36dd971": {"t": 1785753012.2205765, "n": 13867, "h": "bbeb3e68d6b3c0ae", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n });\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n};\n"}, "f26bf02629f4dfcc65961b660324c924a6502c56": {"t": 1785753013.5915282, "n": 11416, "h": "bb14b7344a8ab41d", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n });\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n};\n"}, "8d7d6cef97e2d37c1730b5e6f88dd726bd796c76": {"t": 1785753014.9635184, "n": 9464, "h": "8102d701ce60550c", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt,\n fallbackReason: null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider}`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context);\n }\n\n // All providers exhausted\n throw new Error(\n `[provider-router] All providers failed for ${taskType}. Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n });\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n};\n"}, "185a5045661ec15c14e43f42e8c517fa5d80ead0": {"t": 1785753016.329981, "n": 12287, "h": "b9e5d42b82f19039", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n coding: [\n { provider: \"ollama\", models: [\"qwen2.5-coder\", \"deepseek\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n creative: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-r-plus\"] },\n ],\n\n default: [\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt,\n fallbackReason: null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep) {\n console.log(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider}`);\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context);\n }\n\n // All providers exhausted\n throw new Error(\n `[provider-router] All providers failed for ${taskType}. Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n switch (provider) {\n case \"anthropic\":\n return await _callAnthropic(model, payload, context);\n case \"openai\":\n return await _callOpenAI(model, payload, context);\n case \"gemini\":\n return await _callGemini(model, payload, context);\n case \"ollama\":\n return await _callOllama(model, payload, context);\n case \"mistral\":\n return await _callMistral(model, payload, context);\n case \"cohere\":\n return await _callCohere(model, payload, context);\n case \"deepseek\":\n return await _callDeepSeek(model, payload, context);\n case \"perplexity\":\n return await _callPerplexity(model, payload, context);\n case \"openrouter\":\n return await _callOpenRouter(model, payload, context);\n case \"xai\":\n return await _callXAI(model, payload, context);\n default:\n throw new Error(`Unknown provider: ${provider}`);\n }\n}\n\n// Provider-specific implementations (stubs for now, integrate with existing code)\nasync function _callAnthropic(model, payload, context) {\n const key = process.env.ANTHROPIC_API_KEY;\n if (!key) throw new Error(\"ANTHROPIC_API_KEY not set\");\n // TODO: Integrate with existing Anthropic logic from dream-chat.js, stream-chat.js\n throw new Error(\"_callAnthropic not yet implemented\");\n}\n\nasync function _callOpenAI(model, payload, context) {\n const key = process.env.OPENAI_API_KEY;\n if (!key) throw new Error(\"OPENAI_API_KEY not set\");\n throw new Error(\"_callOpenAI not yet implemented\");\n}\n\nasync function _callGemini(model, payload, context) {\n const key = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;\n if (!key) throw new Error(\"GEMINI_API_KEY not set\");\n throw new Error(\"_callGemini not yet implemented\");\n}\n\nasync function _callOllama(model, payload, context) {\n const ollamaBase = process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n throw new Error(\"_callOllama not yet implemented\");\n}\n\nasync function _callMistral(model, payload, context) {\n const key = process.env.MISTRAL_API_KEY;\n if (!key) throw new Error(\"MISTRAL_API_KEY not set\");\n throw new Error(\"_callMistral not yet implemented\");\n}\n\nasync function _callCohere(model, payload, context) {\n const key = process.env.COHERE_API_KEY;\n if (!key) throw new Error(\"COHERE_API_KEY not set\");\n throw new Error(\"_callCohere not yet implemented\");\n}\n\nasync function _callDeepSeek(model, payload, context) {\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) throw new Error(\"DEEPSEEK_API_KEY not set\");\n throw new Error(\"_callDeepSeek not yet implemented\");\n}\n\nasync function _callPerplexity(model, payload, context) {\n const key = process.env.PERPLEXITY_API_KEY;\n if (!key) throw new Error(\"PERPLEXITY_API_KEY not set\");\n throw new Error(\"_callPerplexity not yet implemented\");\n}\n\nasync function _callOpenRouter(model, payload, context) {\n const key = process.env.OPENROUTER_API_KEY;\n if (!key) throw new Error(\"OPENROUTER_API_KEY not set\");\n throw new Error(\"_callOpenRouter not yet implemented\");\n}\n\nasync function _callXAI(model, payload, context) {\n const key = process.env.XAI_API_KEY;\n if (!key) throw new Error(\"XAI_API_KEY not set\");\n throw new Error(\"_callXAI not yet implemented\");\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n });\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n};\n"}, "5a5e9644f807653081fa5524af8fa317be6683f8": {"t": 1786067397.507044, "n": 20727, "h": "3371021d2a817674", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): unisona.ai/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=unisona.ai/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n // Grok's documented niche (PROVIDERS.md §4): creative tasks, humor/personality.\n // Wired 2026-07-16 (#2531) — the stream-chat transport (OpenAI-compatible,\n // native tool loop) existed but no chain ever dispatched it; key verified live.\n { provider: \"xai\", models: [\"grok-3-mini\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"xai\", models: [\"grok-3-mini\"] }, // #2531 — cloud tail before the local backstop\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.info(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track unisona.ai win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the unisona.ai kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → unisona.ai/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try unisona.ai/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}, "0fae6894c98aab7ab5b987612d5237651735dc08": {"t": 1786068261.1087844, "n": 20727, "h": "3371021d2a817674", "c": "/**\n * Unified Provider Router\n * Single source of truth for all LLM provider selection and fallback logic.\n * Replaces scattered fallback chains in stream-chat.js and dream-chat.js.\n */\n\nconst http = require(\"http\");\nconst https = require(\"https\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { appendJsonlQueued } = require(\"./file-queue\");\n\nconst CACHE_TTL_MS = 60_000;\nconst PROVIDER_CALL_LOG_PATH = path.resolve(__dirname, \"..\", \"..\", \"data\", \"provider-calls.jsonl\");\n// lib/ -> lantern-garage -> apps -> repo root (3 levels up).\nconst PCSF_PROVIDER_PATH = path.resolve(__dirname, \"..\", \"..\", \"..\", \"data\", \"pcsf\", \"provider.pcsf.json\");\n\n// ── PCSF live ranking ────────────────────────────────────────────────────────\n// provider.pcsf.json is the persisted, inspectable provider ranking — refreshed\n// from real leaderboard outcomes by lib/pcsf-refresh.js. It is the SOURCE OF\n// TRUTH for provider order; the static PROVIDER_CHAINS below is the candidate set\n// + cold fallback when PCSF has no ranking yet. Kill-switch: PCSF_ROUTING=0.\nlet _pcsfRoutingCache = { at: 0, byTask: null };\nfunction loadPcsfRouting() {\n if (process.env.PCSF_ROUTING === \"0\") return null;\n const now = Date.now();\n if (now - _pcsfRoutingCache.at < CACHE_TTL_MS) return _pcsfRoutingCache.byTask;\n let byTask = null;\n try {\n const raw = JSON.parse(fs.readFileSync(PCSF_PROVIDER_PATH, \"utf8\"));\n if (raw && raw.routing && raw.routing.by_task_type) byTask = raw.routing.by_task_type;\n } catch { byTask = null; }\n _pcsfRoutingCache = { at: now, byTask };\n return byTask;\n}\n\n/** Reorder a static chain so providers follow the PCSF ranking for this task.\n * Providers absent from the PCSF list keep their static order, after ranked ones. */\nfunction orderChainByPcsf(chain, taskType, byTaskOverride) {\n const byTask = byTaskOverride !== undefined ? byTaskOverride : loadPcsfRouting();\n const order = byTask && (byTask[taskType] || byTask.default);\n if (!Array.isArray(order) || !order.length) return { chain, ranked: false };\n const rank = new Map(order.map((p, i) => [String(p).toLowerCase(), i]));\n const reordered = chain\n .map((step, i) => ({ step, i, r: rank.has(step.provider) ? rank.get(step.provider) : Infinity }))\n .sort((a, b) => (a.r - b.r) || (a.i - b.i))\n .map((x) => x.step);\n return { chain: reordered, ranked: true };\n}\n\n// Provider configuration with fallback chains by task type\nconst PROVIDER_CHAINS = {\n // Kernel chain (#894): unisona.ai/Ouro model first; Claude is explicit last-resort.\n // Controlled by KEYSTONE_ROLLOVER_MODE: \"shadow\"(default/unset)=Claude only (Stage 0,\n // safe); \"default\"=unisona.ai/Ouro first, Claude fallback (Stage 1+).\n // Anthropic tier uses claude-sonnet-5 — Anthropic's mid-tier model built for\n // long-running, multi-step agentic sessions (self-correction, dynamic\n // replanning) which is exactly the kernel/autowork escalation workload.\n kernel: [\n { provider: \"ollama\", models: [\"keystone-ft\", \"ouro:latest\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-5\"] },\n ],\n\n // Local model order here is the coarse fallback; lib/local-model-registry.js is\n // the source of truth for which LOCAL model leads (VRAM-gated, Ouro-default /\n // capability-first aware — see docs/SIGMA0-MODEL-ADAPTER.md). Ouro leads by\n // decision (2026-06-26); Qwen is the opt-in capability lever behind it.\n coding: [\n { provider: \"ollama\", models: [\"ouro:latest\", \"qwen2.5-coder\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\", \"gpt-4o-mini\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n ],\n\n reasoning: [\n { provider: \"anthropic\", models: [\"claude-opus-4-8\", \"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4-turbo\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-pro\"] },\n { provider: \"deepseek\", models: [\"deepseek-chat\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n ],\n\n // #1167: ollama used to lead here too. isProviderHealthy() defaults an unrecorded\n // provider to \"healthy\" and ollama essentially never gets recorded unhealthy (it\n // always returns *something*, even a hallucinated/off-tone answer) — so once the\n // local coder was reliably served, cloud was never reached for ANY general/creative\n // message. Cloud now leads; ollama stays as a genuine offline backstop.\n creative: [\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"openai\", models: [\"gpt-4o\"] },\n // Grok's documented niche (PROVIDERS.md §4): creative tasks, humor/personality.\n // Wired 2026-07-16 (#2531) — the stream-chat transport (OpenAI-compatible,\n // native tool loop) existed but no chain ever dispatched it; key verified live.\n { provider: \"xai\", models: [\"grok-3-mini\"] },\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"cohere\", models: [\"command-a-plus-05-2026\"] },\n { provider: \"ollama\", models: [\"lantern-csf-dream\"] },\n ],\n\n default: [\n { provider: \"gemini\", models: [\"gemini-2.5-flash\"] },\n { provider: \"anthropic\", models: [\"claude-sonnet-4-6\"] },\n { provider: \"openai\", models: [\"gpt-4o-mini\"] },\n { provider: \"mistral\", models: [\"mistral-large-latest\"] },\n { provider: \"xai\", models: [\"grok-3-mini\"] }, // #2531 — cloud tail before the local backstop\n { provider: \"ollama\", models: [\"lantern-csf-dream\", \"qwen2.5-coder\"] },\n ],\n};\n\n// Provider health state (fleet-wide, shared across all requests)\nconst _providerState = {};\nlet _lastHealthCheck = 0;\n\n/**\n * Does this provider have an API key available in the environment?\n * (ollama/local is always \"available\".)\n */\nfunction providerHasKey(provider) {\n const env = process.env;\n switch (String(provider || \"\").toLowerCase()) {\n case \"anthropic\": return !!env.ANTHROPIC_API_KEY;\n case \"openai\": return !!env.OPENAI_API_KEY;\n // Gemini is also reachable keyless via Vertex AI (ADC billing to the Cloud\n // project — the funded path); see lib/gemini-transport.js useVertex().\n case \"gemini\": return !!(env.GEMINI_API_KEY || env.GOOGLE_API_KEY || env.GEMINI_USE_VERTEX === \"1\" || env.VERTEX_PROJECT);\n case \"xai\": case \"grok\": return !!env.XAI_API_KEY;\n case \"mistral\": return !!env.MISTRAL_API_KEY;\n case \"cohere\": return !!env.COHERE_API_KEY;\n case \"deepseek\": return !!env.DEEPSEEK_API_KEY;\n case \"perplexity\": return !!env.PERPLEXITY_API_KEY;\n case \"openrouter\": return !!env.OPENROUTER_API_KEY;\n case \"ollama\": case \"local\": return true;\n default: return false;\n }\n}\n\n/**\n * Select which provider + model to use for a request\n * @param {string} message - User message\n * @param {string} taskType - \"coding\"|\"reasoning\"|\"creative\"|\"default\"\n * @param {string} requestedProvider - Override provider if specified\n * @returns {Promise<{provider, model, chain}>}\n */\nasync function selectProvider(message, taskType = \"default\", requestedProvider = null) {\n // If user explicitly requested a provider, try that first\n if (requestedProvider) {\n const matched = findProviderChain(requestedProvider);\n if (matched) {\n return {\n provider: matched.provider,\n model: matched.models[0],\n chain: matched.models.slice(1),\n reason: `explicit_request:${requestedProvider}`,\n };\n }\n }\n\n // Use task type to select chain, then reorder by the live PCSF ranking\n // (provider.pcsf.json, refreshed from leaderboard outcomes). PCSF is the source\n // of truth for order; the static chain is the candidate set + cold fallback.\n const baseChain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const { chain, ranked: pcsfRanked } = orderChainByPcsf(baseChain, taskType);\n\n // LEADERBOARD_ROUTING: rank cloud(with key) + local together by compositeScore\n // and pick the best on merit (Auto mode only). Default off → legacy chain order.\n if (process.env.LEADERBOARD_ROUTING === \"1\" && !requestedProvider) {\n try {\n const { rankCandidates } = require(\"./model-leaderboard\");\n const { CLOUD_PROVIDERS } = require(\"./leaderboard-routing\");\n const candidates = [];\n for (const step of chain) {\n if (!isProviderHealthy(step.provider)) continue;\n if (step.provider === \"ollama\") {\n // local candidate scored by its MODEL name (how local outcomes record)\n candidates.push({ provider: \"ollama\", model: step.models[0], key: step.models[0] });\n } else if (providerHasKey(step.provider)) {\n // cloud candidate scored by PROVIDER name\n candidates.push({ provider: step.provider, model: step.models[0], key: step.provider });\n }\n }\n if (!candidates.some((c) => c.provider === \"ollama\")) {\n candidates.push({ provider: \"ollama\", model: \"lantern-csf-dream\", key: \"lantern-csf-dream\" });\n }\n const rankedCands = await rankCandidates(candidates, taskType, { cloudSet: CLOUD_PROVIDERS });\n const best = rankedCands[0];\n if (best) {\n return {\n provider: best.provider,\n model: best.model,\n chain: [],\n reason: `leaderboard:${best.score.toFixed(2)}${best.scored ? \"\" : \":coldstart\"}`,\n };\n }\n } catch (e) {\n console.error(\"[provider-router] leaderboard routing error (non-fatal):\", e.message);\n // fall through to legacy chain selection\n }\n }\n\n // Find first healthy provider in chain\n for (const step of chain) {\n if (isProviderHealthy(step.provider)) {\n return {\n provider: step.provider,\n model: step.models[0],\n chain: step.models.slice(1),\n reason: pcsfRanked ? `pcsf_rank:${taskType}` : `task_type:${taskType}`,\n };\n }\n }\n\n // If all providers in chain are unhealthy, log warning and use first anyway\n console.warn(`[provider-router] All providers unhealthy for ${taskType}, using fallback`);\n const fallback = chain[0];\n return {\n provider: fallback.provider,\n model: fallback.models[0],\n chain: fallback.models.slice(1),\n reason: `fallback:all_unhealthy`,\n };\n}\n\n// Σ₀ Fix: Track fallback cost per message to detect and escalate runaway chains\nconst _fallbackContext = new Map(); // messageId → {startTime, attemptCount, totalCost, providers}\n\n/**\n * Call a specific LLM provider with intelligent fallback\n * @param {string} provider\n * @param {string} model\n * @param {object} payload - Provider-specific payload (messages, system prompt, etc.)\n * @param {string} taskType - For context/logging\n * @param {object} context - {agent, recentDreams, csfContext, etc.}\n * @returns {Promise<{content, provider, model, attempt, fallbackReason, latencyMs, tokensUsed, cost}>}\n */\nasync function callProvider(provider, model, payload, taskType = \"default\", context = {}, _globalAttempt = 0) {\n const startTime = Date.now();\n let attempt = 1;\n let lastError = null;\n\n // Σ₀ Fix: Track global attempt count across all providers\n const MAX_FALLBACK_ATTEMPTS = 5; // Escalate after 5 failed attempts (stop the bleed)\n const messageId = JSON.stringify([payload, taskType]).substring(0, 20); // Quick message fingerprint\n\n if (!_fallbackContext.has(messageId)) {\n _fallbackContext.set(messageId, { startTime, attemptCount: 0, providers: [] });\n }\n const fallbackState = _fallbackContext.get(messageId);\n fallbackState.attemptCount++;\n\n // Escalation gate: if we've tried too many providers, stop and return error\n if (fallbackState.attemptCount > MAX_FALLBACK_ATTEMPTS) {\n console.error(`[provider-router] ESCALATION: Exceeded max fallback attempts (${MAX_FALLBACK_ATTEMPTS}) for ${taskType}`);\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] ESCALATION: Message failed across ${fallbackState.providers.join(\", \")}. Stopped after ${MAX_FALLBACK_ATTEMPTS} attempts. Last error: ${lastError?.message || \"unknown\"}`\n );\n }\n\n const chain = PROVIDER_CHAINS[taskType] || PROVIDER_CHAINS.default;\n const providerStep = chain.find((s) => s.provider === provider);\n const modelsToTry = providerStep ? [...providerStep.models] : [model];\n\n // Try models in sequence within this provider\n for (const tryModel of modelsToTry) {\n try {\n const result = await _callProviderImpl(provider, tryModel, payload, context);\n const latencyMs = Date.now() - startTime;\n\n // Log successful call\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"success\",\n latencyMs,\n taskType,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers.join(\" -> \"),\n });\n\n // Record success in provider state\n recordProviderSuccess(provider);\n _fallbackContext.delete(messageId); // Clean up\n\n return {\n content: result.content,\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n fallbackReason: fallbackState.providers.length > 0 ? `Fallback after ${fallbackState.providers.join(\", \")}` : null,\n latencyMs,\n tokensUsed: result.tokensUsed || 0,\n cost: result.cost || 0,\n attemptChain: fallbackState.providers,\n };\n } catch (err) {\n lastError = err;\n const latencyMs = Date.now() - startTime;\n\n // Log failed attempt\n await _logProviderCall({\n provider,\n model: tryModel,\n attempt: fallbackState.attemptCount,\n status: \"failure\",\n errorCode: err.code || err.message.split(\" \")[0],\n latencyMs,\n taskType,\n escalationRisk: fallbackState.attemptCount >= MAX_FALLBACK_ATTEMPTS ? \"HIGH\" : \"normal\",\n });\n\n // Record failure in provider state\n recordProviderFailure(provider, err.code || \"unknown\");\n fallbackState.providers.push(provider);\n\n attempt++;\n }\n }\n\n // All models in this provider failed, try next provider in chain\n const nextProviderStep = chain.find(\n (s, idx) => idx > chain.findIndex((s) => s.provider === provider)\n );\n\n if (nextProviderStep && fallbackState.attemptCount < MAX_FALLBACK_ATTEMPTS) {\n console.info(`[provider-router] Fallback: ${provider} failed, trying ${nextProviderStep.provider} (attempt ${fallbackState.attemptCount}/${MAX_FALLBACK_ATTEMPTS})`);\n // #897: record Claude escalation as a convergence event so the rollover dashboard\n // (#898) can track unisona.ai win/loss rate without grepping logs.\n if (nextProviderStep.provider === \"anthropic\" && provider !== \"anthropic\") {\n try {\n const { emitConvergenceRecord } = require(\"./convergence-records\");\n emitConvergenceRecord({\n hypothesis: `Provider fallback: ${provider} failed → escalating to claude (${taskType})`,\n evidence_ids: [],\n result: {\n type: \"claude_fallback\",\n trigger: \"provider_failed\",\n failed_provider: provider,\n fallback_provider: \"anthropic\",\n fallback_model: nextProviderStep.models[0],\n task_type: taskType,\n failed_providers: [...fallbackState.providers],\n issue_number: context.issueNumber || null,\n keystone_stage: context.keystoneStage || null,\n error: lastError ? String(lastError.message).slice(0, 200) : null,\n },\n confidence: 1.0,\n reasoner: \"provider-router\",\n verified: false,\n verification_notes: \"automatic fallback — not a human decision\",\n }).catch(() => {}); // best-effort, never block the fallback\n } catch (_e) { /* require may fail in tests — never block */ }\n }\n return await callProvider(nextProviderStep.provider, nextProviderStep.models[0], payload, taskType, context, fallbackState.attemptCount);\n }\n\n // Escalation: all providers exhausted OR max attempts exceeded\n _fallbackContext.delete(messageId);\n throw new Error(\n `[provider-router] All providers failed for ${taskType} (${fallbackState.attemptCount} attempts). Last error: ${lastError?.message || \"unknown\"}`\n );\n}\n\n/**\n * Actual provider API call (delegates to provider-specific code)\n * NOTE: Streaming implementations stay in stream-chat.js for now\n * This is used for sync chat and testing\n * @private\n */\nasync function _callProviderImpl(provider, model, payload, context) {\n // For now, sync calls are not implemented here\n // Stream-chat.js has the streaming implementations\n // This stub ensures the routing logic works\n throw new Error(`Sync call not implemented for ${provider}. Use stream-chat.js for streaming.`);\n}\n\n/**\n * Check if a provider is healthy (not rate-limited, not auth failed, etc.)\n */\nfunction isProviderHealthy(provider) {\n const state = _providerState[provider];\n if (!state) return true; // Unknown = assume healthy\n\n // Provider is blocked due to rate limit, skip it\n if (state.blockedUntil && Date.now() < state.blockedUntil) {\n return false;\n }\n\n // Provider has no API key\n if (state.noKey) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Record a successful provider call (for health tracking)\n */\nfunction recordProviderSuccess(provider) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n _providerState[provider].lastSuccess = Date.now();\n _providerState[provider].consecutiveFailures = 0;\n}\n\n/**\n * Record a provider failure (rate limit, auth error, etc.)\n */\nfunction recordProviderFailure(provider, errorCode) {\n if (!_providerState[provider]) {\n _providerState[provider] = {};\n }\n\n _providerState[provider].lastFailure = Date.now();\n _providerState[provider].lastError = errorCode;\n _providerState[provider].consecutiveFailures = (_providerState[provider].consecutiveFailures || 0) + 1;\n\n // Handle rate limits\n if (errorCode === \"429\" || errorCode === \"quota_exceeded\") {\n _providerState[provider].blockedUntil = Date.now() + 60_000; // Block for 60s\n }\n\n // Handle auth failures\n if (errorCode === \"401\" || errorCode === \"403\" || errorCode === \"invalid_api_key\") {\n _providerState[provider].noKey = true;\n }\n\n // After 5 consecutive failures, temporarily block\n if (_providerState[provider].consecutiveFailures >= 5) {\n _providerState[provider].blockedUntil = Date.now() + 30_000;\n }\n}\n\n/**\n * Get current provider health status (for debugging/observability)\n */\nfunction getProviderStatus() {\n return { ..._providerState };\n}\n\n/**\n * Log a provider call for metrics and debugging\n * @private\n */\nasync function _logProviderCall(entry) {\n try {\n await appendJsonlQueued(PROVIDER_CALL_LOG_PATH, {\n timestamp: new Date().toISOString(),\n ...entry,\n }, { rotate: true }); // bound growth — appended on every provider call (#872)\n } catch (err) {\n console.error(\"[provider-router] Failed to log provider call:\", err.message);\n }\n}\n\n/**\n * Find a provider chain by name/alias\n * @private\n */\nfunction findProviderChain(providerName) {\n const name = providerName.toLowerCase();\n for (const chain of Object.values(PROVIDER_CHAINS)) {\n const found = chain.find(\n (step) =>\n step.provider === name ||\n step.provider.startsWith(name) ||\n step.models.some((m) => m.includes(name))\n );\n if (found) return found;\n }\n return null;\n}\n\n/**\n * Select provider for the unisona.ai kernel path (#894). Independent of chat\n * selectProvider so the kernel never inherits the chat surface's Claude default.\n * KEYSTONE_ROLLOVER_MODE: \"default\" → unisona.ai/Ouro first, anthropic last-resort;\n * \"shadow\" (default when unset) → anthropic only (Stage 0, safe).\n * @param {string|null} requestedProvider explicit override from the request\n * @returns {Promise<{provider:string, model:?string, mode:string}>}\n */\nasync function selectKernelProvider(requestedProvider = null) {\n const mode = process.env.KEYSTONE_ROLLOVER_MODE || \"shadow\";\n\n if (requestedProvider) {\n return { provider: requestedProvider, model: null, mode };\n }\n\n if (mode === \"default\") {\n // Try unisona.ai/Ouro first, fall back to Claude only on failure.\n const kernelChain = PROVIDER_CHAINS.kernel;\n for (const step of kernelChain) {\n if (isProviderHealthy(step.provider)) {\n return { provider: step.provider, model: step.models[0], mode };\n }\n }\n }\n\n // Shadow mode (or all kernel providers unhealthy): use Claude.\n return { provider: \"anthropic\", model: \"claude-sonnet-5\", mode };\n}\n\nmodule.exports = {\n selectProvider,\n callProvider,\n isProviderHealthy,\n recordProviderSuccess,\n recordProviderFailure,\n getProviderStatus,\n selectKernelProvider,\n providerHasKey,\n PROVIDER_CHAINS,\n loadPcsfRouting,\n orderChainByPcsf,\n};\n"}}, "apps/lantern-garage/lib/swarm-orchestrator.js": {"eb1ea13d377f332ccf9cee2acea5ea0e27375e48": {"t": 1785746684.3135688, "n": 24009, "h": "35766404958459c3", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst os = require(\"os\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFile } = require(\"child_process\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n // Probe-verified on Vertex us-central1 (2026-07-15). The gemini-3.x ids that sat\n // here 404 on every wire — they do not exist.\n modelChain: [\"gemini-2.5-flash\", \"gemini-2.5-flash-lite\", \"gemini-2.5-pro\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n // Grok via the local CLI (OIDC login in ~/.grok/auth.json), NOT the api.x.ai\n // key path above. On machines where the XAI_API_KEY is out of credits but the\n // user has run `grok login`, this is the ONLY working Grok transport — so the\n // council reaches Grok through the binary instead of a dead API key.\n xai_cli: {\n transport: \"cli\",\n bin: process.env.GROK_BIN || path.join(os.homedir(), \".grok\", \"bin\", \"grok.exe\"),\n defaultModel: \"grok\",\n modelChain: [\"grok\"],\n strengths: [\"chat\", \"reasoning\", \"creative\"],\n costTier: \"local\", // OIDC session, no per-call API key\n streamFormat: \"cli\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"xai_cli\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet); xai_cli = Grok member lens\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"xai_cli\", \"deepseek\", \"anthropic\"], // Grok (xai_cli) takes a member lens; anthropic stays synthesizer\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n // CLI transport authenticates via the binary's own OIDC session, not an env\n // key. \"Availability\" is: the binary exists on disk. Return the bin path so\n // isProviderAvailable() treats a present binary as a usable credential.\n if (config.transport === \"cli\") {\n return fs.existsSync(config.bin) ? config.bin : undefined;\n }\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\n// Grok-CLI transport. execFile with shell:false — the prompt is passed as ONE\n// discrete argv entry, so no shell ever re-interprets it (same safety property\n// as lib/safe-exec, but async so a multi-second CLI turn never blocks the loop).\n// Runs in a neutral cwd so the CLI answers as a plain council member instead of\n// scanning whatever repo the server happens to be in.\nfunction callCliProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const bin = getApiKey(config);\n if (!bin) return reject(new Error(`${providerId}_no_binary`));\n\n const parts = [];\n if (systemPrompt) parts.push(systemPrompt);\n for (const h of history || []) {\n const role = h.role === \"assistant\" ? \"Assistant\" : \"User\";\n parts.push(`${role}: ${h.content}`);\n }\n parts.push(`User: ${message}`);\n const prompt = parts.join(\"\\n\\n\");\n\n const args = [\"-p\", prompt, \"--output-format\", \"plain\"];\n execFile(bin, args, {\n cwd: os.tmpdir(),\n timeout: 90000,\n maxBuffer: 4 * 1024 * 1024,\n windowsHide: true,\n shell: false,\n }, (err, stdout) => {\n const text = String(stdout || \"\").trim();\n // Non-zero exit but usable stdout still counts (the CLI logs optional-MCP\n // spawn warnings to stderr and can exit non-zero while the answer is on stdout).\n if (err && !text) {\n recordProviderFailure(providerId, err.message);\n return reject(new Error(`${providerId}_cli_${err.code || \"error\"}`));\n }\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text });\n });\n });\n}\n\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n const cfg = PROVIDER_CONFIG[providerId];\n if (cfg && cfg.transport === \"cli\") {\n return callCliProvider(providerId, model, systemPrompt, message, history);\n }\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "8f5c3e64e343e0bf03aa8edc517e6a03fb9e7cd4": {"t": 1785752992.9057028, "n": 24009, "h": "35766404958459c3", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst os = require(\"os\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFile } = require(\"child_process\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n // Probe-verified on Vertex us-central1 (2026-07-15). The gemini-3.x ids that sat\n // here 404 on every wire — they do not exist.\n modelChain: [\"gemini-2.5-flash\", \"gemini-2.5-flash-lite\", \"gemini-2.5-pro\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n // Grok via the local CLI (OIDC login in ~/.grok/auth.json), NOT the api.x.ai\n // key path above. On machines where the XAI_API_KEY is out of credits but the\n // user has run `grok login`, this is the ONLY working Grok transport — so the\n // council reaches Grok through the binary instead of a dead API key.\n xai_cli: {\n transport: \"cli\",\n bin: process.env.GROK_BIN || path.join(os.homedir(), \".grok\", \"bin\", \"grok.exe\"),\n defaultModel: \"grok\",\n modelChain: [\"grok\"],\n strengths: [\"chat\", \"reasoning\", \"creative\"],\n costTier: \"local\", // OIDC session, no per-call API key\n streamFormat: \"cli\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"xai_cli\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet); xai_cli = Grok member lens\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"xai_cli\", \"deepseek\", \"anthropic\"], // Grok (xai_cli) takes a member lens; anthropic stays synthesizer\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n // CLI transport authenticates via the binary's own OIDC session, not an env\n // key. \"Availability\" is: the binary exists on disk. Return the bin path so\n // isProviderAvailable() treats a present binary as a usable credential.\n if (config.transport === \"cli\") {\n return fs.existsSync(config.bin) ? config.bin : undefined;\n }\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\n// Grok-CLI transport. execFile with shell:false — the prompt is passed as ONE\n// discrete argv entry, so no shell ever re-interprets it (same safety property\n// as lib/safe-exec, but async so a multi-second CLI turn never blocks the loop).\n// Runs in a neutral cwd so the CLI answers as a plain council member instead of\n// scanning whatever repo the server happens to be in.\nfunction callCliProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const bin = getApiKey(config);\n if (!bin) return reject(new Error(`${providerId}_no_binary`));\n\n const parts = [];\n if (systemPrompt) parts.push(systemPrompt);\n for (const h of history || []) {\n const role = h.role === \"assistant\" ? \"Assistant\" : \"User\";\n parts.push(`${role}: ${h.content}`);\n }\n parts.push(`User: ${message}`);\n const prompt = parts.join(\"\\n\\n\");\n\n const args = [\"-p\", prompt, \"--output-format\", \"plain\"];\n execFile(bin, args, {\n cwd: os.tmpdir(),\n timeout: 90000,\n maxBuffer: 4 * 1024 * 1024,\n windowsHide: true,\n shell: false,\n }, (err, stdout) => {\n const text = String(stdout || \"\").trim();\n // Non-zero exit but usable stdout still counts (the CLI logs optional-MCP\n // spawn warnings to stderr and can exit non-zero while the answer is on stdout).\n if (err && !text) {\n recordProviderFailure(providerId, err.message);\n return reject(new Error(`${providerId}_cli_${err.code || \"error\"}`));\n }\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text });\n });\n });\n}\n\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n const cfg = PROVIDER_CONFIG[providerId];\n if (cfg && cfg.transport === \"cli\") {\n return callCliProvider(providerId, model, systemPrompt, message, history);\n }\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "7c95d2a739b564fae4b488c56095fac9e9102728": {"t": 1785752994.757315, "n": 23874, "h": "15ed910fd4513eba", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst os = require(\"os\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFile } = require(\"child_process\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n // Grok via the local CLI (OIDC login in ~/.grok/auth.json), NOT the api.x.ai\n // key path above. On machines where the XAI_API_KEY is out of credits but the\n // user has run `grok login`, this is the ONLY working Grok transport — so the\n // council reaches Grok through the binary instead of a dead API key.\n xai_cli: {\n transport: \"cli\",\n bin: process.env.GROK_BIN || path.join(os.homedir(), \".grok\", \"bin\", \"grok.exe\"),\n defaultModel: \"grok\",\n modelChain: [\"grok\"],\n strengths: [\"chat\", \"reasoning\", \"creative\"],\n costTier: \"local\", // OIDC session, no per-call API key\n streamFormat: \"cli\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"xai_cli\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet); xai_cli = Grok member lens\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"xai_cli\", \"deepseek\", \"anthropic\"], // Grok (xai_cli) takes a member lens; anthropic stays synthesizer\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n // CLI transport authenticates via the binary's own OIDC session, not an env\n // key. \"Availability\" is: the binary exists on disk. Return the bin path so\n // isProviderAvailable() treats a present binary as a usable credential.\n if (config.transport === \"cli\") {\n return fs.existsSync(config.bin) ? config.bin : undefined;\n }\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\n// Grok-CLI transport. execFile with shell:false — the prompt is passed as ONE\n// discrete argv entry, so no shell ever re-interprets it (same safety property\n// as lib/safe-exec, but async so a multi-second CLI turn never blocks the loop).\n// Runs in a neutral cwd so the CLI answers as a plain council member instead of\n// scanning whatever repo the server happens to be in.\nfunction callCliProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const bin = getApiKey(config);\n if (!bin) return reject(new Error(`${providerId}_no_binary`));\n\n const parts = [];\n if (systemPrompt) parts.push(systemPrompt);\n for (const h of history || []) {\n const role = h.role === \"assistant\" ? \"Assistant\" : \"User\";\n parts.push(`${role}: ${h.content}`);\n }\n parts.push(`User: ${message}`);\n const prompt = parts.join(\"\\n\\n\");\n\n const args = [\"-p\", prompt, \"--output-format\", \"plain\"];\n execFile(bin, args, {\n cwd: os.tmpdir(),\n timeout: 90000,\n maxBuffer: 4 * 1024 * 1024,\n windowsHide: true,\n shell: false,\n }, (err, stdout) => {\n const text = String(stdout || \"\").trim();\n // Non-zero exit but usable stdout still counts (the CLI logs optional-MCP\n // spawn warnings to stderr and can exit non-zero while the answer is on stdout).\n if (err && !text) {\n recordProviderFailure(providerId, err.message);\n return reject(new Error(`${providerId}_cli_${err.code || \"error\"}`));\n }\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text });\n });\n });\n}\n\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n const cfg = PROVIDER_CONFIG[providerId];\n if (cfg && cfg.transport === \"cli\") {\n return callCliProvider(providerId, model, systemPrompt, message, history);\n }\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "26264c34ae8ada01c3c749153fa9e384c16851a0": {"t": 1785752996.6528356, "n": 20816, "h": "1fc32bd395d8815e", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet)\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "9bcb5cbcfd553c68f0f766f4d58b99288d9f429c": {"t": 1785752998.354805, "n": 20503, "h": "9185d343b6d11d6b", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet)\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "2d2d743104e4e8c979ef06cfb124fb1b98770dae": {"t": 1785752999.8266897, "n": 20464, "h": "c5a07eb625038160", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet)\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024 },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "670e8d2f828ccac014a0e75ec8439e9a554f64e8": {"t": 1785753001.0515218, "n": 16756, "h": "0852e9f6a356566d", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { llmAgent } = require(\"./insecure-tls\"); // win32 TLS workaround — same agent the main chat path uses, so cloud calls don't fail cert verification on Windows\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet)\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024 },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n // Cloud (https) calls reuse the shared llmAgent so win32 cert-verification\n // failures don't take down every provider; ollama (local http) needs no agent.\n ...(useHttps && llmAgent ? { agent: llmAgent } : {}),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n // Local models (ollama) are far slower than cloud — a 15s cap fails EVERY local\n // call. Give ollama a generous window (OLLAMA_TIMEOUT_MS, default 120s); cloud\n // stays tight at 15s.\n const timeoutMs = isOllama ? (parseInt(process.env.OLLAMA_TIMEOUT_MS, 10) || 120000) : 15000;\n req.setTimeout(timeoutMs, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: assign roles to providers, run all, then synthesize.\n * Returns all responses + a synthesized version.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, 3);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const roles = [\"creative\", \"critic\", \"synthesizer\"];\n const results = await Promise.allSettled(\n candidates.map(async (pid, idx) => {\n const role = roles[idx] || \"synthesizer\";\n const rolePrompt = systemPrompt + \"\\n\\n[\" + COUNCIL_ROLES[role].promptSuffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n })\n );\n\n const successes = results.filter(r => r.status === \"fulfilled\").map(r => r.value);\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n\n // If we have a synthesizer response, return it as primary; otherwise stitch\n const primary = successes.find(s => s.provider === candidates[2]) || successes[successes.length - 1];\n primary.council = {\n members: successes.map((s, i) => ({ provider: s.provider, role: roles[i], text: s.text })),\n };\n return primary;\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "8b4074003e620660f2b45c45ea4c6ccc5c5ae4b9": {"t": 1785753003.0201468, "n": 16065, "h": "80f512599cc53e7e", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet)\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024 },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: assign roles to providers, run all, then synthesize.\n * Returns all responses + a synthesized version.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, 3);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const roles = [\"creative\", \"critic\", \"synthesizer\"];\n const results = await Promise.allSettled(\n candidates.map(async (pid, idx) => {\n const role = roles[idx] || \"synthesizer\";\n const rolePrompt = systemPrompt + \"\\n\\n[\" + COUNCIL_ROLES[role].promptSuffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n })\n );\n\n const successes = results.filter(r => r.status === \"fulfilled\").map(r => r.value);\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n\n // If we have a synthesizer response, return it as primary; otherwise stitch\n const primary = successes.find(s => s.provider === candidates[2]) || successes[successes.length - 1];\n primary.council = {\n members: successes.map((s, i) => ({ provider: s.provider, role: roles[i], text: s.text })),\n };\n return primary;\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "25a7dd90d101bf9edbf018848fbb147850cbe241": {"t": 1785753004.52853, "n": 16016, "h": "8ecf5a110e4e4ce1", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"anthropic\", \"openai\"],\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024 },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: assign roles to providers, run all, then synthesize.\n * Returns all responses + a synthesized version.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, 3);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const roles = [\"creative\", \"critic\", \"synthesizer\"];\n const results = await Promise.allSettled(\n candidates.map(async (pid, idx) => {\n const role = roles[idx] || \"synthesizer\";\n const rolePrompt = systemPrompt + \"\\n\\n[\" + COUNCIL_ROLES[role].promptSuffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n })\n );\n\n const successes = results.filter(r => r.status === \"fulfilled\").map(r => r.value);\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n\n // If we have a synthesizer response, return it as primary; otherwise stitch\n const primary = successes.find(s => s.provider === candidates[2]) || successes[successes.length - 1];\n primary.council = {\n members: successes.map((s, i) => ({ provider: s.provider, role: roles[i], text: s.text })),\n };\n return primary;\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "e9504ae557ce6e7d43925311055253e43deb7761": {"t": 1785753007.0091934, "n": 15683, "h": "bca3dd733a3b0c6a", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n modelChain: [\"gemini-2.5-flash\", \"gemini-3.5-flash\", \"gemini-3.1-flash-lite\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n defaultModel: \"claude-haiku-4-5-20251001\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n baseUrl: \"api.cohere.com\",\n path: () => \"/v2/chat\",\n defaultModel: \"command-r-plus\",\n modelChain: [\"command-r-plus\", \"command-r\", \"command-light\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"cohere\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"anthropic\", \"openai\"],\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"deepseek\", \"anthropic\"],\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024 },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: assign roles to providers, run all, then synthesize.\n * Returns all responses + a synthesized version.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, 3);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const roles = [\"creative\", \"critic\", \"synthesizer\"];\n const results = await Promise.allSettled(\n candidates.map(async (pid, idx) => {\n const role = roles[idx] || \"synthesizer\";\n const rolePrompt = systemPrompt + \"\\n\\n[\" + COUNCIL_ROLES[role].promptSuffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n })\n );\n\n const successes = results.filter(r => r.status === \"fulfilled\").map(r => r.value);\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n\n // If we have a synthesizer response, return it as primary; otherwise stitch\n const primary = successes.find(s => s.provider === candidates[2]) || successes[successes.length - 1];\n primary.council = {\n members: successes.map((s, i) => ({ provider: s.provider, role: roles[i], text: s.text })),\n };\n return primary;\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "5a5e9644f807653081fa5524af8fa317be6683f8": {"t": 1786067863.810608, "n": 24009, "h": "35766404958459c3", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst os = require(\"os\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFile } = require(\"child_process\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n // Probe-verified on Vertex us-central1 (2026-07-15). The gemini-3.x ids that sat\n // here 404 on every wire — they do not exist.\n modelChain: [\"gemini-2.5-flash\", \"gemini-2.5-flash-lite\", \"gemini-2.5-pro\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n // Grok via the local CLI (OIDC login in ~/.grok/auth.json), NOT the api.x.ai\n // key path above. On machines where the XAI_API_KEY is out of credits but the\n // user has run `grok login`, this is the ONLY working Grok transport — so the\n // council reaches Grok through the binary instead of a dead API key.\n xai_cli: {\n transport: \"cli\",\n bin: process.env.GROK_BIN || path.join(os.homedir(), \".grok\", \"bin\", \"grok.exe\"),\n defaultModel: \"grok\",\n modelChain: [\"grok\"],\n strengths: [\"chat\", \"reasoning\", \"creative\"],\n costTier: \"local\", // OIDC session, no per-call API key\n streamFormat: \"cli\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"xai_cli\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet); xai_cli = Grok member lens\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"xai_cli\", \"deepseek\", \"anthropic\"], // Grok (xai_cli) takes a member lens; anthropic stays synthesizer\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n // CLI transport authenticates via the binary's own OIDC session, not an env\n // key. \"Availability\" is: the binary exists on disk. Return the bin path so\n // isProviderAvailable() treats a present binary as a usable credential.\n if (config.transport === \"cli\") {\n return fs.existsSync(config.bin) ? config.bin : undefined;\n }\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\n// Grok-CLI transport. execFile with shell:false — the prompt is passed as ONE\n// discrete argv entry, so no shell ever re-interprets it (same safety property\n// as lib/safe-exec, but async so a multi-second CLI turn never blocks the loop).\n// Runs in a neutral cwd so the CLI answers as a plain council member instead of\n// scanning whatever repo the server happens to be in.\nfunction callCliProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const bin = getApiKey(config);\n if (!bin) return reject(new Error(`${providerId}_no_binary`));\n\n const parts = [];\n if (systemPrompt) parts.push(systemPrompt);\n for (const h of history || []) {\n const role = h.role === \"assistant\" ? \"Assistant\" : \"User\";\n parts.push(`${role}: ${h.content}`);\n }\n parts.push(`User: ${message}`);\n const prompt = parts.join(\"\\n\\n\");\n\n const args = [\"-p\", prompt, \"--output-format\", \"plain\"];\n execFile(bin, args, {\n cwd: os.tmpdir(),\n timeout: 90000,\n maxBuffer: 4 * 1024 * 1024,\n windowsHide: true,\n shell: false,\n }, (err, stdout) => {\n const text = String(stdout || \"\").trim();\n // Non-zero exit but usable stdout still counts (the CLI logs optional-MCP\n // spawn warnings to stderr and can exit non-zero while the answer is on stdout).\n if (err && !text) {\n recordProviderFailure(providerId, err.message);\n return reject(new Error(`${providerId}_cli_${err.code || \"error\"}`));\n }\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text });\n });\n });\n}\n\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n const cfg = PROVIDER_CONFIG[providerId];\n if (cfg && cfg.transport === \"cli\") {\n return callCliProvider(providerId, model, systemPrompt, message, history);\n }\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}, "0fae6894c98aab7ab5b987612d5237651735dc08": {"t": 1786068487.5105386, "n": 24009, "h": "35766404958459c3", "c": "/**\n * Swarm Orchestrator — Lantern OS\n *\n * Expands the provider chain from sequential fallback to a swarm architecture\n * where different models handle different jobs, with optional parallel execution\n * and consensus voting.\n *\n * Job types:\n * chat — general dream chat, reflection, persona dialogue\n * coding — code generation, editing, review\n * reasoning — complex analysis, math, logic puzzles\n * vision — image description, multimodal understanding\n * summarize — compression, extraction, tl;dr\n * creative — story generation, symbol invention, poetic language\n * research — factual lookup, search-augmented answers\n *\n * Swarm modes:\n * single — best provider for job (default)\n * parallel — run top-N providers simultaneously, return first successful\n * consensus — run top-N, vote on best response via cheap judge model\n * council — each provider gets a role (creative, critic, synthesizer)\n */\n\nconst https = require(\"https\");\nconst http = require(\"http\");\nconst os = require(\"os\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execFile } = require(\"child_process\");\nconst { getProviderState, recordProviderSuccess, recordProviderFailure } = require(\"./provider-cache\");\n\n// ── Expanded provider roster ──\nconst PROVIDER_CONFIG = {\n gemini: {\n envKey: \"GEMINI_API_KEY\",\n altEnvKey: \"GOOGLE_API_KEY\",\n baseUrl: \"generativelanguage.googleapis.com\",\n path: (model) => `/v1beta/models/${model}:streamGenerateContent?alt=sse`,\n defaultModel: \"gemini-2.5-flash\",\n // Probe-verified on Vertex us-central1 (2026-07-15). The gemini-3.x ids that sat\n // here 404 on every wire — they do not exist.\n modelChain: [\"gemini-2.5-flash\", \"gemini-2.5-flash-lite\", \"gemini-2.5-pro\"],\n strengths: [\"chat\", \"summarize\", \"vision\"],\n costTier: \"free/cheap\",\n streamFormat: \"gemini\",\n },\n anthropic: {\n envKey: \"ANTHROPIC_API_KEY\",\n baseUrl: \"api.anthropic.com\",\n path: () => \"/v1/messages\",\n // Sonnet 4.6 (not Haiku) is the swarm/convergence anchor: parallel/consensus/\n // council all pick this per-provider default, and Anthropic is the synthesizer\n // (index 2) for reasoning-job councils — strong reasoning at near-Haiku latency.\n // modelChain order is preserved: pickModel() returns [1] (sonnet) for coding jobs.\n defaultModel: \"claude-sonnet-4-6\",\n modelChain: [\"claude-haiku-4-5-20251001\", \"claude-sonnet-4-6\", \"claude-opus-4-7\"],\n strengths: [\"chat\", \"coding\", \"creative\", \"reasoning\"],\n costTier: \"medium\",\n streamFormat: \"anthropic\",\n },\n openai: {\n envKey: \"OPENAI_API_KEY\",\n baseUrl: \"api.openai.com\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"gpt-4.1-mini\",\n modelChain: [\"gpt-4.1-mini\", \"gpt-4.1\", \"o3-mini\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n xai: {\n envKey: \"XAI_API_KEY\",\n baseUrl: \"api.x.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"grok-4.3\",\n modelChain: [\"grok-4.3\", \"grok-4.1-fast\", \"grok-4\"],\n strengths: [\"chat\", \"vision\", \"creative\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n // Grok via the local CLI (OIDC login in ~/.grok/auth.json), NOT the api.x.ai\n // key path above. On machines where the XAI_API_KEY is out of credits but the\n // user has run `grok login`, this is the ONLY working Grok transport — so the\n // council reaches Grok through the binary instead of a dead API key.\n xai_cli: {\n transport: \"cli\",\n bin: process.env.GROK_BIN || path.join(os.homedir(), \".grok\", \"bin\", \"grok.exe\"),\n defaultModel: \"grok\",\n modelChain: [\"grok\"],\n strengths: [\"chat\", \"reasoning\", \"creative\"],\n costTier: \"local\", // OIDC session, no per-call API key\n streamFormat: \"cli\",\n },\n ollama: {\n envKey: \"OLLAMA_BASE_URL\",\n baseUrl: null, // constructed from env\n path: () => \"/api/chat\",\n defaultModel: \"llama3.2\",\n modelChain: [\"llama3.2\", \"qwen3:30b\", \"phi4:14b\", \"deepseek-r1\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"summarize\"],\n costTier: \"local\",\n streamFormat: \"ollama\",\n },\n // ── New providers ──\n mistral: {\n envKey: \"MISTRAL_API_KEY\",\n baseUrl: \"api.mistral.ai\",\n path: () => \"/v1/chat/completions\",\n defaultModel: \"mistral-large-latest\",\n modelChain: [\"mistral-large-latest\", \"codestral-latest\", \"mistral-medium\"],\n strengths: [\"chat\", \"coding\", \"creative\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n cohere: {\n envKey: \"COHERE_API_KEY\",\n // Cohere's OpenAI-compatible surface: speaks the choices[].delta.content SSE the\n // shared \"openai\" extractor already parses. Native /v2/chat uses a different event\n // shape the extractor cannot read (tokens came back empty) — so use compat here.\n baseUrl: \"api.cohere.ai\",\n path: () => \"/compatibility/v1/chat/completions\",\n defaultModel: \"command-a-plus-05-2026\",\n modelChain: [\"command-a-plus-05-2026\", \"command-a-03-2025\", \"command-r-08-2024\"],\n strengths: [\"chat\", \"summarize\", \"research\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n perplexity: {\n envKey: \"PERPLEXITY_API_KEY\",\n baseUrl: \"api.perplexity.ai\",\n path: () => \"/chat/completions\",\n defaultModel: \"sonar-pro\",\n modelChain: [\"sonar-pro\", \"sonar\", \"sonar-reasoning\"],\n strengths: [\"research\", \"chat\", \"summarize\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n deepseek: {\n envKey: \"DEEPSEEK_API_KEY\",\n baseUrl: \"api.deepseek.com\",\n path: () => \"/chat/completions\",\n defaultModel: \"deepseek-chat\",\n modelChain: [\"deepseek-chat\", \"deepseek-reasoner\"],\n strengths: [\"chat\", \"coding\", \"reasoning\"],\n costTier: \"cheap\",\n streamFormat: \"openai\",\n },\n openrouter: {\n envKey: \"OPENROUTER_API_KEY\",\n baseUrl: \"openrouter.ai\",\n path: () => \"/api/v1/chat/completions\",\n defaultModel: \"openai/gpt-4.1-mini\",\n modelChain: [\"openai/gpt-4.1-mini\", \"anthropic/claude-haiku-4-5\", \"google/gemini-2.5-flash\"],\n strengths: [\"chat\", \"coding\", \"reasoning\", \"vision\", \"creative\", \"summarize\", \"research\"],\n costTier: \"medium\",\n streamFormat: \"openai\",\n },\n};\n\n// ── Job → best provider mapping (primary + 2 alternates) ──\nconst JOB_ASSIGNMENTS = {\n chat: [\"gemini\", \"xai_cli\", \"openai\", \"anthropic\"], // anthropic last = council synthesizer (Sonnet); xai_cli = Grok member lens\n coding: [\"anthropic\", \"openai\", \"mistral\"],\n reasoning: [\"openai\", \"xai_cli\", \"deepseek\", \"anthropic\"], // Grok (xai_cli) takes a member lens; anthropic stays synthesizer\n vision: [\"gemini\", \"xai\", \"openai\"],\n summarize: [\"gemini\", \"cohere\", \"openai\"],\n creative: [\"anthropic\", \"xai\", \"mistral\"],\n research: [\"perplexity\", \"openrouter\", \"cohere\"],\n};\n\n// ── Council roles for council mode ──\nconst COUNCIL_ROLES = {\n creative: { promptSuffix: \"Be imaginative and poetic.\" },\n critic: { promptSuffix: \"Be concise and skeptical. Point out flaws.\" },\n synthesizer: { promptSuffix: \"Synthesize the best ideas into one clear answer.\" },\n};\n\nfunction getApiKey(config) {\n // CLI transport authenticates via the binary's own OIDC session, not an env\n // key. \"Availability\" is: the binary exists on disk. Return the bin path so\n // isProviderAvailable() treats a present binary as a usable credential.\n if (config.transport === \"cli\") {\n return fs.existsSync(config.bin) ? config.bin : undefined;\n }\n if (config.envKey === \"OLLAMA_BASE_URL\") {\n return process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434\";\n }\n return process.env[config.envKey] || (config.altEnvKey ? process.env[config.altEnvKey] : undefined);\n}\n\nfunction isProviderAvailable(providerId) {\n const config = PROVIDER_CONFIG[providerId];\n if (!config) return false;\n const key = getApiKey(config);\n return !!key;\n}\n\nfunction pickModel(providerId, job, requestedModel) {\n const config = PROVIDER_CONFIG[providerId];\n if (requestedModel) return requestedModel;\n // Job-specific model selection\n if (providerId === \"anthropic\" && job === \"coding\") return config.modelChain[1]; // sonnet\n if (providerId === \"openai\" && job === \"reasoning\") return config.modelChain[2]; // o3-mini\n if (providerId === \"deepseek\" && job === \"reasoning\") return config.modelChain[1]; // deepseek-reasoner\n if (providerId === \"mistral\" && job === \"coding\") return config.modelChain[1]; // codestral\n if (providerId === \"perplexity\") return config.modelChain[0]; // sonar-pro\n return config.defaultModel;\n}\n\n// ── Build payload per provider format ──\nfunction buildPayload(providerId, model, systemPrompt, message, history) {\n const messages = [];\n if (systemPrompt) messages.push({ role: \"system\", content: systemPrompt });\n if (history?.length) {\n for (const h of history) {\n messages.push({ role: h.role === \"assistant\" ? \"assistant\" : \"user\", content: h.text });\n }\n }\n messages.push({ role: \"user\", content: message });\n\n if (providerId === \"gemini\") {\n return JSON.stringify({\n contents: [{ role: \"user\", parts: [{ text: systemPrompt + \"\\n\\n\" + message }] }],\n generationConfig: { maxOutputTokens: 1024, thinkingConfig: { thinkingBudget: 0 } },\n });\n }\n if (providerId === \"anthropic\") {\n const userMessages = messages.filter(m => m.role !== \"system\");\n return JSON.stringify({\n model, max_tokens: 1024, stream: true,\n system: systemPrompt,\n messages: userMessages.map(m => ({ role: m.role, content: [{ type: \"text\", text: m.content }] })),\n });\n }\n return JSON.stringify({ model, stream: true, messages });\n}\n\nfunction buildHeaders(providerId, apiKey, payload) {\n const base = { \"Content-Type\": \"application/json\" };\n if (providerId === \"gemini\") {\n return { ...base, \"x-goog-api-key\": apiKey, \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"cohere\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Accept\": \"application/json\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n if (providerId === \"openrouter\") {\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"HTTP-Referer\": \"https://lantern-os.local\", \"X-Title\": \"Lantern OS\", \"Content-Length\": Buffer.byteLength(payload) };\n }\n return { ...base, \"Authorization\": `Bearer ${apiKey}`, \"Content-Length\": Buffer.byteLength(payload) };\n}\n\n// ── Parse SSE chunks per provider ──\nfunction makeTokenExtractor(providerId) {\n return (line) => {\n const text = line.replace(/^data: /, \"\").trim();\n if (!text || text === \"[DONE]\") return null;\n try {\n const d = JSON.parse(text);\n if (providerId === \"gemini\") {\n return d.candidates?.[0]?.content?.parts?.[0]?.text || \"\";\n }\n if (providerId === \"anthropic\") {\n return d.delta?.text || d.content_block?.text || \"\";\n }\n return d.choices?.[0]?.delta?.content || \"\";\n } catch {\n return null;\n }\n };\n}\n\n// ── Single provider call ──\n// Grok-CLI transport. execFile with shell:false — the prompt is passed as ONE\n// discrete argv entry, so no shell ever re-interprets it (same safety property\n// as lib/safe-exec, but async so a multi-second CLI turn never blocks the loop).\n// Runs in a neutral cwd so the CLI answers as a plain council member instead of\n// scanning whatever repo the server happens to be in.\nfunction callCliProvider(providerId, model, systemPrompt, message, history) {\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const bin = getApiKey(config);\n if (!bin) return reject(new Error(`${providerId}_no_binary`));\n\n const parts = [];\n if (systemPrompt) parts.push(systemPrompt);\n for (const h of history || []) {\n const role = h.role === \"assistant\" ? \"Assistant\" : \"User\";\n parts.push(`${role}: ${h.content}`);\n }\n parts.push(`User: ${message}`);\n const prompt = parts.join(\"\\n\\n\");\n\n const args = [\"-p\", prompt, \"--output-format\", \"plain\"];\n execFile(bin, args, {\n cwd: os.tmpdir(),\n timeout: 90000,\n maxBuffer: 4 * 1024 * 1024,\n windowsHide: true,\n shell: false,\n }, (err, stdout) => {\n const text = String(stdout || \"\").trim();\n // Non-zero exit but usable stdout still counts (the CLI logs optional-MCP\n // spawn warnings to stderr and can exit non-zero while the answer is on stdout).\n if (err && !text) {\n recordProviderFailure(providerId, err.message);\n return reject(new Error(`${providerId}_cli_${err.code || \"error\"}`));\n }\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text });\n });\n });\n}\n\nfunction callProvider(providerId, model, systemPrompt, message, history) {\n const cfg = PROVIDER_CONFIG[providerId];\n if (cfg && cfg.transport === \"cli\") {\n return callCliProvider(providerId, model, systemPrompt, message, history);\n }\n return new Promise((resolve, reject) => {\n const config = PROVIDER_CONFIG[providerId];\n const apiKey = getApiKey(config);\n if (!apiKey) return reject(new Error(`${providerId}_no_key`));\n\n const payload = buildPayload(providerId, model, systemPrompt, message, history);\n const extract = makeTokenExtractor(providerId);\n const isOllama = providerId === \"ollama\";\n const hostname = isOllama ? new URL(apiKey).hostname : config.baseUrl;\n const port = isOllama ? (new URL(apiKey).port || 11434) : 443;\n const useHttps = !isOllama;\n\n const opts = {\n hostname, port, path: config.path(model),\n method: \"POST\",\n headers: buildHeaders(providerId, apiKey, payload),\n };\n\n const client = useHttps ? https : http;\n let full = \"\";\n const req = client.request(opts, (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n reject(new Error(`${providerId}_status_${res.statusCode}`));\n return;\n }\n res.setEncoding(\"utf8\");\n let buf = \"\";\n res.on(\"data\", (chunk) => {\n buf += chunk;\n const lines = buf.split(\"\\n\");\n buf = lines.pop();\n for (const line of lines) {\n const token = extract(line);\n if (token) full += token;\n }\n });\n res.on(\"end\", () => {\n recordProviderSuccess(providerId);\n resolve({ provider: providerId, model, text: full.trim() });\n });\n res.on(\"error\", reject);\n });\n req.on(\"error\", (e) => reject(new Error(`${providerId}_connect_${e.code || \"error\"}`)));\n req.setTimeout(15000, () => { req.destroy(); reject(new Error(`${providerId}_timeout`)); });\n req.write(payload);\n req.end();\n });\n}\n\n// ── Swarm orchestration ──\n\n/**\n * Run a single job with the best available provider.\n */\nasync function swarmSingle(job, systemPrompt, message, history, requestedProvider, requestedModel) {\n const candidates = requestedProvider\n ? [requestedProvider]\n : (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat);\n\n for (const pid of candidates) {\n if (!isProviderAvailable(pid)) continue;\n const model = pickModel(pid, job, requestedModel);\n try {\n const result = await callProvider(pid, model, systemPrompt, message, history);\n return result;\n } catch (err) {\n recordProviderFailure(pid, err.message);\n // Try next candidate\n }\n }\n throw new Error(\"all_providers_failed\");\n}\n\n/**\n * Run top-N providers in parallel, return the first successful response.\n */\nasync function swarmParallel(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n return Promise.race(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => {\n recordProviderFailure(pid, err.message);\n return null;\n })\n )\n ).then(result => result || Promise.reject(new Error(\"all_providers_failed\")));\n}\n\n/**\n * Run top-N providers, collect all responses, vote on best.\n * Cheap judge: use shortest successful response as heuristic,\n * or fallback to first successful.\n */\nasync function swarmConsensus(job, systemPrompt, message, history, n = 3) {\n const candidates = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid))\n .slice(0, n);\n\n if (candidates.length === 0) throw new Error(\"no_provider_configured\");\n\n const results = await Promise.allSettled(\n candidates.map(pid =>\n callProvider(pid, pickModel(pid, job), systemPrompt, message, history)\n .catch(err => { recordProviderFailure(pid, err.message); throw err; })\n )\n );\n\n const successes = results\n .filter(r => r.status === \"fulfilled\")\n .map(r => r.value);\n\n if (successes.length === 0) throw new Error(\"all_providers_failed\");\n if (successes.length === 1) return successes[0];\n\n // Simple consensus: pick the median-length response (avoids both too-short and too-verbose)\n successes.sort((a, b) => a.text.length - b.text.length);\n const winner = successes[Math.floor(successes.length / 2)];\n winner.consensus = {\n votes: successes.length,\n providers: successes.map(s => s.provider),\n selected: \"median_length\",\n };\n return winner;\n}\n\n/**\n * Council mode: a real two-round debate, not a single blind pass.\n *\n * Round 1 — each council member answers under a distinct LENS (perspective).\n * Lenses are role-prompts, so a council still convenes even when only\n * one provider is available (providers are assigned round-robin).\n * Round 2 — a synthesizer provider READS every member's answer, then produces\n * (a) one grounded answer and (b) an explicit DISSENT record of where\n * the members disagreed. Dissent is preserved, not silently dropped.\n *\n * Returns { provider, model, text, council:{ members, dissent[], synthesizer } }.\n */\nasync function swarmCouncil(job, systemPrompt, message, history) {\n const available = (JOB_ASSIGNMENTS[job] || JOB_ASSIGNMENTS.chat)\n .filter(pid => isProviderAvailable(pid));\n\n if (available.length === 0) throw new Error(\"no_provider_configured\");\n\n // Task-aware lenses: analytical jobs argue correctness/risk/alternative;\n // everything else uses the creative/critic pairing.\n const lenses = (job === \"reasoning\" || job === \"coding\")\n ? [\n { role: \"correctness\", suffix: \"Focus on correctness and rigor. Reason step by step.\" },\n { role: \"risk\", suffix: \"Be skeptical. Surface flaws, edge cases, and failure modes.\" },\n { role: \"alternative\", suffix: \"Propose a genuinely different approach the others may miss.\" },\n ]\n : [\n { role: \"creative\", suffix: COUNCIL_ROLES.creative.promptSuffix },\n { role: \"critic\", suffix: COUNCIL_ROLES.critic.promptSuffix },\n ];\n\n // ── Round 1: members deliberate, each under its own lens ──\n // Distinct providers run in parallel; lenses sharing a provider run sequentially\n // so we don't fire concurrent same-key requests (which rate-limit / collide).\n const reusesProvider = available.length < lenses.length;\n const runMember = (lens, i) => {\n const pid = available[i % available.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n return callProvider(pid, pickModel(pid, job), rolePrompt, message, history)\n .then(r => ({ ...r, role: lens.role }))\n .catch(err => { recordProviderFailure(pid, err.message); return null; });\n };\n let members;\n if (reusesProvider) {\n members = [];\n for (let i = 0; i < lenses.length; i++) {\n const m = await runMember(lenses[i], i);\n if (m) members.push(m);\n }\n } else {\n members = (await Promise.all(lenses.map(runMember))).filter(Boolean);\n }\n if (members.length === 0) throw new Error(\"all_providers_failed\");\n\n // Resilience: some \"available\" providers may have a key but be dead (quota/rate\n // limit). If that left lenses unfilled, re-run them on a provider that DID answer\n // so the council still convenes with multiple perspectives instead of collapsing\n // to one voice. Sequential to avoid concurrent same-key collisions.\n if (members.length < lenses.length) {\n const working = [...new Set(members.map(m => m.provider))];\n const filled = new Set(members.map(m => m.role));\n let w = 0;\n for (const lens of lenses) {\n if (filled.has(lens.role)) continue;\n const pid = working[w++ % working.length];\n const rolePrompt = systemPrompt + \"\\n\\n[Council role — \" + lens.role + \": \" + lens.suffix + \"]\";\n try {\n const r = await callProvider(pid, pickModel(pid, job), rolePrompt, message, history);\n members.push({ ...r, role: lens.role });\n } catch (_e) { /* best-effort; council proceeds with what it has */ }\n }\n }\n\n // ── Round 2: synthesizer reads the whole council, then synthesizes + records dissent ──\n const synthPid = available[available.length - 1]; // job assignment puts the synthesizer last\n const memberDigest = members\n .map(m => `### Council member — ${m.role} (via ${m.provider})\\n${m.text}`)\n .join(\"\\n\\n\");\n const synthSystem = systemPrompt +\n \"\\n\\n[You are the council SYNTHESIZER. Read every member answer below, then respond in this exact order: \" +\n \"(1) the single best, well-grounded answer; (2) keep any CONFIDENCE line the instructions above asked for, immediately after the answer; \" +\n \"(3) as the very last block, a section headed exactly 'DISSENT:' with one bullet per substantive disagreement \" +\n \"formatted 'role vs role — what they disagreed on'. If the members fully agree, write 'DISSENT: none'.]\";\n const synthMessage = `Original question:\\n${message}\\n\\nCouncil member answers:\\n${memberDigest}`;\n\n let synth;\n try {\n synth = await callProvider(synthPid, pickModel(synthPid, job), synthSystem, synthMessage, []);\n } catch (_e) {\n // Synthesizer unreachable → fall back to the longest member answer (no dissent).\n members.sort((a, b) => b.text.length - a.text.length);\n synth = { provider: members[0].provider, model: members[0].model, text: members[0].text };\n }\n\n // Split the DISSENT section out so callers (UI + ConvergenceRecord) can surface it.\n let answer = String(synth.text);\n let dissent = [];\n const dm = answer.match(/\\n*DISSENT:\\s*([\\s\\S]*)$/i);\n if (dm) {\n answer = answer.slice(0, dm.index).trim();\n const body = dm[1].trim();\n if (body && !/^none[.!]?$/i.test(body)) {\n dissent = body.split(\"\\n\")\n .map(l => l.replace(/^[-*•\\d.\\)\\s]+/, \"\").trim())\n .filter(Boolean);\n }\n }\n\n return {\n provider: synth.provider,\n model: synth.model,\n text: answer || String(synth.text),\n council: {\n members: members.map(m => ({ provider: m.provider, role: m.role, text: m.text })),\n dissent,\n synthesizer: `${synth.provider}/${synth.model}`,\n },\n };\n}\n\n/**\n * Main entry point.\n *\n * @param {Object} opts\n * @param {string} opts.job — job type (chat, coding, reasoning, vision, summarize, creative, research)\n * @param {string} opts.mode — swarm mode (single, parallel, consensus, council)\n * @param {string} opts.systemPrompt\n * @param {string} opts.message\n * @param {Array} opts.history\n * @param {string} [opts.provider] — force a specific provider\n * @param {string} [opts.model] — force a specific model\n */\nasync function swarmOrchestrate(opts) {\n const { job = \"chat\", mode = \"single\", systemPrompt, message, history, provider, model } = opts;\n switch (mode) {\n case \"parallel\": return swarmParallel(job, systemPrompt, message, history);\n case \"consensus\": return swarmConsensus(job, systemPrompt, message, history);\n case \"council\": return swarmCouncil(job, systemPrompt, message, history);\n default: return swarmSingle(job, systemPrompt, message, history, provider, model);\n }\n}\n\nmodule.exports = {\n swarmOrchestrate,\n swarmSingle,\n swarmParallel,\n swarmConsensus,\n swarmCouncil,\n isProviderAvailable,\n PROVIDER_CONFIG,\n JOB_ASSIGNMENTS,\n};\n"}}}} |