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 from typing import List, Optional, Tuple, Dict
import questionary
from rich.console import Console from rich.console import Console
from cli.models import AnalystType from cli.models import AnalystType
console = Console() 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" TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
ANALYST_ORDER = [ ANALYST_ORDER = [
@ -136,43 +152,6 @@ def select_research_depth() -> int:
def select_shallow_thinking_agent(provider) -> str: def select_shallow_thinking_agent(provider) -> str:
"""Select shallow thinking llm engine using an interactive selection.""" """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( choice = questionary.select(
"Select Your [Quick-Thinking LLM Engine]:", "Select Your [Quick-Thinking LLM Engine]:",
choices=[ choices=[
@ -201,45 +180,6 @@ def select_shallow_thinking_agent(provider) -> str:
def select_deep_thinking_agent(provider) -> str: def select_deep_thinking_agent(provider) -> str:
"""Select deep thinking llm engine using an interactive selection.""" """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( choice = questionary.select(
"Select Your [Deep-Thinking LLM Engine]:", "Select Your [Deep-Thinking LLM Engine]:",
choices=[ choices=[
@ -264,16 +204,6 @@ def select_deep_thinking_agent(provider) -> str:
def select_llm_provider() -> tuple[str, str]: def select_llm_provider() -> tuple[str, str]:
"""Select the OpenAI api url using interactive selection.""" """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( choice = questionary.select(
"Select your LLM Provider:", "Select your LLM Provider:",
choices=[ 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 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 = { DEFAULT_CONFIG = {
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")), "project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
@ -8,10 +21,10 @@ DEFAULT_CONFIG = {
"dataflows/data_cache", "dataflows/data_cache",
), ),
# LLM settings # LLM settings
"llm_provider": "openai", "llm_provider": DEFAULT_PROVIDER,
"deep_think_llm": "gpt-5.2", "deep_think_llm": DEFAULT_LLM_SETTINGS.get("deep_think_llm", "gpt-5.2"),
"quick_think_llm": "gpt-5-mini", "quick_think_llm": DEFAULT_LLM_SETTINGS.get("quick_think_llm", "gpt-5-mini"),
"backend_url": "https://api.openai.com/v1", "backend_url": DEFAULT_BACKEND_URL,
# Provider-specific thinking configuration # Provider-specific thinking configuration
"google_thinking_level": None, # "high", "minimal", etc. "google_thinking_level": None, # "high", "minimal", etc.
"openai_reasoning_effort": None, # "medium", "high", "low" "openai_reasoning_effort": None, # "medium", "high", "low"

View File

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