mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-25 17:18:11 +00:00
165 lines
6 KiB
Python
165 lines
6 KiB
Python
"""Anthropic prompt caching strategy.
|
|
|
|
The default layout uses 4 cache_control breakpoints: the static system
|
|
prefix, the end of the system prompt, and the last 2 non-system messages.
|
|
When a static system prefix is unavailable, it falls back to one system
|
|
breakpoint plus the last 3 messages. All markers use the same TTL (5m or 1h).
|
|
This preserves intra-session caching while allowing new sessions to reuse the
|
|
stable system-prompt prefix.
|
|
|
|
Pure functions -- no class state, no AIAgent dependency.
|
|
"""
|
|
|
|
import copy
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
def _apply_cache_marker(msg: dict, cache_marker: dict, native_anthropic: bool = False) -> None:
|
|
"""Add cache_control to a single message, handling all format variations."""
|
|
role = msg.get("role", "")
|
|
content = msg.get("content")
|
|
|
|
if role == "tool" and native_anthropic:
|
|
# Native Anthropic layout: top-level marker; the adapter moves it
|
|
# inside the tool_result block.
|
|
msg["cache_control"] = cache_marker
|
|
return
|
|
|
|
if content is None or content == "":
|
|
if role == "tool" and not native_anthropic:
|
|
# OpenRouter rejects top-level cache_control on role:tool (silent
|
|
# hang) and an empty message has no content part to carry the
|
|
# marker — skip. Non-empty tool content falls through below and
|
|
# gets the marker on a content part, which OpenRouter honors.
|
|
return
|
|
if role == "assistant" and not native_anthropic:
|
|
# Empty assistant turns are pure tool_calls. A top-level marker
|
|
# here is ignored on the envelope layout, so skip.
|
|
return
|
|
msg["cache_control"] = cache_marker
|
|
return
|
|
|
|
if isinstance(content, str):
|
|
msg["content"] = [
|
|
{"type": "text", "text": content, "cache_control": cache_marker}
|
|
]
|
|
return
|
|
|
|
if isinstance(content, list) and content:
|
|
last = content[-1]
|
|
if isinstance(last, dict):
|
|
last["cache_control"] = cache_marker
|
|
|
|
|
|
def _can_carry_marker(msg: dict, native_anthropic: bool) -> bool:
|
|
"""True if a marker on this message is actually honored by the provider.
|
|
|
|
On the native Anthropic layout every message works (top-level markers are
|
|
relocated by the adapter). On the envelope layout (OpenRouter et al.) only
|
|
markers inside content parts are honored: empty-content messages (e.g.
|
|
assistant turns that are pure tool_calls) and empty tool messages would
|
|
receive a top-level marker the provider ignores — wasting one of the four
|
|
breakpoints. Skip those so the breakpoints land on messages that count.
|
|
"""
|
|
if native_anthropic:
|
|
return True
|
|
content = msg.get("content")
|
|
if content is None or content == "":
|
|
return False
|
|
if isinstance(content, list):
|
|
# _apply_cache_marker only marks the LAST content part, so the carrier
|
|
# predicate must agree: a list whose last element isn't a dict cannot
|
|
# actually receive a marker and would waste a breakpoint. Mirror the
|
|
# `content` truthiness + last-element-dict check in _apply_cache_marker.
|
|
return bool(content) and isinstance(content[-1], dict)
|
|
return isinstance(content, str)
|
|
|
|
|
|
def _build_marker(ttl: str) -> Dict[str, str]:
|
|
"""Build a cache_control marker dict for the given TTL ('5m' or '1h')."""
|
|
marker: Dict[str, str] = {"type": "ephemeral"}
|
|
if ttl == "1h":
|
|
marker["ttl"] = "1h"
|
|
return marker
|
|
|
|
|
|
def _apply_system_cache_markers(
|
|
message: dict,
|
|
cache_marker: dict,
|
|
static_system_prefix: str | None,
|
|
*,
|
|
native_anthropic: bool,
|
|
) -> int:
|
|
"""Mark the static system prefix and full prompt when they can be split.
|
|
|
|
The system prompt remains one stored string. Splitting it only in the
|
|
outgoing request keeps session persistence and non-Anthropic transports
|
|
unchanged while making the stable prefix independently cacheable.
|
|
"""
|
|
content = message.get("content")
|
|
if (
|
|
isinstance(static_system_prefix, str)
|
|
and static_system_prefix
|
|
and isinstance(content, str)
|
|
and content.startswith(static_system_prefix)
|
|
):
|
|
suffix = content[len(static_system_prefix):]
|
|
if suffix:
|
|
message["content"] = [
|
|
{
|
|
"type": "text",
|
|
"text": static_system_prefix,
|
|
"cache_control": cache_marker,
|
|
},
|
|
{"type": "text", "text": suffix, "cache_control": cache_marker},
|
|
]
|
|
return 2
|
|
|
|
_apply_cache_marker(message, cache_marker, native_anthropic=native_anthropic)
|
|
return 1
|
|
|
|
|
|
def apply_anthropic_cache_control(
|
|
api_messages: List[Dict[str, Any]],
|
|
cache_ttl: str = "5m",
|
|
native_anthropic: bool = False,
|
|
static_system_prefix: str | None = None,
|
|
) -> List[Dict[str, Any]]:
|
|
"""Apply Anthropic cache-control markers to API messages.
|
|
|
|
When ``static_system_prefix`` exactly matches the beginning of a string
|
|
system prompt, it receives an early marker and the full system prompt gets
|
|
a trailing marker. The remaining two markers target the latest cacheable
|
|
non-system messages. Without that prefix, the legacy system-and-3 layout
|
|
is retained.
|
|
|
|
Returns:
|
|
Deep copy of messages with cache_control breakpoints injected.
|
|
"""
|
|
messages = copy.deepcopy(api_messages)
|
|
if not messages:
|
|
return messages
|
|
|
|
marker = _build_marker(cache_ttl)
|
|
|
|
breakpoints_used = 0
|
|
|
|
if messages[0].get("role") == "system":
|
|
breakpoints_used = _apply_system_cache_markers(
|
|
messages[0],
|
|
marker,
|
|
static_system_prefix,
|
|
native_anthropic=native_anthropic,
|
|
)
|
|
|
|
remaining = 4 - breakpoints_used
|
|
non_sys = [
|
|
i
|
|
for i in range(len(messages))
|
|
if messages[i].get("role") != "system"
|
|
and _can_carry_marker(messages[i], native_anthropic=native_anthropic)
|
|
]
|
|
for idx in non_sys[-remaining:]:
|
|
_apply_cache_marker(messages[idx], marker, native_anthropic=native_anthropic)
|
|
|
|
return messages
|