Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Enable FAL prompt cache validation (#301)
Browse files* Enable FAL prompt cache validation
Co-authored-by: OpenAI Codex <codex@openai.com>
* Address FAL prompt cache review
Co-authored-by: OpenAI Codex <codex@openai.com>
* Clarify session usage estimate copy
Co-authored-by: OpenAI Codex <codex@openai.com>
* Update usage estimate copy
Co-authored-by: OpenAI Codex <codex@openai.com>
---------
Co-authored-by: OpenAI Codex <codex@openai.com>
agent/core/prompt_caching.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
"""Prompt-cache helpers for HF Router FAL requests.
|
| 2 |
|
| 3 |
The HF Router/OpenRouter path uses provider-native prompt caching. Anthropic
|
| 4 |
-
models
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
Headers like ``X-OpenRouter-Cache`` control response caching, not prompt
|
| 7 |
caching through this route.
|
| 8 |
"""
|
|
@@ -67,6 +70,9 @@ def with_prompt_cache_params(
|
|
| 67 |
if _is_openai_gpt55(llm_params):
|
| 68 |
updates["prompt_cache_key"] = stable_session_id
|
| 69 |
|
|
|
|
|
|
|
|
|
|
| 70 |
if _is_openai_gpt55(llm_params):
|
| 71 |
updates["prompt_cache_retention"] = "24h"
|
| 72 |
|
|
|
|
| 1 |
"""Prompt-cache helpers for HF Router FAL requests.
|
| 2 |
|
| 3 |
The HF Router/OpenRouter path uses provider-native prompt caching. Anthropic
|
| 4 |
+
models keep explicit JSON ``cache_control`` content blocks for compatibility,
|
| 5 |
+
and also need the top-level ``cache_control`` hint on the OpenAI-compatible HF
|
| 6 |
+
Router path; the explicit markers alone are accepted there but do not produce
|
| 7 |
+
cache writes. OpenAI models cache eligible prefixes automatically and accept
|
| 8 |
+
routing/retention hints in the body.
|
| 9 |
Headers like ``X-OpenRouter-Cache`` control response caching, not prompt
|
| 10 |
caching through this route.
|
| 11 |
"""
|
|
|
|
| 70 |
if _is_openai_gpt55(llm_params):
|
| 71 |
updates["prompt_cache_key"] = stable_session_id
|
| 72 |
|
| 73 |
+
if _uses_explicit_cache_control(llm_params):
|
| 74 |
+
updates["cache_control"] = dict(_CACHE_CONTROL)
|
| 75 |
+
|
| 76 |
if _is_openai_gpt55(llm_params):
|
| 77 |
updates["prompt_cache_retention"] = "24h"
|
| 78 |
|
agent/core/telemetry.py
CHANGED
|
@@ -52,14 +52,18 @@ def extract_usage(response_or_chunk: Any) -> dict:
|
|
| 52 |
|
| 53 |
cache_read = _g("cache_read_input_tokens")
|
| 54 |
cache_creation = _g("cache_creation_input_tokens")
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
if details
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
return {
|
| 65 |
"prompt_tokens": int(prompt),
|
|
|
|
| 52 |
|
| 53 |
cache_read = _g("cache_read_input_tokens")
|
| 54 |
cache_creation = _g("cache_creation_input_tokens")
|
| 55 |
+
details = _g("prompt_tokens_details", None)
|
| 56 |
+
|
| 57 |
+
if not cache_read and details is not None:
|
| 58 |
+
if isinstance(details, dict):
|
| 59 |
+
cache_read = details.get("cached_tokens", 0) or 0
|
| 60 |
+
else:
|
| 61 |
+
cache_read = getattr(details, "cached_tokens", 0) or 0
|
| 62 |
+
if not cache_creation and details is not None:
|
| 63 |
+
if isinstance(details, dict):
|
| 64 |
+
cache_creation = details.get("cache_write_tokens", 0) or 0
|
| 65 |
+
else:
|
| 66 |
+
cache_creation = getattr(details, "cache_write_tokens", 0) or 0
|
| 67 |
|
| 68 |
return {
|
| 69 |
"prompt_tokens": int(prompt),
|
frontend/src/components/UsageMeter.tsx
CHANGED
|
@@ -230,7 +230,7 @@ export default function UsageMeter() {
|
|
| 230 |
Usage
|
| 231 |
</Typography>
|
| 232 |
<Typography variant="caption" color="text.secondary">
|
| 233 |
-
|
| 234 |
</Typography>
|
| 235 |
|
| 236 |
{error ? (
|
|
|
|
| 230 |
Usage
|
| 231 |
</Typography>
|
| 232 |
<Typography variant="caption" color="text.secondary">
|
| 233 |
+
Estimated from HF account usage per session.
|
| 234 |
</Typography>
|
| 235 |
|
| 236 |
{error ? (
|
tests/unit/test_prompt_caching.py
CHANGED
|
@@ -182,10 +182,19 @@ def test_prompt_cache_params_add_session_id_for_fal_router_model():
|
|
| 182 |
cached_params = with_prompt_cache_params(llm_params, session_id="session-1")
|
| 183 |
|
| 184 |
assert cached_params is not llm_params
|
| 185 |
-
assert cached_params["extra_body"] == {
|
|
|
|
|
|
|
|
|
|
| 186 |
assert "extra_body" not in llm_params
|
| 187 |
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
def test_prompt_cache_params_merges_gpt55_cache_hints():
|
| 190 |
llm_params = {
|
| 191 |
**_gpt55_fal_params(),
|
|
|
|
| 182 |
cached_params = with_prompt_cache_params(llm_params, session_id="session-1")
|
| 183 |
|
| 184 |
assert cached_params is not llm_params
|
| 185 |
+
assert cached_params["extra_body"] == {
|
| 186 |
+
"session_id": "session-1",
|
| 187 |
+
"cache_control": {"type": "ephemeral"},
|
| 188 |
+
}
|
| 189 |
assert "extra_body" not in llm_params
|
| 190 |
|
| 191 |
|
| 192 |
+
def test_prompt_cache_params_adds_anthropic_cache_control_without_session_id():
|
| 193 |
+
cached_params = with_prompt_cache_params(_anthropic_fal_params())
|
| 194 |
+
|
| 195 |
+
assert cached_params["extra_body"] == {"cache_control": {"type": "ephemeral"}}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
def test_prompt_cache_params_merges_gpt55_cache_hints():
|
| 199 |
llm_params = {
|
| 200 |
**_gpt55_fal_params(),
|
tests/unit/test_telemetry_usage.py
CHANGED
|
@@ -13,6 +13,25 @@ class FakeSession:
|
|
| 13 |
self.events.append(event)
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
@pytest.mark.asyncio
|
| 17 |
async def test_record_hf_job_complete_emits_runtime_cost(monkeypatch):
|
| 18 |
async def fake_catalog():
|
|
|
|
| 13 |
self.events.append(event)
|
| 14 |
|
| 15 |
|
| 16 |
+
def test_extract_usage_reads_hf_router_cache_write_tokens():
|
| 17 |
+
response = SimpleNamespace(
|
| 18 |
+
usage=SimpleNamespace(
|
| 19 |
+
prompt_tokens=100,
|
| 20 |
+
completion_tokens=10,
|
| 21 |
+
total_tokens=110,
|
| 22 |
+
prompt_tokens_details=SimpleNamespace(
|
| 23 |
+
cached_tokens=80,
|
| 24 |
+
cache_write_tokens=20,
|
| 25 |
+
),
|
| 26 |
+
)
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
usage = telemetry.extract_usage(response)
|
| 30 |
+
|
| 31 |
+
assert usage["cache_read_tokens"] == 80
|
| 32 |
+
assert usage["cache_creation_tokens"] == 20
|
| 33 |
+
|
| 34 |
+
|
| 35 |
@pytest.mark.asyncio
|
| 36 |
async def test_record_hf_job_complete_emits_runtime_cost(monkeypatch):
|
| 37 |
async def fake_catalog():
|