mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-09 02:54:37 +00:00
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""Tests for Anthropic cache-breakpoint injection on forwarded requests.
|
|
|
|
Anthropic prompt caching is explicit; a client that doesn't recognise a routstr
|
|
URL as Anthropic-backed never sends ``cache_control`` markers, so caching never
|
|
engages over routstr. ``prepare_request_body`` must stamp the standard
|
|
breakpoints for Anthropic-family models while always deferring to client-set
|
|
markers and never touching automatic-cache providers.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("UPSTREAM_BASE_URL", "http://test")
|
|
os.environ.setdefault("UPSTREAM_API_KEY", "test")
|
|
os.environ.setdefault("LIGHTNING_ADDRESS", "test@stm.to")
|
|
|
|
from routstr.upstream import GenericUpstreamProvider
|
|
from routstr.upstream.cache_breakpoints import (
|
|
body_has_cache_control,
|
|
inject_anthropic_cache_breakpoints,
|
|
is_explicit_cache_model,
|
|
)
|
|
|
|
|
|
def _chat_body() -> dict:
|
|
return {
|
|
"model": "anthropic/claude-sonnet-4.5",
|
|
"stream": True,
|
|
"messages": [
|
|
{"role": "system", "content": "You are concise."},
|
|
{"role": "user", "content": "Hello"},
|
|
],
|
|
"tools": [
|
|
{"type": "function", "function": {"name": "a"}},
|
|
{"type": "function", "function": {"name": "b"}},
|
|
],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Model detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_id,expected",
|
|
[
|
|
("anthropic/claude-sonnet-4.5", True),
|
|
("claude-haiku-4-5-20251001", True),
|
|
# Alibaba explicit-cache models share Anthropic's wire format
|
|
("qwen/qwen3-max", True),
|
|
("qwen/qwen3-coder-plus", True),
|
|
("deepseek/deepseek-v3.2", True),
|
|
# Automatic-cache providers need no markers
|
|
("openai/gpt-4o", False),
|
|
("google/gemini-2.5-flash", False),
|
|
("deepseek/deepseek-chat", False),
|
|
("qwen/qwen3.5-plus-02-15", False), # snapshot, no explicit caching
|
|
(None, False),
|
|
],
|
|
)
|
|
def test_is_explicit_cache_model(model_id: str | None, expected: bool) -> None:
|
|
assert is_explicit_cache_model(model_id) is expected
|
|
|
|
|
|
def test_is_explicit_cache_model_uses_fallbacks() -> None:
|
|
# routstr id is opaque but a forwarded/canonical alias reveals the family.
|
|
assert is_explicit_cache_model("model-xyz", None, "anthropic/claude-opus-4.1")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Breakpoint placement
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_injects_three_breakpoints() -> None:
|
|
data = _chat_body()
|
|
assert inject_anthropic_cache_breakpoints(data) is True
|
|
|
|
# system prompt promoted to array form with a marker
|
|
system = data["messages"][0]["content"]
|
|
assert system == [
|
|
{
|
|
"type": "text",
|
|
"text": "You are concise.",
|
|
"cache_control": {"type": "ephemeral"},
|
|
}
|
|
]
|
|
# last tool marked
|
|
assert data["tools"][-1]["cache_control"] == {"type": "ephemeral"}
|
|
assert "cache_control" not in data["tools"][0]
|
|
# last user message marked
|
|
user = data["messages"][1]["content"]
|
|
assert user[-1]["cache_control"] == {"type": "ephemeral"}
|
|
|
|
|
|
def test_defers_to_client_supplied_cache_control() -> None:
|
|
data = _chat_body()
|
|
data["messages"][1]["content"] = [
|
|
{"type": "text", "text": "Hello", "cache_control": {"type": "ephemeral"}}
|
|
]
|
|
assert body_has_cache_control(data) is True
|
|
# No additional stamping when the client already controls caching.
|
|
assert inject_anthropic_cache_breakpoints(data) is False
|
|
assert "cache_control" not in data["tools"][-1]
|
|
|
|
|
|
def test_marks_last_text_part_of_array_content() -> None:
|
|
data = _chat_body()
|
|
data["messages"][1]["content"] = [
|
|
{"type": "text", "text": "first"},
|
|
{"type": "image_url", "image_url": {"url": "x"}},
|
|
{"type": "text", "text": "last"},
|
|
]
|
|
inject_anthropic_cache_breakpoints(data)
|
|
parts = data["messages"][1]["content"]
|
|
assert parts[2]["cache_control"] == {"type": "ephemeral"}
|
|
assert "cache_control" not in parts[0]
|
|
|
|
|
|
def test_noop_without_messages() -> None:
|
|
assert inject_anthropic_cache_breakpoints({"prompt": "x"}) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# prepare_request_body integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _model(model_id: str): # type: ignore[no-untyped-def]
|
|
from routstr.payment.models import Architecture, Model, Pricing
|
|
|
|
return Model(
|
|
id=model_id,
|
|
name=model_id,
|
|
created=0,
|
|
description="",
|
|
context_length=200000,
|
|
architecture=Architecture(
|
|
modality="text->text",
|
|
input_modalities=["text"],
|
|
output_modalities=["text"],
|
|
tokenizer="Claude",
|
|
instruct_type=None,
|
|
),
|
|
pricing=Pricing(prompt=0.0, completion=0.0),
|
|
)
|
|
|
|
|
|
def _openrouter_provider() -> "GenericUpstreamProvider":
|
|
# OpenRouter endpoint via the generic provider — recognised by base URL.
|
|
return GenericUpstreamProvider(base_url="https://openrouter.ai/api/v1")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"model_id", ["anthropic/claude-sonnet-4.5", "qwen/qwen3-max", "deepseek/deepseek-v3.2"]
|
|
)
|
|
def test_prepare_request_body_injects_for_explicit_models(model_id: str) -> None:
|
|
provider = _openrouter_provider()
|
|
body = json.dumps(_chat_body()).encode()
|
|
out = provider.prepare_request_body(body, _model(model_id))
|
|
assert out is not None
|
|
data = json.loads(out)
|
|
assert body_has_cache_control(data) is True
|
|
assert data["tools"][-1]["cache_control"] == {"type": "ephemeral"}
|
|
|
|
|
|
def test_prepare_request_body_skips_for_automatic_provider_model() -> None:
|
|
provider = _openrouter_provider()
|
|
body = json.dumps(_chat_body()).encode()
|
|
out = provider.prepare_request_body(body, _model("openai/gpt-4o"))
|
|
assert out is not None
|
|
data = json.loads(out)
|
|
assert body_has_cache_control(data) is False
|
|
|
|
|
|
def test_prepare_request_body_skips_when_upstream_rejects_markers() -> None:
|
|
# Claude id but a non-OpenRouter/Anthropic upstream → must NOT inject,
|
|
# since the markers could be rejected by an upstream that doesn't accept them.
|
|
from routstr.upstream import GenericUpstreamProvider
|
|
|
|
provider = GenericUpstreamProvider(base_url="https://some-gateway.example/v1")
|
|
body = json.dumps(_chat_body()).encode()
|
|
out = provider.prepare_request_body(body, _model("anthropic/claude-sonnet-4.5"))
|
|
assert out is not None
|
|
data = json.loads(out)
|
|
assert body_has_cache_control(data) is False
|