Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Cyprien <courtot.c@gmail.com>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: josephine-delas <57808586+josephine-delas@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Laure Hugo 2026-06-25 16:08:45 +02:00 committed by GitHub
parent 725d3a56ce
commit e607ccbb00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
242 changed files with 7372 additions and 1974 deletions

View file

@ -0,0 +1,98 @@
from __future__ import annotations
import json
from typing import Any, ClassVar, Protocol
import httpx
import pytest
import respx
from tests.backend.data import Chunk, JsonResponse
from vibe.core.types import AssistantEvent, BaseEvent
def assistant_text(events: list[BaseEvent]) -> str:
return "".join(
e.content for e in events if isinstance(e, AssistantEvent) and e.content
).strip()
class ProviderMocks(Protocol):
"""Canonical wire payloads a provider must produce for the shared e2e suite."""
def answer(self, text: str) -> JsonResponse:
"""A non-streaming assistant turn that replies with `text`."""
...
def text_stream(self, text: str) -> list[Chunk]:
"""A streaming assistant turn whose deltas reassemble into `text`."""
...
def tool_call(self, name: str, arguments: dict[str, Any]) -> JsonResponse:
"""A non-streaming turn that calls tool `name` with `arguments`."""
...
def reasoning_answer(self, text: str, reasoning: str) -> JsonResponse:
"""A non-streaming turn that thinks `reasoning`, then replies `text`."""
...
def reasoning_tool_call_stream(
self, name: str, arguments: dict[str, Any], *, reasoning: str
) -> list[Chunk]:
"""A streamed turn that thinks `reasoning`, then calls tool `name`."""
...
class ProviderAPI:
"""Stubs a provider's completion wire; the AgentLoop stays the subject.
Subclasses describe one provider via `base_url`/`post_path`; override
`setup_monkeypatch` for non-HTTP stubs (e.g. credentials) a provider needs.
"""
base_url: ClassVar[str]
post_path: ClassVar[str]
route: respx.Route
def setup_router(self, router: respx.MockRouter) -> None:
"""Bind the completion route; subclasses extend for extra wires/defaults."""
self.route = router.post(self.post_path)
@classmethod
def setup_monkeypatch(cls, monkeypatch: pytest.MonkeyPatch) -> None:
"""Apply non-HTTP stubs (e.g. credential lookups) before use."""
def reply(self, *completions: dict[str, Any]) -> None:
responses = [httpx.Response(200, json=completion) for completion in completions]
if len(responses) == 1:
self.route.mock(return_value=responses[0])
else:
self.route.mock(side_effect=responses)
def reply_stream(self, chunks: list[bytes]) -> None:
self.route.mock(return_value=self._stream_response(chunks))
def reply_streams(self, *chunk_lists: list[bytes]) -> None:
self.route.mock(
side_effect=[self._stream_response(chunks) for chunks in chunk_lists]
)
@staticmethod
def _stream_response(chunks: list[bytes]) -> httpx.Response:
return httpx.Response(
200,
stream=httpx.ByteStream(stream=b"\n\n".join(chunks)),
headers={"Content-Type": "text/event-stream"},
)
@property
def request_json(self) -> dict[str, Any]:
return json.loads(self.route.calls.last.request.content)
def model_facing_text(self, index: int) -> str:
"""Return the text (raw prompt) that the model would have seen in the request
at the given call index.
"""
body = json.loads(self.route.calls[index].request.content)
return json.dumps([m for m in body["messages"] if m.get("role") != "system"])