Skip to content

Commit 8ec30bd

Browse files
committed
Add per-request model_name/agent_name/response_id tracking via RequestUsage.metadata
1 parent 9fcc68f commit 8ec30bd

File tree

5 files changed

+143
-18
lines changed

5 files changed

+143
-18
lines changed

docs/usage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ The SDK automatically tracks usage for each API request in `request_usage_entrie
5454
```python
5555
result = await Runner.run(agent, "What's the weather in Tokyo?")
5656

57-
for request in enumerate(result.context_wrapper.usage.request_usage_entries):
58-
print(f"Request {i + 1}: {request.input_tokens} in, {request.output_tokens} out")
57+
for i, request in enumerate(result.context_wrapper.usage.request_usage_entries):
58+
print(f"Request {i + 1}: Input={request.input_tokens} tokens, Output={request.output_tokens} tokens, metadata={request.metadata}")
5959
```
6060

6161
## Accessing usage with sessions

src/agents/models/interface.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def include_data(self) -> bool:
3636
class Model(abc.ABC):
3737
"""The base interface for calling an LLM."""
3838

39+
# The model name. Subclasses can set this in __init__.
40+
model: str = ""
41+
3942
@abc.abstractmethod
4043
async def get_response(
4144
self,

src/agents/run.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,14 @@ async def _run_single_turn_streamed(
14541454
usage=usage,
14551455
response_id=event.response.id,
14561456
)
1457-
context_wrapper.usage.add(usage)
1457+
context_wrapper.usage.add(
1458+
usage,
1459+
metadata={
1460+
"model_name": model.model,
1461+
"agent_name": agent.name,
1462+
"response_id": event.response.id,
1463+
},
1464+
)
14581465

14591466
if isinstance(event, ResponseOutputItemDoneEvent):
14601467
output_item = event.item
@@ -1872,7 +1879,14 @@ async def _get_new_response(
18721879
prompt=prompt_config,
18731880
)
18741881

1875-
context_wrapper.usage.add(new_response.usage)
1882+
context_wrapper.usage.add(
1883+
new_response.usage,
1884+
metadata={
1885+
"model_name": model.model,
1886+
"agent_name": agent.name,
1887+
"response_id": new_response.response_id,
1888+
},
1889+
)
18761890

18771891
# If we have run hooks, or if the agent has hooks, we need to call them after the LLM call
18781892
await asyncio.gather(

src/agents/usage.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import field
4-
from typing import Annotated
4+
from typing import Annotated, Any
55

66
from openai.types.completion_usage import CompletionTokensDetails, PromptTokensDetails
77
from openai.types.responses.response_usage import InputTokensDetails, OutputTokensDetails
@@ -50,6 +50,9 @@ class RequestUsage:
5050
output_tokens_details: OutputTokensDetails
5151
"""Details about the output tokens for this individual request."""
5252

53+
metadata: dict[str, Any] = field(default_factory=dict)
54+
"""Additional metadata for this request (e.g., model_name, agent_name, response_id)."""
55+
5356

5457
@dataclass
5558
class Usage:
@@ -97,13 +100,18 @@ def __post_init__(self) -> None:
97100
if self.output_tokens_details.reasoning_tokens is None:
98101
self.output_tokens_details = OutputTokensDetails(reasoning_tokens=0)
99102

100-
def add(self, other: Usage) -> None:
103+
def add(
104+
self,
105+
other: Usage,
106+
metadata: dict[str, Any] | None = None,
107+
) -> None:
101108
"""Add another Usage object to this one, aggregating all fields.
102109
103110
This method automatically preserves request_usage_entries.
104111
105112
Args:
106113
other: The Usage object to add to this one.
114+
metadata: Additional metadata for this request
107115
"""
108116
self.requests += other.requests if other.requests else 0
109117
self.input_tokens += other.input_tokens if other.input_tokens else 0
@@ -128,6 +136,7 @@ def add(self, other: Usage) -> None:
128136
total_tokens=other.total_tokens,
129137
input_tokens_details=other.input_tokens_details,
130138
output_tokens_details=other.output_tokens_details,
139+
metadata=metadata or {},
131140
)
132141
self.request_usage_entries.append(request_usage)
133142
elif other.request_usage_entries:

tests/test_usage.py

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ def test_usage_add_aggregates_all_fields():
2222
total_tokens=15,
2323
)
2424

25-
u1.add(u2)
25+
u1.add(
26+
u2,
27+
metadata={
28+
"model_name": "gpt-5",
29+
"agent_name": "test-agent",
30+
"response_id": "resp-1",
31+
},
32+
)
2633

2734
assert u1.requests == 3
2835
assert u1.input_tokens == 17
@@ -43,7 +50,14 @@ def test_usage_add_aggregates_with_none_values():
4350
total_tokens=15,
4451
)
4552

46-
u1.add(u2)
53+
u1.add(
54+
u2,
55+
metadata={
56+
"model_name": "gpt-5",
57+
"agent_name": "test-agent",
58+
"response_id": "resp-1",
59+
},
60+
)
4761

4862
assert u1.requests == 2
4963
assert u1.input_tokens == 7
@@ -61,13 +75,21 @@ def test_request_usage_creation():
6175
total_tokens=300,
6276
input_tokens_details=InputTokensDetails(cached_tokens=10),
6377
output_tokens_details=OutputTokensDetails(reasoning_tokens=20),
78+
metadata={
79+
"model_name": "gpt-5",
80+
"agent_name": "test-agent",
81+
"response_id": "resp-123",
82+
},
6483
)
6584

6685
assert request_usage.input_tokens == 100
6786
assert request_usage.output_tokens == 200
6887
assert request_usage.total_tokens == 300
6988
assert request_usage.input_tokens_details.cached_tokens == 10
7089
assert request_usage.output_tokens_details.reasoning_tokens == 20
90+
assert request_usage.metadata["model_name"] == "gpt-5"
91+
assert request_usage.metadata["agent_name"] == "test-agent"
92+
assert request_usage.metadata["response_id"] == "resp-123"
7193

7294

7395
def test_usage_add_preserves_single_request():
@@ -82,7 +104,14 @@ def test_usage_add_preserves_single_request():
82104
total_tokens=300,
83105
)
84106

85-
u1.add(u2)
107+
u1.add(
108+
u2,
109+
metadata={
110+
"model_name": "gpt-5",
111+
"agent_name": "test-agent",
112+
"response_id": "resp-1",
113+
},
114+
)
86115

87116
# Should preserve the request usage details
88117
assert len(u1.request_usage_entries) == 1
@@ -92,6 +121,9 @@ def test_usage_add_preserves_single_request():
92121
assert request_usage.total_tokens == 300
93122
assert request_usage.input_tokens_details.cached_tokens == 10
94123
assert request_usage.output_tokens_details.reasoning_tokens == 20
124+
assert request_usage.metadata["model_name"] == "gpt-5"
125+
assert request_usage.metadata["agent_name"] == "test-agent"
126+
assert request_usage.metadata["response_id"] == "resp-1"
95127

96128

97129
def test_usage_add_ignores_zero_token_requests():
@@ -106,7 +138,14 @@ def test_usage_add_ignores_zero_token_requests():
106138
total_tokens=0,
107139
)
108140

109-
u1.add(u2)
141+
u1.add(
142+
u2,
143+
metadata={
144+
"model_name": "gpt-5",
145+
"agent_name": "test-agent",
146+
"response_id": "resp-1",
147+
},
148+
)
110149

111150
# Should not create a request_usage_entry for zero tokens
112151
assert len(u1.request_usage_entries) == 0
@@ -124,7 +163,14 @@ def test_usage_add_ignores_multi_request_usage():
124163
total_tokens=300,
125164
)
126165

127-
u1.add(u2)
166+
u1.add(
167+
u2,
168+
metadata={
169+
"model_name": "gpt-5",
170+
"agent_name": "test-agent",
171+
"response_id": "resp-1",
172+
},
173+
)
128174

129175
# Should not create a request usage entry for multi-request usage
130176
assert len(u1.request_usage_entries) == 0
@@ -142,7 +188,14 @@ def test_usage_add_merges_existing_request_usage_entries():
142188
output_tokens_details=OutputTokensDetails(reasoning_tokens=20),
143189
total_tokens=300,
144190
)
145-
u1.add(u2)
191+
u1.add(
192+
u2,
193+
metadata={
194+
"model_name": "gpt-5",
195+
"agent_name": "agent-1",
196+
"response_id": "resp-1",
197+
},
198+
)
146199

147200
# Create second usage with request_usage_entries
148201
u3 = Usage(
@@ -154,7 +207,14 @@ def test_usage_add_merges_existing_request_usage_entries():
154207
total_tokens=125,
155208
)
156209

157-
u1.add(u3)
210+
u1.add(
211+
u3,
212+
metadata={
213+
"model_name": "gpt-5",
214+
"agent_name": "agent-2",
215+
"response_id": "resp-2",
216+
},
217+
)
158218

159219
# Should have both request_usage_entries
160220
assert len(u1.request_usage_entries) == 2
@@ -164,12 +224,16 @@ def test_usage_add_merges_existing_request_usage_entries():
164224
assert first.input_tokens == 100
165225
assert first.output_tokens == 200
166226
assert first.total_tokens == 300
227+
assert first.metadata["agent_name"] == "agent-1"
228+
assert first.metadata["response_id"] == "resp-1"
167229

168230
# Second request
169231
second = u1.request_usage_entries[1]
170232
assert second.input_tokens == 50
171233
assert second.output_tokens == 75
172234
assert second.total_tokens == 125
235+
assert second.metadata["agent_name"] == "agent-2"
236+
assert second.metadata["response_id"] == "resp-2"
173237

174238

175239
def test_usage_add_with_pre_existing_request_usage_entries():
@@ -185,7 +249,14 @@ def test_usage_add_with_pre_existing_request_usage_entries():
185249
output_tokens_details=OutputTokensDetails(reasoning_tokens=20),
186250
total_tokens=300,
187251
)
188-
u1.add(u2)
252+
u1.add(
253+
u2,
254+
metadata={
255+
"model_name": "gpt-5",
256+
"agent_name": "agent-1",
257+
"response_id": "resp-1",
258+
},
259+
)
189260

190261
# Create another usage with request_usage_entries
191262
u3 = Usage(
@@ -198,7 +269,14 @@ def test_usage_add_with_pre_existing_request_usage_entries():
198269
)
199270

200271
# Add u3 to u1
201-
u1.add(u3)
272+
u1.add(
273+
u3,
274+
metadata={
275+
"model_name": "gpt-5",
276+
"agent_name": "agent-2",
277+
"response_id": "resp-2",
278+
},
279+
)
202280

203281
# Should have both request_usage_entries
204282
assert len(u1.request_usage_entries) == 2
@@ -228,7 +306,14 @@ def test_anthropic_cost_calculation_scenario():
228306
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
229307
total_tokens=150_000,
230308
)
231-
usage.add(req1)
309+
usage.add(
310+
req1,
311+
metadata={
312+
"model_name": "gpt-5",
313+
"agent_name": "test-agent",
314+
"response_id": "resp-1",
315+
},
316+
)
232317

233318
# Second request: 150K input tokens
234319
req2 = Usage(
@@ -239,7 +324,14 @@ def test_anthropic_cost_calculation_scenario():
239324
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
240325
total_tokens=225_000,
241326
)
242-
usage.add(req2)
327+
usage.add(
328+
req2,
329+
metadata={
330+
"model_name": "gpt-5",
331+
"agent_name": "test-agent",
332+
"response_id": "resp-2",
333+
},
334+
)
243335

244336
# Third request: 80K input tokens
245337
req3 = Usage(
@@ -250,7 +342,14 @@ def test_anthropic_cost_calculation_scenario():
250342
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
251343
total_tokens=120_000,
252344
)
253-
usage.add(req3)
345+
usage.add(
346+
req3,
347+
metadata={
348+
"model_name": "gpt-5",
349+
"agent_name": "test-agent",
350+
"response_id": "resp-3",
351+
},
352+
)
254353

255354
# Verify aggregated totals
256355
assert usage.requests == 3

0 commit comments

Comments
 (0)