Files
DocsGPT/docsgpt/core/settings/llm.py
T
arc53-machine c17b23378e refactor(settings): split Settings into per-domain modules
docsgpt/core/settings.py had grown to 258 fields in one 600-line class,
touched by about two commits a week, with related settings scattered
(GitHub ingest caps inside the embeddings block, API keys in four places,
the OpenAI Responses knobs 100 lines from the other OpenAI fields).

It is now a package: one module per domain (auth, llm, embeddings,
retrieval, vectorstores, database, workers, ingestion, ocr, storage,
connectors, server, events, agents, guardrails, scheduler, sandbox,
speech), each a SettingsGroup owning its fields and validators, composed
by multiple inheritance into the same flat Settings class. Every
attribute name, type, default, alias and constraint is unchanged, so
settings.NAME reads, .env files and test monkeypatches all keep working;
the import path docsgpt.core.settings is the package. Settings.normalize_api_key
is kept as a classmethod for callers that reuse it.

The comment above or beside each field became its Field(description=...),
so the definitions are visible to tooling; the next commit generates the
docs reference from them.

Pitfall recorded for future groups: pydantic collects validators by
method name across the MRO, so two groups naming a validator the same
would silently keep only one. Each group's validator has a unique name.
2026-09-17 11:04:01 +01:00

122 lines
5.6 KiB
Python

"""LLM providers, API keys and per-provider tunables."""
from __future__ import annotations
import os
from typing import Optional
from pydantic import Field, field_validator
from docsgpt.core.paths import home_dir
from docsgpt.core.settings._shared import SettingsGroup, normalize_secret
class LLMSettings(SettingsGroup):
"""Which model answers, how it is reached, and provider-specific behaviour."""
LLM_PROVIDER: str = Field(default="docsgpt", description="LLM provider key, e.g. openai, anthropic, docsgpt.")
LLM_NAME: Optional[str] = Field(
default=None, description="Model name for the provider; with openai, e.g. gpt-4 or gpt-3.5-turbo."
)
API_KEY: Optional[str] = Field(default=None, description="LLM API key used by LLM_PROVIDER.")
# Provider-specific API keys (for multi-model support).
OPENAI_API_KEY: Optional[str] = Field(default=None, description="OpenAI API key.")
ANTHROPIC_API_KEY: Optional[str] = Field(default=None, description="Anthropic API key.")
GOOGLE_API_KEY: Optional[str] = Field(default=None, description="Google AI API key.")
GROQ_API_KEY: Optional[str] = Field(default=None, description="Groq API key.")
HUGGINGFACE_API_KEY: Optional[str] = Field(default=None, description="Hugging Face API key.")
OPEN_ROUTER_API_KEY: Optional[str] = Field(default=None, description="OpenRouter API key.")
NOVITA_API_KEY: Optional[str] = Field(default=None, description="Novita API key.")
OPENAI_API_BASE: Optional[str] = Field(default=None, description="Azure OpenAI API base URL.")
OPENAI_API_VERSION: Optional[str] = Field(default=None, description="Azure OpenAI API version.")
AZURE_DEPLOYMENT_NAME: Optional[str] = Field(default=None, description="Azure deployment name for answering.")
AZURE_EMBEDDINGS_DEPLOYMENT_NAME: Optional[str] = Field(
default=None, description="Azure deployment name for embeddings."
)
OPENAI_BASE_URL: Optional[str] = Field(
default=None, description="Base URL for OpenAI-compatible model servers."
)
LLM_PATH: str = Field(
default=os.path.join(str(home_dir()), "models/docsgpt-7b-f16.gguf"),
description="Path to the local GGUF model used by the llama.cpp provider.",
)
FALLBACK_LLM_PROVIDER: Optional[str] = Field(default=None, description="Provider for the fallback LLM.")
FALLBACK_LLM_NAME: Optional[str] = Field(default=None, description="Model name for the fallback LLM.")
FALLBACK_LLM_API_KEY: Optional[str] = Field(default=None, description="API key for the fallback LLM.")
TITLE_MODEL_ID: Optional[str] = Field(
default=None, description="Optional cheaper model for conversation titles; unset reuses the answer model."
)
MODELS_CONFIG_DIR: Optional[str] = Field(
default=None,
description=(
"Directory of operator-supplied model YAMLs, loaded after the built-in catalog; later wins on "
"duplicate model id. See docsgpt/core/models/README.md."
),
)
DEFAULT_LLM_TOKEN_LIMIT: int = Field(
default=128000, description="Context window assumed when the model is not found in the registry."
)
RESERVED_TOKENS: dict = Field(
default={"system_prompt": 500, "current_query": 500, "safety_buffer": 1000},
description="Tokens held back from the context window for the system prompt, the query and a safety buffer.",
)
CACHE_REDIS_URL: str = Field(default="redis://localhost:6379/2", description="Redis URL for the LLM cache.")
# OpenAI Responses API.
OPENAI_RESPONSES_STORE: bool = Field(
default=False,
description=(
"True persists Responses API calls server-side so previous_response_id can chain turns. False keeps "
"them stateless, carrying reasoning across the tool loop as encrypted items."
),
)
OPENAI_RESPONSES_CHAIN_ACROSS_TURNS: bool = Field(
default=True,
description=(
"Cross-turn previous_response_id chaining (store mode only). The chained transcript lives on the "
"provider and is invisible to every local guard, so it is bounded: a turn starts from the local "
"history when the previous turn's reported prompt already reached the budget (default: the model's "
"context window) or when the conversation was compressed after that turn was produced."
),
)
OPENAI_RESPONSES_CHAIN_BUDGET_TOKENS: Optional[int] = Field(
default=None, description="Prompt-token budget for cross-turn chaining; unset uses the model's context window."
)
OPENAI_RESPONSES_TRUNCATION_AUTO: bool = Field(
default=False,
description=(
'Send truncation: "auto" so the provider drops the oldest input items instead of failing every '
"request once a chain exceeds the model's window."
),
)
OPENAI_PROMPT_CACHE_KEY: bool = Field(
default=True,
description=(
"Route a user's Responses API calls to the same prompt-cache shard with an opaque per-user key."
),
)
OPENAI_PROMPT_CACHE_RETENTION: Optional[str] = Field(
default=None, description="Request extended prompt-cache retention where the provider offers it."
)
OPENAI_REASONING_SUMMARY: str = Field(
default="auto", description="Reasoning summary mode requested from the Responses API."
)
@field_validator(
"API_KEY",
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GOOGLE_API_KEY",
"GROQ_API_KEY",
"HUGGINGFACE_API_KEY",
"NOVITA_API_KEY",
"FALLBACK_LLM_API_KEY",
mode="before",
)
@classmethod
def _normalize_llm_secrets(cls, v):
return normalize_secret(v)