e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Unit tests for prompt_cache_ttl threading on the Anthropic path.
|
|
|
|
Anthropic's ``cache_control`` marker takes an optional ``ttl``: default 5m
|
|
pool, ``ttl:"1h"`` the 1h pool. These tests pin the outbound body shape:
|
|
"1h" puts ``ttl:"1h"`` on both markers; default omits the field; garbage
|
|
values are silently dropped.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from core.inference import external_provider as ep_mod
|
|
from core.inference.external_provider import ExternalProviderClient
|
|
|
|
|
|
def _drive(coro):
|
|
return asyncio.new_event_loop().run_until_complete(coro)
|
|
|
|
|
|
def _make_client() -> ExternalProviderClient:
|
|
return ExternalProviderClient(
|
|
provider_type = "anthropic",
|
|
base_url = "https://api.anthropic.com/v1",
|
|
api_key = "sk-ant-test",
|
|
)
|
|
|
|
|
|
def _capture(monkeypatch, ttl = None) -> dict:
|
|
captured: dict = {}
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
captured["body"] = json.loads(request.content.decode("utf-8"))
|
|
captured["headers"] = dict(request.headers)
|
|
return httpx.Response(
|
|
200,
|
|
content = (b"event: message_stop\n" b'data: {"type": "message_stop"}\n\n'),
|
|
headers = {"content-type": "text/event-stream"},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
ep_mod,
|
|
"_http_client",
|
|
httpx.AsyncClient(transport = httpx.MockTransport(handler)),
|
|
)
|
|
|
|
async def run():
|
|
client = _make_client()
|
|
async for _ in client.stream_chat_completion(
|
|
messages = [
|
|
{"role": "system", "content": "Be brief."},
|
|
{"role": "user", "content": "hi"},
|
|
],
|
|
model = "claude-opus-4-7",
|
|
temperature = 0.7,
|
|
top_p = 0.95,
|
|
max_tokens = 32,
|
|
enable_prompt_caching = True,
|
|
prompt_cache_ttl = ttl,
|
|
):
|
|
pass
|
|
await client.close()
|
|
|
|
_drive(run())
|
|
return captured
|
|
|
|
|
|
def _cache_controls(body: dict) -> list[dict]:
|
|
"""Pull every cache_control marker from the system block + tail message."""
|
|
out = []
|
|
sys_blocks = body.get("system") or []
|
|
if isinstance(sys_blocks, list):
|
|
for b in sys_blocks:
|
|
if isinstance(b, dict) and "cache_control" in b:
|
|
out.append(b["cache_control"])
|
|
msgs = body.get("messages") or []
|
|
if msgs:
|
|
tail = msgs[-1].get("content")
|
|
if isinstance(tail, list):
|
|
for b in tail:
|
|
if isinstance(b, dict) and "cache_control" in b:
|
|
out.append(b["cache_control"])
|
|
return out
|
|
|
|
|
|
# ── default (omitted) writes into the 5m pool ──────────────────────
|
|
|
|
|
|
def test_omitted_ttl_uses_default_5m_pool(monkeypatch):
|
|
captured = _capture(monkeypatch, ttl = None)
|
|
ccs = _cache_controls(captured["body"])
|
|
assert len(ccs) == 2, ccs
|
|
for cc in ccs:
|
|
assert cc == {"type": "ephemeral"}, cc
|
|
|
|
|
|
# ── explicit 5m round-trips as-is ─────────────────────────────────
|
|
|
|
|
|
def test_explicit_5m_ttl_round_trips(monkeypatch):
|
|
captured = _capture(monkeypatch, ttl = "5m")
|
|
ccs = _cache_controls(captured["body"])
|
|
assert len(ccs) == 2, ccs
|
|
for cc in ccs:
|
|
assert cc == {"type": "ephemeral", "ttl": "5m"}, cc
|
|
|
|
|
|
# ── 1h writes the new pool field on every marker ───────────────────
|
|
|
|
|
|
def test_1h_ttl_writes_into_1h_pool(monkeypatch):
|
|
captured = _capture(monkeypatch, ttl = "1h")
|
|
ccs = _cache_controls(captured["body"])
|
|
assert len(ccs) == 2, ccs
|
|
for cc in ccs:
|
|
assert cc == {"type": "ephemeral", "ttl": "1h"}, cc
|
|
|
|
|
|
def test_1h_ttl_does_not_send_extended_cache_ttl_beta_header(monkeypatch):
|
|
# The extended-cache-ttl-2025-04-11 beta header is now GA (verified live
|
|
# 2026-05-22); 1h TTL works with no beta header. Pin so a regression that
|
|
# re-adds the header surfaces here.
|
|
captured = _capture(monkeypatch, ttl = "1h")
|
|
beta = captured["headers"].get("anthropic-beta", "")
|
|
assert "extended-cache-ttl-2025-04-11" not in beta, beta
|
|
|
|
|
|
def test_5m_ttl_does_not_send_extended_cache_ttl_beta_header(monkeypatch):
|
|
captured = _capture(monkeypatch, ttl = "5m")
|
|
beta = captured["headers"].get("anthropic-beta", "")
|
|
assert "extended-cache-ttl-2025-04-11" not in beta, beta
|
|
|
|
|
|
# ── unknown values are dropped, not forwarded ──────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize("bogus", ["6m", "2h", "", "forever", "1d", "0", "1"])
|
|
def test_unknown_ttl_silently_dropped(monkeypatch, bogus):
|
|
captured = _capture(monkeypatch, ttl = bogus)
|
|
ccs = _cache_controls(captured["body"])
|
|
assert len(ccs) == 2, ccs
|
|
for cc in ccs:
|
|
# Bogus TTLs must not round-trip; marker stays at default (no ttl = 5m).
|
|
assert cc == {"type": "ephemeral"}, cc
|
|
|
|
|
|
# ── opt-out still skips cache_control entirely ─────────────────────
|
|
|
|
|
|
def test_opt_out_skips_cache_control(monkeypatch):
|
|
captured: dict = {}
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
captured["body"] = json.loads(request.content.decode("utf-8"))
|
|
return httpx.Response(
|
|
200,
|
|
content = b'event: message_stop\ndata: {"type": "message_stop"}\n\n',
|
|
headers = {"content-type": "text/event-stream"},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
ep_mod,
|
|
"_http_client",
|
|
httpx.AsyncClient(transport = httpx.MockTransport(handler)),
|
|
)
|
|
|
|
async def run():
|
|
client = _make_client()
|
|
async for _ in client.stream_chat_completion(
|
|
messages = [
|
|
{"role": "system", "content": "Be brief."},
|
|
{"role": "user", "content": "hi"},
|
|
],
|
|
model = "claude-opus-4-7",
|
|
temperature = 0.7,
|
|
top_p = 0.95,
|
|
max_tokens = 32,
|
|
enable_prompt_caching = False,
|
|
prompt_cache_ttl = "1h", # ignored when caching is off
|
|
):
|
|
pass
|
|
await client.close()
|
|
|
|
_drive(run())
|
|
assert _cache_controls(captured["body"]) == []
|