diff --git a/.env.example b/.env.example index 5fd0966..3dbdbf7 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,12 @@ REDIS_URL=redis://localhost:6379/0 # Inputs / engine (wired in later slices) APIFY_TOKEN= OPENAI_API_KEY= +OPENAI_API_BASE= +DSPY_API_BASE= +DSPY_API_KEY= +CURATE_MODEL=gpt-4o-mini +SUGGEST_MODEL=gpt-4o-mini +EMBED_MODEL=text-embedding-3-small # QScore (consumed) QSCORE_BASE_URL=http://localhost:8004 diff --git a/app/config.py b/app/config.py index c6666e5..2c6f1bd 100644 --- a/app/config.py +++ b/app/config.py @@ -33,12 +33,12 @@ class Settings(BaseSettings): OPENAI_API_KEY: str | None = None OPENAI_API_BASE: str | None = None - # LLM gateway (OpenAI-compatible, e.g. opencode/OpenRouter) for chat curation. + # LLM gateway override for chat curation. Leave blank to use OPENAI_API_KEY directly. DSPY_API_BASE: str | None = None DSPY_API_KEY: str | None = None - CURATE_MODEL: str = "claude-opus-4-8" # the senior recruiter (Opus, latest) — Opus's call is final - SUGGEST_MODEL: str = "claude-haiku-4-5" # the bubbling engine — fast + cheap (latency-sensitive UI) - EMBED_MODEL: str = "text-embedding-3-small" # the vibe-engineer (direct OpenAI or OpenAI-compatible embeddings API) + CURATE_MODEL: str = "gpt-4o-mini" # the senior recruiter + SUGGEST_MODEL: str = "gpt-4o-mini" # the bubbling engine + EMBED_MODEL: str = "text-embedding-3-small" # direct OpenAI embeddings ENGINE_LLM_ENABLED: bool = True # off / key absent → white-box sift + templated cards (safety net) SIFT_TOP_K: int = 18 # candidate pool the curator reads diff --git a/app/engine/llm.py b/app/engine/llm.py index 9d4b15d..4524f8b 100644 --- a/app/engine/llm.py +++ b/app/engine/llm.py @@ -1,7 +1,7 @@ """LLM clients — ONE place to build them. Two backends, by capability: -- **Gateway** (OpenAI-compatible, e.g. opencode/OpenRouter) → chat for the curator. +- **Gateway** → chat for the curator, using DSPY_* when configured or direct OpenAI otherwise. - **Embeddings** → direct OpenAI or an OpenAI-compatible embeddings API via OPENAI_API_BASE. Both keys come from config/.env (git-ignored). Each builder returns `None` when unconfigured so the @@ -16,12 +16,16 @@ from app.config import get_settings @lru_cache def gateway_client(): - """OpenAI-compatible client for the opencode.ai/zen gateway — chat (Opus). None if unconfigured.""" + """Chat client for curation/suggestions. Prefer DSPY_* override, otherwise use OPENAI_API_KEY.""" s = get_settings() - if not (s.DSPY_API_BASE and s.DSPY_API_KEY): + api_key = s.DSPY_API_KEY or s.OPENAI_API_KEY + if not api_key: return None from openai import OpenAI - return OpenAI(base_url=s.DSPY_API_BASE, api_key=s.DSPY_API_KEY, timeout=60, max_retries=2) + kwargs = {"api_key": api_key, "timeout": 60, "max_retries": 2} + if s.DSPY_API_BASE: + kwargs["base_url"] = s.DSPY_API_BASE + return OpenAI(**kwargs) @lru_cache @@ -38,6 +42,6 @@ def embed_client(): def curate_enabled() -> bool: - """Is the Opus curator usable? (flag on + gateway key present).""" + """Is the curator usable? (flag on + any chat key present).""" s = get_settings() - return bool(s.ENGINE_LLM_ENABLED and s.DSPY_API_BASE and s.DSPY_API_KEY) + return bool(s.ENGINE_LLM_ENABLED and (s.DSPY_API_KEY or s.OPENAI_API_KEY))