12 KiB
Phase 7: Redo and Optimize LLM Integration - Context
Gathered: 2026-06-02 Status: Ready for planning
## Phase BoundaryPhase 7 delivers a fully refactored, production-reliable AI provider layer: a system_settings DB table so admins can configure all provider settings interactively, a GenericOpenAIProvider class that covers all OpenAI-compatible providers, native Gemini and Mistral integrations, JSON-mode structured output enforced across all providers, Celery-based retry with exponential backoff, per-provider context window configuration with smart truncation, a fix for the LMStudio Linux connectivity issue, and a re-classify UI button for failed documents. The admin panel gains an AI Providers configuration section.
Structured Output
- D-01: All OpenAI-compatible providers (OpenAI, Ollama, LMStudio, Groq, Gemini-compat, Mistral-compat, xAI/Grok, DeepSeek, OpenRouter) must pass
response_format={"type": "json_object"}in everyclassify()andsuggest_topics()call. - D-02: Keep
parse_classification()andparse_suggestions()inai/utils.pyas a last-resort fallback only — they are called when a provider does not supportresponse_format(e.g., some local Ollama models that ignore the parameter). - D-03: Anthropic structured output strategy is left to Claude's discretion (see below).
Provider Factory & Configuration
- D-04: All AI provider settings (API keys, base URLs, model names, active provider) are stored in a new
system_settingsDB table. Env vars (existingDEFAULT_AI_PROVIDER,DEFAULT_AI_MODEL, API key vars) serve as startup defaults that populate the table on first boot, but the DB record is authoritative at runtime. - D-05: API keys stored in
system_settingsmust be encrypted at rest — use the same HKDF/AES-GCM pattern already established for cloud credentials inbackend/storage/cloud_utils.py. - D-06:
get_provider()accepts typed PydanticProviderConfigmodels (not raw dicts). The classifier assembles aProviderConfigfrom the DB record — no more inline dict construction. - D-07: Client lifecycle (singleton, pooled, per-request) is left to Claude's discretion per provider — the researcher must document the optimal pattern for each provider so new providers can follow the same established pattern.
- D-08: Admin panel gains an "AI Providers" configuration section where admins can set the active provider, configure API keys/base URLs/models, and test connectivity via the existing
health_check()method.
Retry & Resilience
- D-09: The
extract_and_classifyCelery task uses Celery's built-in retry mechanism (self.retry(exc=…, countdown=…, max_retries=3)) with exponential backoff: first retry at 30 s, second at 90 s, third at 270 s. - D-10: After all retries are exhausted the document status remains
classification_failed. No silent failure. - D-11: The document card in the frontend shows a "Classification failed" badge when
status == "classification_failed". A "Re-analyze" button is shown that callsPOST /api/documents/{id}/classifyto re-queue the Celery task.
Context Window Strategy
- D-12:
MAX_AI_CHARSis replaced by a per-providercontext_charsvalue declared in each provider's config (orProviderConfigdefault). The global constant is removed. - D-13: Smart truncation strategy: take the first 60% of the per-provider limit + the last 40%. This captures both document introduction and conclusion, which carry the most topic signal for long documents.
LMStudio Linux Fix
- D-14: Add
extra_hosts: ["host.docker.internal:host-gateway"]to thebackendandcelery-workerservices indocker-compose.yml. This is the standard Linux Docker fix for accessing host services. - D-15: LMStudio base URL and model are configurable via the admin AI Providers panel (D-08), so users are not locked to the hardcoded
host.docker.internal:1234default.
New Providers
- D-16: Introduce a
GenericOpenAIProvider(base_url, api_key, model, context_chars)class inbackend/ai/generic_openai_provider.py. It subclassesOpenAIProviderand adds JSON-mode enforcement. Named presets (Groq, xAI/Grok, DeepSeek, OpenRouter, Gemini-compat, Mistral-compat) are factory shortcuts that pass the knownbase_urldefault. - D-17: Providers to add in this phase: Google Gemini (via OpenAI-compat endpoint at
generativelanguage.googleapis.com/v1beta/openai/), Mistral (via OpenAI-compat), Groq, xAI/Grok, DeepSeek, OpenRouter — all viaGenericOpenAIProvider. - D-18: Mistral uses the OpenAI-compat endpoint (not the native
mistralaiSDK) to avoid adding a new dependency. Can be revisited in a later phase if Mistral-specific features (embeddings, FIM) are needed.
Claude's Discretion
- D-03 (Anthropic output): Researcher should determine whether Anthropic uses native
tool_useJSON schema, Anthropic'sresponse_format(if supported), or prompt-enforced JSON. Choose the most reliable option that is consistent with the JSON-mode-everywhere principle (D-01). - D-07 (Client lifecycle): Researcher must investigate and document optimal client instantiation per provider (singleton, per-request, pooled). The documentation must be clear enough that adding a new provider in the future does not require re-researching this.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Existing AI provider code
backend/ai/base.py—AIProviderABC andClassificationResultdataclass; the interface all providers must implementbackend/ai/utils.py—strip_code_fences,parse_classification,parse_suggestions; fallback parsers that remain but are demotedbackend/ai/__init__.py—get_provider(settings: dict)factory; this is the function being replaced/refactoredbackend/ai/anthropic_provider.py— current Anthropic implementation; reference for what the refactored version must preservebackend/ai/openai_provider.py— current OpenAI/base-compat implementation;GenericOpenAIProviderbuilds on thisbackend/ai/ollama_provider.py— one-line subclass pattern for Ollama; shows the current named-preset approachbackend/ai/lmstudio_provider.py— same pattern as Ollama; root cause of Linux connection issue is here (hardcodedhost.docker.internal)
Classification orchestrator
backend/services/classifier.py—classify_document()andsuggest_topics_for_document(); inline_settingsdict construction that must be replaced withProviderConfig(D-06)backend/tasks/document_tasks.py—extract_and_classifyCelery task; retry logic (D-09) goes here
Configuration and encryption
backend/config.py— currentdefault_ai_provider,default_ai_model,system_promptsettings; these become DB defaults (D-04)backend/storage/cloud_utils.py— HKDF/AES-GCM encryption pattern that D-05 requires for API key storage insystem_settings
Infrastructure
docker-compose.yml—extra_hostsmust be added tobackendandcelery-workerservices (D-14)
Architecture context
.planning/codebase/ARCHITECTURE.md— system overview; AI providers shown at bottom-right of the diagramCLAUDE.md§"Backend: shared module map" —backend/ai/utils.pymust remain the single location for AI parsing helpers
ROADMAP
.planning/ROADMAP.mdPhase 7 entry — goal and dependencies (depends on Phase 6)
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
OpenAIProvider(backend/ai/openai_provider.py):GenericOpenAIProvidersubclasses this directly — JSON mode is added in one override.- HKDF key derivation in
backend/storage/cloud_utils.py:_derive_fernet_key(): the same pattern (fresh HKDF instance per call, never cache) must be replicated forsystem_settingsAPI key encryption (D-05). write_audit_log()inbackend/services/storage.py: admin AI config changes should be logged as audit events.- Existing
health_check()on all providers: reuse this for the "Test connection" button in the admin AI Providers panel (D-08).
Established Patterns
- Celery retry:
document_tasks.pycurrently catches exceptions and returns{"status": "classification_failed"}. Refactor toraise self.retry(exc=e, countdown=30, max_retries=3)in the task decorator. - StorageBackend ABC pattern: the
AIProviderABC (base.py) already mirrors this. New providers follow the same subclassing pattern. - Admin endpoint whitelist:
_user_to_dict()pattern inbackend/api/admin.py— newsystem_settingsendpoints must never return encrypted API keys in responses (same principle ascredentials_enc). get_provider()factory: currently a flatif/elifchain; refactor to a registry dict{"anthropic": AnthropicProvider, "groq": GenericOpenAIProvider, …}to make adding new providers O(1).
Integration Points
backend/api/admin.py— new AI Providers endpoints (GET /api/admin/ai-config,PUT /api/admin/ai-config) added here; admin-only viaget_current_admindep.backend/services/classifier.py— replace inline_settingsdict with aload_provider_config(session)helper that reads fromsystem_settingstable.frontend/src/views/AdminView.vue/frontend/src/components/admin/— newAdminAITab.vuecomponent following the existing tab pattern (AdminUsersTab, AdminQuotasTab, AdminAITab).frontend/src/components/documents/DocumentCard.vue— add "Classification failed" badge and "Re-analyze" button (D-11).
</code_context>
## Specific Ideas- LMStudio Linux fix is purely a Docker Compose
extra_hostsaddition + JSON mode enforcement — no changes tolmstudio_provider.pylogic itself needed beyond what the provider refactor provides. GenericOpenAIProvidernamed presets: the factory registry entry for"groq"passesbase_url="https://api.groq.com/openai/v1", for"xai"passesbase_url="https://api.x.ai/v1", for"deepseek"passesbase_url="https://api.deepseek.com", for"openrouter"passesbase_url="https://openrouter.ai/api/v1", for"gemini"passesbase_url="https://generativelanguage.googleapis.com/v1beta/openai/".- Admin panel "AI Providers" section should show: active provider selector, per-provider accordion (API key field masked, base URL, model, context window size), "Test connection" button that calls
health_check().
- Chunk + merge multi-call classification — splitting long documents into N chunks and merging results was considered but deferred. More accurate for very long docs but 2-5x API cost. Worth a future phase once the provider layer is stable.
- Native
mistralaiSDK — Mistral-specific features (embeddings, FIM/code completion) are not needed for document classification. OpenAI-compat is sufficient now; revisit when embedding-based retrieval is added. - Token counting — true token counting per provider (using
tiktokenor provider-specific tokenizers) was considered for context window management. Per-providercontext_charsapproximation is sufficient for Phase 7; precise token counting can be added in a future optimization phase. - Streaming classification — streaming LLM responses for faster UX was identified but deferred; classification is a background Celery task so streaming to the user is not straightforward without a websocket layer.
Phase: 07-redo-and-optimize-llm-integration Context gathered: 2026-06-02