ml-intern-api / agent /core /prompt_caching.py
abidlabs's picture
abidlabs HF Staff
Deploy ML Intern API (backend + self-documenting frontend)
1635e66 verified
Raw
History Blame Contribute Delete
7.28 kB
"""Prompt-cache helpers for HF Router FAL requests.
The HF Router/OpenRouter path uses provider-native prompt caching. Anthropic
models keep explicit JSON ``cache_control`` content blocks for compatibility,
and also need the top-level ``cache_control`` hint on the OpenAI-compatible HF
Router path; the explicit markers alone are accepted there but do not produce
cache writes. OpenAI models cache eligible prefixes automatically and accept
routing/retention hints in the body.
Headers like ``X-OpenRouter-Cache`` control response caching, not prompt
caching through this route.
"""
from typing import Any
from agent.core.model_ids import HF_ROUTER_BASE_URL
_CACHE_CONTROL = {"type": "ephemeral"}
_CACHEABLE_ROLES = {"system", "user"}
_HF_ROUTER_SESSION_ID_MAX_LENGTH = 256
HF_ROUTER_SESSION_ID_HEADER = "X-HF-Session-id"
def router_session_id_for(session: Any) -> str | None:
"""Return the usage-window-scoped Router session ID for a runtime session."""
billing_session_id = getattr(session, "inference_billing_session_id", None)
if isinstance(billing_session_id, str) and billing_session_id:
return billing_session_id
session_id = getattr(session, "session_id", None)
if isinstance(session_id, str) and session_id:
return session_id
return None
def _is_hf_router_request(llm_params: dict[str, Any]) -> bool:
api_base = str(llm_params.get("api_base") or "").rstrip("/")
return api_base == HF_ROUTER_BASE_URL
def _is_fal_router_request(llm_params: dict[str, Any]) -> bool:
return _is_hf_router_request(llm_params) and ":fal" in _router_model(llm_params)
def _router_model(llm_params: dict[str, Any]) -> str:
model = str(llm_params.get("model") or "")
return model.removeprefix("openai/")
def _uses_explicit_cache_control(llm_params: dict[str, Any]) -> bool:
if not _is_fal_router_request(llm_params):
return False
return _router_model(llm_params).startswith("anthropic/")
def _is_openai_gpt55(llm_params: dict[str, Any]) -> bool:
if not _is_fal_router_request(llm_params):
return False
return _router_model(llm_params).startswith("openai/gpt-5.5")
def _merge_extra_body(
llm_params: dict[str, Any], updates: dict[str, Any]
) -> dict[str, Any]:
if not updates:
return llm_params
cached_params = dict(llm_params)
extra_body = dict(cached_params.get("extra_body") or {})
extra_body.update(updates)
cached_params["extra_body"] = extra_body
return cached_params
def _merge_extra_headers(
llm_params: dict[str, Any], updates: dict[str, str]
) -> dict[str, Any]:
if not updates:
return llm_params
cached_params = dict(llm_params)
extra_headers = dict(cached_params.get("extra_headers") or {})
extra_headers.update(updates)
cached_params["extra_headers"] = extra_headers
return cached_params
def with_prompt_cache_params(
llm_params: dict[str, Any],
*,
session_id: str | None = None,
) -> dict[str, Any]:
"""Return LiteLLM params with provider-native prompt-cache body hints."""
updates: dict[str, Any] = {}
headers: dict[str, str] = {}
if session_id and _is_hf_router_request(llm_params):
stable_session_id = session_id[:_HF_ROUTER_SESSION_ID_MAX_LENGTH]
headers[HF_ROUTER_SESSION_ID_HEADER] = stable_session_id
if _is_openai_gpt55(llm_params):
updates["prompt_cache_key"] = stable_session_id
if _uses_explicit_cache_control(llm_params):
updates["cache_control"] = dict(_CACHE_CONTROL)
if _is_openai_gpt55(llm_params):
updates["prompt_cache_retention"] = "24h"
return _merge_extra_headers(_merge_extra_body(llm_params, updates), headers)
def _message_role(message: Any) -> str | None:
if isinstance(message, dict):
role = message.get("role")
else:
role = getattr(message, "role", None)
return role if isinstance(role, str) else None
def _message_content(message: Any) -> Any:
if isinstance(message, dict):
return message.get("content")
return getattr(message, "content", None)
def _message_to_dict(message: Any) -> dict[str, Any]:
if isinstance(message, dict):
return dict(message)
if hasattr(message, "model_dump"):
return message.model_dump(exclude_none=True)
raise TypeError(f"Unsupported message type for prompt caching: {type(message)!r}")
def _has_cacheable_text(content: Any) -> bool:
if isinstance(content, str):
return bool(content)
if not isinstance(content, list):
return False
return any(
isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
and bool(block.get("text"))
for block in content
)
def _cache_target_index(messages: list[Any]) -> int | None:
if len(messages) < 2:
return None
for idx in range(len(messages) - 2, -1, -1):
message = messages[idx]
if _message_role(message) not in _CACHEABLE_ROLES:
continue
if _has_cacheable_text(_message_content(message)):
return idx
return None
def _content_with_cache_control(content: Any) -> list[dict[str, Any]]:
if isinstance(content, str):
return [
{"type": "text", "text": content, "cache_control": dict(_CACHE_CONTROL)}
]
blocks = [dict(block) if isinstance(block, dict) else block for block in content]
for idx in range(len(blocks) - 1, -1, -1):
block = blocks[idx]
if (
isinstance(block, dict)
and block.get("type") == "text"
and isinstance(block.get("text"), str)
and bool(block.get("text"))
):
cached = dict(block)
cached["cache_control"] = dict(_CACHE_CONTROL)
blocks[idx] = cached
break
return blocks
def _tools_with_cache_control(tools: list[dict] | None) -> list[dict] | None:
if not tools:
return tools
cached_tools = list(tools)
last_tool = dict(cached_tools[-1])
last_tool["cache_control"] = dict(_CACHE_CONTROL)
cached_tools[-1] = last_tool
return cached_tools
def with_prompt_caching(
messages: list[Any],
tools: list[dict] | None,
llm_params: dict[str, Any],
) -> tuple[list[Any], list[dict] | None]:
"""Return outgoing messages with explicit cache breakpoints when needed.
The newest message is treated as dynamic. For Anthropic FAL models, the
cache breakpoint is placed on the closest earlier system/user text block so
provider-side caching covers the stable prefix without changing persisted
conversation history. The final tool spec is also marked so stable tool
definitions are cached.
"""
if not _uses_explicit_cache_control(llm_params):
return messages, tools
cached_tools = _tools_with_cache_control(tools)
idx = _cache_target_index(messages)
if idx is None:
return messages, cached_tools
cached_message = _message_to_dict(messages[idx])
cached_message["content"] = _content_with_cache_control(
cached_message.get("content")
)
cached_messages = list(messages)
cached_messages[idx] = cached_message
return cached_messages, cached_tools