Compare commits

...

3 Commits

Author SHA1 Message Date
Patrick Buckley 03a109d14b chore: bump version to 1.3.1 2026-04-16 09:14:43 -07:00
Patrick Buckley f0a3cae3b5 feat: add Claude Opus 4.7 support (#357)
- Add claude-opus-4-7 capability entry (1M ctx, 128K output, adaptive
  thinking, supports_temperature=False, thinking_display=summarized)
- Suppress temperature param for Opus 4.7 (API returns 400)
- Add thinking display opt-in via new ModelCapabilities.thinking_display
  field - Opus 4.7 omits thinking by default, always send summarized
- Add xhigh effort level to mapping and Opus 4.7 effort_levels
- Add xhigh/max options to skill template dropdowns in admin console
- Align reasoning effort label capitalization across all console dropdowns
- Update example config to reference claude-opus-4-7
- 10 new tests with regression guards for Opus 4.6 backward compat

Verified against live API: streaming and completion calls succeed.

(cherry picked from commit 30c89f46c6)
2026-04-16 09:11:59 -07:00
Patrick Buckley f290eb4880 chore: bump version to 1.3.0 2026-04-13 17:18:42 -07:00
8 changed files with 173 additions and 16 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "turnstone"
version = "1.3.0a3"
version = "1.3.1"
description = "Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."
readme = "README.md"
license = "BUSL-1.1"
+134
View File
@@ -1200,6 +1200,31 @@ class TestAnthropicHelpers:
assert caps.token_param == "max_tokens"
assert caps.thinking_mode == "adaptive"
def test_capabilities_opus_4_7(self) -> None:
from turnstone.core.providers._anthropic import AnthropicProvider
provider = AnthropicProvider()
caps = provider.get_capabilities("claude-opus-4-7")
assert caps.context_window == 1000000
assert caps.max_output_tokens == 128000
assert caps.thinking_mode == "adaptive"
assert caps.supports_effort is True
assert "xhigh" in caps.effort_levels
assert caps.supports_temperature is False
assert caps.thinking_display == "summarized"
assert caps.supports_web_search is True
assert caps.supports_tool_search is True
assert caps.supports_vision is True
def test_capabilities_opus_4_7_dated(self) -> None:
from turnstone.core.providers._anthropic import AnthropicProvider
provider = AnthropicProvider()
caps = provider.get_capabilities("claude-opus-4-7-20260416")
assert caps.context_window == 1000000
assert caps.supports_temperature is False
assert caps.thinking_display == "summarized"
def test_capabilities_lookup_unknown(self) -> None:
from turnstone.core.providers._anthropic import AnthropicProvider
@@ -1910,6 +1935,18 @@ class TestAnthropicReasoningNone:
assert "thinking" in result
assert result["thinking"]["budget_tokens"] == 1024
def test_map_xhigh_effort(self) -> None:
from turnstone.core.providers._anthropic import _map_reasoning_to_effort
result = _map_reasoning_to_effort("xhigh", ("low", "medium", "high", "xhigh", "max"))
assert result == "xhigh"
def test_map_xhigh_rejected_by_model_without_it(self) -> None:
from turnstone.core.providers._anthropic import _map_reasoning_to_effort
result = _map_reasoning_to_effort("xhigh", ("low", "medium", "high", "max"))
assert result is None
# ===========================================================================
# TestWebSearch — provider-native web search
@@ -3068,6 +3105,103 @@ class TestAnthropicPromptCaching:
assert "cache_control" in kwargs
assert kwargs["cache_control"] == {"type": "ephemeral"}
def test_opus_4_7_no_temperature_in_kwargs(self) -> None:
"""Opus 4.7 rejects temperature — must not appear in kwargs."""
caps = self.provider.get_capabilities("claude-opus-4-7")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="high",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-7",
tools=None,
)
assert "temperature" not in kwargs
def test_opus_4_6_still_has_temperature(self) -> None:
"""Opus 4.6 must still send temperature (regression guard)."""
caps = self.provider.get_capabilities("claude-opus-4-6")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="high",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-6",
tools=None,
)
assert "temperature" in kwargs
assert kwargs["temperature"] == 1.0 # forced for adaptive thinking
def test_opus_4_7_thinking_display_summarized(self) -> None:
"""Opus 4.7 must opt in to thinking display with 'summarized'."""
caps = self.provider.get_capabilities("claude-opus-4-7")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="high",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-7",
tools=None,
)
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
def test_opus_4_6_thinking_no_display(self) -> None:
"""Opus 4.6 adaptive thinking should not include display key."""
caps = self.provider.get_capabilities("claude-opus-4-6")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="high",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-6",
tools=None,
)
assert kwargs["thinking"] == {"type": "adaptive"}
def test_opus_4_7_xhigh_effort(self) -> None:
"""Opus 4.7 xhigh effort passes through to output_config."""
caps = self.provider.get_capabilities("claude-opus-4-7")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="xhigh",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-7",
tools=None,
)
assert kwargs["output_config"] == {"effort": "xhigh"}
def test_xhigh_effort_not_applied_to_opus_4_6(self) -> None:
"""xhigh is not a valid effort level for Opus 4.6 — should be ignored."""
caps = self.provider.get_capabilities("claude-opus-4-6")
kwargs = self.provider._build_thinking_and_kwargs(
caps=caps,
reasoning_effort="xhigh",
extra_params=None,
max_tokens=8192,
temperature=0.5,
converted_msgs=[{"role": "user", "content": "hi"}],
system_prompt="",
model="claude-opus-4-6",
tools=None,
)
assert "output_config" not in kwargs
@patch("turnstone.core.providers._anthropic._ensure_anthropic")
def test_streaming_message_start_cache_metrics(self, mock_ensure: MagicMock) -> None:
"""Cache metrics from message_start flow into UsageInfo."""
+1 -1
View File
@@ -39,7 +39,7 @@
# supports_web_search = false
#
# [models.claude]
# name = "claude-opus-4-6"
# name = "claude-opus-4-7"
# provider = "anthropic"
# --- Database (turnstone, node, console) ---
+1 -1
View File
@@ -1,3 +1,3 @@
"""turnstone - Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."""
__version__ = "1.3.0a3"
__version__ = "1.3.1"
+11 -7
View File
@@ -1280,6 +1280,8 @@ window.TURNSTONE_KB_SHORTCUTS = [
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">Extra High</option>
<option value="max">Max</option>
</select>
</div>
<div><label for="csk-max-tokens">Max Tokens</label><input id="csk-max-tokens" type="number" min="1" placeholder="System default"></div>
@@ -1399,6 +1401,8 @@ window.TURNSTONE_KB_SHORTCUTS = [
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">Extra High</option>
<option value="max">Max</option>
</select>
</div>
<div><label for="esk-max-tokens">Max Tokens</label><input id="esk-max-tokens" type="number" min="1" placeholder="System default"></div>
@@ -1559,13 +1563,13 @@ window.TURNSTONE_KB_SHORTCUTS = [
<label for="model-reasoning-effort">Reasoning Effort <span style="font-weight:400;text-transform:none">(empty = use global default)</span></label>
<select id="model-reasoning-effort">
<option value="">Global default</option>
<option value="none">none</option>
<option value="minimal">minimal</option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
<option value="xhigh">xhigh</option>
<option value="max">max</option>
<option value="none">None</option>
<option value="minimal">Minimal</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="xhigh">Extra High</option>
<option value="max">Max</option>
</select>
<label for="model-capabilities">Capabilities <span style="font-weight:400;text-transform:none">(JSON)</span></label>
<textarea id="model-capabilities" rows="3" placeholder='{"supports_vision": true}' style="font-family:var(--font-mono);font-size:11px"></textarea>
+23 -5
View File
@@ -83,6 +83,19 @@ _ANTHROPIC_DEFAULT = ModelCapabilities(
)
_ANTHROPIC_CAPABILITIES: dict[str, ModelCapabilities] = {
"claude-opus-4-7": ModelCapabilities(
context_window=1000000,
max_output_tokens=128000,
token_param="max_tokens",
thinking_mode="adaptive",
supports_effort=True,
effort_levels=("low", "medium", "high", "xhigh", "max"),
supports_web_search=True,
supports_tool_search=True,
supports_vision=True,
supports_temperature=False,
thinking_display="summarized",
),
"claude-opus-4-6": ModelCapabilities(
context_window=1000000,
max_output_tokens=128000,
@@ -139,7 +152,7 @@ def _map_reasoning_to_effort(
valid_levels: tuple[str, ...],
) -> str | None:
"""Map turnstone reasoning_effort to Anthropic effort parameter."""
mapping = {"low": "low", "medium": "medium", "high": "high", "max": "max"}
mapping = {"low": "low", "medium": "medium", "high": "high", "xhigh": "xhigh", "max": "max"}
effort = mapping.get(reasoning_effort)
if effort and effort in valid_levels:
return effort
@@ -226,8 +239,12 @@ class AnthropicProvider:
"""Build the full kwargs dict with thinking mode and effort params."""
thinking_params: dict[str, Any] = {}
if caps.thinking_mode == "adaptive":
thinking_params = {"thinking": {"type": "adaptive"}}
temperature = 1.0 # Required with thinking
thinking_dict: dict[str, Any] = {"type": "adaptive"}
if caps.thinking_display:
thinking_dict["display"] = caps.thinking_display
thinking_params = {"thinking": thinking_dict}
if caps.supports_temperature:
temperature = 1.0 # Required with thinking
elif caps.thinking_mode == "manual":
thinking_params = self._reasoning_params(reasoning_effort, extra_params, max_tokens)
if thinking_params:
@@ -237,12 +254,13 @@ class AnthropicProvider:
"model": model,
"messages": converted_msgs,
caps.token_param: max_tokens,
"temperature": temperature,
# Automatic prompt caching — the API places the cache breakpoint
# on the last cacheable block and advances it as conversation grows.
# 90% input cost reduction on cache hits; 1.25x write on first turn.
"cache_control": {"type": "ephemeral"},
}
if caps.supports_temperature:
kwargs["temperature"] = temperature
if system_prompt:
kwargs["system"] = system_prompt
if tools:
@@ -252,7 +270,7 @@ class AnthropicProvider:
kwargs["tools"] = anthropic_tools
kwargs.update(thinking_params)
# Effort param for models that support it (Opus 4.6, Sonnet 4.6, Opus 4.5)
# Effort param for models that support it (Opus 4.7, Opus 4.6, Sonnet 4.6, Opus 4.5)
if caps.supports_effort and reasoning_effort:
effort = _map_reasoning_to_effort(reasoning_effort, caps.effort_levels)
if effort:
+1
View File
@@ -82,6 +82,7 @@ class ModelCapabilities:
supports_tool_search: bool = False
supports_vision: bool = False
supports_tool_advisories: bool = True
thinking_display: str = "" # "summarized" for models that omit thinking by default
def _lookup_capabilities(
Generated
+1 -1
View File
@@ -2506,7 +2506,7 @@ wheels = [
[[package]]
name = "turnstone"
version = "1.3.0a3"
version = "1.3.1"
source = { editable = "." }
dependencies = [
{ name = "alembic" },