Moteur OpenRouter (google/gemini-3.5-flash-lite), par defaut

Meme consigne, meme encodage, meme repli local que le moteur Gemini direct ;
seuls l'adresse, l'authentification (Bearer) et le format de requete
(input_audio, compatible OpenAI) changent. Cle : trousseau zonza-openrouter,
puis OPENROUTER_API_KEY. Un 200 portant un objet error est traite comme un
echec, pas comme une transcription vide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 08:35:11 +02:00
co-authored by Claude Opus 5.5
parent 9be2ea030b
commit 50b12795d3
6 changed files with 198 additions and 17 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Moteur OpenRouter : même modèle Gemini, facturé et routé par OpenRouter.
Aucun test ne touche le réseau : l'envoi HTTP et la lecture de la clé sont
injectés. Le repli local, lui, est déjà éprouvé dans test_gemini.py.
"""
import base64
import numpy as np
import pytest
from moteur import choisir_transcripteur
from moteur.gemini import ErreurGemini, TranscripteurAvecRepli
from moteur.openrouter import TranscripteurOpenRouter, lire_cle_openrouter
def _reponse(texte):
return {"choices": [{"message": {"role": "assistant", "content": texte}}]}
def _transcripteur(envoyer, cle="cle-test"):
return TranscripteurOpenRouter(
"google/gemini-x", "fr", "Vocabulaire : Gitea, iCloud.",
lire_cle=lambda: cle, envoyer=envoyer,
)
def test_la_requete_porte_l_audio_le_modele_et_le_vocabulaire():
vu = {}
def envoyer(url, entetes, corps, delai_s):
vu.update(url=url, entetes=entetes, corps=corps)
return _reponse("Bonjour.")
_transcripteur(envoyer).transcrire(np.zeros(1600, dtype="float32"))
assert vu["url"] == "https://openrouter.ai/api/v1/chat/completions"
assert vu["entetes"]["Authorization"] == "Bearer cle-test"
assert vu["corps"]["model"] == "google/gemini-x"
systeme, utilisateur = vu["corps"]["messages"]
assert "Gitea" in systeme["content"] and "iCloud" in systeme["content"]
audio = next(p["input_audio"] for p in utilisateur["content"] if p["type"] == "input_audio")
assert audio["format"] == "wav"
assert base64.b64decode(audio["data"])[:4] == b"RIFF"
def test_la_cle_ne_part_jamais_dans_l_url():
vu = {}
_transcripteur(lambda url, e, c, d: vu.setdefault("url", url) and _reponse("x")) \
.transcrire(np.zeros(1600, dtype="float32"))
assert "cle-test" not in vu["url"]
def test_le_texte_rendu_est_nettoye():
t = _transcripteur(lambda *a: _reponse(" Je pousse sur Gitea. "))
assert t.transcrire(np.zeros(1600, dtype="float32")) == "Je pousse sur Gitea."
def test_une_erreur_rendue_dans_un_200_est_une_erreur_explicite():
# OpenRouter peut répondre 200 avec un objet `error` (fournisseur en panne).
t = _transcripteur(lambda *a: {"error": {"code": 502, "message": "fournisseur indisponible"}})
with pytest.raises(ErreurGemini) as e:
t.transcrire(np.zeros(1600, dtype="float32"))
assert "fournisseur indisponible" in str(e.value)
def test_une_reponse_sans_choix_est_une_erreur_explicite():
t = _transcripteur(lambda *a: {"choices": []})
with pytest.raises(ErreurGemini):
t.transcrire(np.zeros(1600, dtype="float32"))
def test_une_cle_absente_est_signalee_des_le_warmup_sans_appel():
appels = []
t = _transcripteur(lambda *a: appels.append(a), cle=None)
with pytest.raises(ErreurGemini) as e:
t.warmup()
assert "zonza-openrouter" in str(e.value)
assert appels == []
def test_la_cle_se_lit_sous_son_propre_nom():
assert lire_cle_openrouter(trousseau=lambda: None,
environ={"OPENROUTER_API_KEY": "or", "GEMINI_API_KEY": "g"}) == "or"
assert lire_cle_openrouter(trousseau=lambda: None, environ={"GEMINI_API_KEY": "g"}) is None
def test_la_fabrique_rend_openrouter_enveloppe_d_un_repli():
t = choisir_transcripteur(nom="openrouter", modele="google/gemini-x", langue="fr", amorce="")
assert isinstance(t, TranscripteurAvecRepli)
assert isinstance(t.principal, TranscripteurOpenRouter)