Three root causes fixed:
1. docker-compose.yml — celery-worker and celery-beat missing PYTHONPATH=/app
ForkPoolWorker processes inherit sys.path with '' (CWD), but fork can change
CWD so lazy imports like `from db.session import ...` raised ModuleNotFoundError.
Adding PYTHONPATH=/app ensures /app is always explicit in the path.
2. provider_config.py — lmstudio SUPPORTS_JSON_MODE set to False
LM Studio only accepts response_format type 'json_schema' or 'text', not
'json_object'. GenericOpenAIProvider now omits response_format for lmstudio
and relies on parse_classification() fallback (same path as Gemini).
3. generic_openai_provider.py — max_tokens raised 1024→4096 (classify) / 256→1024 (suggest)
Qwen3 thinking models (like qwen/qwen3.5-9b) consume reasoning tokens from the
same budget. With max_tokens=1024 the model exhausts the budget in the thinking
trace, leaving content=''. 4096 gives room for both thinking and the JSON output.
Also updates lmstudio PROVIDER_DEFAULTS model to qwen/qwen3.5-9b (confirmed loaded
in LM Studio) and context_chars to 32_000 (Qwen3 context window).
Tested: classify(invoice_text, ['Finance','Legal','HR']) → topics=['Finance'] ✓
Backend tests: 376 passed ✓
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrites ai/__init__.py with _REGISTRY dict (10 entries) replacing the if/elif chain
- get_provider() accepts ProviderConfig (not raw dict), raises ValueError on unknown
- Resolves effective values: api_key fallback to "not-needed", model/base_url/context_chars
from PROVIDER_DEFAULTS when config fields are empty/zero
- SUPPORTS_JSON_MODE lookup sets supports_json_mode on GenericOpenAIProvider instances
- AnthropicProvider instantiated without base_url (Plan 03 will widen the ctor)
- provider_config.py: context_chars default changed to 0 (sentinel for PROVIDER_DEFAULTS
resolution); 8000 default was inconsistent with O(1) factory pattern (Rule 1 fix)
- Creates backend/ai/provider_config.py as a pure data module (no provider imports)
- ProviderConfig(BaseModel) with extra=forbid: provider_id, api_key, base_url, model, context_chars
- PROVIDER_DEFAULTS dict with all 10 providers and verbatim base_url/model/context_chars from RESEARCH.md
- SUPPORTS_JSON_MODE dict with gemini=False per JSON Compatibility Matrix (D-02)
- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
shared by all AI providers; removes duplicated private functions from
anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
_validate_password_strength from admin.py and auth.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>