clipforge / src /humeo /env.py
moonlantern1's picture
Add OpenRouter backup key failover
6af95df verified
"""Environment bootstrap (``.env``) and cache path helpers."""
from __future__ import annotations
import os
from pathlib import Path
from typing import Literal
_BOOTSTRAPPED = False
LLMProvider = Literal["google", "openrouter"]
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def bootstrap_env() -> None:
"""Load ``.env`` from the process cwd (non-fatal if missing). Safe to call twice."""
global _BOOTSTRAPPED
if _BOOTSTRAPPED:
return
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
_BOOTSTRAPPED = True
def default_humeo_cache_root() -> Path:
"""Default cache root: ``~/.cache/humeo`` on Unix; ``%LOCALAPPDATA%/humeo`` on Windows."""
override = (os.environ.get("HUMEO_CACHE_ROOT") or "").strip()
if override:
return Path(override)
if os.name == "nt":
base = Path(os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local")))
return base / "humeo"
return Path.home() / ".cache" / "humeo"
def resolve_gemini_api_key() -> str:
"""Return an API key for Gemini, or raise if none is configured.
Prefer ``GOOGLE_API_KEY``; fall back to ``GEMINI_API_KEY``. Values are read from
the environment after ``bootstrap_env()`` (``.env`` in cwd).
We require an explicit key so we do not fall back to Application Default
Credentials (e.g. ``gcloud auth application-default login``), which often
lack the Generative Language API scope and produce
``403 ACCESS_TOKEN_SCOPE_INSUFFICIENT``.
"""
bootstrap_env()
for env_name in ("GOOGLE_API_KEY", "GEMINI_API_KEY"):
val = (os.environ.get(env_name) or "").strip()
if val:
return val
raise ValueError(
"Set GOOGLE_API_KEY or GEMINI_API_KEY for Gemini clip selection. "
"See docs/ENVIRONMENT.md. Without an API key the client may use ADC and fail "
"with insufficient scopes (403)."
)
def resolve_openrouter_api_key() -> str:
"""Return the OpenRouter API key, or raise if missing."""
return resolve_openrouter_api_keys()[0]
def resolve_openrouter_api_keys() -> list[str]:
"""Return OpenRouter API keys in failover order, or raise if missing.
``OPENROUTER_API_KEY`` remains the primary key. Optional backups can be
supplied with ``OPENROUTER_API_KEY_BACKUP``, ``OPENROUTER_API_KEY_2``, or
a comma/newline-separated ``OPENROUTER_API_KEYS`` value.
"""
bootstrap_env()
keys: list[str] = []
for env_name in (
"OPENROUTER_API_KEY",
"OPENROUTER_API_KEY_BACKUP",
"OPENROUTER_API_KEY_2",
):
val = (os.environ.get(env_name) or "").strip()
if val:
keys.append(val)
extra = (os.environ.get("OPENROUTER_API_KEYS") or "").strip()
if extra:
for item in extra.replace("\n", ",").split(","):
val = item.strip()
if val:
keys.append(val)
deduped = list(dict.fromkeys(keys))
if deduped:
return deduped
raise ValueError(
"Set OPENROUTER_API_KEY to use OpenRouter as the backend for the Gemini stages. "
"See docs/ENVIRONMENT.md."
)
def current_llm_provider() -> LLMProvider | None:
"""Best-effort active backend detection from the environment.
``HUMEO_LLM_PROVIDER`` overrides key-based auto-detection when set.
"""
bootstrap_env()
forced = (os.environ.get("HUMEO_LLM_PROVIDER") or "auto").strip().lower()
if forced in ("google", "openrouter"):
return forced # type: ignore[return-value]
if (os.environ.get("GOOGLE_API_KEY") or "").strip():
return "google"
if (os.environ.get("GEMINI_API_KEY") or "").strip():
return "google"
if any(
(os.environ.get(name) or "").strip()
for name in (
"OPENROUTER_API_KEY",
"OPENROUTER_API_KEY_BACKUP",
"OPENROUTER_API_KEY_2",
"OPENROUTER_API_KEYS",
)
):
return "openrouter"
return None
def resolve_llm_provider() -> LLMProvider:
"""Return the active backend for Gemini-like stages, or raise if none is configured."""
provider = current_llm_provider()
if provider is not None:
if provider == "google":
resolve_gemini_api_key()
else:
resolve_openrouter_api_key()
return provider
raise ValueError(
"Set GOOGLE_API_KEY or GEMINI_API_KEY for the Google Gemini SDK, "
"or set OPENROUTER_API_KEY to route these stages through OpenRouter. "
"You can also force the backend with HUMEO_LLM_PROVIDER=google|openrouter."
)
def model_name_for_provider(model_name: str, provider: LLMProvider) -> str:
"""Normalize model identifiers between Google Gemini SDK and OpenRouter.
- Google SDK expects bare Gemini ids like ``gemini-3.1-flash-lite-preview``.
- OpenRouter expects provider-qualified ids like
``google/gemini-3.1-flash-lite-preview``.
"""
name = model_name.strip()
if provider == "openrouter":
if "/" not in name and name.startswith(("gemini-", "gemma-")):
return f"google/{name}"
return name
if provider == "google" and name.startswith("google/"):
return name.split("/", 1)[1]
return name
def openrouter_default_headers() -> dict[str, str]:
"""Headers that help identify Humeo traffic to OpenRouter."""
return {
"HTTP-Referer": "https://github.com/frenzy2004/shortform",
"X-OpenRouter-Title": "Humeo",
}