mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
1e7ad7bcb6
Every single-shot lane (model_turn: judges, titles, compaction, web-fetch extraction, perception, eval, optimizer) now samples through the provider's streaming entry and accumulates via a shared drain_stream(), deleting create_completion from the Protocol and all three adapters (xai/google inherit). Request shaping can no longer drift between the two consumption styles, and callers keep the exact CompletionResult contract. The drain mirrors the main loop's proven chunk semantics: per-field max-merge for usage (Anthropic splits prompt/completion across message_start/message_delta), tool-call assembly by delta index, provider_blocks from the terminal emission, trailing citation info folded back into content (byte-matching the old format_citations append), mid-stream status pings dropped. Also in this change: - model_turn grows cancel_ref; both judges wire their run_with_deadline abandon paths to a new StreamAbortRef (deadline.py) that closes the SDK stream — a timed-out judge call now aborts its HTTP read instead of pinning a daemon thread until the next upstream chunk. The append hook covers the arrival race, mirroring ChatSession._CancelRef. - Responses streaming gains the response.incomplete terminal handler (truncated runs were mislabeled finish=stop and lost final usage AND collected provider_blocks) and a refusal handler ([Refused: …] content, matching the retired non-streaming rendering). Both also fix the main chat loop, which shared the gaps. - supports_streaming capability flag deleted (zero readers) along with its admin capability tile; o1-era models that reject streaming need a model alias pointing at a current model (release-noted). - Helpers that existed only for the deleted transport go with it: Responses._parse_response, chat/google._extract_tool_calls. Known behavioral deltas (release-noted): OpenAI-compatible servers that ignore stream_options.include_usage stop producing usage rows on these lanes; multiple Anthropic text blocks concatenate without the old "\n" joint (matching the main loop); model_turn lanes no longer risk client read-timeouts on long generations — the reason the Anthropic adapter already drained a stream internally. Tests: new test_drain_stream.py pins the accumulator rules; shared fakes (as_stream, fake_chat_stream, fake_anthropic_stream) migrate 11 suites to the streaming transport, with the task-agent and adapter suites now exercising the real _iter_stream + drain path end to end.
542 lines
23 KiB
Python
542 lines
23 KiB
Python
"""Tests for the ``anthropic-compatible`` provider lane.
|
|
|
|
Local servers (vLLM) expose Anthropic's ``/v1/messages`` wire surface for
|
|
arbitrary checkpoints. The lane reuses ``AnthropicProvider`` with
|
|
``compat=True``: identical message translation, but capabilities come from
|
|
``_ANTHROPIC_COMPAT_DEFAULT`` for every model (the static Claude table
|
|
never applies), native server-side tools are not injected, and operator
|
|
``server_compat["extra_body"]`` overrides ride the Anthropic SDK's
|
|
``extra_body`` — the channel for vLLM's ``chat_template_kwargs`` reasoning
|
|
toggle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import os
|
|
import sys
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from tests._session_helpers import make_session as _make_session
|
|
from turnstone.core.providers._anthropic import AnthropicProvider
|
|
from turnstone.core.providers._protocol import ModelCapabilities
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _capture_client() -> MagicMock:
|
|
"""Build a fake Anthropic client whose ``messages.stream`` records kwargs."""
|
|
stream_ctx = MagicMock()
|
|
stream_ctx.__enter__ = MagicMock(return_value=iter([]))
|
|
stream_ctx.__exit__ = MagicMock(return_value=False)
|
|
client = MagicMock()
|
|
client.messages.stream.return_value = stream_ctx
|
|
return client
|
|
|
|
|
|
_WEB_SEARCH_FUNCTION_TOOL = {
|
|
"type": "function",
|
|
"function": {
|
|
"name": "web_search",
|
|
"description": "Search the web",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCompatCapabilities
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCompatCapabilities:
|
|
"""Capability resolution on the compat lane."""
|
|
|
|
def test_compat_capability_defaults(self) -> None:
|
|
provider = AnthropicProvider(compat=True)
|
|
caps = provider.get_capabilities("deepseek-ai/DeepSeek-V4-Flash")
|
|
assert caps.token_param == "max_tokens"
|
|
assert caps.thinking_mode == "none"
|
|
assert caps.supports_web_search is False
|
|
assert caps.supports_tool_search is False
|
|
assert caps.supports_vision is False
|
|
assert caps.supports_reasoning_replay is True
|
|
assert caps.supports_temperature is True
|
|
|
|
def test_claude_id_does_not_pick_up_static_table(self) -> None:
|
|
"""A Claude-named local checkpoint must not inherit Claude API caps."""
|
|
provider = AnthropicProvider(compat=True)
|
|
caps = provider.get_capabilities("claude-opus-4-6")
|
|
assert caps.context_window == 200000
|
|
assert caps.thinking_mode == "none"
|
|
assert caps.supports_web_search is False
|
|
# The real lane still resolves the static entry.
|
|
real_caps = AnthropicProvider().get_capabilities("claude-opus-4-6")
|
|
assert real_caps.context_window == 1000000
|
|
assert real_caps.thinking_mode == "adaptive"
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCompatWireShape
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCompatWireShape:
|
|
"""Body-inspecting tests on the kwargs handed to ``messages.stream``."""
|
|
|
|
def setup_method(self) -> None:
|
|
self.provider = AnthropicProvider(compat=True)
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_compat_no_web_search_swap_no_temp_force(self, mock_ensure: MagicMock) -> None:
|
|
"""No native web_search swap, no temperature=1 forcing, max_tokens param."""
|
|
client = _capture_client()
|
|
list(
|
|
self.provider.create_streaming(
|
|
client=client,
|
|
model="deepseek-ai/DeepSeek-V4-Flash",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
tools=[_WEB_SEARCH_FUNCTION_TOOL],
|
|
temperature=0.6,
|
|
)
|
|
)
|
|
kwargs = client.messages.stream.call_args[1]
|
|
sent_tools = kwargs["tools"]
|
|
assert sent_tools == [
|
|
{
|
|
"name": "web_search",
|
|
"description": "Search the web",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"],
|
|
},
|
|
}
|
|
]
|
|
assert all(t.get("type") != "web_search_20250305" for t in sent_tools)
|
|
assert kwargs["temperature"] == 0.6
|
|
assert "thinking" not in kwargs
|
|
assert "max_tokens" in kwargs
|
|
assert "max_completion_tokens" not in kwargs
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_extra_params_passthrough_to_extra_body(self, mock_ensure: MagicMock) -> None:
|
|
"""server_compat extra_body (chat_template_kwargs) reaches the SDK."""
|
|
client = _capture_client()
|
|
list(
|
|
self.provider.create_streaming(
|
|
client=client,
|
|
model="deepseek-ai/DeepSeek-V4-Flash",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
extra_params={"chat_template_kwargs": {"thinking": False}},
|
|
)
|
|
)
|
|
kwargs = client.messages.stream.call_args[1]
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"thinking": False}}
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_internal_keys_not_leaked(self, mock_ensure: MagicMock) -> None:
|
|
"""Real-lane request bodies stay byte-identical with thinking overrides.
|
|
|
|
``thinking_budget_tokens`` is consumed by ``_reasoning_params`` and
|
|
must never surface as wire ``extra_body`` — a leaked key would change
|
|
every real-Anthropic request that threads a thinking override.
|
|
Negative-tested: fails when the ``_INTERNAL_EXTRA_PARAMS`` exclusion
|
|
is removed from ``_build_thinking_and_kwargs``. The effort knob is
|
|
explicit: unset effort means thinking OFF (the budget override
|
|
modifies a thinking block, it never creates one).
|
|
"""
|
|
provider = AnthropicProvider()
|
|
client = _capture_client()
|
|
list(
|
|
provider.create_streaming(
|
|
client=client,
|
|
model="claude-sonnet-4-5",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
reasoning_effort="medium",
|
|
extra_params={"thinking_budget_tokens": 2048},
|
|
)
|
|
)
|
|
kwargs = client.messages.stream.call_args[1]
|
|
assert "extra_body" not in kwargs
|
|
assert kwargs["thinking"] == {"type": "enabled", "budget_tokens": 2048}
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCompatReasoningControl
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCompatReasoningControl:
|
|
"""Session effort knob → ``chat_template_kwargs`` on the compat lane.
|
|
|
|
vLLM's ``/v1/messages`` ignores the native ``thinking`` param — the
|
|
reasoning levers live in the chat template.
|
|
``merge_reasoning_template_kwargs`` maps the knob onto
|
|
``caps.thinking_param`` (manual: knob "none" = off, mirroring
|
|
``_reasoning_params``; adaptive: always on) and ``caps.effort_param``
|
|
(graded value for gpt-oss-style templates). Verified live against
|
|
qwen3.6 on vLLM 2026-07-03: ``{"enable_thinking": false}`` disables
|
|
thinking, unknown chat_template_kwargs keys are silently ignored.
|
|
"""
|
|
|
|
_MANUAL_CAPS = ModelCapabilities(
|
|
token_param="max_tokens",
|
|
thinking_mode="manual",
|
|
thinking_param="enable_thinking",
|
|
)
|
|
|
|
def setup_method(self) -> None:
|
|
self.provider = AnthropicProvider(compat=True)
|
|
|
|
def _stream_kwargs(
|
|
self,
|
|
caps: ModelCapabilities | None,
|
|
reasoning_effort: str,
|
|
extra_params: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
client = _capture_client()
|
|
with patch("turnstone.core.providers._anthropic._ensure_anthropic"):
|
|
list(
|
|
self.provider.create_streaming(
|
|
client=client,
|
|
model="qwen3.6-27b",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
temperature=0.6,
|
|
reasoning_effort=reasoning_effort,
|
|
extra_params=extra_params,
|
|
capabilities=caps,
|
|
)
|
|
)
|
|
return client.messages.stream.call_args[1]
|
|
|
|
def test_manual_toggle_on(self) -> None:
|
|
"""Any non-none effort turns the toggle on AND carries the graded
|
|
value under the fallback key — the user's effort setting always
|
|
reaches the wire; a template that doesn't reference the kwarg
|
|
ignores it."""
|
|
kwargs = self._stream_kwargs(self._MANUAL_CAPS, "medium")
|
|
assert kwargs["extra_body"] == {
|
|
"chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "medium"}
|
|
}
|
|
assert "thinking" not in kwargs
|
|
assert kwargs["temperature"] == 0.6 # never forced to 1.0 on compat
|
|
|
|
def test_manual_toggle_explicit_off(self) -> None:
|
|
"""The explicit "none" knob disables thinking — native manual-mode
|
|
parity; no effort key rides when thinking is off."""
|
|
kwargs = self._stream_kwargs(self._MANUAL_CAPS, "none")
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}}
|
|
assert "thinking" not in kwargs
|
|
|
|
def test_manual_unset_injects_nothing(self) -> None:
|
|
"""An UNSET knob (no rung of the assignment scheme resolved a
|
|
value) injects no toggle at all — the template's own default
|
|
rules, matching "if not set, we don't send it". Distinct from
|
|
the explicit "none" off-switch above."""
|
|
kwargs = self._stream_kwargs(self._MANUAL_CAPS, "")
|
|
assert "extra_body" not in kwargs
|
|
assert "thinking" not in kwargs
|
|
|
|
def test_adaptive_always_on(self) -> None:
|
|
"""Adaptive never knob-disables — native-adaptive contract, no native
|
|
dict; the graded value rides for on-positions only."""
|
|
caps = dataclasses.replace(self._MANUAL_CAPS, thinking_mode="adaptive")
|
|
kwargs = self._stream_kwargs(caps, "high")
|
|
assert kwargs["extra_body"] == {
|
|
"chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "high"}
|
|
}
|
|
assert "thinking" not in kwargs
|
|
assert kwargs["temperature"] == 0.6
|
|
kwargs = self._stream_kwargs(caps, "none")
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"enable_thinking": True}}
|
|
assert "thinking" not in kwargs
|
|
assert kwargs["temperature"] == 0.6
|
|
|
|
def test_default_caps_inject_nothing(self) -> None:
|
|
"""Untouched compat defaults (thinking_mode=none) keep today's wire."""
|
|
kwargs = self._stream_kwargs(None, "medium")
|
|
assert "extra_body" not in kwargs
|
|
assert "thinking" not in kwargs
|
|
|
|
def test_effort_param_validated_against_values(self) -> None:
|
|
"""Off-list knob rounds up onto the declared values (ceiling-capped),
|
|
never sent raw — and never snaps DOWN to the default."""
|
|
caps = dataclasses.replace(
|
|
self._MANUAL_CAPS,
|
|
effort_param="reasoning_effort",
|
|
reasoning_effort_values=("low", "medium", "high"),
|
|
default_reasoning_effort="medium",
|
|
)
|
|
kwargs = self._stream_kwargs(caps, "xhigh")
|
|
assert kwargs["extra_body"] == {
|
|
"chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "high"}
|
|
}
|
|
|
|
def test_effort_param_freeform_without_values(self) -> None:
|
|
"""No declared values → knob forwarded as-is, template is authority."""
|
|
caps = ModelCapabilities(
|
|
token_param="max_tokens",
|
|
effort_param="reasoning_effort",
|
|
)
|
|
kwargs = self._stream_kwargs(caps, "xhigh")
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"reasoning_effort": "xhigh"}}
|
|
|
|
def test_effort_param_omitted_on_none(self) -> None:
|
|
"""Knob "none" sends no effort key (and toggles thinking off)."""
|
|
caps = dataclasses.replace(
|
|
self._MANUAL_CAPS,
|
|
effort_param="reasoning_effort",
|
|
reasoning_effort_values=("low", "medium", "high"),
|
|
)
|
|
kwargs = self._stream_kwargs(caps, "none")
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}}
|
|
|
|
def test_operator_override_wins(self) -> None:
|
|
"""server_compat chat_template_kwargs entries beat the knob mapping."""
|
|
kwargs = self._stream_kwargs(
|
|
self._MANUAL_CAPS,
|
|
"none",
|
|
extra_params={"chat_template_kwargs": {"enable_thinking": True}, "foo": 1},
|
|
)
|
|
assert kwargs["extra_body"] == {
|
|
"chat_template_kwargs": {"enable_thinking": True},
|
|
"foo": 1,
|
|
}
|
|
|
|
def test_caller_extra_params_not_mutated(self) -> None:
|
|
"""The session's extra_params dict must never be written through."""
|
|
extra = {"chat_template_kwargs": {"foo": 1}}
|
|
self._stream_kwargs(self._MANUAL_CAPS, "medium", extra_params=extra)
|
|
assert extra == {"chat_template_kwargs": {"foo": 1}}
|
|
|
|
def test_no_output_config_on_compat(self) -> None:
|
|
"""supports_effort must not leak Anthropic output_config to vLLM."""
|
|
caps = dataclasses.replace(
|
|
self._MANUAL_CAPS,
|
|
supports_effort=True,
|
|
effort_levels=("low", "medium", "high"),
|
|
)
|
|
kwargs = self._stream_kwargs(caps, "high")
|
|
assert "output_config" not in kwargs
|
|
assert kwargs["extra_body"] == {
|
|
"chat_template_kwargs": {"enable_thinking": True, "reasoning_effort": "high"}
|
|
}
|
|
|
|
def test_create_streaming_same_injection(self) -> None:
|
|
"""The public streaming entry shares _build_thinking_and_kwargs."""
|
|
client = MagicMock()
|
|
client.messages.stream.return_value.__enter__.return_value = iter([])
|
|
with patch("turnstone.core.providers._anthropic._ensure_anthropic"):
|
|
list(
|
|
self.provider.create_streaming(
|
|
client=client,
|
|
model="qwen3.6-27b",
|
|
messages=[{"role": "user", "content": "hi"}],
|
|
reasoning_effort="none",
|
|
capabilities=self._MANUAL_CAPS,
|
|
)
|
|
)
|
|
kwargs = client.messages.stream.call_args[1]
|
|
assert kwargs["extra_body"] == {"chat_template_kwargs": {"enable_thinking": False}}
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCompatFactory
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCompatFactory:
|
|
"""create_provider / create_client routing for the compat lane."""
|
|
|
|
def test_create_provider_anthropic_compatible(self) -> None:
|
|
from turnstone.core.providers import create_provider
|
|
|
|
provider = create_provider("anthropic-compatible")
|
|
assert provider.provider_name == "anthropic-compatible"
|
|
assert provider is not create_provider("anthropic")
|
|
assert create_provider("anthropic-compatible") is provider
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_create_client_anthropic_compatible(self, mock_ensure: MagicMock) -> None:
|
|
"""base_url forwards verbatim; empty api_key is omitted entirely."""
|
|
from turnstone.core.providers import create_client
|
|
|
|
mock_anthropic_cls = MagicMock()
|
|
mock_mod = MagicMock()
|
|
mock_mod.Anthropic = mock_anthropic_cls
|
|
mock_ensure.return_value = mock_mod
|
|
|
|
create_client("anthropic-compatible", base_url="http://vllm-host:8000", api_key="")
|
|
mock_anthropic_cls.assert_called_once_with(base_url="http://vllm-host:8000")
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_create_client_strips_v1_suffix(self, mock_ensure: MagicMock) -> None:
|
|
"""A /v1-suffixed base_url (openai-compatible muscle memory) is
|
|
normalized for the compat lane — the SDK appends /v1/... itself,
|
|
so the verbatim URL would request /v1/v1/messages and 404."""
|
|
from turnstone.core.providers import create_client
|
|
|
|
mock_anthropic_cls = MagicMock()
|
|
mock_mod = MagicMock()
|
|
mock_mod.Anthropic = mock_anthropic_cls
|
|
mock_ensure.return_value = mock_mod
|
|
|
|
for suffixed in ("http://vllm-host:8000/v1", "http://vllm-host:8000/v1/"):
|
|
mock_anthropic_cls.reset_mock()
|
|
create_client("anthropic-compatible", base_url=suffixed, api_key="")
|
|
mock_anthropic_cls.assert_called_once_with(base_url="http://vllm-host:8000")
|
|
|
|
# A base_url that strips to nothing stays verbatim so the typo
|
|
# fails loudly in httpx instead of silently targeting the SDK's
|
|
# prod default.
|
|
mock_anthropic_cls.reset_mock()
|
|
create_client("anthropic-compatible", base_url="/v1", api_key="")
|
|
mock_anthropic_cls.assert_called_once_with(base_url="/v1")
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_create_client_requires_base_url(self, mock_ensure: MagicMock) -> None:
|
|
"""Empty base_url fails at construction — the local-only lane must
|
|
never fall back to the SDK's https://api.anthropic.com default."""
|
|
from turnstone.core.providers import create_client
|
|
|
|
with pytest.raises(ValueError, match="anthropic-compatible requires base_url"):
|
|
create_client("anthropic-compatible", base_url="", api_key="dummy")
|
|
mock_ensure.return_value.Anthropic.assert_not_called()
|
|
|
|
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
|
|
def test_create_client_real_lane_base_url_untouched(self, mock_ensure: MagicMock) -> None:
|
|
"""The real anthropic lane forwards base_url verbatim — the /v1
|
|
normalization is compat-lane-only."""
|
|
from turnstone.core.providers import create_client
|
|
|
|
mock_anthropic_cls = MagicMock()
|
|
mock_mod = MagicMock()
|
|
mock_mod.Anthropic = mock_anthropic_cls
|
|
mock_ensure.return_value = mock_mod
|
|
|
|
create_client("anthropic", base_url="http://proxy:9000/v1", api_key="k")
|
|
mock_anthropic_cls.assert_called_once_with(api_key="k", base_url="http://proxy:9000/v1")
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCliScope
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCliScope:
|
|
def test_cli_rejects_compat_provider_id(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The lane is registry-only — the CLI --provider flag does not grow."""
|
|
from turnstone import cli
|
|
|
|
monkeypatch.setattr(sys, "argv", ["turnstone", "--provider", "anthropic-compatible"])
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
cli.main()
|
|
assert excinfo.value.code == 2
|
|
|
|
|
|
# ===========================================================================
|
|
# TestCompatSessionPlumbing
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCompatSessionPlumbing:
|
|
"""ChatSession capability merge + extra_params gate for the lane."""
|
|
|
|
def test_per_model_capability_override_merge(self, tmp_db: Any) -> None:
|
|
"""Per-model capabilities win over _ANTHROPIC_COMPAT_DEFAULT fields."""
|
|
from turnstone.core.model_registry import ModelConfig, ModelRegistry
|
|
from turnstone.core.providers import create_provider
|
|
|
|
cfg = ModelConfig(
|
|
alias="vllm-messages",
|
|
base_url="http://localhost:8000",
|
|
api_key="dummy",
|
|
model="deepseek-ai/DeepSeek-V4-Flash",
|
|
provider="anthropic-compatible",
|
|
capabilities={"supports_mid_conversation_system": True, "context_window": 131072},
|
|
)
|
|
registry = ModelRegistry(models={"vllm-messages": cfg}, default="vllm-messages")
|
|
session = _make_session(registry=registry, model_alias="vllm-messages")
|
|
provider = create_provider("anthropic-compatible")
|
|
caps = session._resolve_capabilities(
|
|
provider, "deepseek-ai/DeepSeek-V4-Flash", "vllm-messages"
|
|
)
|
|
assert caps.supports_mid_conversation_system is True
|
|
assert caps.context_window == 131072
|
|
# Untouched fields keep the compat-lane defaults.
|
|
assert caps.token_param == "max_tokens"
|
|
assert caps.thinking_mode == "none"
|
|
assert caps.supports_web_search is False
|
|
assert caps.supports_vision is False
|
|
|
|
def test_session_extra_params_gate(self, tmp_db: Any) -> None:
|
|
"""server_compat extra_body forwards for the compat lane, not real Anthropic."""
|
|
from turnstone.core.model_registry import ModelConfig, ModelRegistry
|
|
from turnstone.core.providers import create_provider
|
|
|
|
session = _make_session(reasoning_effort="medium")
|
|
cfg = ModelConfig(
|
|
alias="vllm-messages",
|
|
base_url="http://localhost:8000",
|
|
api_key="dummy",
|
|
model="deepseek-ai/DeepSeek-V4-Flash",
|
|
provider="anthropic-compatible",
|
|
server_compat={"extra_body": {"chat_template_kwargs": {"thinking": False}}},
|
|
)
|
|
session._registry = ModelRegistry(models={"vllm-messages": cfg}, default="vllm-messages")
|
|
session._model_alias = "vllm-messages"
|
|
|
|
session._provider = create_provider("anthropic-compatible")
|
|
assert session._provider_extra_params() == {"chat_template_kwargs": {"thinking": False}}
|
|
|
|
session._provider = create_provider("anthropic")
|
|
assert session._provider_extra_params() is None
|
|
|
|
|
|
# ===========================================================================
|
|
# TestLiveCompatStream
|
|
# ===========================================================================
|
|
|
|
|
|
@pytest.mark.live
|
|
@pytest.mark.skipif(
|
|
not os.environ.get("TURNSTONE_LIVE_ANTHROPIC_COMPAT_URL"),
|
|
reason="TURNSTONE_LIVE_ANTHROPIC_COMPAT_URL not set",
|
|
)
|
|
class TestLiveCompatStream:
|
|
"""One real streamed turn against a vLLM /v1/messages endpoint."""
|
|
|
|
def test_live_compat_streamed_turn(self) -> None:
|
|
from turnstone.core.providers import create_client, create_provider
|
|
|
|
base_url = os.environ["TURNSTONE_LIVE_ANTHROPIC_COMPAT_URL"]
|
|
model = os.environ.get(
|
|
"TURNSTONE_LIVE_ANTHROPIC_COMPAT_MODEL", "deepseek-ai/DeepSeek-V4-Flash"
|
|
)
|
|
client = create_client("anthropic-compatible", base_url=base_url, api_key="dummy")
|
|
provider = create_provider("anthropic-compatible")
|
|
chunks = list(
|
|
provider.create_streaming(
|
|
client=client,
|
|
model=model,
|
|
messages=[{"role": "user", "content": "Reply with the single word: pong"}],
|
|
max_tokens=64,
|
|
extra_params={"chat_template_kwargs": {"thinking": False}},
|
|
)
|
|
)
|
|
content = "".join(c.content_delta or "" for c in chunks)
|
|
assert content.strip()
|
|
assert any(c.finish_reason for c in chunks)
|
|
assert any(c.usage is not None for c in chunks)
|
|
assert not any(c.reasoning_delta for c in chunks)
|