11"""
2- Output handler for serializing generative benchmark reports to JSON.
2+ Output handler for serializing generative benchmark reports to JSON (both summary and full) .
33
4- This module implements a file-based output for saving benchmark results in JSON format.
5- It extends GenerativeBenchmarkerOutput and supports both directory and explicit file path
6- output, automatically creating parent directories as needed.
4+ This module implements a dual-output JSON handler that saves both:
5+ 1. Summary JSON - Excludes large fields like individual requests and detailed metrics
6+ 2. Full JSON - Contains complete benchmark data including all requests
7+
8+ Both files are saved to the same directory with clear naming conventions.
79"""
810
911from __future__ import annotations
1820from guidellm .benchmark .outputs .output import GenerativeBenchmarkerOutput
1921from guidellm .benchmark .schemas import GenerativeBenchmarksReport
2022
21- __all__ = ["GenerativeBenchmarkerSummaryJson" ]
23+ __all__ = ["GenerativeBenchmarkerDualJson" , "AutoMarshalJSONEncoder" ]
24+
2225
26+ class AutoMarshalJSONEncoder (json .JSONEncoder ):
27+ """
28+ Custom JSON encoder with auto-marshal support (similar to Golang's MarshalJSON).
2329
24- @GenerativeBenchmarkerOutput .register ("summary_json" )
25- class GenerativeBenchmarkerSummaryJson (GenerativeBenchmarkerOutput ):
30+ This encoder automatically checks if objects have __class_json__() or __json__()
31+ methods and calls them for serialization, providing a Golang-like interface for
32+ custom JSON marshaling in Python.
2633 """
27- Output handler for serializing benchmark reports to JSON files.
2834
29- This class saves generative benchmark reports to a specified file or directory in JSON format.
30- If a directory is provided, a default filename is used. Certain fields can be excluded from the output.
35+ def default (self , o ):
36+ """
37+ Override default serialization for non-serializable objects.
38+
39+ Args:
40+ o: Object to serialize.
41+
42+ Returns:
43+ Serializable representation of the object.
44+ """
45+ # Check if the object has a __class_json__ method (for class objects)
46+ if isinstance (o , type ) and hasattr (o , "__class_json__" ):
47+ return o .__class_json__ ()
48+
49+ # Check if the object has a __json__ method (for instances)
50+ if hasattr (o , "__json__" ) and callable (getattr (o , "__json__" )):
51+ return o .__json__ ()
52+
53+ # Handle class/type objects (like response handler classes)
54+ if isinstance (o , type ):
55+ # Try to find the registered name for this handler class
56+ from guidellm .backends .response_handlers import (
57+ GenerationResponseHandlerFactory ,
58+ )
59+
60+ registry = GenerationResponseHandlerFactory .registry or {}
61+ class_to_name = {v : k for k , v in registry .items ()}
62+
63+ handler_name = class_to_name .get (o )
64+ if handler_name :
65+ return handler_name
66+ else :
67+ # Fallback: use the full class name
68+ return f"{ o .__module__ } .{ o .__name__ } "
69+
70+ # Let the base class handle other types or raise TypeError
71+ return super ().default (o )
72+
73+
74+ @GenerativeBenchmarkerOutput .register ("dual_json" )
75+ class GenerativeBenchmarkerDualJson (GenerativeBenchmarkerOutput ):
76+ """
77+ Output handler for serializing benchmark reports to both summary and full JSON files.
78+
79+ This class saves two JSON files:
80+ 1. Summary JSON - Excludes large fields (requests, detailed metrics) for quick overview
81+ 2. Full JSON - Contains complete benchmark data including all requests and metrics
82+
83+ If a directory is provided, default filenames are used. If a file path is provided,
84+ the summary uses that path and the full version adds a suffix.
3185
3286 Example:
33- output = GenerativeBenchmarkerSummaryJson(output_path="/path/to/output.json")
34- result_path = await output.finalize(report)
87+ # Using directory
88+ output = GenerativeBenchmarkerDualJson(output_path="/path/to/dir")
89+ # Creates: /path/to/dir/benchmarks.json (summary)
90+ # /path/to/dir/benchmarks.full.json (full)
91+
92+ # Using file path
93+ output = GenerativeBenchmarkerDualJson(output_path="/path/to/results.json")
94+ # Creates: /path/to/results.json (summary)
95+ # /path/to/results.full.json (full)
3596 """
3697
3798 DEFAULT_FILE : ClassVar [str ] = "benchmarks.json"
@@ -79,7 +140,7 @@ def validated_kwargs(
79140 output_path = (
80141 output_path if isinstance (output_path , Path ) else Path (output_path )
81142 )
82- if output_path .suffix .lower () == ".summary_json " :
143+ if output_path .suffix .lower () == ".dual_json " :
83144 output_path = output_path .with_suffix (".json" )
84145 validated ["output_path" ] = output_path
85146
@@ -91,29 +152,45 @@ def validated_kwargs(
91152
92153 async def finalize (self , report : GenerativeBenchmarksReport ) -> Path :
93154 """
94- Serialize and save the benchmark report to the configured output path in JSON format .
155+ Serialize and save the benchmark report to both summary and full JSON files .
95156
96157 Args:
97158 report: The generative benchmarks report to serialize.
98159 Returns:
99- Path to the saved report file.
160+ Path to the saved summary report file.
100161 """
101- output_path = self .output_path
102- if output_path .is_dir ():
103- output_path = output_path / self .DEFAULT_FILE
162+ # Determine output paths
163+ summary_path = self .output_path
164+ if summary_path .is_dir ():
165+ summary_path = summary_path / self .DEFAULT_FILE
166+
167+ # Create full path by inserting ".full" before the extension
168+ full_path = (
169+ summary_path .parent / f"{ summary_path .stem } .full{ summary_path .suffix } "
170+ )
104171
105- output_path .parent .mkdir (parents = True , exist_ok = True )
172+ # Ensure parent directory exists
173+ summary_path .parent .mkdir (parents = True , exist_ok = True )
106174
107- # Exclude specified fields from the report, but keep a small error sample
175+ # Prepare data
108176 full_dict = report .model_dump ()
109177 summary_dict = report .model_dump (exclude = self .EXCLUDE_FIELDS )
110178 self ._attach_error_samples (summary_dict , full_dict )
111- save_str = json .dumps (summary_dict , indent = 4 )
112179
113- with output_path .open ("w" , encoding = "utf-8" ) as file :
114- file .write (save_str )
180+ # Use custom encoder to handle response handler classes
181+ encoder_cls = AutoMarshalJSONEncoder
182+
183+ # Save summary JSON
184+ summary_str = json .dumps (summary_dict , indent = 4 , cls = encoder_cls )
185+ with summary_path .open ("w" , encoding = "utf-8" ) as file :
186+ file .write (summary_str )
187+
188+ # Save full JSON
189+ full_str = json .dumps (full_dict , indent = 4 , cls = encoder_cls )
190+ with full_path .open ("w" , encoding = "utf-8" ) as file :
191+ file .write (full_str )
115192
116- return output_path
193+ return summary_path
117194
118195 def _attach_error_samples (
119196 self , summary_dict : dict [str , Any ], full_dict : dict [str , Any ]
0 commit comments