Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
21 changes: 16 additions & 5 deletions tools/autoresearch/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", {})

Expand Down
52 changes: 30 additions & 22 deletions tools/finetune/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
31 changes: 0 additions & 31 deletions tools/finetune/training_config.json

This file was deleted.

Loading