@@ -377,3 +377,236 @@ def test_usage_normalizes_chat_completions_types():
377377
378378 assert isinstance (usage .output_tokens_details , OutputTokensDetails )
379379 assert usage .output_tokens_details .reasoning_tokens == 100
380+
381+
382+ # ============================================================================
383+ # Tests for agent_name and model_name on RequestUsage (issue #2100)
384+ # ============================================================================
385+
386+
387+ def test_request_usage_default_agent_model_names_are_none ():
388+ """Backward-compat: RequestUsage without agent_name/model_name defaults to None."""
389+ entry = RequestUsage (
390+ input_tokens = 10 ,
391+ output_tokens = 5 ,
392+ total_tokens = 15 ,
393+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
394+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
395+ )
396+ assert entry .agent_name is None
397+ assert entry .model_name is None
398+
399+
400+ def test_request_usage_with_agent_and_model_names ():
401+ """RequestUsage can be created with explicit agent_name and model_name."""
402+ entry = RequestUsage (
403+ input_tokens = 10 ,
404+ output_tokens = 5 ,
405+ total_tokens = 15 ,
406+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
407+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
408+ agent_name = "Math Tutor" ,
409+ model_name = "gpt-4o" ,
410+ )
411+ assert entry .agent_name == "Math Tutor"
412+ assert entry .model_name == "gpt-4o"
413+
414+
415+ def test_usage_add_propagates_agent_and_model_names ():
416+ """Usage.add() with agent_name/model_name annotates the RequestUsage entry."""
417+ parent = Usage ()
418+ child = Usage (
419+ requests = 1 ,
420+ input_tokens = 65 ,
421+ output_tokens = 13 ,
422+ total_tokens = 78 ,
423+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
424+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
425+ )
426+ parent .add (child , agent_name = "Code Reviewer" , model_name = "gpt-4o-mini" )
427+
428+ assert len (parent .request_usage_entries ) == 1
429+ entry = parent .request_usage_entries [0 ]
430+ assert entry .agent_name == "Code Reviewer"
431+ assert entry .model_name == "gpt-4o-mini"
432+ assert entry .input_tokens == 65
433+ assert entry .output_tokens == 13
434+
435+
436+ def test_usage_add_without_agent_model_names_stays_none ():
437+ """Usage.add() without agent/model names leaves them as None (backward compat)."""
438+ parent = Usage ()
439+ child = Usage (
440+ requests = 1 ,
441+ input_tokens = 20 ,
442+ output_tokens = 10 ,
443+ total_tokens = 30 ,
444+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
445+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
446+ )
447+ parent .add (child )
448+
449+ assert len (parent .request_usage_entries ) == 1
450+ entry = parent .request_usage_entries [0 ]
451+ assert entry .agent_name is None
452+ assert entry .model_name is None
453+
454+
455+ def test_usage_add_merge_existing_entries_applies_agent_model_names ():
456+ """When merging existing request_usage_entries, agent/model names are applied to unset ones."""
457+ # An existing entry without names
458+ existing_entry = RequestUsage (
459+ input_tokens = 100 ,
460+ output_tokens = 50 ,
461+ total_tokens = 150 ,
462+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
463+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
464+ )
465+ parent = Usage ()
466+ child = Usage (
467+ requests = 2 , # not 1, so it won't auto-create a new entry
468+ input_tokens = 100 ,
469+ output_tokens = 50 ,
470+ total_tokens = 150 ,
471+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
472+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
473+ request_usage_entries = [existing_entry ],
474+ )
475+ parent .add (child , agent_name = "Triage Agent" , model_name = "gpt-4o" )
476+
477+ assert len (parent .request_usage_entries ) == 1
478+ assert parent .request_usage_entries [0 ].agent_name == "Triage Agent"
479+ assert parent .request_usage_entries [0 ].model_name == "gpt-4o"
480+
481+
482+ def test_usage_add_merge_existing_entries_does_not_overwrite_names ():
483+ """Existing agent/model names on entries are not overwritten during merge."""
484+ existing_entry = RequestUsage (
485+ input_tokens = 100 ,
486+ output_tokens = 50 ,
487+ total_tokens = 150 ,
488+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
489+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
490+ agent_name = "Already Named Agent" ,
491+ model_name = "already-named-model" ,
492+ )
493+ parent = Usage ()
494+ child = Usage (
495+ requests = 2 ,
496+ input_tokens = 100 ,
497+ output_tokens = 50 ,
498+ total_tokens = 150 ,
499+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
500+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
501+ request_usage_entries = [existing_entry ],
502+ )
503+ parent .add (child , agent_name = "New Agent Name" , model_name = "new-model" )
504+
505+ # The existing names should NOT be overwritten
506+ assert parent .request_usage_entries [0 ].agent_name == "Already Named Agent"
507+ assert parent .request_usage_entries [0 ].model_name == "already-named-model"
508+
509+
510+ @pytest .mark .asyncio
511+ async def test_runner_run_populates_agent_name_in_request_usage ():
512+ """Integration: Running an agent populates agent_name in RequestUsage entries."""
513+ from agents .usage import Usage as AgentUsage
514+
515+ model_usage = AgentUsage (
516+ requests = 1 ,
517+ input_tokens = 42 ,
518+ output_tokens = 8 ,
519+ total_tokens = 50 ,
520+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
521+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
522+ )
523+ fake = FakeModel (initial_output = [get_text_message ("hello" )])
524+ fake .set_hardcoded_usage (model_usage )
525+ agent = Agent (name = "My Assistant" , model = fake )
526+
527+ result = await Runner .run (agent , input = "hi" )
528+
529+ entries = result .context_wrapper .usage .request_usage_entries
530+ assert len (entries ) == 1
531+ assert entries [0 ].agent_name == "My Assistant"
532+
533+
534+ @pytest .mark .asyncio
535+ async def test_runner_run_populates_model_name_in_request_usage ():
536+ """Integration: Running an agent populates model_name in RequestUsage entries."""
537+ from agents .usage import Usage as AgentUsage
538+
539+ model_usage = AgentUsage (
540+ requests = 1 ,
541+ input_tokens = 30 ,
542+ output_tokens = 10 ,
543+ total_tokens = 40 ,
544+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
545+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
546+ )
547+ # FakeModel doesn't expose a `.model` attribute by default → model_name should be None
548+ # We give it one to test that model_name is picked up.
549+ fake = FakeModel (initial_output = [get_text_message ("ok" )])
550+ fake .model = "test-model-name" # type: ignore[attr-defined]
551+ fake .set_hardcoded_usage (model_usage )
552+ agent = Agent (name = "Model-Aware Agent" , model = fake )
553+
554+ result = await Runner .run (agent , input = "ping" )
555+
556+ entries = result .context_wrapper .usage .request_usage_entries
557+ assert len (entries ) == 1
558+ assert entries [0 ].model_name == "test-model-name"
559+
560+
561+ @pytest .mark .asyncio
562+ async def test_multi_agent_run_attributes_usage_to_correct_agents ():
563+ """Multi-agent scenario: each RequestUsage entry has the right agent_name."""
564+
565+ from agents .usage import Usage as AgentUsage
566+ from tests .test_responses import get_handoff_tool_call
567+
568+ # Two separate models so we can track which agent's usage is which
569+ triage_usage = AgentUsage (
570+ requests = 1 ,
571+ input_tokens = 100 ,
572+ output_tokens = 10 ,
573+ total_tokens = 110 ,
574+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
575+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
576+ )
577+ specialist_usage = AgentUsage (
578+ requests = 1 ,
579+ input_tokens = 200 ,
580+ output_tokens = 20 ,
581+ total_tokens = 220 ,
582+ input_tokens_details = InputTokensDetails (cached_tokens = 0 ),
583+ output_tokens_details = OutputTokensDetails (reasoning_tokens = 0 ),
584+ )
585+
586+ specialist_model = FakeModel (initial_output = [get_text_message ("specialist done" )])
587+ specialist_model .set_hardcoded_usage (specialist_usage )
588+ specialist_agent = Agent (name = "Specialist Agent" , model = specialist_model )
589+
590+ triage_model = FakeModel ()
591+ triage_model .add_multiple_turn_outputs (
592+ [
593+ [get_handoff_tool_call (specialist_agent )],
594+ ]
595+ )
596+ triage_model .set_hardcoded_usage (triage_usage )
597+ triage_agent = Agent (name = "Triage Agent" , model = triage_model , handoffs = [specialist_agent ])
598+
599+ result = await Runner .run (triage_agent , input = "route me" )
600+
601+ all_entries = result .context_wrapper .usage .request_usage_entries
602+ assert len (all_entries ) == 2 , f"Expected 2 request entries, got { len (all_entries )} "
603+
604+ agent_names = [e .agent_name for e in all_entries ]
605+ assert "Triage Agent" in agent_names , f"Expected 'Triage Agent' in { agent_names } "
606+ assert "Specialist Agent" in agent_names , f"Expected 'Specialist Agent' in { agent_names } "
607+
608+ triage_entry = next (e for e in all_entries if e .agent_name == "Triage Agent" )
609+ assert triage_entry .input_tokens == 100
610+
611+ specialist_entry = next (e for e in all_entries if e .agent_name == "Specialist Agent" )
612+ assert specialist_entry .input_tokens == 200
0 commit comments