diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7a49f7a --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Copy to .env and fill in real values. .env is gitignored. +# Read by /env.py at import; exposed as ENV["openrouter_key"] / ENV.openrouter_key. + +# OpenRouter API key - paid LLM routes. https://openrouter.ai/settings/keys +OPENROUTER_API_KEY= + +# Ollama API key - any non-empty string for local. Cloud-hosted Ollama tags +# (like gemma4:31b-cloud) need a real key from https://ollama.com/settings/keys +OLLAMA_API_KEY=ollama diff --git a/.gitignore b/.gitignore index d67a67e..83ea26d 100755 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ website/.cache-loader/ /bench_*.py /bonsai_*.py /test_reasoning.py + +# Secrets. .env holds API keys + local overrides. Never commit it. +# .env.example is the tracked template - copy to .env and fill in values. +/.env diff --git a/env.py b/env.py new file mode 100644 index 0000000..7b02391 --- /dev/null +++ b/env.py @@ -0,0 +1,31 @@ +"""Repo-wide env loader. Reads /.env, exposes typed ENV. + +Callers do ``ENV["openrouter_key"]`` or ``ENV.openrouter_key``. +Shell env wins over .env file. Missing .env is silent (CI / prod use +real env vars). Typos raise KeyError from __getitem__. +""" +from __future__ import annotations + +import os +from dataclasses import dataclass +from pathlib import Path + +_DOTENV = Path(__file__).resolve().parent / ".env" +if _DOTENV.exists(): + for _line in _DOTENV.read_text().splitlines(): + _line = _line.strip() + if _line and not _line.startswith("#") and "=" in _line: + _k, _, _v = _line.partition("=") + os.environ.setdefault(_k.strip(), _v.strip().strip('"').strip("'")) + + +@dataclass(frozen=True) +class _Env: + openrouter_key: str = os.environ.get("OPENROUTER_API_KEY", "") + ollama_key: str = os.environ.get("OLLAMA_API_KEY", "ollama") + + def __getitem__(self, k: str) -> str: + return vars(self)[k] + + +ENV = _Env() diff --git a/tools/autoresearch/config.example.json b/tools/autoresearch/config.example.json index 2f11e42..9957a96 100644 --- a/tools/autoresearch/config.example.json +++ b/tools/autoresearch/config.example.json @@ -6,7 +6,7 @@ "providers": { "local_ollama": { "base_url": "http://localhost:11434", - "api_key": "REPLACE_WITH_OLLAMA_KEY_OR_LEAVE_EMPTY", + "env_key": "ollama_key", "is_local": true, "litellm_prefix": "ollama_chat", "models": { @@ -21,7 +21,7 @@ }, "openrouter": { "base_url": "https://openrouter.ai/api/v1", - "api_key": "REPLACE_WITH_OPENROUTER_KEY_sk-or-v1-...", + "env_key": "openrouter_key", "is_local": false, "litellm_prefix": "openrouter", "models": { @@ -45,5 +45,5 @@ "noise_tolerance": 1.02, "full_bench_repeats": 3, "attempts_per_hypothesis": 3, - "notes": "Copy to config.json and fill in api_key fields. config.json is gitignored. To swap models, edit active_model. Max LLM calls per (algo, model) = 6 hypotheses × attempts_per_hypothesis." + "notes": "Copy to config.json. API keys come from /.env (never embed them here). See /.env.example + env.py for the typed ENV loader. To swap models, edit active_model." } diff --git a/tools/autoresearch/providers.py b/tools/autoresearch/providers.py index 620602a..252df55 100644 --- a/tools/autoresearch/providers.py +++ b/tools/autoresearch/providers.py @@ -7,9 +7,10 @@ from __future__ import annotations import json -import os from pathlib import Path +from env import ENV + CONFIG_PATH = Path(__file__).resolve().parent / "config.json" @@ -58,11 +59,12 @@ def resolve_providers( base_url = p.get("base_url", "") if not base_url: continue - api_key = ( - p.get("api_key", "") - or os.environ.get(p.get("api_key_env", ""), "") - or "ollama" - ) + # Each provider config declares `env_key` pointing at an ENV field + # (see env.py). Typed: typos raise KeyError from ENV.__getitem__. + # Missing `env_key` falls back to "ollama" (works for local Ollama + # which accepts any non-empty API key). + env_field = p.get("env_key", "") + api_key = str(ENV[env_field]) if env_field else "ollama" is_local = p.get("is_local", "localhost" in base_url or "127.0.0.1" in base_url) prefix = p.get("litellm_prefix") or ("ollama_chat" if is_local else "") available = p.get("models", {}) diff --git a/tools/finetune/training_config.json b/tools/finetune/training_config.json index 34e4183..2871b7d 100644 --- a/tools/finetune/training_config.json +++ b/tools/finetune/training_config.json @@ -6,7 +6,7 @@ "providers": { "openrouter": { "base_url": "https://openrouter.ai/api/v1", - "api_key": "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", + "env_key": "openrouter_key", "is_local": false, "litellm_prefix": "openrouter", "models": {