fix(assistant): #91 le SSE renvoie le modele reellement utilise (tag fournisseur - modele complet)
This commit is contained in:
@@ -85,6 +85,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
ré-ancrage automatique de Chrome (le fil est reconstruit à chaque token, ce qui
|
ré-ancrage automatique de Chrome (le fil est reconstruit à chaque token, ce qui
|
||||||
décalait la vue de ~33 px) et une passe de correction recale le défilement d'après la
|
décalait la vue de ~33 px) et une passe de correction recale le défilement d'après la
|
||||||
géométrie mesurée.
|
géométrie mesurée.
|
||||||
|
- **#91 — Tag fournisseur/modèle incomplet** : le backend renvoyait le modèle *demandé
|
||||||
|
par le client* (`req.model or ""`), donc une valeur vide dès que le client s'en remet
|
||||||
|
au défaut du fournisseur — le tag se réduisait à « openrouter ». `_effective_model()`
|
||||||
|
résout désormais le modèle réellement utilisé dans les deux flux SSE (`/chat` et
|
||||||
|
`/agent`) : le tag affiche « openrouter · openai/gpt-4o-mini ». Couvert par
|
||||||
|
`tests/test_bookslm.py::TestEffectiveModel` et le test SSE de l'endpoint agent.
|
||||||
|
|
||||||
- **BUG-041 — Assistant IA bloqué sur un répertoire vide** : l'assistant ne
|
- **BUG-041 — Assistant IA bloqué sur un répertoire vide** : l'assistant ne
|
||||||
renvoie plus `⚠ Error: Aucun fichier markdown trouvé dans ce dossier` (HTTP 404).
|
renvoie plus `⚠ Error: Aucun fichier markdown trouvé dans ce dossier` (HTTP 404).
|
||||||
|
|||||||
@@ -317,6 +317,22 @@ def _resolve_provider_name(requested: str | None) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_model(provider: str | None, requested: str | None) -> str:
|
||||||
|
"""Model actually used for a request.
|
||||||
|
|
||||||
|
The client may leave `model` empty (provider default) — reporting the raw
|
||||||
|
request would show nothing in the "provider · model" tag, so the provider's
|
||||||
|
configured default is returned instead.
|
||||||
|
"""
|
||||||
|
if requested:
|
||||||
|
return requested
|
||||||
|
if not provider:
|
||||||
|
return ""
|
||||||
|
from backend.ai import PROVIDERS
|
||||||
|
|
||||||
|
return PROVIDERS.get(provider, {}).get("model", "") or ""
|
||||||
|
|
||||||
|
|
||||||
def _tool_event_sse(rec) -> str:
|
def _tool_event_sse(rec) -> str:
|
||||||
"""Serialize one executed tool call as an SSE ``tool`` event."""
|
"""Serialize one executed tool call as an SSE ``tool`` event."""
|
||||||
payload = json.dumps(
|
payload = json.dumps(
|
||||||
@@ -407,7 +423,11 @@ async def api_bookslm_chat(
|
|||||||
max_tokens=4096,
|
max_tokens=4096,
|
||||||
):
|
):
|
||||||
data = json.dumps(
|
data = json.dumps(
|
||||||
{"token": token, "provider": cfg_name, "model": req.model or ""},
|
{
|
||||||
|
"token": token,
|
||||||
|
"provider": cfg_name,
|
||||||
|
"model": _effective_model(cfg_name, req.model),
|
||||||
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
yield f"event: message\ndata: {data}\n\n"
|
yield f"event: message\ndata: {data}\n\n"
|
||||||
@@ -532,7 +552,7 @@ async def api_bookslm_agent(
|
|||||||
{
|
{
|
||||||
"token": result.content,
|
"token": result.content,
|
||||||
"provider": cfg_name,
|
"provider": cfg_name,
|
||||||
"model": req.model or "",
|
"model": _effective_model(cfg_name, req.model),
|
||||||
"iterations": result.iterations,
|
"iterations": result.iterations,
|
||||||
"stopped": result.stopped,
|
"stopped": result.stopped,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,10 @@
|
|||||||
(déjà émis par `/chat` et `/agent`) sur le message assistant.
|
(déjà émis par `/chat` et `/agent`) sur le message assistant.
|
||||||
- [x] **C2.** `.bookslm-msg-meta` au-dessus du bloc assistant :
|
- [x] **C2.** `.bookslm-msg-meta` au-dessus du bloc assistant :
|
||||||
« fournisseur · modèle » (persisté avec la session, visible au rechargement).
|
« fournisseur · modèle » (persisté avec la session, visible au rechargement).
|
||||||
|
- [x] **C3.** Le backend émet le modèle **réellement utilisé** :
|
||||||
|
`_effective_model(provider, req.model)` résout le défaut du fournisseur quand le
|
||||||
|
client ne précise pas de modèle (avant : `req.model or ""` → tag réduit au seul
|
||||||
|
fournisseur). Idem dans les deux flux SSE (`/chat` et `/agent`).
|
||||||
|
|
||||||
## D. Barre d'actions — ✅ livré
|
## D. Barre d'actions — ✅ livré
|
||||||
- [x] **D1.** `_appendActionBar()` sous chaque bloc non vide : bouton Copier pour
|
- [x] **D1.** `_appendActionBar()` sous chaque bloc non vide : bouton Copier pour
|
||||||
|
|||||||
@@ -967,3 +967,60 @@ class TestBooksLMAgentEndpoint:
|
|||||||
assert executed.get("ran") is True
|
assert executed.get("ran") is True
|
||||||
assert "C'est fait." in resp2.text
|
assert "C'est fait." in resp2.text
|
||||||
assert "event: message" in resp2.text
|
assert "event: message" in resp2.text
|
||||||
|
|
||||||
|
def test_agent_message_reports_effective_model(self, bookslm_client, monkeypatch):
|
||||||
|
"""The SSE payload carries the model really used, not the raw request.
|
||||||
|
|
||||||
|
The client may leave `model` empty (provider default) — reporting it
|
||||||
|
verbatim left the "provider · model" tag in the UI with the provider
|
||||||
|
only.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import backend.bookslm_routes as routes
|
||||||
|
from backend.ai import PROVIDERS
|
||||||
|
from backend.ai_chat import LLMResponse
|
||||||
|
|
||||||
|
async def fake_chat_completion(messages, **kwargs):
|
||||||
|
return LLMResponse(content="Bonjour.")
|
||||||
|
|
||||||
|
monkeypatch.setattr(routes, "chat_completion", fake_chat_completion)
|
||||||
|
monkeypatch.setattr(routes, "_resolve_provider_name", lambda requested: "openrouter")
|
||||||
|
|
||||||
|
token, _ = _login_bookslm(bookslm_client)
|
||||||
|
resp = bookslm_client.post(
|
||||||
|
"/api/ai/bookslm/agent",
|
||||||
|
json={"vault": "TestVault", "directory": "", "message": "salut", "mode": "directory"},
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
match = re.search(r"event: message\ndata: (.*)", resp.text)
|
||||||
|
assert match, resp.text
|
||||||
|
data = json.loads(match.group(1))
|
||||||
|
assert data["provider"] == "openrouter"
|
||||||
|
expected = PROVIDERS.get("openrouter", {}).get("model", "") or ""
|
||||||
|
assert data["model"] == expected, data
|
||||||
|
assert data["model"], "provider default model must not be reported empty"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEffectiveModel:
|
||||||
|
"""Unit tests for the provider/model tag payload helper."""
|
||||||
|
|
||||||
|
def test_requested_model_wins(self):
|
||||||
|
from backend.bookslm_routes import _effective_model
|
||||||
|
|
||||||
|
assert _effective_model("openrouter", "openai/gpt-4o-mini") == "openai/gpt-4o-mini"
|
||||||
|
|
||||||
|
def test_falls_back_to_provider_default(self):
|
||||||
|
from backend.ai import PROVIDERS
|
||||||
|
from backend.bookslm_routes import _effective_model
|
||||||
|
|
||||||
|
default = PROVIDERS.get("openrouter", {}).get("model", "")
|
||||||
|
assert _effective_model("openrouter", None) == default
|
||||||
|
assert _effective_model("openrouter", "") == default
|
||||||
|
|
||||||
|
def test_unknown_provider_or_missing_provider_is_empty(self):
|
||||||
|
from backend.bookslm_routes import _effective_model
|
||||||
|
|
||||||
|
assert _effective_model("nope-not-a-provider", None) == ""
|
||||||
|
assert _effective_model(None, None) == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user