From 1d34406d1bacdb33d180f0e7e37bb0cb099ece5e Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 20 Oct 2026 15:33:07 +0200 Subject: [PATCH] fix(ai): request final responses from Qwen3.5 --- .../2026-10-20-qwen35-compatibility.md | 9 ++++ tools/summarizer/app.py | 2 + tools/summarizer/tests/test_app.py | 50 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 docs/infrastructure/2026-10-20-qwen35-compatibility.md diff --git a/docs/infrastructure/2026-10-20-qwen35-compatibility.md b/docs/infrastructure/2026-10-20-qwen35-compatibility.md new file mode 100644 index 0000000..d450996 --- /dev/null +++ b/docs/infrastructure/2026-10-20-qwen35-compatibility.md @@ -0,0 +1,9 @@ +# Qwen3.5 compatibility preparation + +The owner requested `qwen3.5:9b-q4_K_M` after live deployment. The model is downloaded, but the active sidecar still uses the previous model. This change prepares the existing adapter; it does not activate a model or qualify the withheld P5-13 capability. + +The existing adapter consumes Ollama's final `response`. A synthetic local probe found an empty response with the Qwen3.5 thinking default; a20October probe with `think:false` returned the requested synthetic JSON and no thinking text. The adapter now sets that request field only when the configured base family is exactly `qwen3.5`. Existing temperature, JSON mode, timeouts, response parsing, other model payloads and external-provider consent/fallback remain unchanged. Custom namespace aliases are outside this exact family match. + +Seven new offline cases verify both text/JSON for the requested tag and bare family, and unchanged payloads for qwen2.5, qwen3 and a lookalike family. Four Qwen3.5 cases failed ordinarily before the two-line change; three compatibility cases passed. The full sidecar suite then passed **56/56**, zero failures/errors/skips,8.45seconds,20October13:32UTC. The prior49-case baseline grows by seven; no model calls are part of these tests. Raw RED/GREEN logs and JUnit are retained under `.artifacts/release-20261020/model-compat/`. + +Live activation, deployed adapter identity, GPU residency and final-response smoke remain pending behind the release. P5-13's deployed-identity/366-call/independent-human-review/UI requirements remain unmet and its review feature stays disabled. Production readiness remains NO-GO independently of the owner's release authorization. diff --git a/tools/summarizer/app.py b/tools/summarizer/app.py index 9409640..85c2947 100644 --- a/tools/summarizer/app.py +++ b/tools/summarizer/app.py @@ -646,6 +646,8 @@ def _ollama_generate(prompt: str, *, json_mode: bool, temperature: float, timeou "stream": False, "options": {"temperature": temperature}, } + if OLLAMA_MODEL.split(":", 1)[0] == "qwen3.5": + payload["think"] = False if json_mode: payload["format"] = "json" body = _http_post_json(f"{OLLAMA_BASE_URL}/api/generate", payload, {}, timeout) diff --git a/tools/summarizer/tests/test_app.py b/tools/summarizer/tests/test_app.py index 866d683..5729c2e 100644 --- a/tools/summarizer/tests/test_app.py +++ b/tools/summarizer/tests/test_app.py @@ -8,6 +8,7 @@ import textwrap import zipfile from pathlib import Path +import pytest from docx import Document from fastapi.testclient import TestClient from PIL import Image @@ -253,6 +254,55 @@ def test_provider_defaults_to_ollama_and_is_unchanged(monkeypatch): assert captured["body"]["options"]["temperature"] == 0.1 +@pytest.mark.parametrize("ollama_model", ["qwen3.5:9b-q4_K_M", "qwen3.5"]) +@pytest.mark.parametrize("json_mode", [False, True]) +def test_qwen35_requests_nonthinking_output_without_consuming_thinking(monkeypatch, ollama_model, json_mode): + module = load_app_module(monkeypatch, ollama_model=ollama_model) + captured = {} + output = '{"score": 7}' if json_mode else "Rewritten CV text" + + def fake_urlopen(req, timeout=None): + captured["body"] = json.loads(req.data.decode("utf-8")) + captured["timeout"] = timeout + captured["url"] = req.full_url + return _FakeResponse({ + "response": output if captured["body"].get("think") is False else "", + "thinking": "Private reasoning must never become candidate text.", + }) + + monkeypatch.setattr(module.urllib_request, "urlopen", fake_urlopen) + result = module._ollama_generate_json("synthetic prompt") if json_mode else module._ollama_generate_text("synthetic prompt") + + assert result == ({"score": 7} if json_mode else output) + expected = { + "model": ollama_model, + "prompt": "synthetic prompt", + "stream": False, + "options": {"temperature": 0.1 if json_mode else 0.2}, + "think": False, + } + if json_mode: + expected["format"] = "json" + assert captured["body"] == expected + assert captured["url"] == f"{module.OLLAMA_BASE_URL}/api/generate" + assert captured["timeout"] == (120 if json_mode else 180) + + +@pytest.mark.parametrize("ollama_model", ["qwen2.5:7b", "qwen3:8b", "qwen3.5-plus:9b"]) +def test_other_ollama_model_payloads_remain_unchanged(monkeypatch, ollama_model): + module = load_app_module(monkeypatch, ollama_model=ollama_model) + captured = {} + _install_fake_urlopen(monkeypatch, module, {"response": " Candidate text ", "thinking": "Reasoning"}, captured) + + assert module._ollama_generate_text("synthetic prompt") == "Candidate text" + assert captured["body"] == { + "model": ollama_model, + "prompt": "synthetic prompt", + "stream": False, + "options": {"temperature": 0.2}, + } + + def test_local_success_wins_even_when_external_fallback_is_permitted(monkeypatch): monkeypatch.setenv("AI_PROVIDER", "gemini") monkeypatch.setenv("GEMINI_API_KEY", "test-key")