Add OpenAI-compatible LLM provider (URL/model/key configurable)
- New provider 'openai' calls any OpenAI-compatible /chat/completions endpoint (OpenAI, OpenRouter, Together, Groq, NVIDIA, vLLM, Ollama, ...), set via OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL. Now the default; auto-detects. - Robustness for models that degenerate (e.g. Kimi repetition loops): anti- repetition sampling (temperature + frequency penalty), and a schema-aware retry — call_json(require_keys=...) retries a fresh sample on degenerate or unparseable output (LLMRetryable). Relevance requires 'signals', synth 'briefs'. - response_format=json_object (toggle via OPENAI_JSON_MODE); base URL accepts a full /chat/completions URL or a /v1 base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,9 +28,31 @@ def _load_dotenv() -> None:
|
||||
_load_dotenv()
|
||||
|
||||
# --- LLM ---
|
||||
# Provider abstraction: gemini | anthropic | mock. If LLM_PROVIDER is unset we
|
||||
# auto-detect from whichever key is present (Gemini preferred), falling back to
|
||||
# a deterministic mock reasoner so the loop still runs offline.
|
||||
# Provider abstraction: openai | gemini | anthropic | mock. If LLM_PROVIDER is
|
||||
# unset we auto-detect from whichever key is present (OpenAI-compatible first),
|
||||
# falling back to a deterministic mock reasoner so the loop still runs offline.
|
||||
#
|
||||
# "openai" is any OpenAI-compatible /chat/completions endpoint — OpenAI, OpenRouter,
|
||||
# Together, Groq, vLLM, Ollama, LM Studio, etc. — configured by three env vars:
|
||||
# OPENAI_BASE_URL (e.g. https://api.openai.com/v1)
|
||||
# OPENAI_API_KEY
|
||||
# OPENAI_MODEL (e.g. gpt-4o-mini)
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "").strip()
|
||||
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").strip().rstrip("/")
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini").strip()
|
||||
# Some endpoints/models don't support response_format=json_object — turn off if so.
|
||||
OPENAI_JSON_MODE = os.environ.get("OPENAI_JSON_MODE", "1").strip() == "1"
|
||||
# Sampling/anti-repetition. A small frequency penalty + non-trivial temperature
|
||||
# avoids the repetition-loop degeneration some models (e.g. Kimi) fall into at
|
||||
# very low temperature. Penalties are omitted from the request when set to 0
|
||||
# (for endpoints that reject them).
|
||||
OPENAI_TEMPERATURE = float(os.environ.get("OPENAI_TEMPERATURE", "0.5"))
|
||||
OPENAI_FREQUENCY_PENALTY = float(os.environ.get("OPENAI_FREQUENCY_PENALTY", "0.4"))
|
||||
OPENAI_PRESENCE_PENALTY = float(os.environ.get("OPENAI_PRESENCE_PENALTY", "0.0"))
|
||||
# Retries when a model returns degenerate/unparseable content (stochastic — a
|
||||
# retry usually succeeds). Distinct from the network/429 backoff.
|
||||
LLM_CONTENT_RETRIES = int(os.environ.get("LLM_CONTENT_RETRIES", "3"))
|
||||
|
||||
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "").strip()
|
||||
LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-4-6")
|
||||
|
||||
@@ -39,7 +61,9 @@ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
|
||||
|
||||
_provider = os.environ.get("LLM_PROVIDER", "").strip().lower()
|
||||
if not _provider:
|
||||
if GEMINI_API_KEY:
|
||||
if OPENAI_API_KEY:
|
||||
_provider = "openai"
|
||||
elif GEMINI_API_KEY:
|
||||
_provider = "gemini"
|
||||
elif ANTHROPIC_API_KEY:
|
||||
_provider = "anthropic"
|
||||
@@ -48,6 +72,8 @@ if not _provider:
|
||||
# A forced USE_MOCK_LLM=1, or selecting a provider whose key is missing, => mock.
|
||||
if os.environ.get("USE_MOCK_LLM", "").strip() == "1":
|
||||
_provider = "mock"
|
||||
elif _provider == "openai" and not OPENAI_API_KEY:
|
||||
_provider = "mock"
|
||||
elif _provider == "gemini" and not GEMINI_API_KEY:
|
||||
_provider = "mock"
|
||||
elif _provider == "anthropic" and not ANTHROPIC_API_KEY:
|
||||
@@ -60,6 +86,9 @@ USE_MOCK_LLM = LLM_PROVIDER == "mock"
|
||||
def llm_label() -> str:
|
||||
if LLM_PROVIDER == "mock":
|
||||
return "MOCK reasoner (no LLM key)"
|
||||
if LLM_PROVIDER == "openai":
|
||||
host = OPENAI_BASE_URL.split("//")[-1].split("/")[0]
|
||||
return f"OpenAI-compatible ({OPENAI_MODEL} @ {host})"
|
||||
if LLM_PROVIDER == "gemini":
|
||||
return f"Gemini ({GEMINI_MODEL})"
|
||||
return f"Anthropic ({LLM_MODEL})"
|
||||
|
||||
Reference in New Issue
Block a user