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>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""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)
|