fix(xiaomi): URL MiMo + header api-key + fallback polling admin SSE
CI / lint (push) Successful in 37s
CI / security (push) Successful in 32s
CI / test (push) Successful in 48s
CI / build (push) Successful in 22s
CI / e2e (push) Successful in 6m1s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

1. **fix(xiaomi): la clé Xiaomi échouait avec 'Name or service not known'**
   - URL précédente api.xiaomi.com n'existe pas (DNS fail)
   - Vraie URL: https://api.xiaomimimo.com/v1/models
   - Xiaomi MiMo utilise un header custom 'api-key:' (PAS 'Authorization: Bearer')
   - Modèles par défaut mis à jour: mimo-v2.5-pro, mimo-v2.5, mimo-v2.5-asr, etc.
   - Le chat dans ai.py supporte maintenant un header custom via auth_header_name

2. **fix(admin): EventSource 503 → fallback polling**
   - Ajout d'un fallback: si EventSource ne reçoit jamais le premier event
     (cookie expiré, réseau bloqué, proxy timeout), on bascule sur du polling
     /api/admin/stats toutes les 5s. Le UI continue de se mettre à jour.
   - Cleanup correct du timer dans disconnectSSE

3. **fix(test_ai_models)**: 2 nouveaux tests de régression
   - test_xiaomi_uses_api_key_header_not_bearer: vérifie URL + header
   - test_xiaomi_test_endpoint_uses_real_url: vérifie /api/config/ai-keys/test
   - Patche backend.ai ET backend.main (import local)
   - Header case-insensitive (urllib normalise à 'Api-key', HTTP est insensible)

Vérifié :
- pytest : 504 passed (502 + 2 nouveaux Xiaomi)
- frontend unit : 7 passed
- validate-imports : 30 modules / 204 exports
- pane-manager JSDOM : 9/9
- ruff check backend/ : All checks passed
This commit is contained in:
2026-09-07 12:47:24 -04:00
parent 7ff9b854b6
commit d441481930
4 changed files with 205 additions and 27 deletions
+106
View File
@@ -202,3 +202,109 @@ class TestListModelsEndpoint:
data = resp.json()
assert "source" in data
assert data["source"] in ("live", "fallback")
def test_xiaomi_uses_api_key_header_not_bearer(self, admin_client, monkeypatch):
"""Regression test: Xiaomi MiMo must use `api-key` header, NOT Authorization.
Without this, the live call always fails with 401 even with a valid key.
"""
# Fake key — patch BOTH the source module AND the imported reference
# in backend.main (which does `from backend.ai import ... get_ai_key`).
import backend.ai as aimod
import backend.main as bmain
monkeypatch.setattr(aimod, "get_ai_key", lambda name: "fake-xiaomi-key")
if hasattr(bmain, "get_ai_key"):
monkeypatch.setattr(bmain, "get_ai_key", lambda name: "fake-xiaomi-key")
captured = {}
def fake_Request(url, *args, **kwargs):
captured["url"] = url
captured["headers"] = dict(kwargs.get("headers") or {})
class FakeResp:
def __enter__(self): return self
def __exit__(self, *a): pass
def read(self):
return b'{"object":"list","data":[{"id":"mimo-v2.5-pro","object":"model","owned_by":"xiaomi"}]}'
captured["response"] = FakeResp()
return captured["response"]
def fake_urlopen(req, timeout=None):
return req
# Patch urllib.request at the point where backend.main imported it.
import urllib.request as global_urllib_mod
monkeypatch.setattr(global_urllib_mod, "Request", fake_Request, raising=True)
monkeypatch.setattr(global_urllib_mod, "urlopen", fake_urlopen, raising=True)
token = _login_admin(admin_client)
resp = admin_client.get(
"/api/config/ai-models",
params={"provider": "xiaomi"},
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 200, f"resp: {resp.text[:300]}"
# Verify the URL + headers used.
assert captured.get("url"), f"Request was not called (captured={captured!r})"
assert captured["url"].startswith("https://api.xiaomimimo.com/v1/models"), (
f"unexpected URL: {captured.get('url')!r}"
)
hdrs = {k.lower(): v for k, v in captured.get("headers", {}).items()}
assert "api-key" in hdrs, f"missing api-key header in {hdrs!r}"
assert hdrs["api-key"] == "fake-xiaomi-key"
assert "authorization" not in hdrs, f"Authorization leaked: {hdrs!r}"
def test_xiaomi_test_endpoint_uses_real_url(self, admin_client, monkeypatch):
"""The /api/config/ai-keys/test endpoint must also use api.xiaomimimo.com.
Regression: it previously used api.xiaomi.com which doesn't exist
(DNS error Name or service not known).
"""
# Patch BOTH the source module AND the imported reference in backend.main
import backend.ai as aimod
import backend.main as bmain
monkeypatch.setattr(aimod, "get_ai_key", lambda name: "fake-key")
if hasattr(bmain, "get_ai_key"):
monkeypatch.setattr(bmain, "get_ai_key", lambda name: "fake-key")
import urllib.request as global_urllib_mod
captured_urls = []
captured_headers = []
def fake_urlopen(req, timeout=None):
captured_urls.append(req.full_url)
captured_headers.append(dict(req.headers))
raise OSError("simulated network error")
monkeypatch.setattr(global_urllib_mod, "urlopen", fake_urlopen, raising=True)
token = _login_admin(admin_client)
admin_client.post(
"/api/config/ai-keys/test",
headers={"Authorization": f"Bearer {token}"},
)
# Find the xiaomi URL among the captured calls.
xiaomi_idx = next(
(i for i, u in enumerate(captured_urls) if "xiaomi" in u.lower()),
None,
)
assert xiaomi_idx is not None, (
f"xiaomi URL not found in captured URLs: {captured_urls!r}"
)
xiaomi_url = captured_urls[xiaomi_idx]
# Must use the real MiMo endpoint, NOT the dead api.xiaomi.com.
assert "api.xiaomimimo.com" in xiaomi_url, (
f"xiaomi test URL is wrong: {xiaomi_url!r}"
)
# Must use api-key header, not Authorization. urllib.request
# normalizes header names to title-case ("Api-key"), but HTTP
# headers are case-insensitive on the wire — both forms are valid.
xiaomi_hdrs = {k.lower(): v for k, v in captured_headers[xiaomi_idx].items()}
assert "api-key" in xiaomi_hdrs, (
f"xiaomi api-key header missing: {xiaomi_hdrs!r}"
)
assert xiaomi_hdrs["api-key"] == "fake-key"
# Bearer prefix must NOT be in the api-key value
assert "Bearer" not in xiaomi_hdrs["api-key"]