Fix: build_models resolves separate AsyncOpenAI clients for judge and embedding models

Previously, only the judge_model's profile was looked up and its AsyncOpenAI

client was shared with embedding_factory. When embedding_model has a different

base_url/api_key (e.g. Qwen3-Embedding-4B on SiliconFlow vs gpt-5 on another

gateway), the embedding calls silently used the wrong URL, causing 402/404 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
wangwei
2026-07-01 21:15:09 +08:00
co-authored by Copilot
parent 3a82d8c487
commit f6e10145cd
2 changed files with 147 additions and 6 deletions
+134
View File
@@ -0,0 +1,134 @@
"""Tests that build_models resolves separate clients for judge and embedding models."""
from unittest.mock import MagicMock, patch, call
from rag_eval.metrics.factory import build_models
from rag_eval.settings import EvaluationSettings
def _settings():
return EvaluationSettings(_env_file=None)
def _patch_profiles(profiles_by_model: dict):
"""Patch profile_manager.list_all() to return fake profiles."""
fake_profiles = []
for model, base_url in profiles_by_model.items():
p = MagicMock()
p.model = model
p.base_url = base_url
p.api_key = "sk-test"
p.timeout_seconds = 30
fake_profiles.append(p)
return fake_profiles
def test_build_models_uses_separate_clients_for_different_profiles(monkeypatch):
"""When judge and embedding models have different profiles, two AsyncOpenAI clients are created."""
created_clients = []
class _FakeClient:
def __init__(self, **kwargs):
created_clients.append(kwargs.copy())
fake_profiles = _patch_profiles({
"gpt-5": "http://llm-gateway/v1",
"Qwen3-Embedding-4B": "https://api.siliconflow.cn/v1",
})
import webapp.services.profile_manager as pm_mod
monkeypatch.setattr(pm_mod.profile_manager, "list_all", lambda: fake_profiles)
from ragas.llms.base import InstructorBaseRagasLLM
from ragas.embeddings.base import BaseRagasEmbedding
with patch("rag_eval.metrics.factory.AsyncOpenAI", side_effect=_FakeClient), \
patch("rag_eval.metrics.factory.llm_factory", return_value=MagicMock(spec=InstructorBaseRagasLLM)), \
patch("rag_eval.metrics.factory.embedding_factory", return_value=MagicMock(spec=BaseRagasEmbedding)):
build_models("gpt-5", "Qwen3-Embedding-4B", _settings())
# Two distinct clients should have been created with different base_urls.
assert len(created_clients) == 2
base_urls = {c["base_url"] for c in created_clients}
assert "http://llm-gateway/v1" in base_urls
assert "https://api.siliconflow.cn/v1" in base_urls
def test_build_models_shares_client_when_same_profile(monkeypatch):
"""When judge and embedding models resolve to the same settings, only one client is created."""
created_clients = []
class _FakeClient:
def __init__(self, **kwargs):
created_clients.append(kwargs.copy())
# Both models map to the same gateway.
fake_profiles = _patch_profiles({
"gpt-5": "http://same-gateway/v1",
"text-embedding-3-small": "http://same-gateway/v1",
})
import webapp.services.profile_manager as pm_mod
monkeypatch.setattr(pm_mod.profile_manager, "list_all", lambda: fake_profiles)
from ragas.llms.base import InstructorBaseRagasLLM
from ragas.embeddings.base import BaseRagasEmbedding
with patch("rag_eval.metrics.factory.AsyncOpenAI", side_effect=_FakeClient), \
patch("rag_eval.metrics.factory.llm_factory", return_value=MagicMock(spec=InstructorBaseRagasLLM)), \
patch("rag_eval.metrics.factory.embedding_factory", return_value=MagicMock(spec=BaseRagasEmbedding)):
build_models("gpt-5", "text-embedding-3-small", _settings())
# Same settings → only one client needed.
assert len(created_clients) == 1
def test_embedding_factory_receives_embedding_client(monkeypatch):
"""embedding_factory is called with the client resolved from the embedding model's profile."""
emb_client_ref = []
llm_client_ref = []
class _LLMClient:
label = "llm"
def __init__(self, **kwargs):
llm_client_ref.append(self)
class _EmbClient:
label = "emb"
def __init__(self, **kwargs):
emb_client_ref.append(self)
fake_profiles = _patch_profiles({
"gpt-5": "http://llm-gw/v1",
"Qwen3-Embedding-4B": "https://api.siliconflow.cn/v1",
})
import webapp.services.profile_manager as pm_mod
monkeypatch.setattr(pm_mod.profile_manager, "list_all", lambda: fake_profiles)
from ragas.llms.base import InstructorBaseRagasLLM
from ragas.embeddings.base import BaseRagasEmbedding
emb_factory_calls = []
def _fake_emb_factory(provider, model, client):
emb_factory_calls.append({"model": model, "client": client})
return MagicMock(spec=BaseRagasEmbedding)
call_count = [0]
def _client_factory(**kwargs):
call_count[0] += 1
if "llm-gw" in kwargs.get("base_url", ""):
return _LLMClient(**kwargs)
return _EmbClient(**kwargs)
with patch("rag_eval.metrics.factory.AsyncOpenAI", side_effect=_client_factory), \
patch("rag_eval.metrics.factory.llm_factory", return_value=MagicMock(spec=InstructorBaseRagasLLM)), \
patch("rag_eval.metrics.factory.embedding_factory", side_effect=_fake_emb_factory):
build_models("gpt-5", "Qwen3-Embedding-4B", _settings())
assert len(emb_factory_calls) == 1
# The client passed to embedding_factory must be the siliconflow client, not the LLM client.
assert isinstance(emb_factory_calls[0]["client"], _EmbClient), (
"embedding_factory should receive the embedding-model client, not the judge-model client"
)