v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.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: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com> Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com> Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
ad0d5c9520
commit
3f8487f761
197 changed files with 10819 additions and 2830 deletions
68
tests/backend/test_image_encoding_cache.py
Normal file
68
tests/backend/test_image_encoding_cache.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.llm.backend._image import (
|
||||
ImageReadError,
|
||||
_encode_cached,
|
||||
to_base64,
|
||||
to_data_uri,
|
||||
)
|
||||
from vibe.core.types import ImageAttachment
|
||||
|
||||
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_cache() -> None:
|
||||
_encode_cached.cache_clear()
|
||||
|
||||
|
||||
def _att(tmp_path: Path, name: str = "shot.png") -> ImageAttachment:
|
||||
p = tmp_path / name
|
||||
p.write_bytes(PNG_BYTES)
|
||||
return ImageAttachment(path=p, alias=name, mime_type="image/png")
|
||||
|
||||
|
||||
def test_repeated_calls_hit_cache_and_skip_disk(tmp_path: Path) -> None:
|
||||
att = _att(tmp_path)
|
||||
|
||||
first = to_base64(att)
|
||||
second = to_base64(att)
|
||||
third = to_data_uri(att)
|
||||
|
||||
assert first == second == base64.b64encode(PNG_BYTES).decode("ascii")
|
||||
assert third == f"data:image/png;base64,{first}"
|
||||
info = _encode_cached.cache_info()
|
||||
assert info.misses == 1
|
||||
assert info.hits == 2
|
||||
|
||||
|
||||
def test_cache_invalidates_when_file_mtime_changes(tmp_path: Path) -> None:
|
||||
att = _att(tmp_path)
|
||||
to_base64(att)
|
||||
assert _encode_cached.cache_info().misses == 1
|
||||
|
||||
new_bytes = PNG_BYTES + b"\x01"
|
||||
att.path.write_bytes(new_bytes)
|
||||
# Force a distinct mtime even on coarse-resolution filesystems.
|
||||
import os
|
||||
|
||||
stat = att.path.stat()
|
||||
os.utime(att.path, ns=(stat.st_atime_ns, stat.st_mtime_ns + 1_000_000))
|
||||
|
||||
refreshed = to_base64(att)
|
||||
assert refreshed == base64.b64encode(new_bytes).decode("ascii")
|
||||
assert _encode_cached.cache_info().misses == 2
|
||||
|
||||
|
||||
def test_missing_file_raises_image_read_error(tmp_path: Path) -> None:
|
||||
att = ImageAttachment(
|
||||
path=tmp_path / "nope.png", alias="nope.png", mime_type="image/png"
|
||||
)
|
||||
|
||||
with pytest.raises(ImageReadError):
|
||||
to_data_uri(att)
|
||||
146
tests/backend/test_image_mapping.py
Normal file
146
tests/backend/test_image_mapping.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from collections.abc import Sequence
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.llm.backend.anthropic import AnthropicAdapter
|
||||
from vibe.core.llm.backend.base import APIAdapter
|
||||
from vibe.core.llm.backend.generic import OpenAIAdapter
|
||||
from vibe.core.llm.backend.mistral import MistralMapper
|
||||
from vibe.core.llm.backend.openai_responses import OpenAIResponsesAdapter
|
||||
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
|
||||
from vibe.core.types import ImageAttachment, LLMMessage, Role
|
||||
|
||||
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
|
||||
EXPECTED_B64 = base64.b64encode(PNG_BYTES).decode("ascii")
|
||||
EXPECTED_DATA_URI = f"data:image/png;base64,{EXPECTED_B64}"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def image_attachment(tmp_path: Path) -> ImageAttachment:
|
||||
path = tmp_path / "shot.png"
|
||||
path.write_bytes(PNG_BYTES)
|
||||
return ImageAttachment(path=path, alias="shot.png", mime_type="image/png")
|
||||
|
||||
|
||||
def _user_message(image: ImageAttachment) -> LLMMessage:
|
||||
return LLMMessage(role=Role.user, content="describe this", images=[image])
|
||||
|
||||
|
||||
class _FakeProvider:
|
||||
name = "mistral"
|
||||
reasoning_field_name = "reasoning_content"
|
||||
|
||||
|
||||
def _adapter_payload(
|
||||
adapter: APIAdapter,
|
||||
messages: Sequence[LLMMessage],
|
||||
*,
|
||||
model: str = "gpt-4o",
|
||||
**overrides: Any,
|
||||
) -> dict[str, Any]:
|
||||
kwargs: dict[str, Any] = {
|
||||
"model_name": model,
|
||||
"messages": list(messages),
|
||||
"temperature": 0.0,
|
||||
"tools": None,
|
||||
"max_tokens": None,
|
||||
"tool_choice": None,
|
||||
"enable_streaming": False,
|
||||
"provider": _FakeProvider(),
|
||||
"api_key": "k",
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
prepared = adapter.prepare_request(**kwargs)
|
||||
return json.loads(prepared.body.decode("utf-8"))
|
||||
|
||||
|
||||
def test_mistral_mapper_emits_image_url_chunk(
|
||||
image_attachment: ImageAttachment,
|
||||
) -> None:
|
||||
mapper = MistralMapper()
|
||||
prepared = mapper.prepare_message(_user_message(image_attachment))
|
||||
|
||||
dumped = prepared.model_dump()
|
||||
assert dumped["role"] == "user"
|
||||
parts = dumped["content"]
|
||||
assert parts[0] == {"type": "text", "text": "describe this"}
|
||||
assert parts[1]["type"] == "image_url"
|
||||
assert parts[1]["image_url"]["url"] == EXPECTED_DATA_URI
|
||||
|
||||
|
||||
def test_openai_adapter_emits_image_url_part(image_attachment: ImageAttachment) -> None:
|
||||
payload = _adapter_payload(OpenAIAdapter(), [_user_message(image_attachment)])
|
||||
msg = payload["messages"][0]
|
||||
assert msg["role"] == "user"
|
||||
assert msg["content"][0] == {"type": "text", "text": "describe this"}
|
||||
assert msg["content"][1] == {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": EXPECTED_DATA_URI},
|
||||
}
|
||||
assert "images" not in msg
|
||||
|
||||
|
||||
def test_openai_responses_adapter_emits_input_image_part(
|
||||
image_attachment: ImageAttachment,
|
||||
) -> None:
|
||||
payload = _adapter_payload(
|
||||
OpenAIResponsesAdapter(), [_user_message(image_attachment)]
|
||||
)
|
||||
user = payload["input"][0]
|
||||
assert user["role"] == "user"
|
||||
assert user["content"][0] == {"type": "input_text", "text": "describe this"}
|
||||
assert user["content"][1] == {"type": "input_image", "image_url": EXPECTED_DATA_URI}
|
||||
|
||||
|
||||
def test_anthropic_adapter_emits_image_source(
|
||||
image_attachment: ImageAttachment,
|
||||
) -> None:
|
||||
payload = _adapter_payload(
|
||||
AnthropicAdapter(), [_user_message(image_attachment)], model="claude-3-5-sonnet"
|
||||
)
|
||||
msg = payload["messages"][0]
|
||||
assert msg["role"] == "user"
|
||||
assert msg["content"][0] == {"type": "text", "text": "describe this"}
|
||||
image_block = msg["content"][1]
|
||||
assert image_block["type"] == "image"
|
||||
assert image_block["source"] == {
|
||||
"type": "base64",
|
||||
"media_type": "image/png",
|
||||
"data": EXPECTED_B64,
|
||||
}
|
||||
# prepare_request stamps cache_control on the last block of the last user turn
|
||||
assert image_block["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
|
||||
def test_reasoning_adapter_emits_image_url_part(
|
||||
image_attachment: ImageAttachment,
|
||||
) -> None:
|
||||
payload = _adapter_payload(ReasoningAdapter(), [_user_message(image_attachment)])
|
||||
msg = payload["messages"][0]
|
||||
assert msg["role"] == "user"
|
||||
assert msg["content"][0] == {"type": "text", "text": "describe this"}
|
||||
assert msg["content"][1] == {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": EXPECTED_DATA_URI},
|
||||
}
|
||||
|
||||
|
||||
def test_text_only_user_message_keeps_string_content() -> None:
|
||||
text_msg = LLMMessage(role=Role.user, content="hi")
|
||||
|
||||
mistral_prepared = MistralMapper().prepare_message(text_msg)
|
||||
assert mistral_prepared.model_dump()["content"] == "hi"
|
||||
|
||||
anthropic_payload = _adapter_payload(
|
||||
AnthropicAdapter(), [text_msg], model="claude-3-5-sonnet"
|
||||
)
|
||||
# Anthropic emits parts even for text-only; keep as wrapped text part
|
||||
text_block = anthropic_payload["messages"][0]["content"][0]
|
||||
assert text_block["type"] == "text"
|
||||
assert text_block["text"] == "hi"
|
||||
|
|
@ -91,7 +91,8 @@ def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
|
|||
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()
|
||||
x509
|
||||
.CertificateBuilder()
|
||||
.subject_name(ca_name)
|
||||
.issuer_name(ca_name)
|
||||
.public_key(ca_key.public_key())
|
||||
|
|
@ -105,7 +106,8 @@ def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
|
|||
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()
|
||||
x509
|
||||
.CertificateBuilder()
|
||||
.subject_name(server_name)
|
||||
.issuer_name(ca_cert.subject)
|
||||
.public_key(server_key.public_key())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue