fix(celery+lmstudio): PYTHONPATH for forked workers + qwen3.5-9b thinking model support

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>
This commit is contained in:
curo1305
2026-06-05 00:00:26 +02:00
co-authored by Claude Sonnet 4.6
parent ca43e653aa
commit ac2dded35b
3 changed files with 11 additions and 8 deletions
+2 -2
View File
@@ -60,7 +60,7 @@ class GenericOpenAIProvider(OpenAIProvider):
) )
create_kwargs = dict( create_kwargs = dict(
model=self._model, model=self._model,
max_tokens=1024, max_tokens=4096,
messages=[ messages=[
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
{"role": "user", "content": user_msg}, {"role": "user", "content": user_msg},
@@ -87,7 +87,7 @@ class GenericOpenAIProvider(OpenAIProvider):
) )
create_kwargs = dict( create_kwargs = dict(
model=self._model, model=self._model,
max_tokens=256, max_tokens=1024,
messages=[ messages=[
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
{"role": "user", "content": user_msg}, {"role": "user", "content": user_msg},
+7 -6
View File
@@ -83,16 +83,17 @@ PROVIDER_DEFAULTS: dict[str, dict] = {
}, },
"lmstudio": { "lmstudio": {
"base_url": "http://host.docker.internal:1234/v1", "base_url": "http://host.docker.internal:1234/v1",
"model": "gemma-4-e4b-it", "model": "qwen/qwen3.5-9b",
"context_chars": 8_000, "context_chars": 32_000,
}, },
} }
# Whether the provider honours response_format={"type": "json_object"}. # Whether the provider honours response_format={"type": "json_object"}.
# Gemini's OpenAI-compat endpoint does NOT support the string form (D-02/D-03). # Gemini's OpenAI-compat endpoint does NOT support the string form (D-02/D-03).
# Ollama and LMStudio accept the parameter but some models ignore it — the # LMStudio only accepts "json_schema" or "text" (not "json_object") — set False
# GenericOpenAIProvider always wraps the raw response with parse_classification() # so GenericOpenAIProvider omits response_format and relies on parse_classification().
# regardless, so they are left as True (the parameter is still sent). # Ollama accepts the parameter but some models ignore it — left True; the
# GenericOpenAIProvider always wraps the raw response with parse_classification().
SUPPORTS_JSON_MODE: dict[str, bool] = { SUPPORTS_JSON_MODE: dict[str, bool] = {
"openai": True, "openai": True,
"anthropic": True, "anthropic": True,
@@ -103,5 +104,5 @@ SUPPORTS_JSON_MODE: dict[str, bool] = {
"openrouter": True, "openrouter": True,
"mistral": True, "mistral": True,
"ollama": True, "ollama": True,
"lmstudio": True, "lmstudio": False,
} }
+2
View File
@@ -102,6 +102,7 @@ services:
- CLOUD_CREDS_KEY=${CLOUD_CREDS_KEY} - CLOUD_CREDS_KEY=${CLOUD_CREDS_KEY}
- SECRET_KEY=${SECRET_KEY} - SECRET_KEY=${SECRET_KEY}
- PYTHONDONTWRITEBYTECODE=1 - PYTHONDONTWRITEBYTECODE=1
- PYTHONPATH=/app
labels: labels:
logging: "promtail" logging: "promtail"
volumes: volumes:
@@ -134,6 +135,7 @@ services:
- MINIO_BUCKET=${MINIO_BUCKET} - MINIO_BUCKET=${MINIO_BUCKET}
- REDIS_URL=${REDIS_URL} - REDIS_URL=${REDIS_URL}
- PYTHONDONTWRITEBYTECODE=1 - PYTHONDONTWRITEBYTECODE=1
- PYTHONPATH=/app
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
command: celery -A celery_app beat --loglevel=info --schedule /tmp/celerybeat-schedule command: celery -A celery_app beat --loglevel=info --schedule /tmp/celerybeat-schedule