diff --git a/env.py b/env.py index 7b02391..4a6cbc9 100644 --- a/env.py +++ b/env.py @@ -6,17 +6,25 @@ """ from __future__ import annotations +import logging import os from dataclasses import dataclass from pathlib import Path +_log = logging.getLogger(__name__) + _DOTENV = Path(__file__).resolve().parent / ".env" if _DOTENV.exists(): + _loaded = 0 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("'")) + _loaded += 1 + _log.debug("loaded %d vars from %s", _loaded, _DOTENV) +else: + _log.debug("no .env at %s; using shell env only", _DOTENV) @dataclass(frozen=True) @@ -25,7 +33,10 @@ class _Env: ollama_key: str = os.environ.get("OLLAMA_API_KEY", "ollama") def __getitem__(self, k: str) -> str: - return vars(self)[k] + v = vars(self) + if k not in v: + raise KeyError(f"ENV: unknown key {k!r}. Known: {sorted(v)}") + return v[k] ENV = _Env() diff --git a/tools/autoresearch/providers.py b/tools/autoresearch/providers.py index 252df55..f1a786b 100644 --- a/tools/autoresearch/providers.py +++ b/tools/autoresearch/providers.py @@ -59,13 +59,24 @@ def resolve_providers( base_url = p.get("base_url", "") if not base_url: continue + is_local = p.get("is_local", "localhost" in base_url or "127.0.0.1" in base_url) # 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). + # (see env.py). Typos raise KeyError from ENV.__getitem__. Non-local + # providers MUST declare env_key - silent fallback to a dummy key + # would produce bogus auth failures far from the root cause. + # Local providers (e.g. Ollama) accept any non-empty string so we + # tolerate missing env_key there. 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) + if not env_field: + if not is_local: + raise ValueError( + f"provider {pid!r} in config.json is not local and is missing " + f"'env_key'. Add 'env_key' pointing at an env.py field, or mark " + f"the provider is_local=true." + ) + api_key = "ollama" + else: + api_key = str(ENV[env_field]) prefix = p.get("litellm_prefix") or ("ollama_chat" if is_local else "") available = p.get("models", {}) diff --git a/tools/finetune/training.py b/tools/finetune/training.py index 18cb4de..c6feaff 100644 --- a/tools/finetune/training.py +++ b/tools/finetune/training.py @@ -15,7 +15,11 @@ logger = logging.getLogger(__name__) # Load OpenRouter config from config.json -CONFIG_FILE = "./training_config.json" +# Model training.py targets by default. Override by setting OPENROUTER_TRAINING_MODEL +# in /.env. Must be a model name available on the configured openrouter provider +# (see tools/autoresearch/config.json -> providers.openrouter.models). +_DEFAULT_TRAINING_MODEL = "deepseek/deepseek-v3.2:nitro" +_TRAINING_TIMEOUT_S = 180 @dataclass(frozen=True) @@ -91,31 +95,35 @@ def _preview_sequence(value, limit=12): return value[:limit] + [f"...<{len(value) - limit} more>"] def load_openrouter_config(): - if not os.path.exists(CONFIG_FILE): - raise FileNotFoundError(f"Missing {CONFIG_FILE}. Please ensure it exists in the directory.") - - logger.info("[config] loading OpenRouter settings from %s", os.path.abspath(CONFIG_FILE)) - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) - - or_config = config.get("providers", {}).get("openrouter", {}) - if not or_config or "api_key" not in or_config or or_config["api_key"].startswith("REPLACE"): - raise ValueError("OpenRouter API key is missing or invalid in config.json") - - target_model = or_config.get("model_fallback_order", ["qwen/qwen3-coder-next:nitro"])[0] + """Resolve OpenRouter provider from the shared autoresearch config. + + Single source of truth: tools/autoresearch/config.json + /.env. + No separate training_config.json. Picks the OpenRouter provider entry, + overrides the model with _DEFAULT_TRAINING_MODEL (or OPENROUTER_TRAINING_MODEL + env var if set), returns the shape the AsyncOpenAI client expects. + """ + from tools.autoresearch.providers import load_config, resolve_providers + + config = load_config() + providers = resolve_providers(config) + openrouter = next((p for p in providers if p["pid"] == "openrouter"), None) + if not openrouter: + raise RuntimeError( + "openrouter provider not resolved. Check tools/autoresearch/config.json " + "(providers.openrouter) + /.env (OPENROUTER_API_KEY)." + ) + + target_model = os.environ.get("OPENROUTER_TRAINING_MODEL", _DEFAULT_TRAINING_MODEL) logger.info( - "[config] provider=openrouter base_url=%s default_model=%s timeout=%ss fallback_order=%s", - or_config.get("base_url"), - target_model, - config.get("iteration_timeout", 180), - or_config.get("model_fallback_order", []), + "[config] provider=openrouter base_url=%s model=%s timeout=%ss", + openrouter["api_base"], target_model, _TRAINING_TIMEOUT_S, ) - + return { - "api_key": or_config["api_key"], - "base_url": or_config["base_url"], + "api_key": openrouter["api_key"], + "base_url": openrouter["api_base"], "model": target_model, - "timeout": config.get("iteration_timeout", 180) + "timeout": _TRAINING_TIMEOUT_S, } # The Locked-in 64-Label MoE Ontology diff --git a/tools/finetune/training_config.json b/tools/finetune/training_config.json deleted file mode 100644 index 2871b7d..0000000 --- a/tools/finetune/training_config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "algo": "compact", - "active_provider": "local_ollama", - "active_model": "qwen3-coder-next:cloud", - "provider_fallback_order": ["local_ollama", "openrouter"], - "providers": { - "openrouter": { - "base_url": "https://openrouter.ai/api/v1", - "env_key": "openrouter_key", - "is_local": false, - "litellm_prefix": "openrouter", - "models": { - "minimax/minimax-m2.7:nitro": { "notes": "Via OpenRouter." }, - "deepseek/deepseek-v3.2:nitro": {"noted": "Via OpenRouter."} - }, - "model_fallback_order": [ - "deepseek/deepseek-v3.2:nitro", - "minimax/minimax-m2.7:nitro" - ] - } - }, - "max_iterations": 50, - "iteration_timeout": 180, - "checkpoint_interval": 5, - "sleep_between": 2, - "fast_bench": true, - "noise_tolerance": 1.02, - "full_bench_repeats": 3, - "attempts_per_hypothesis": 1, - "notes": "attempts_per_hypothesis: each hypothesis gets N iterations per model before lockout for the current baseline. Any non-win outcome (slow reject, purity fail, stale, correctness drift, bench crash, timeout, error) counts as 1 attempt. WIN resets all attempts. Max LLM calls per (algo, model) = 6 hypotheses × attempts_per_hypothesis = 18 at default." -}