From 52a2967f61e340b600e367718c437bac454e28c7 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 14 Apr 2026 11:48:15 +0200 Subject: [PATCH] Dev AI config: env var overrides in config_reader, LM Studio via .env config_reader.py now merges environment variables (AI_PROVIDER, LMSTUDIO_BASE_URL, LMSTUDIO_API_KEY, LMSTUDIO_MODEL, OLLAMA_*, ANTHROPIC_*) on top of the JSON config file, so the dev .env file can pin the AI connection without writing to the shared config volume. docker-compose.dev.yml loads features/doc-service/.env (gitignored) into the doc-service container so the token is never committed. .env.example updated with all supported override variables and comments. Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.dev.yml | 1 + features/doc-service/.env.example | 18 ++++++ .../doc-service/app/services/config_reader.py | 62 ++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e058e68..ad7dc16 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -25,5 +25,6 @@ services: doc-service: command: sh scripts/start_dev.sh + env_file: ./features/doc-service/.env # gitignored — holds local AI credentials volumes: - ./features/doc-service:/app diff --git a/features/doc-service/.env.example b/features/doc-service/.env.example index 129e4ce..c3565d6 100644 --- a/features/doc-service/.env.example +++ b/features/doc-service/.env.example @@ -1,3 +1,21 @@ DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/destroying_sap DATA_DIR=/data/documents CONFIG_PATH=/config/doc_service_config.json + +# Optional AI provider overrides — if set, these take precedence over +# whatever is stored in doc_service_config.json. +# Useful for pinning a dev environment to a specific local AI instance +# without touching the shared config volume. +# +# AI_PROVIDER=lmstudio +# LMSTUDIO_BASE_URL=http://host.docker.internal:1234/v1 +# LMSTUDIO_API_KEY=your-lm-studio-token +# LMSTUDIO_MODEL=local-model +# +# AI_PROVIDER=ollama +# OLLAMA_BASE_URL=http://host.docker.internal:11434/v1 +# OLLAMA_MODEL=llama3.2 +# +# AI_PROVIDER=anthropic +# ANTHROPIC_API_KEY=sk-ant-... +# ANTHROPIC_MODEL=claude-haiku-4-5-20251001 diff --git a/features/doc-service/app/services/config_reader.py b/features/doc-service/app/services/config_reader.py index 5c37aa2..9f4992d 100644 --- a/features/doc-service/app/services/config_reader.py +++ b/features/doc-service/app/services/config_reader.py @@ -2,10 +2,23 @@ Reads doc_service_config.json from the shared config volume. Caches the result for 30 seconds to avoid hitting the filesystem on every request. Uses asyncio.to_thread so the synchronous file read doesn't block the event loop. + +Env var overrides (take precedence over the JSON config file, never committed): + AI_PROVIDER — "lmstudio" | "ollama" | "anthropic" + LMSTUDIO_BASE_URL — e.g. http://host.docker.internal:1234/v1 + LMSTUDIO_API_KEY + LMSTUDIO_MODEL + OLLAMA_BASE_URL + OLLAMA_MODEL + OLLAMA_API_KEY + ANTHROPIC_API_KEY + ANTHROPIC_MODEL """ import asyncio import json +import os import time +from copy import deepcopy from pathlib import Path from app.core.config import settings @@ -31,9 +44,52 @@ _CACHE_TTL = 30.0 def _read_config_sync() -> dict: path = Path(settings.CONFIG_PATH) if not path.exists(): - return _DEFAULT_CONFIG.copy() - with open(path) as f: - return json.load(f) + base = deepcopy(_DEFAULT_CONFIG) + else: + with open(path) as f: + base = json.load(f) + return _apply_env_overrides(base) + + +def _apply_env_overrides(config: dict) -> dict: + """ + Merge environment variable overrides into the config dict. + Env vars win over whatever is stored in the JSON file. + This lets the dev .env file pin the AI connection without writing to the + shared volume (which would affect all users). + """ + cfg = deepcopy(config) + ai = cfg.setdefault("ai", {}) + + if provider := os.environ.get("AI_PROVIDER"): + ai["provider"] = provider + + # LM Studio + lms = ai.setdefault("lmstudio", {}) + if v := os.environ.get("LMSTUDIO_BASE_URL"): + lms["base_url"] = v + if v := os.environ.get("LMSTUDIO_API_KEY"): + lms["api_key"] = v + if v := os.environ.get("LMSTUDIO_MODEL"): + lms["model"] = v + + # Ollama + oll = ai.setdefault("ollama", {}) + if v := os.environ.get("OLLAMA_BASE_URL"): + oll["base_url"] = v + if v := os.environ.get("OLLAMA_MODEL"): + oll["model"] = v + if v := os.environ.get("OLLAMA_API_KEY"): + oll["api_key"] = v + + # Anthropic + ant = ai.setdefault("anthropic", {}) + if v := os.environ.get("ANTHROPIC_API_KEY"): + ant["api_key"] = v + if v := os.environ.get("ANTHROPIC_MODEL"): + ant["model"] = v + + return cfg async def load_doc_config() -> dict: