From 114ada791b77d673c237abc6e9ccb7d963a2df6b Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Tue, 9 Jun 2026 10:45:11 -0700 Subject: [PATCH] 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 --- pyproject.toml | 2 +- tests/test_providers.py | 29 ++++++++++++++++++++++++++ turnstone/core/providers/_anthropic.py | 24 ++++++++++++++++++++- turnstone/core/providers/_protocol.py | 3 ++- turnstone/core/session.py | 12 ++++++----- turnstone/core/tool_advisory.py | 13 ++++++------ turnstone/prompts/__init__.py | 6 +++--- 7 files changed, 72 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4e8ddd7..b8cae995 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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) diff --git a/tests/test_providers.py b/tests/test_providers.py index af6d07b7..b087d112 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -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 diff --git a/turnstone/core/providers/_anthropic.py b/turnstone/core/providers/_anthropic.py index 52a236ef..0512c8f2 100644 --- a/turnstone/core/providers/_anthropic.py +++ b/turnstone/core/providers/_anthropic.py @@ -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 diff --git a/turnstone/core/providers/_protocol.py b/turnstone/core/providers/_protocol.py index efa4c8e9..f13f02ed 100644 --- a/turnstone/core/providers/_protocol.py +++ b/turnstone/core/providers/_protocol.py @@ -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 diff --git a/turnstone/core/session.py b/turnstone/core/session.py index 235a25ef..983a0b06 100644 --- a/turnstone/core/session.py +++ b/turnstone/core/session.py @@ -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 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 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 ```` 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. diff --git a/turnstone/core/tool_advisory.py b/turnstone/core/tool_advisory.py index 016165fd..21df95c5 100644 --- a/turnstone/core/tool_advisory.py +++ b/turnstone/core/tool_advisory.py @@ -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": , "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 ```` 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 +```` 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": , "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 ```` -# 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 +# ```` 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 ```` fence via :func:`turnstone.core.fence.wrap` (applied in diff --git a/turnstone/prompts/__init__.py b/turnstone/prompts/__init__.py index e6c4d8cb..5cbcbd44 100644 --- a/turnstone/prompts/__init__.py +++ b/turnstone/prompts/__init__.py @@ -92,9 +92,9 @@ def build_operator_instruction_declaration(nonce: str) -> str: model trusts only ``>`` blocks and treats every other ````-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"