Skip to content

Commit ea97c6d

Browse files
authored
fix(langchain): handle Anthropic cache_creation nested dict in usage (#1747)
1 parent 77b242a commit ea97c6d

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

langfuse/langchain/CallbackHandler.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,28 @@ def _parse_usage_model(usage: Union[pydantic.BaseModel, dict]) -> Any:
16851685
0, usage_model[f"input_modality_{item['modality']}"] - value
16861686
)
16871687

1688+
# Anthropic extended prompt caching — cache_creation is a nested dict
1689+
# keyed by cache tier, e.g. {"ephemeral_5m_input_tokens": 2048, "ephemeral_1h_input_tokens": 0}
1690+
if "cache_creation" in usage_model and isinstance(
1691+
usage_model["cache_creation"], dict
1692+
):
1693+
cache_creation = usage_model.pop("cache_creation")
1694+
total_cache_creation_tokens = 0
1695+
1696+
for key, value in cache_creation.items():
1697+
if not isinstance(value, int):
1698+
continue
1699+
1700+
usage_model[f"cache_creation_{key}"] = value
1701+
total_cache_creation_tokens += value
1702+
1703+
# Aggregate mirrors Anthropic's legacy cache_creation_input_tokens field;
1704+
# keep the API-provided value if it is already present
1705+
if total_cache_creation_tokens > 0:
1706+
usage_model.setdefault(
1707+
"cache_creation_input_tokens", total_cache_creation_tokens
1708+
)
1709+
16881710
usage_model = {k: v for k, v in usage_model.items() if isinstance(v, int)}
16891711

16901712
return usage_model if usage_model else None

tests/unit/test_parse_usage_model.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,60 @@ def test_prompt_tokens_details_dict_empty():
7878
assert result["output"] == 100
7979

8080

81+
def test_anthropic_cache_creation_nested_dict():
82+
"""Anthropic extended prompt caching: cache_creation nested dict is flattened."""
83+
usage = {
84+
"input_tokens": 9454,
85+
"output_tokens": 380,
86+
"cache_read_input_tokens": 0,
87+
"cache_creation": {
88+
"ephemeral_1h_input_tokens": 0,
89+
"ephemeral_5m_input_tokens": 2048,
90+
},
91+
}
92+
result = _parse_usage_model(usage)
93+
assert result["input"] == 9454
94+
assert result["output"] == 380
95+
assert result["cache_creation_ephemeral_1h_input_tokens"] == 0
96+
assert result["cache_creation_ephemeral_5m_input_tokens"] == 2048
97+
assert result["cache_creation_input_tokens"] == 2048
98+
# No nested dict may survive into the final usage model
99+
assert all(isinstance(v, int) for v in result.values())
100+
101+
102+
def test_anthropic_cache_creation_keeps_existing_aggregate():
103+
"""API-provided cache_creation_input_tokens is not overwritten by the computed sum."""
104+
usage = {
105+
"input_tokens": 100,
106+
"output_tokens": 10,
107+
"cache_creation_input_tokens": 3000,
108+
"cache_creation": {
109+
"ephemeral_1h_input_tokens": 1000,
110+
"ephemeral_5m_input_tokens": 2000,
111+
},
112+
}
113+
result = _parse_usage_model(usage)
114+
assert result["cache_creation_input_tokens"] == 3000
115+
assert result["cache_creation_ephemeral_1h_input_tokens"] == 1000
116+
assert result["cache_creation_ephemeral_5m_input_tokens"] == 2000
117+
118+
119+
def test_anthropic_cache_creation_all_zero():
120+
"""All-zero cache_creation tiers: flattened keys kept, no aggregate invented."""
121+
usage = {
122+
"input_tokens": 50,
123+
"output_tokens": 5,
124+
"cache_creation": {
125+
"ephemeral_1h_input_tokens": 0,
126+
"ephemeral_5m_input_tokens": 0,
127+
},
128+
}
129+
result = _parse_usage_model(usage)
130+
assert result["cache_creation_ephemeral_1h_input_tokens"] == 0
131+
assert result["cache_creation_ephemeral_5m_input_tokens"] == 0
132+
assert "cache_creation_input_tokens" not in result
133+
134+
81135
def test_priority_tier_not_subtracted():
82136
"""Priority tier: 'priority' and 'priority_*' keys must NOT be subtracted."""
83137
usage = {

0 commit comments

Comments
 (0)