Skip to content

Commit bccca6d

Browse files
refactor(usage): narrow request attribution to agent name
1 parent 617a77c commit bccca6d

3 files changed

Lines changed: 23 additions & 123 deletions

File tree

src/agents/run_internal/run_loop.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -260,31 +260,6 @@ def _should_attach_generic_agent_error(exc: Exception) -> bool:
260260
)
261261

262262

263-
def _get_model_name(model: Any) -> str | None:
264-
"""Extract the string model name from a Model instance, if available.
265-
266-
Most built-in model implementations (``OpenAIResponsesModel``,
267-
``OpenAIChatCompletionsModel``) expose a ``model`` attribute that contains
268-
the underlying model name string. This helper retrieves it in a
269-
forward-compatible, duck-typed way so that third-party model
270-
implementations are handled gracefully.
271-
272-
The function tries ``model`` first, then ``model_name`` for
273-
implementations that prefer that name. Any exception raised while
274-
resolving the attribute (for example, a custom descriptor or property
275-
that throws) is swallowed so that usage accounting can never crash the
276-
run, and ``None`` is returned when no string-valued name is available.
277-
"""
278-
for attr in ("model", "model_name"):
279-
try:
280-
value = getattr(model, attr, None)
281-
except Exception:
282-
continue
283-
if isinstance(value, str) and value:
284-
return value
285-
return None
286-
287-
288263
async def _should_persist_stream_items(
289264
*,
290265
session: Session | None,
@@ -1641,7 +1616,6 @@ async def rewind_model_request() -> None:
16411616
context_wrapper.usage.add(
16421617
final_response.usage,
16431618
agent_name=public_agent.name,
1644-
model_name=_get_model_name(model),
16451619
)
16461620
await asyncio.gather(
16471621
(
@@ -1923,7 +1897,6 @@ async def rewind_model_request() -> None:
19231897
context_wrapper.usage.add(
19241898
new_response.usage,
19251899
agent_name=public_agent.name,
1926-
model_name=_get_model_name(model),
19271900
)
19281901

19291902
await asyncio.gather(

src/agents/usage.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def deserialize_usage(usage_data: Mapping[str, Any]) -> Usage:
4444
OutputTokensDetails(reasoning_tokens=0),
4545
),
4646
agent_name=entry.get("agent_name", None),
47-
model_name=entry.get("model_name", None),
4847
)
4948
)
5049

@@ -85,14 +84,6 @@ class RequestUsage:
8584
token usage and costs to specific agents in multi-agent workflows.
8685
"""
8786

88-
model_name: str | None = None
89-
"""Name of the model used for this request, if available.
90-
91-
Populated automatically when an agent makes a model call so that callers can attribute
92-
token usage and costs to specific models.
93-
"""
94-
95-
9687
def _normalize_input_tokens_details(
9788
v: InputTokensDetails | PromptTokensDetails | None,
9889
) -> InputTokensDetails:
@@ -175,7 +166,6 @@ def add(
175166
other: Usage,
176167
*,
177168
agent_name: str | None = None,
178-
model_name: str | None = None,
179169
) -> None:
180170
"""Add another Usage object to this one, aggregating all fields.
181171
@@ -185,8 +175,6 @@ def add(
185175
other: The Usage object to add to this one.
186176
agent_name: Optional name of the agent making this request, used to annotate the
187177
resulting ``RequestUsage`` entry for per-agent cost attribution.
188-
model_name: Optional name of the model used for this request, used to annotate the
189-
resulting ``RequestUsage`` entry for per-model cost attribution.
190178
"""
191179
self.requests += other.requests if other.requests else 0
192180
self.input_tokens += other.input_tokens if other.input_tokens else 0
@@ -239,9 +227,6 @@ def add(
239227
agent_name=agent_name
240228
if (agent_name is not None and entry.agent_name is None)
241229
else entry.agent_name,
242-
model_name=model_name
243-
if (model_name is not None and entry.model_name is None)
244-
else entry.model_name,
245230
)
246231
self.request_usage_entries.append(annotated_entry)
247232
else:
@@ -256,12 +241,11 @@ def add(
256241
input_tokens_details=input_details,
257242
output_tokens_details=output_details,
258243
agent_name=agent_name,
259-
model_name=model_name,
260244
)
261245
self.request_usage_entries.append(request_usage)
262246
elif other.request_usage_entries:
263247
# If the other Usage already has individual request breakdowns, merge them.
264-
# Apply agent_name/model_name to entries that don't already have them set,
248+
# Apply agent_name to entries that don't already have it set,
265249
# but copy each entry rather than mutating the original objects in place
266250
# to avoid silent mis-attribution when the same Usage is added multiple times.
267251
for entry in other.request_usage_entries:
@@ -274,9 +258,6 @@ def add(
274258
agent_name=agent_name
275259
if (agent_name is not None and entry.agent_name is None)
276260
else entry.agent_name,
277-
model_name=model_name
278-
if (model_name is not None and entry.model_name is None)
279-
else entry.model_name,
280261
)
281262
self.request_usage_entries.append(annotated_entry)
282263

@@ -309,8 +290,6 @@ def _serialize_request_entry(entry: RequestUsage) -> dict[str, Any]:
309290
}
310291
if entry.agent_name is not None:
311292
result["agent_name"] = entry.agent_name
312-
if entry.model_name is not None:
313-
result["model_name"] = entry.model_name
314293
return result
315294

316295
return {

tests/test_usage.py

Lines changed: 22 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,12 @@ def test_usage_normalizes_chat_completions_types():
380380

381381

382382
# ============================================================================
383-
# Tests for agent_name and model_name on RequestUsage (issue #2100)
383+
# Tests for agent_name on RequestUsage (issue #2100)
384384
# ============================================================================
385385

386386

387-
def test_request_usage_default_agent_model_names_are_none():
388-
"""Backward-compat: RequestUsage without agent_name/model_name defaults to None."""
387+
def test_request_usage_default_agent_name_is_none():
388+
"""Backward-compat: RequestUsage without agent_name defaults to None."""
389389
entry = RequestUsage(
390390
input_tokens=10,
391391
output_tokens=5,
@@ -394,14 +394,13 @@ def test_request_usage_default_agent_model_names_are_none():
394394
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
395395
)
396396
assert entry.agent_name is None
397-
assert entry.model_name is None
398397

399398

400-
def test_serialize_deserialize_roundtrip_preserves_agent_and_model_names():
401-
"""JSON round-trip must preserve agent_name and model_name on each entry.
399+
def test_serialize_deserialize_roundtrip_preserves_agent_name():
400+
"""JSON round-trip must preserve agent_name on each entry.
402401
403402
This guards against a regression where serialize_usage drops the new
404-
attribution fields, or deserialize_usage forgets to read them back.
403+
attribution field, or deserialize_usage forgets to read it back.
405404
Both branches of the conditional emit (entry-with-name and entry-without-name)
406405
are exercised so the all-None fast path can't silently strip the keys.
407406
"""
@@ -414,7 +413,6 @@ def test_serialize_deserialize_roundtrip_preserves_agent_and_model_names():
414413
input_tokens_details=InputTokensDetails(cached_tokens=0),
415414
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
416415
agent_name="Math Tutor",
417-
model_name="gpt-4o",
418416
)
419417
unnamed_entry = RequestUsage(
420418
input_tokens=2,
@@ -438,32 +436,28 @@ def test_serialize_deserialize_roundtrip_preserves_agent_and_model_names():
438436
restored_unnamed = restored.request_usage_entries[1]
439437

440438
assert restored_named.agent_name == "Math Tutor"
441-
assert restored_named.model_name == "gpt-4o"
442439
assert restored_named.input_tokens == 10
443440
assert restored_named.output_tokens == 5
444441

445442
assert restored_unnamed.agent_name is None
446-
assert restored_unnamed.model_name is None
447443
assert restored_unnamed.input_tokens == 2
448444

449445

450-
def test_request_usage_with_agent_and_model_names():
451-
"""RequestUsage can be created with explicit agent_name and model_name."""
446+
def test_request_usage_with_agent_name():
447+
"""RequestUsage can be created with an explicit agent_name."""
452448
entry = RequestUsage(
453449
input_tokens=10,
454450
output_tokens=5,
455451
total_tokens=15,
456452
input_tokens_details=InputTokensDetails(cached_tokens=0),
457453
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
458454
agent_name="Math Tutor",
459-
model_name="gpt-4o",
460455
)
461456
assert entry.agent_name == "Math Tutor"
462-
assert entry.model_name == "gpt-4o"
463457

464458

465-
def test_usage_add_propagates_agent_and_model_names():
466-
"""Usage.add() with agent_name/model_name annotates the RequestUsage entry."""
459+
def test_usage_add_propagates_agent_name():
460+
"""Usage.add() with agent_name annotates the RequestUsage entry."""
467461
parent = Usage()
468462
child = Usage(
469463
requests=1,
@@ -473,18 +467,17 @@ def test_usage_add_propagates_agent_and_model_names():
473467
input_tokens_details=InputTokensDetails(cached_tokens=0),
474468
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
475469
)
476-
parent.add(child, agent_name="Code Reviewer", model_name="gpt-4o-mini")
470+
parent.add(child, agent_name="Code Reviewer")
477471

478472
assert len(parent.request_usage_entries) == 1
479473
entry = parent.request_usage_entries[0]
480474
assert entry.agent_name == "Code Reviewer"
481-
assert entry.model_name == "gpt-4o-mini"
482475
assert entry.input_tokens == 65
483476
assert entry.output_tokens == 13
484477

485478

486-
def test_usage_add_without_agent_model_names_stays_none():
487-
"""Usage.add() without agent/model names leaves them as None (backward compat)."""
479+
def test_usage_add_without_agent_name_stays_none():
480+
"""Usage.add() without agent_name leaves it as None (backward compat)."""
488481
parent = Usage()
489482
child = Usage(
490483
requests=1,
@@ -499,19 +492,17 @@ def test_usage_add_without_agent_model_names_stays_none():
499492
assert len(parent.request_usage_entries) == 1
500493
entry = parent.request_usage_entries[0]
501494
assert entry.agent_name is None
502-
assert entry.model_name is None
503495

504496

505497
def test_usage_add_single_request_preserves_prebuilt_entry_attribution():
506-
"""Single-request Usage with request_usage_entries keeps agent/model when add() has no kwargs."""
498+
"""Single-request Usage with request_usage_entries keeps agent name when add() has no kwargs."""
507499
inner = RequestUsage(
508500
input_tokens=20,
509501
output_tokens=10,
510502
total_tokens=30,
511503
input_tokens_details=InputTokensDetails(cached_tokens=0),
512504
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
513505
agent_name="Prior Run Agent",
514-
model_name="prior-model",
515506
)
516507
child = Usage(
517508
requests=1,
@@ -528,11 +519,10 @@ def test_usage_add_single_request_preserves_prebuilt_entry_attribution():
528519
assert len(parent.request_usage_entries) == 1
529520
out = parent.request_usage_entries[0]
530521
assert out.agent_name == "Prior Run Agent"
531-
assert out.model_name == "prior-model"
532522

533523

534-
def test_usage_add_merge_existing_entries_applies_agent_model_names():
535-
"""When merging existing request_usage_entries, agent/model names are applied to unset ones."""
524+
def test_usage_add_merge_existing_entries_applies_agent_name():
525+
"""When merging existing request_usage_entries, agent_name is applied to unset ones."""
536526
# An existing entry without names
537527
existing_entry = RequestUsage(
538528
input_tokens=100,
@@ -551,23 +541,21 @@ def test_usage_add_merge_existing_entries_applies_agent_model_names():
551541
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
552542
request_usage_entries=[existing_entry],
553543
)
554-
parent.add(child, agent_name="Triage Agent", model_name="gpt-4o")
544+
parent.add(child, agent_name="Triage Agent")
555545

556546
assert len(parent.request_usage_entries) == 1
557547
assert parent.request_usage_entries[0].agent_name == "Triage Agent"
558-
assert parent.request_usage_entries[0].model_name == "gpt-4o"
559548

560549

561-
def test_usage_add_merge_existing_entries_does_not_overwrite_names():
562-
"""Existing agent/model names on entries are not overwritten during merge."""
550+
def test_usage_add_merge_existing_entries_does_not_overwrite_agent_name():
551+
"""Existing agent_name on entries is not overwritten during merge."""
563552
existing_entry = RequestUsage(
564553
input_tokens=100,
565554
output_tokens=50,
566555
total_tokens=150,
567556
input_tokens_details=InputTokensDetails(cached_tokens=0),
568557
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
569558
agent_name="Already Named Agent",
570-
model_name="already-named-model",
571559
)
572560
parent = Usage()
573561
child = Usage(
@@ -579,11 +567,10 @@ def test_usage_add_merge_existing_entries_does_not_overwrite_names():
579567
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
580568
request_usage_entries=[existing_entry],
581569
)
582-
parent.add(child, agent_name="New Agent Name", model_name="new-model")
570+
parent.add(child, agent_name="New Agent Name")
583571

584-
# The existing names should NOT be overwritten
572+
# The existing name should NOT be overwritten
585573
assert parent.request_usage_entries[0].agent_name == "Already Named Agent"
586-
assert parent.request_usage_entries[0].model_name == "already-named-model"
587574

588575

589576
@pytest.mark.asyncio
@@ -610,33 +597,6 @@ async def test_runner_run_populates_agent_name_in_request_usage():
610597
assert entries[0].agent_name == "My Assistant"
611598

612599

613-
@pytest.mark.asyncio
614-
async def test_runner_run_populates_model_name_in_request_usage():
615-
"""Integration: Running an agent populates model_name in RequestUsage entries."""
616-
from agents.usage import Usage as AgentUsage
617-
618-
model_usage = AgentUsage(
619-
requests=1,
620-
input_tokens=30,
621-
output_tokens=10,
622-
total_tokens=40,
623-
input_tokens_details=InputTokensDetails(cached_tokens=0),
624-
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
625-
)
626-
# FakeModel doesn't expose a `.model` attribute by default → model_name should be None
627-
# We give it one to test that model_name is picked up.
628-
fake = FakeModel(initial_output=[get_text_message("ok")])
629-
fake.model = "test-model-name" # type: ignore[attr-defined]
630-
fake.set_hardcoded_usage(model_usage)
631-
agent = Agent(name="Model-Aware Agent", model=fake)
632-
633-
result = await Runner.run(agent, input="ping")
634-
635-
entries = result.context_wrapper.usage.request_usage_entries
636-
assert len(entries) == 1
637-
assert entries[0].model_name == "test-model-name"
638-
639-
640600
@pytest.mark.asyncio
641601
async def test_multi_agent_run_attributes_usage_to_correct_agents():
642602
"""Multi-agent scenario: each RequestUsage entry has the right agent_name."""
@@ -663,12 +623,10 @@ async def test_multi_agent_run_attributes_usage_to_correct_agents():
663623
)
664624

665625
specialist_model = FakeModel(initial_output=[get_text_message("specialist done")])
666-
specialist_model.model = "gpt-4o-specialist" # type: ignore[attr-defined]
667626
specialist_model.set_hardcoded_usage(specialist_usage)
668627
specialist_agent = Agent(name="Specialist Agent", model=specialist_model)
669628

670629
triage_model = FakeModel()
671-
triage_model.model = "gpt-4o-triage" # type: ignore[attr-defined]
672630
triage_model.add_multiple_turn_outputs(
673631
[
674632
[get_handoff_tool_call(specialist_agent)],
@@ -688,16 +646,9 @@ async def test_multi_agent_run_attributes_usage_to_correct_agents():
688646

689647
triage_entry = next(e for e in all_entries if e.agent_name == "Triage Agent")
690648
assert triage_entry.input_tokens == 100
691-
assert triage_entry.model_name == "gpt-4o-triage", (
692-
f"Triage entry model_name should be 'gpt-4o-triage', got {triage_entry.model_name!r}"
693-
)
694649

695650
specialist_entry = next(e for e in all_entries if e.agent_name == "Specialist Agent")
696651
assert specialist_entry.input_tokens == 200
697-
assert specialist_entry.model_name == "gpt-4o-specialist", (
698-
"Specialist entry model_name should be 'gpt-4o-specialist', "
699-
f"got {specialist_entry.model_name!r}"
700-
)
701652

702653

703654
def test_add_does_not_mutate_other_entries() -> None:
@@ -716,7 +667,6 @@ def test_add_does_not_mutate_other_entries() -> None:
716667
input_tokens_details=InputTokensDetails(cached_tokens=0),
717668
output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
718669
agent_name=None,
719-
model_name=None,
720670
)
721671

722672
# Build a Usage that already has request_usage_entries (requests != 1 path)
@@ -729,13 +679,11 @@ def test_add_does_not_mutate_other_entries() -> None:
729679
)
730680

731681
agg = Usage()
732-
agg.add(other, agent_name="MyAgent", model_name="gpt-4o")
682+
agg.add(other, agent_name="MyAgent")
733683

734684
# The aggregator should have a copy with the annotation applied
735685
assert len(agg.request_usage_entries) == 1
736686
assert agg.request_usage_entries[0].agent_name == "MyAgent"
737-
assert agg.request_usage_entries[0].model_name == "gpt-4o"
738687

739688
# The original entry must NOT be mutated
740689
assert source_entry.agent_name is None, "Original entry was mutated!"
741-
assert source_entry.model_name is None, "Original entry was mutated!"

0 commit comments

Comments
 (0)