mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
feat(providers): add Claude Fable 5 to the Anthropic provider
- claude-fable-5 capability entry: 1M context / 128K output, adaptive
thinking (summarized display), effort low..max incl. xhigh, no
sampling params, web + tool search, vision, reasoning replay, native
mid-conversation system messages
- document the Fable 5 wire quirk at the capability table: an explicit
thinking={"type": "disabled"} is a 400 on this model; the adaptive
branch never emits "disabled", so adaptive-or-omitted is preserved
- widen the native mid-conversation-system comments from opus-4-8-only
to opus-4-8 + fable-5 (protocol, provider, tool_advisory, prompts,
session)
- raise the anthropic SDK floor 0.39 -> 0.108: 0.39 predates every
named kwarg the provider sends (output_config 0.77, top-level
cache_control 0.83, mid-conversation system blocks 0.105); 0.108
adds claude-fable-5
- tests: capability assertions for claude-fable-5 + dated-variant
prefix match
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"openai>=2.37",
|
||||
"anthropic>=0.39",
|
||||
"anthropic>=0.108", # claude-fable-5 support; hard runtime floor is 0.105 (mid-conversation system blocks)
|
||||
"httpx>=0.28",
|
||||
"mcp>=1.27",
|
||||
"starlette>=1.0.1", # PYSEC-2026-161: host-header path-injection in URL reconstruction (auth-bypass on apps comparing reconstructed URL paths)
|
||||
|
||||
@@ -1407,6 +1407,35 @@ class TestAnthropicHelpers:
|
||||
assert caps.token_param == "max_tokens"
|
||||
assert caps.thinking_mode == "adaptive"
|
||||
|
||||
def test_capabilities_fable_5(self) -> None:
|
||||
from turnstone.core.providers._anthropic import AnthropicProvider
|
||||
|
||||
provider = AnthropicProvider()
|
||||
caps = provider.get_capabilities("claude-fable-5")
|
||||
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 "max" 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
|
||||
assert caps.supports_reasoning_replay is True
|
||||
assert caps.supports_mid_conversation_system is True
|
||||
|
||||
def test_capabilities_fable_5_dated(self) -> None:
|
||||
from turnstone.core.providers._anthropic import AnthropicProvider
|
||||
|
||||
provider = AnthropicProvider()
|
||||
caps = provider.get_capabilities("claude-fable-5-20260815")
|
||||
assert caps.context_window == 1000000
|
||||
assert caps.supports_temperature is False
|
||||
assert caps.thinking_display == "summarized"
|
||||
assert caps.supports_mid_conversation_system is True
|
||||
|
||||
def test_capabilities_opus_4_8(self) -> None:
|
||||
from turnstone.core.providers._anthropic import AnthropicProvider
|
||||
|
||||
|
||||
@@ -84,6 +84,27 @@ _ANTHROPIC_DEFAULT = ModelCapabilities(
|
||||
)
|
||||
|
||||
_ANTHROPIC_CAPABILITIES: dict[str, ModelCapabilities] = {
|
||||
# Fable 5: same wire surface as opus-4-8 (adaptive-only thinking, no
|
||||
# sampling params, prefill rejected) with one extra constraint — an
|
||||
# explicit thinking={"type": "disabled"} is a 400 on this model; thinking
|
||||
# must be adaptive or the param omitted entirely. The adaptive branch in
|
||||
# _build_thinking_and_kwargs never emits "disabled", so this is safe as
|
||||
# long as thinking_mode stays "adaptive".
|
||||
"claude-fable-5": 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",
|
||||
supports_reasoning_replay=True,
|
||||
supports_mid_conversation_system=True,
|
||||
),
|
||||
"claude-opus-4-8": ModelCapabilities(
|
||||
context_window=1000000,
|
||||
max_output_tokens=128000,
|
||||
@@ -361,7 +382,8 @@ class AnthropicProvider:
|
||||
(``ChatSession._try_stream`` / ``_utility_completion``) always
|
||||
pass the resolved flag explicitly.
|
||||
|
||||
``supports_mid_conversation_system`` (claude-opus-4-8) makes the
|
||||
``supports_mid_conversation_system`` (claude-opus-4-8,
|
||||
claude-fable-5) makes the
|
||||
system-role handling position-aware: leading system/developer
|
||||
messages (the base prompt) still hoist into the top-level
|
||||
``system`` param, but a system message appearing AFTER a
|
||||
|
||||
@@ -116,7 +116,8 @@ class ModelCapabilities:
|
||||
# invalidating the cached prefix. When False, ``system``-role messages must
|
||||
# be hoisted into the top-level ``system`` param (the universal fallback).
|
||||
# Available on the Claude API only (NOT Bedrock / Vertex / Foundry), on
|
||||
# NextOpus (claude-opus-4-8) only; no beta header required.
|
||||
# claude-opus-4-8 (validated header-less) and claude-fable-5 (same
|
||||
# documented wire surface); no beta header required.
|
||||
supports_mid_conversation_system: bool = False
|
||||
# Phase 3 reranker calibration — populated by calibrate-on-detect; read by
|
||||
# ChatSession._bm25_rerank_threshold. A non-empty rerank_scale is the
|
||||
|
||||
@@ -2734,9 +2734,10 @@ class ChatSession:
|
||||
else None
|
||||
)
|
||||
# Operator-instruction trust anchor — declared only on the fold path.
|
||||
# The native mid-conversation-system path (claude-opus-4-8) delivers
|
||||
# operator turns as real {"role":"system"} messages with no fence, so
|
||||
# no <system-reminder_{nonce}> marker appears and no declaration applies.
|
||||
# The native mid-conversation-system path (claude-opus-4-8,
|
||||
# claude-fable-5) delivers operator turns as real {"role":"system"}
|
||||
# messages with no fence, so no <system-reminder_{nonce}> marker
|
||||
# appears and no declaration applies.
|
||||
if caps is not None and not caps.supports_mid_conversation_system:
|
||||
dev_parts.append("\n\n" + build_operator_instruction_declaration(self._envelope_nonce))
|
||||
# Tool search hint (client-side mode only — native mode needs no hint).
|
||||
@@ -2932,8 +2933,9 @@ class ChatSession:
|
||||
:func:`turnstone.core.lowering.fold_system_turns`: non-native
|
||||
models get each turn wrapped as a nonce-delimited
|
||||
``<system-reminder>`` block on the preceding turn; native
|
||||
mid-conversation-system models (claude-opus-4-8) keep them inline
|
||||
for the Anthropic converter to emit as real ``system`` messages.
|
||||
mid-conversation-system models (claude-opus-4-8, claude-fable-5)
|
||||
keep them inline for the Anthropic converter to emit as real
|
||||
``system`` messages.
|
||||
|
||||
Messages without a foldable system turn pass through unchanged
|
||||
(same object reference) so the common case is allocation-free.
|
||||
|
||||
@@ -4,9 +4,9 @@ Operator-level context injected mid-session (output-guard findings, user
|
||||
interjections, metacognitive nudges, skill hints) lives in the conversation
|
||||
trajectory as first-class ``{"role": "system", "_source": <kind>, "content":
|
||||
...}`` turns (see :func:`make_system_turn`). At the wire boundary each turn is
|
||||
either kept inline (native mid-conversation system messages — claude-opus-4-8)
|
||||
or folded into the preceding turn as a nonce-delimited ``<system-reminder_
|
||||
{nonce}>`` fence for every other model. The fence mechanism (mint / neutralise
|
||||
either kept inline (native mid-conversation system messages — claude-opus-4-8,
|
||||
claude-fable-5) or folded into the preceding turn as a nonce-delimited
|
||||
``<system-reminder_{nonce}>`` fence for every other model. The fence mechanism (mint / neutralise
|
||||
/ wrap) lives in :mod:`turnstone.core.fence`, shared with the output-guard judge
|
||||
so the two trust boundaries cannot drift; ``lowering.fold_system_turns``
|
||||
applies it.
|
||||
@@ -99,8 +99,8 @@ def render_user_interjection(message: str, priority: str) -> str:
|
||||
# ``{"role": "system", "_source": <kind>, "content": ...}`` messages rather
|
||||
# than spliced into a neighbouring turn's ``content``. At the wire boundary a
|
||||
# system turn is either kept inline (native mid-conversation system messages —
|
||||
# claude-opus-4-8) or folded into the preceding turn as a ``<system-reminder>``
|
||||
# block (every other model). ``_source`` classifies the turn for UI rendering
|
||||
# claude-opus-4-8, claude-fable-5) or folded into the preceding turn as a
|
||||
# ``<system-reminder>`` block (every other model). ``_source`` classifies the turn for UI rendering
|
||||
# and replay; it rides the persisted ``_source`` column and is stripped before
|
||||
# the LLM wire by ``sanitize_messages``. See ``ChatSession`` for the producers
|
||||
# and the fold-or-keep pass.
|
||||
@@ -149,7 +149,8 @@ def make_system_turn(source: str, content: str, **meta: Any) -> dict[str, Any]:
|
||||
structured fields never reach the model.
|
||||
|
||||
``content`` is stored and — on the native mid-conversation-system path
|
||||
(claude-opus-4-8) — sent to the model verbatim, so fence-escaping is NOT
|
||||
(claude-opus-4-8, claude-fable-5) — sent to the model verbatim, so
|
||||
fence-escaping is NOT
|
||||
done here. It belongs to the fallback fold step, which wraps the content
|
||||
in a nonce-delimited ``<system-reminder_{nonce}>`` fence via
|
||||
:func:`turnstone.core.fence.wrap` (applied in
|
||||
|
||||
@@ -92,9 +92,9 @@ def build_operator_instruction_declaration(nonce: str) -> str:
|
||||
model trusts only ``<system-reminder_<nonce>>`` blocks and treats every
|
||||
other ``<system-reminder>``-style marker (e.g. one forged in tool output,
|
||||
files, or web pages) as untrusted data. Emitted only when the model uses
|
||||
the fold path — the native mid-conversation-system path (claude-opus-4-8)
|
||||
delivers operator turns as real ``{"role":"system"}`` messages with no
|
||||
fence, so no marker appears.
|
||||
the fold path — the native mid-conversation-system path (claude-opus-4-8,
|
||||
claude-fable-5) delivers operator turns as real ``{"role":"system"}``
|
||||
messages with no fence, so no marker appears.
|
||||
"""
|
||||
return (
|
||||
"## Operator instructions\n"
|
||||
|
||||
Reference in New Issue
Block a user