@@ -132,8 +132,129 @@ async def run_certamen_tournament(
132132 }
133133
134134
135+ _SECTION_DIVIDER = "---\n \n "
136+
137+
138+ async def _run_single_models (
139+ certamen : Certamen , question : str
140+ ) -> list [dict [str , Any ]]:
141+ results = []
142+ for model_name in certamen .healthy_models :
143+ print ("\n " + "=" * 80 )
144+ print (f"APPROACH 1: Single Model ({ model_name } ) + Chain-of-Thought" )
145+ print ("=" * 80 + "\n " )
146+ try :
147+ result = await run_single_model_with_cot (
148+ question , model_name , certamen
149+ )
150+ results .append (result )
151+ print ("\n ✅ Complete!" )
152+ print (f" Duration: { result ['duration_seconds' ]:.1f} s" )
153+ print (f" Cost: ${ result ['cost_estimate' ]:.4f} " )
154+ if not result ["cost_estimate" ]:
155+ print (" (Local/free model - no API costs)" )
156+ except Exception as e :
157+ logger .error (f"Failed to run { model_name } : { e } " )
158+ print (f"\n ❌ Failed: { e } " )
159+ print (" Continuing with other models..." )
160+ return results
161+
162+
163+ def _print_cost_summary (
164+ single_model_results : list [dict [str , Any ]],
165+ tournament_result : dict [str , Any ],
166+ ) -> None :
167+ if not single_model_results :
168+ return
169+ print ("\n 📊 Cost Summary:" )
170+ print ("-" * 80 )
171+ for i , result in enumerate (single_model_results ):
172+ print (
173+ f" Single Model #{ i + 1 } ({ result ['model' ]} ): ${ result ['cost_estimate' ]:.4f} "
174+ )
175+ print (f" Certamen Tournament: ${ tournament_result ['cost_actual' ]:.4f} " )
176+ avg_single_cost = sum (
177+ r ["cost_estimate" ] for r in single_model_results
178+ ) / len (single_model_results )
179+ if avg_single_cost > 0 :
180+ print (
181+ f"\n Tournament vs Avg Single Model: { tournament_result ['cost_actual' ] / avg_single_cost :.2f} x cost"
182+ )
183+ print ("-" * 80 )
184+
185+
186+ def _write_micro_report (
187+ output_path : Path ,
188+ question : str ,
189+ config_path : str ,
190+ single_model_results : list [dict [str , Any ]],
191+ tournament_result : dict [str , Any ],
192+ ) -> None :
193+ with open (output_path , "w" ) as f :
194+ f .write (
195+ "# Micro-Benchmark Results: Single Model vs Certamen Framework\n \n "
196+ )
197+ f .write (f"Date: { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )} \n \n " )
198+ f .write (f"Config: `{ config_path } `\n \n " )
199+ f .write ("## Test Question\n \n " )
200+ f .write (f"{ question } \n \n " )
201+ f .write (_SECTION_DIVIDER )
202+
203+ for i , single_result in enumerate (single_model_results ):
204+ f .write (f"## Approach 1.{ i + 1 } : Single Model\n \n " )
205+ f .write (f"Model: { single_result ['model' ]} \n \n " )
206+ f .write (
207+ f"Duration: { single_result ['duration_seconds' ]:.1f} seconds\n \n "
208+ )
209+ f .write (f"Cost: ${ single_result ['cost_estimate' ]:.4f} \n \n " )
210+ f .write ("### Response\n \n " )
211+ f .write (f"{ single_result ['response' ]} \n \n " )
212+
213+ f .write (_SECTION_DIVIDER )
214+ f .write ("## Approach 2: Certamen Framework Tournament\n \n " )
215+ f .write (f"Champion: { tournament_result ['champion_model' ]} \n \n " )
216+ f .write (
217+ f"Duration: { tournament_result ['duration_seconds' ]:.1f} seconds "
218+ f"({ tournament_result ['duration_seconds' ] / 60 :.1f} minutes)\n \n "
219+ )
220+ f .write (f"Total Cost: ${ tournament_result ['cost_actual' ]:.4f} \n \n " )
221+
222+ if tournament_result .get ("cost_by_model" ):
223+ f .write ("### Cost Breakdown by Model\n \n " )
224+ for model_key , cost in tournament_result ["cost_by_model" ].items ():
225+ f .write (f"- { model_key } : ${ cost :.4f} \n " )
226+ f .write ("\n " )
227+
228+ if single_model_results :
229+ cost_estimate = single_model_results [0 ]["cost_estimate" ]
230+ cost_actual = tournament_result ["cost_actual" ]
231+ cost_multiple = (
232+ f"{ cost_actual / cost_estimate :.1f} x"
233+ if cost_estimate > 0
234+ else "N/A (free/local baseline)"
235+ )
236+ f .write (f"Cost Multiple vs Single Model: { cost_multiple } \n \n " )
237+ f .write (
238+ f"Time Multiple vs Single Model: "
239+ f"{ tournament_result ['duration_seconds' ] / single_model_results [0 ]['duration_seconds' ]:.1f} x\n \n "
240+ )
241+
242+ if tournament_result ["eliminated_models" ]:
243+ f .write (
244+ f"Eliminated Models: { ', ' .join (str (m ) for m in tournament_result ['eliminated_models' ])} \n \n "
245+ )
246+
247+ f .write ("### Champion Response\n \n " )
248+ f .write (f"{ tournament_result ['response' ]} \n \n " )
249+ f .write (_SECTION_DIVIDER )
250+
251+ model_names = [res ["model" ] for res in single_model_results ] + [
252+ "Certamen Framework"
253+ ]
254+ f .write (generate_manual_evaluation_template (model_names ))
255+
256+
135257async def main (args : dict [str , Any ] | None = None ) -> None :
136- """Run micro-benchmark."""
137258 if args is None :
138259 parser = argparse .ArgumentParser (
139260 description = "Micro-Benchmark: Single Model vs Certamen Framework"
@@ -145,7 +266,7 @@ async def main(args: dict[str, Any] | None = None) -> None:
145266 help = "Path to configuration file (REQUIRED - no defaults)" ,
146267 )
147268 parsed_args = parser .parse_args ()
148- args = vars (parsed_args ) # Convert to dict
269+ args = vars (parsed_args )
149270
150271 config_path = args .get ("config" )
151272 if not config_path :
@@ -155,21 +276,16 @@ async def main(args: dict[str, Any] | None = None) -> None:
155276 "The framework does not use fallback configurations."
156277 )
157278
158- # Load config and inject outputs_dir
159279 from certamen .config .loader import Config
160280
161281 config_obj = Config (config_path )
162282 if not config_obj .load ():
163283 raise RuntimeError (f"Failed to load configuration from { config_path } " )
164284
165- # Benchmark explicitly sets outputs_dir to current directory
166285 config_obj .config_data ["outputs_dir" ] = "."
167-
168- # Initialize Certamen (loads config, secrets, models, runs health check)
169286 logger .info ("Initializing Certamen..." )
170287 certamen = await Certamen .from_settings (settings = config_obj .config_data )
171288
172- # Get question from config
173289 question = certamen .config .config_data .get ("question" )
174290 if not question :
175291 raise ValueError (
@@ -190,7 +306,6 @@ async def main(args: dict[str, Any] | None = None) -> None:
190306 print (f"\n Test Question:\n { question } " )
191307 print ()
192308
193- # Check if we have any healthy models
194309 if not certamen .is_ready :
195310 print ("\n ❌ ERROR: No healthy models available" )
196311 if certamen .failed_models :
@@ -205,31 +320,8 @@ async def main(args: dict[str, Any] | None = None) -> None:
205320 for model_key , error in certamen .failed_models .items ():
206321 print (f" - { model_key } : { error } " )
207322
208- single_model_results = []
323+ single_model_results = await _run_single_models ( certamen , question )
209324
210- # Run single models
211- for model_name in certamen .healthy_models :
212- print ("\n " + "=" * 80 )
213- print (f"APPROACH 1: Single Model ({ model_name } ) + Chain-of-Thought" )
214- print ("=" * 80 + "\n " )
215-
216- try :
217- single_result = await run_single_model_with_cot (
218- question , model_name , certamen
219- )
220- single_model_results .append (single_result )
221-
222- print ("\n ✅ Complete!" )
223- print (f" Duration: { single_result ['duration_seconds' ]:.1f} s" )
224- print (f" Cost: ${ single_result ['cost_estimate' ]:.4f} " )
225- if single_result ["cost_estimate" ] == 0.0 :
226- print (" (Local/free model - no API costs)" )
227- except Exception as e :
228- logger .error (f"Failed to run { model_name } : { e } " )
229- print (f"\n ❌ Failed: { e } " )
230- print (" Continuing with other models..." )
231-
232- # Run tournament
233325 print ("\n " + "=" * 80 )
234326 print ("APPROACH 2: Certamen Framework Tournament" )
235327 print ("=" * 80 + "\n " )
@@ -238,114 +330,34 @@ async def main(args: dict[str, Any] | None = None) -> None:
238330
239331 print ("\n ✅ Complete!" )
240332 print (
241- f" Duration: { tournament_result ['duration_seconds' ]:.1f} s ({ tournament_result ['duration_seconds' ] / 60 :.1f} min)"
333+ f" Duration: { tournament_result ['duration_seconds' ]:.1f} s "
334+ f"({ tournament_result ['duration_seconds' ] / 60 :.1f} min)"
242335 )
243336 print (f" Total Cost: ${ tournament_result ['cost_actual' ]:.4f} " )
244337
245- # Show cost breakdown
246338 if tournament_result .get ("cost_by_model" ):
247339 print ("\n Cost Breakdown:" )
248340 for model_key , cost in tournament_result ["cost_by_model" ].items ():
249341 print (f" - { model_key } : ${ cost :.4f} " )
250342
251- # Save results
252343 print ("\n " + "=" * 80 )
253344 print ("BENCHMARK RESULTS" )
254345 print ("=" * 80 )
255346
256- # Print cost summary
257- if single_model_results :
258- print ("\n 📊 Cost Summary:" )
259- print ("-" * 80 )
260- for i , result in enumerate (single_model_results ):
261- print (
262- f" Single Model #{ i + 1 } ({ result ['model' ]} ): ${ result ['cost_estimate' ]:.4f} "
263- )
264- print (
265- f" Certamen Tournament: ${ tournament_result ['cost_actual' ]:.4f} "
266- )
347+ _print_cost_summary (single_model_results , tournament_result )
267348
268- # Show comparison
269- avg_single_cost = sum (
270- r ["cost_estimate" ] for r in single_model_results
271- ) / len (single_model_results )
272- if avg_single_cost > 0 :
273- print (
274- f"\n Tournament vs Avg Single Model: { tournament_result ['cost_actual' ] / avg_single_cost :.2f} x cost"
275- )
276- print ("-" * 80 )
277-
278- # Save reports relative to project root
279349 reports_dir = Path (__file__ ).parent / "reports"
280350 reports_dir .mkdir (parents = True , exist_ok = True )
281351 output_path = reports_dir / "micro_benchmark_results.md"
282352
283- with open (output_path , "w" ) as f :
284- f .write (
285- "# Micro-Benchmark Results: Single Model vs Certamen Framework\n \n "
286- )
287- f .write (f"Date: { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )} \n \n " )
288- f .write (f"Config: `{ config_path } `\n \n " )
289-
290- f .write ("## Test Question\n \n " )
291- f .write (f"{ question } \n \n " )
292-
293- f .write ("---\n \n " )
294-
295- for i , single_result in enumerate (single_model_results ):
296- f .write (f"## Approach 1.{ i + 1 } : Single Model\n \n " )
297- f .write (f"Model: { single_result ['model' ]} \n \n " )
298- f .write (
299- f"Duration: { single_result ['duration_seconds' ]:.1f} seconds\n \n "
300- )
301- f .write (f"Cost: ${ single_result ['cost_estimate' ]:.4f} \n \n " )
302- f .write ("### Response\n \n " )
303- f .write (f"{ single_result ['response' ]} \n \n " )
304-
305- f .write ("---\n \n " )
306-
307- f .write ("## Approach 2: Certamen Framework Tournament\n \n " )
308- f .write (f"Champion: { tournament_result ['champion_model' ]} \n \n " )
309- f .write (
310- f"Duration: { tournament_result ['duration_seconds' ]:.1f} seconds ({ tournament_result ['duration_seconds' ] / 60 :.1f} minutes)\n \n "
311- )
312- f .write (f"Total Cost: ${ tournament_result ['cost_actual' ]:.4f} \n \n " )
313-
314- # Cost breakdown by model
315- if tournament_result .get ("cost_by_model" ):
316- f .write ("### Cost Breakdown by Model\n \n " )
317- for model_key , cost in tournament_result ["cost_by_model" ].items ():
318- f .write (f"- { model_key } : ${ cost :.4f} \n " )
319- f .write ("\n " )
320-
321- # Cost and time comparisons
322- if single_model_results :
323- cost_estimate = single_model_results [0 ]["cost_estimate" ]
324- cost_actual = tournament_result ["cost_actual" ]
325- if cost_estimate > 0 :
326- cost_multiple = f"{ cost_actual / cost_estimate :.1f} x"
327- else :
328- cost_multiple = "N/A (free/local baseline)"
329- f .write (f"Cost Multiple vs Single Model: { cost_multiple } \n \n " )
330-
331- f .write (
332- f"Time Multiple vs Single Model: { tournament_result ['duration_seconds' ] / single_model_results [0 ]['duration_seconds' ]:.1f} x\n \n "
333- )
334-
335- if tournament_result ["eliminated_models" ]:
336- f .write (
337- f"Eliminated Models: { ', ' .join (str (m ) for m in tournament_result ['eliminated_models' ])} \n \n "
338- )
339-
340- f .write ("### Champion Response\n \n " )
341- f .write (f"{ tournament_result ['response' ]} \n \n " )
342-
343- f .write ("---\n \n " )
344-
345- model_names = [res ["model" ] for res in single_model_results ] + [
346- "Certamen Framework"
347- ]
348- f .write (generate_manual_evaluation_template (model_names ))
353+ await asyncio .to_thread (
354+ _write_micro_report ,
355+ output_path ,
356+ question ,
357+ config_path ,
358+ single_model_results ,
359+ tournament_result ,
360+ )
349361
350362 print (f"\n 📄 Results saved to: { output_path } " )
351363 print ()
0 commit comments