Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Peter Evers <peter.evers@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
140 lines
5 KiB
Python
140 lines
5 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime, timedelta
|
|
from ipaddress import IPv4Address
|
|
from pathlib import Path
|
|
import ssl
|
|
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
|
import pytest
|
|
|
|
from tests.e2e.mock_server import StreamingMockServer
|
|
from vibe.core.config import ModelConfig, ProviderConfig
|
|
from vibe.core.llm.backend.generic import GenericBackend
|
|
from vibe.core.types import Backend, LLMMessage, Role
|
|
from vibe.core.utils import build_ssl_context, configure_ssl_context
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _TLSMaterial:
|
|
ca_file: str
|
|
cert_file: str
|
|
key_file: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _HttpsStreamingMockServer:
|
|
server: StreamingMockServer
|
|
ca_file: str
|
|
|
|
|
|
@pytest.fixture
|
|
def https_streaming_mock_server(tmp_path: Path) -> Iterator[_HttpsStreamingMockServer]:
|
|
tls = _write_tls_material(tmp_path)
|
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
ssl_context.load_cert_chain(certfile=tls.cert_file, keyfile=tls.key_file)
|
|
server = StreamingMockServer(ssl_context=ssl_context)
|
|
server.start()
|
|
try:
|
|
yield _HttpsStreamingMockServer(server=server, ca_file=tls.ca_file)
|
|
finally:
|
|
server.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generic_backend_streaming_uses_ssl_cert_file(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
https_streaming_mock_server: _HttpsStreamingMockServer,
|
|
) -> None:
|
|
monkeypatch.setenv("SSL_CERT_FILE", https_streaming_mock_server.ca_file)
|
|
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
|
|
configure_ssl_context(enable_system_trust_store=False)
|
|
build_ssl_context.cache_clear()
|
|
|
|
chunks = []
|
|
try:
|
|
provider = ProviderConfig(
|
|
name="mock-provider",
|
|
api_base=https_streaming_mock_server.server.api_base,
|
|
api_key_env_var="MISTRAL_API_KEY",
|
|
backend=Backend.GENERIC,
|
|
)
|
|
model = ModelConfig(
|
|
name="mock-model", provider="mock-provider", alias="mock-model"
|
|
)
|
|
|
|
async with GenericBackend(provider=provider, timeout=5.0) as backend:
|
|
chunks = [
|
|
chunk
|
|
async for chunk in backend.complete_streaming(
|
|
model=model, messages=[LLMMessage(role=Role.user, content="Greet")]
|
|
)
|
|
]
|
|
finally:
|
|
configure_ssl_context(enable_system_trust_store=False)
|
|
build_ssl_context.cache_clear()
|
|
|
|
content = "".join(chunk.message.content or "" for chunk in chunks)
|
|
request_payload = https_streaming_mock_server.server.requests[-1]
|
|
assert content == "Hello from mock server"
|
|
assert request_payload.get("stream") is True
|
|
assert request_payload.get("model") == "mock-model"
|
|
|
|
|
|
def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
|
|
now = datetime.now(UTC)
|
|
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Vibe test CA")])
|
|
ca_cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(ca_name)
|
|
.issuer_name(ca_name)
|
|
.public_key(ca_key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now - timedelta(minutes=1))
|
|
.not_valid_after(now + timedelta(days=1))
|
|
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
|
.sign(ca_key, hashes.SHA256())
|
|
)
|
|
|
|
server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
server_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "127.0.0.1")])
|
|
server_cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(server_name)
|
|
.issuer_name(ca_cert.subject)
|
|
.public_key(server_key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now - timedelta(minutes=1))
|
|
.not_valid_after(now + timedelta(days=1))
|
|
.add_extension(
|
|
x509.SubjectAlternativeName([x509.IPAddress(IPv4Address("127.0.0.1"))]),
|
|
critical=False,
|
|
)
|
|
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
|
.add_extension(
|
|
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False
|
|
)
|
|
.sign(ca_key, hashes.SHA256())
|
|
)
|
|
|
|
ca_file = tmp_path / "ca.pem"
|
|
cert_file = tmp_path / "server.pem"
|
|
key_file = tmp_path / "server.key"
|
|
ca_file.write_bytes(ca_cert.public_bytes(serialization.Encoding.PEM))
|
|
cert_file.write_bytes(server_cert.public_bytes(serialization.Encoding.PEM))
|
|
key_file.write_bytes(
|
|
server_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
)
|
|
return _TLSMaterial(
|
|
ca_file=str(ca_file), cert_file=str(cert_file), key_file=str(key_file)
|
|
)
|