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
439 lines
19 KiB
Python
439 lines
19 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
|
|
|
|
"""Edge-case coverage for Anthropic fast-mode + refusal wiring.
|
|
|
|
Complements ``test_anthropic_fast_mode_and_refusal.py`` (happy path) with
|
|
dated snapshots, strict opt-in (future Opus families do not auto-enable),
|
|
multi-beta header merging, refusal stream ordering, and the
|
|
non-destruction guarantee for unset/None fast_mode.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import re
|
|
|
|
import httpx
|
|
|
|
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 _empty_message_sse(model: str = "claude-opus-4-7") -> bytes:
|
|
return (
|
|
b'event: message_start\ndata: {"type":"message_start","message":'
|
|
b'{"id":"m1","content":[],"model":"' + model.encode() + b'",'
|
|
b'"role":"assistant","stop_reason":null,"usage":'
|
|
b'{"input_tokens":1,"output_tokens":1}}}\n\n'
|
|
b'event: message_delta\ndata: {"type":"message_delta",'
|
|
b'"delta":{"stop_reason":"end_turn"}}\n\n'
|
|
b'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
)
|
|
|
|
|
|
def _refusal_sse(model: str = "claude-opus-4-7") -> bytes:
|
|
return (
|
|
b'event: message_start\ndata: {"type":"message_start","message":'
|
|
b'{"id":"m1","content":[],"model":"' + model.encode() + b'",'
|
|
b'"role":"assistant","stop_reason":null,"usage":'
|
|
b'{"input_tokens":1,"output_tokens":1}}}\n\n'
|
|
b'event: content_block_start\ndata: {"type":"content_block_start",'
|
|
b'"index":0,"content_block":{"type":"text","text":""}}\n\n'
|
|
b'event: content_block_delta\ndata: {"type":"content_block_delta",'
|
|
b'"index":0,"delta":{"type":"text_delta","text":"Hello."}}\n\n'
|
|
b'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n'
|
|
b'event: message_delta\ndata: {"type":"message_delta",'
|
|
b'"delta":{"stop_reason":"refusal"}}\n\n'
|
|
b'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
)
|
|
|
|
|
|
def _capture(
|
|
monkeypatch,
|
|
sse: bytes = b"",
|
|
**kwargs,
|
|
) -> tuple[dict, list[str]]:
|
|
"""Install a MockTransport, drive one streamed call, return body+lines."""
|
|
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 = sse or _empty_message_sse(kwargs.get("model", "claude-opus-4-7")),
|
|
headers = {"content-type": "text/event-stream"},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
ep_mod,
|
|
"_http_client",
|
|
httpx.AsyncClient(transport = httpx.MockTransport(handler)),
|
|
)
|
|
|
|
out_lines: list[str] = []
|
|
|
|
async def run():
|
|
client = _make_client()
|
|
try:
|
|
extra = {}
|
|
for key in (
|
|
"enabled_tools",
|
|
"compaction_threshold",
|
|
"fast_mode",
|
|
):
|
|
if key in kwargs:
|
|
extra[key] = kwargs[key]
|
|
async for line in client.stream_chat_completion(
|
|
messages = [{"role": "user", "content": "hi"}],
|
|
model = kwargs.get("model", "claude-opus-4-7"),
|
|
temperature = 0.7,
|
|
top_p = 0.95,
|
|
max_tokens = 32,
|
|
**extra,
|
|
):
|
|
out_lines.append(line)
|
|
finally:
|
|
await client.close()
|
|
|
|
_drive(run())
|
|
return captured, out_lines
|
|
|
|
|
|
# ──────────────────────────── dated snapshot prefix ────────────────────────────
|
|
def test_fast_mode_attaches_on_dated_opus_4_7_snapshot(monkeypatch):
|
|
"""Dated snapshot ``claude-opus-4-7-2026-02-01`` must match the prefix."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-7-2026-02-01")
|
|
assert cap["body"].get("speed") == "fast", cap["body"]
|
|
assert "fast-mode-2026-02-01" in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_attaches_on_dated_opus_4_6_snapshot(monkeypatch):
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-6-2026-02-01")
|
|
assert cap["body"].get("speed") == "fast", cap["body"]
|
|
assert "fast-mode-2026-02-01" in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
# ──────────────────────────── strict opt-in semantics ────────────────────────────
|
|
def test_fast_mode_does_not_auto_enable_on_future_opus_4_8(monkeypatch):
|
|
"""Future ``claude-opus-4-8`` must not auto-enable; per-family opt-in."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-8")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_does_not_auto_enable_on_future_opus_5(monkeypatch):
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-5")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_does_not_auto_enable_on_sonnet_dated_snapshot(monkeypatch):
|
|
"""Sonnet snapshots share the compaction prefix but not fast_mode."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-sonnet-4-6-2026-02-01")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
# ──────────────────────────── beta header merge ────────────────────────────
|
|
def _beta_parts(headers: dict) -> list[str]:
|
|
raw = headers.get("anthropic-beta", "")
|
|
return [p.strip() for p in raw.split(",") if p.strip()]
|
|
|
|
|
|
def test_fast_mode_merges_with_code_execution_beta(monkeypatch):
|
|
"""fast_mode + code_execution -> two comma-separated betas, no overwrite."""
|
|
cap, _ = _capture(
|
|
monkeypatch,
|
|
fast_mode = True,
|
|
model = "claude-opus-4-7",
|
|
enabled_tools = ["code_execution"],
|
|
)
|
|
parts = _beta_parts(cap["headers"])
|
|
assert "fast-mode-2026-02-01" in parts, cap["headers"]
|
|
assert any(p.startswith("code-execution-") for p in parts), cap["headers"]
|
|
# No duplicates.
|
|
assert len(parts) == len(set(parts)), parts
|
|
|
|
|
|
def test_fast_mode_merges_with_compaction_beta(monkeypatch):
|
|
"""fast_mode + compaction_threshold >= 50K -> both betas present."""
|
|
cap, _ = _capture(
|
|
monkeypatch,
|
|
fast_mode = True,
|
|
model = "claude-opus-4-7",
|
|
compaction_threshold = 100_000,
|
|
)
|
|
parts = _beta_parts(cap["headers"])
|
|
assert "fast-mode-2026-02-01" in parts, cap["headers"]
|
|
assert "compact-2026-01-12" in parts, cap["headers"]
|
|
|
|
|
|
def test_fast_mode_merges_with_code_execution_and_compaction(monkeypatch):
|
|
"""Three betas coexist in one comma-separated header, no duplicates."""
|
|
cap, _ = _capture(
|
|
monkeypatch,
|
|
fast_mode = True,
|
|
model = "claude-opus-4-7",
|
|
enabled_tools = ["code_execution"],
|
|
compaction_threshold = 100_000,
|
|
)
|
|
parts = _beta_parts(cap["headers"])
|
|
assert "fast-mode-2026-02-01" in parts
|
|
assert "compact-2026-01-12" in parts
|
|
assert any(p.startswith("code-execution-") for p in parts), parts
|
|
assert len(parts) >= 3
|
|
assert len(parts) == len(set(parts)), parts
|
|
|
|
|
|
def test_fast_mode_beta_value_is_pinned(monkeypatch):
|
|
"""Pin the exact beta tag ``fast-mode-2026-02-01`` from the docs."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-7")
|
|
parts = _beta_parts(cap["headers"])
|
|
assert "fast-mode-2026-02-01" in parts, parts
|
|
# Reject obvious typos.
|
|
assert not any(p.startswith("fastmode-") for p in parts), parts
|
|
assert not any("fast_mode" in p for p in parts), parts
|
|
|
|
|
|
# ──────────────────────────── non-destruction guarantee ────────────────────────────
|
|
def test_fast_mode_unset_is_byte_identical_to_omitted(monkeypatch):
|
|
"""``fast_mode=None`` must produce the same body/headers as omission."""
|
|
cap_none, _ = _capture(monkeypatch, fast_mode = None, model = "claude-opus-4-7")
|
|
|
|
# Re-run without passing fast_mode at all.
|
|
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 = _empty_message_sse(),
|
|
headers = {"content-type": "text/event-stream"},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
ep_mod,
|
|
"_http_client",
|
|
httpx.AsyncClient(transport = httpx.MockTransport(handler)),
|
|
)
|
|
|
|
async def run():
|
|
client = _make_client()
|
|
try:
|
|
async for _ in client.stream_chat_completion(
|
|
messages = [{"role": "user", "content": "hi"}],
|
|
model = "claude-opus-4-7",
|
|
temperature = 0.7,
|
|
top_p = 0.95,
|
|
max_tokens = 32,
|
|
):
|
|
pass
|
|
finally:
|
|
await client.close()
|
|
|
|
_drive(run())
|
|
|
|
assert cap_none["body"] == captured["body"], (cap_none["body"], captured["body"])
|
|
# Headers vary by httpx-injected fields (host, connection); compare
|
|
# the load-bearing ones.
|
|
for key in ("anthropic-version", "x-api-key", "content-type"):
|
|
assert cap_none["headers"].get(key) == captured["headers"].get(key), key
|
|
assert "anthropic-beta" not in cap_none["headers"]
|
|
assert "anthropic-beta" not in captured["headers"]
|
|
assert "speed" not in cap_none["body"]
|
|
assert "speed" not in captured["body"]
|
|
|
|
|
|
def test_fast_mode_false_on_opus_4_7_byte_identical_to_unset(monkeypatch):
|
|
"""``fast_mode=False`` produces the same outbound shape as unset."""
|
|
cap_false, _ = _capture(monkeypatch, fast_mode = False, model = "claude-opus-4-7")
|
|
assert "speed" not in cap_false["body"], cap_false["body"]
|
|
assert "fast-mode-2026-02-01" not in cap_false["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
# ──────────────────────────── refusal stream ordering ────────────────────────────
|
|
def test_refusal_notice_appears_before_content_filter_chunk(monkeypatch):
|
|
"""The notice content delta must precede the finish_reason chunk."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse(), model = "claude-opus-4-7")
|
|
notice_idx = next(i for i, l in enumerate(lines) if "stopped by Anthropic" in l)
|
|
filter_idx = next(i for i, l in enumerate(lines) if '"finish_reason": "content_filter"' in l)
|
|
assert notice_idx < filter_idx, (notice_idx, filter_idx, lines)
|
|
|
|
|
|
def test_refusal_tool_event_emitted_exactly_once(monkeypatch):
|
|
"""A single refusal emits the chat-adapter drop signal exactly once."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse())
|
|
body = "\n".join(lines)
|
|
count = body.count('"_toolEvent": {"type": "anthropic_refusal"}')
|
|
assert count == 1, (count, body)
|
|
|
|
|
|
def test_refusal_text_carries_no_html_sentinel(monkeypatch):
|
|
"""Visible refusal text must not embed a ``studio:anthropic-refusal``
|
|
sentinel; the drop signal rides _toolEvent only."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse())
|
|
body = "\n".join(lines)
|
|
assert "studio:anthropic-refusal" not in body, body
|
|
|
|
|
|
def test_refusal_handling_works_on_sonnet_model(monkeypatch):
|
|
"""Refusal handling is provider-side; Sonnet refusals must also surface."""
|
|
_, lines = _capture(
|
|
monkeypatch, sse = _refusal_sse("claude-sonnet-4-6"), model = "claude-sonnet-4-6"
|
|
)
|
|
body = "\n".join(lines)
|
|
assert "stopped by Anthropic's safety classifier" in body, body
|
|
assert '"_toolEvent": {"type": "anthropic_refusal"}' in body, body
|
|
assert '"finish_reason": "content_filter"' in body, body
|
|
|
|
|
|
def test_refusal_preserves_partial_assistant_text(monkeypatch):
|
|
"""Partial deltas already streamed must precede the refusal notice."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse(), model = "claude-opus-4-7")
|
|
body = "\n".join(lines)
|
|
hello_idx = body.index("Hello.")
|
|
notice_idx = body.index("stopped by Anthropic")
|
|
assert hello_idx < notice_idx, (hello_idx, notice_idx)
|
|
|
|
|
|
def test_refusal_chunk_is_proper_openai_delta_shape(monkeypatch):
|
|
"""The notice rides ``choices[0].delta.content`` (not a finish chunk);
|
|
OpenAI-spec clients treat it as ordinary streamed text."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse(), model = "claude-opus-4-7")
|
|
# Find the chunk that carries the refusal text.
|
|
notice_chunk = None
|
|
for line in lines:
|
|
if line.startswith("data: ") and "stopped by Anthropic" in line:
|
|
notice_chunk = json.loads(line[len("data: ") :])
|
|
break
|
|
assert notice_chunk is not None, lines
|
|
choice = notice_chunk["choices"][0]
|
|
assert "delta" in choice and "content" in choice["delta"], notice_chunk
|
|
# Must NOT carry a finish_reason itself -- that comes on the next chunk.
|
|
assert choice.get("finish_reason") in (None,), notice_chunk
|
|
# Refusal text is plain-spoken; no embedded sentinel.
|
|
assert "studio:anthropic-refusal" not in choice["delta"]["content"]
|
|
|
|
|
|
def test_refusal_tool_event_chunk_shape(monkeypatch):
|
|
"""Drop signal rides a Studio `_toolEvent` envelope (delta={},
|
|
finish_reason=null); the frontend latches on
|
|
`_toolEvent.type == "anthropic_refusal"`."""
|
|
_, lines = _capture(monkeypatch, sse = _refusal_sse(), model = "claude-opus-4-7")
|
|
refusal_chunk = None
|
|
for line in lines:
|
|
if line.startswith("data: ") and "anthropic_refusal" in line:
|
|
refusal_chunk = json.loads(line[len("data: ") :])
|
|
break
|
|
assert refusal_chunk is not None, lines
|
|
assert refusal_chunk["_toolEvent"] == {"type": "anthropic_refusal"}, refusal_chunk
|
|
choice = refusal_chunk["choices"][0]
|
|
assert choice["delta"] == {}, refusal_chunk
|
|
assert choice["finish_reason"] is None, refusal_chunk
|
|
|
|
|
|
# ──────────────────────────── future-proofing ────────────────────────────
|
|
def test_fast_mode_prefix_tuple_matches_capability_doc(monkeypatch):
|
|
"""Tuple must exactly match the two families in the upstream docs:
|
|
https://platform.claude.com/docs/en/build-with-claude/fast-mode."""
|
|
from core.inference.external_provider import _ANTHROPIC_FAST_MODE_PREFIXES
|
|
assert set(_ANTHROPIC_FAST_MODE_PREFIXES) == {
|
|
"claude-opus-4-7",
|
|
"claude-opus-4-6",
|
|
}, _ANTHROPIC_FAST_MODE_PREFIXES
|
|
|
|
|
|
def test_fast_mode_speed_field_value_is_literal_fast(monkeypatch):
|
|
"""Pin the wire value to the literal string ``"fast"``."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-7")
|
|
assert cap["body"]["speed"] == "fast", cap["body"]
|
|
|
|
|
|
def test_fast_mode_dropped_on_opus_4_5_dated_snapshot(monkeypatch):
|
|
"""Previous-family snapshots like ``claude-opus-4-5-2025-...`` must not match."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-5-2025-08-01")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_rejects_prefix_collision_4_70(monkeypatch):
|
|
"""IDs like ``claude-opus-4-70`` / ``-4-7b`` must not match the prefix."""
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-70")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_rejects_prefix_collision_4_7b(monkeypatch):
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-7b")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
def test_fast_mode_rejects_prefix_collision_4_6_extra(monkeypatch):
|
|
cap, _ = _capture(monkeypatch, fast_mode = True, model = "claude-opus-4-60")
|
|
assert "speed" not in cap["body"], cap["body"]
|
|
assert "fast-mode-2026-02-01" not in cap["headers"].get("anthropic-beta", "")
|
|
|
|
|
|
# ──────────────────────────── usage.speed propagation ────────────────────────────
|
|
def _fast_speed_sse(model: str = "claude-opus-4-7", speed: str = "fast") -> bytes:
|
|
return (
|
|
b'event: message_start\ndata: {"type":"message_start","message":'
|
|
b'{"id":"m1","content":[],"model":"' + model.encode() + b'",'
|
|
b'"role":"assistant","stop_reason":null,"usage":'
|
|
b'{"input_tokens":4,"output_tokens":1}}}\n\n'
|
|
b'event: content_block_start\ndata: {"type":"content_block_start",'
|
|
b'"index":0,"content_block":{"type":"text","text":""}}\n\n'
|
|
b'event: content_block_delta\ndata: {"type":"content_block_delta",'
|
|
b'"index":0,"delta":{"type":"text_delta","text":"hi"}}\n\n'
|
|
b'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n'
|
|
b'event: message_delta\ndata: {"type":"message_delta",'
|
|
b'"delta":{"stop_reason":"end_turn"},'
|
|
b'"usage":{"output_tokens":5,"speed":"' + speed.encode() + b'"}}\n\n'
|
|
b'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
)
|
|
|
|
|
|
def test_usage_speed_propagates_to_final_usage_chunk_fast(monkeypatch):
|
|
"""``usage.speed == "fast"`` from upstream must reach the Studio usage chunk."""
|
|
_, lines = _capture(monkeypatch, sse = _fast_speed_sse(speed = "fast"))
|
|
usage_lines = [l for l in lines if l.startswith("data: ") and '"usage"' in l]
|
|
assert usage_lines, lines
|
|
parsed = [json.loads(l[len("data: ") :]) for l in usage_lines]
|
|
speeds = [p["usage"].get("speed") for p in parsed if "usage" in p]
|
|
assert "fast" in speeds, parsed
|
|
|
|
|
|
def test_usage_speed_propagates_to_final_usage_chunk_standard(monkeypatch):
|
|
_, lines = _capture(monkeypatch, sse = _fast_speed_sse(speed = "standard"))
|
|
parsed = [
|
|
json.loads(l[len("data: ") :]) for l in lines if l.startswith("data: ") and '"usage"' in l
|
|
]
|
|
speeds = [p["usage"].get("speed") for p in parsed if "usage" in p]
|
|
assert "standard" in speeds, parsed
|
|
|
|
|
|
def test_usage_speed_absent_when_anthropic_does_not_report(monkeypatch):
|
|
"""Studio must not invent ``usage.speed`` when upstream omits it."""
|
|
_, lines = _capture(monkeypatch)
|
|
parsed = [
|
|
json.loads(l[len("data: ") :]) for l in lines if l.startswith("data: ") and '"usage"' in l
|
|
]
|
|
for p in parsed:
|
|
usage = p.get("usage") or {}
|
|
assert "speed" not in usage, p
|