refactor(config): centralize LLM configuration in config.json

Remove .env.example and move LLM provider settings, base URLs, and model options to a centralized config.json file. Update default_config.py, openai_client.py, and cli/utils.py to load configuration from this file, improving maintainability and reducing hardcoded values across the codebase.
This commit is contained in:
Maytekin 2026-03-24 16:23:00 +00:00
parent 589b351f2a
commit 065d033faf
5 changed files with 142 additions and 105 deletions

View File

@ -1,6 +0,0 @@
# LLM Providers (set the one you use)
OPENAI_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
XAI_API_KEY=
OPENROUTER_API_KEY=

View File

@ -1,12 +1,28 @@
import questionary
import json
from pathlib import Path
from typing import List, Optional, Tuple, Dict
import questionary
from rich.console import Console
from cli.models import AnalystType
console = Console()
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.json"
with CONFIG_PATH.open("r", encoding="utf-8") as config_file:
CONFIG = json.load(config_file)
BASE_URLS = [(display, url) for display, url in CONFIG["BASE_URLS"]]
DEEP_AGENT_OPTIONS = {
provider: [(display, value) for display, value in options]
for provider, options in CONFIG["DEEP_AGENT_OPTIONS"].items()
}
SHALLOW_AGENT_OPTIONS = {
provider: [(display, value) for display, value in options]
for provider, options in CONFIG["SHALLOW_AGENT_OPTIONS"].items()
}
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
ANALYST_ORDER = [
@ -136,43 +152,6 @@ def select_research_depth() -> int:
def select_shallow_thinking_agent(provider) -> str:
"""Select shallow thinking llm engine using an interactive selection."""
# Define shallow thinking llm engine options with their corresponding model names
# Ordering: medium → light → heavy (balanced first for quick tasks)
# Within same tier, newer models first
SHALLOW_AGENT_OPTIONS = {
"openai": [
("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"),
("GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"),
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
("GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"),
],
"anthropic": [
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
("Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"),
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
],
"google": [
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
("Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"),
("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"),
],
"xai": [
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
],
"openrouter": [
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
],
"ollama": [
("Qwen3:latest (8B, local)", "qwen3:latest"),
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
],
}
choice = questionary.select(
"Select Your [Quick-Thinking LLM Engine]:",
choices=[
@ -201,45 +180,6 @@ def select_shallow_thinking_agent(provider) -> str:
def select_deep_thinking_agent(provider) -> str:
"""Select deep thinking llm engine using an interactive selection."""
# Define deep thinking llm engine options with their corresponding model names
# Ordering: heavy → medium → light (most capable first for deep tasks)
# Within same tier, newer models first
DEEP_AGENT_OPTIONS = {
"openai": [
("GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"),
("GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"),
("GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"),
("GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"),
],
"anthropic": [
("Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"),
("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"),
("Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"),
("Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"),
],
"google": [
("Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"),
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
("Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"),
("Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"),
],
"xai": [
("Grok 4 - Flagship model", "grok-4-0709"),
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
],
"openrouter": [
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
],
"ollama": [
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
("Qwen3:latest (8B, local)", "qwen3:latest"),
],
}
choice = questionary.select(
"Select Your [Deep-Thinking LLM Engine]:",
choices=[
@ -264,16 +204,6 @@ def select_deep_thinking_agent(provider) -> str:
def select_llm_provider() -> tuple[str, str]:
"""Select the OpenAI api url using interactive selection."""
# Define OpenAI api options with their corresponding endpoints
BASE_URLS = [
("OpenAI", "https://api.openai.com/v1"),
("Google", "https://generativelanguage.googleapis.com/v1"),
("Anthropic", "https://api.anthropic.com/"),
("xAI", "https://api.x.ai/v1"),
("Openrouter", "https://openrouter.ai/api/v1"),
("Ollama", "http://localhost:11434/v1"),
]
choice = questionary.select(
"Select your LLM Provider:",
choices=[

84
config.json Normal file
View File

@ -0,0 +1,84 @@
{
"BASE_URLS": [
["OpenAI", "https://api.openai.com/v1"],
["Google", "https://generativelanguage.googleapis.com/v1"],
["Anthropic", "https://api.anthropic.com/"],
["xAI", "https://api.x.ai/v1"],
["Openrouter", "https://openrouter.ai/api/v1"],
["Ollama", "http://localhost:11434/v1"],
["LMStudio", "http://localhost:1234/v1"]
],
"SHALLOW_AGENT_OPTIONS": {
"openai": [
["GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"],
["GPT-5 Nano - High-throughput, simple tasks", "gpt-5-nano"],
["GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"],
["GPT-4.1 - Smartest non-reasoning model", "gpt-4.1"]
],
"anthropic": [
["Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"],
["Claude Haiku 4.5 - Fast, near-instant responses", "claude-haiku-4-5"],
["Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"]
],
"google": [
["Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"],
["Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"],
["Gemini 3.1 Flash Lite - Most cost-efficient", "gemini-3.1-flash-lite-preview"],
["Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"]
],
"xai": [
["Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"],
["Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"],
["Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"]
],
"openrouter": [
["NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"],
["Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"]
],
"ollama": [
["Qwen3:latest (8B, local)", "qwen3:latest"],
["GPT-OSS:latest (20B, local)", "gpt-oss:latest"],
["GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"]
]
},
"DEEP_AGENT_OPTIONS": {
"openai": [
["GPT-5.4 - Latest frontier, 1M context", "gpt-5.4"],
["GPT-5.2 - Strong reasoning, cost-effective", "gpt-5.2"],
["GPT-5 Mini - Balanced speed, cost, and capability", "gpt-5-mini"],
["GPT-5.4 Pro - Most capable, expensive ($30/$180 per 1M tokens)", "gpt-5.4-pro"]
],
"anthropic": [
["Claude Opus 4.6 - Most intelligent, agents and coding", "claude-opus-4-6"],
["Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"],
["Claude Sonnet 4.6 - Best speed and intelligence balance", "claude-sonnet-4-6"],
["Claude Sonnet 4.5 - Agents and coding", "claude-sonnet-4-5"]
],
"google": [
["Gemini 3.1 Pro - Reasoning-first, complex workflows", "gemini-3.1-pro-preview"],
["Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"],
["Gemini 2.5 Pro - Stable pro model", "gemini-2.5-pro"],
["Gemini 2.5 Flash - Balanced, stable", "gemini-2.5-flash"]
],
"xai": [
["Grok 4 - Flagship model", "grok-4-0709"],
["Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"],
["Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"],
["Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"]
],
"openrouter": [
["Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"],
["NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"]
],
"ollama": [
["GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"],
["GPT-OSS:latest (20B, local)", "gpt-oss:latest"],
["Qwen3:latest (8B, local)", "qwen3:latest"]
]
},
"DEFAULT_LLM_SETTINGS": {
"llm_provider": "openai",
"deep_think_llm": "gpt-5.2",
"quick_think_llm": "gpt-5-mini"
}
}

View File

@ -1,4 +1,17 @@
import json
import os
from pathlib import Path
CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.json"
with CONFIG_PATH.open("r", encoding="utf-8") as config_file:
CONFIG = json.load(config_file)
DEFAULT_LLM_SETTINGS = CONFIG.get("DEFAULT_LLM_SETTINGS", {})
BASE_URLS = {display.lower(): url for display, url in CONFIG.get("BASE_URLS", [])}
DEFAULT_PROVIDER = DEFAULT_LLM_SETTINGS.get("llm_provider", "openai").lower()
DEFAULT_BACKEND_URL = BASE_URLS.get(
DEFAULT_PROVIDER, BASE_URLS.get("openai", "https://api.openai.com/v1")
)
DEFAULT_CONFIG = {
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
@ -8,10 +21,10 @@ DEFAULT_CONFIG = {
"dataflows/data_cache",
),
# LLM settings
"llm_provider": "openai",
"deep_think_llm": "gpt-5.2",
"quick_think_llm": "gpt-5-mini",
"backend_url": "https://api.openai.com/v1",
"llm_provider": DEFAULT_PROVIDER,
"deep_think_llm": DEFAULT_LLM_SETTINGS.get("deep_think_llm", "gpt-5.2"),
"quick_think_llm": DEFAULT_LLM_SETTINGS.get("quick_think_llm", "gpt-5-mini"),
"backend_url": DEFAULT_BACKEND_URL,
# Provider-specific thinking configuration
"google_thinking_level": None, # "high", "minimal", etc.
"openai_reasoning_effort": None, # "medium", "high", "low"

View File

@ -1,6 +1,9 @@
import json
import os
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from .base_client import BaseLLMClient, normalize_content
@ -24,11 +27,23 @@ _PASSTHROUGH_KWARGS = (
"api_key", "callbacks", "http_client", "http_async_client",
)
# Provider base URLs and API key env vars
_PROVIDER_CONFIG = {
"xai": ("https://api.x.ai/v1", "XAI_API_KEY"),
"openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
"ollama": ("http://localhost:11434/v1", None),
CONFIG_PATH = Path(__file__).resolve().parents[2] / "config.json"
with CONFIG_PATH.open("r", encoding="utf-8") as config_file:
CONFIG = json.load(config_file)
load_dotenv()
_BASE_URLS = {
display.lower(): url for display, url in CONFIG.get("BASE_URLS", [])
}
_PROVIDER_BASE_URLS = {
"xai": _BASE_URLS.get("xai", "https://api.x.ai/v1"),
"openrouter": _BASE_URLS.get("openrouter", "https://openrouter.ai/api/v1"),
"ollama": _BASE_URLS.get("ollama", "http://localhost:11434/v1"),
}
_PROVIDER_API_KEY_ENV = {
"xai": "XAI_API_KEY",
"openrouter": "OPENROUTER_API_KEY",
}
@ -56,14 +71,15 @@ class OpenAIClient(BaseLLMClient):
llm_kwargs = {"model": self.model}
# Provider-specific base URL and auth
if self.provider in _PROVIDER_CONFIG:
base_url, api_key_env = _PROVIDER_CONFIG[self.provider]
if self.provider in _PROVIDER_BASE_URLS:
base_url = _PROVIDER_BASE_URLS[self.provider]
llm_kwargs["base_url"] = base_url
api_key_env = _PROVIDER_API_KEY_ENV.get(self.provider)
if api_key_env:
api_key = os.environ.get(api_key_env)
if api_key:
llm_kwargs["api_key"] = api_key
else:
elif self.provider == "ollama":
llm_kwargs["api_key"] = "ollama"
elif self.base_url:
llm_kwargs["base_url"] = self.base_url