33import static dev .braintrust .json .BraintrustJsonMapper .fromJson ;
44import static dev .braintrust .json .BraintrustJsonMapper .toJson ;
55
6+ import com .fasterxml .jackson .databind .node .NullNode ;
67import com .sun .net .httpserver .HttpExchange ;
78import com .sun .net .httpserver .HttpHandler ;
89import com .sun .net .httpserver .HttpServer ;
@@ -180,32 +181,36 @@ private void handleList(HttpExchange exchange) throws IOException {
180181
181182 Map <String , Object > metadata = new LinkedHashMap <>();
182183
183- Map <String , Map <String , Object >> parametersMap = new LinkedHashMap <>();
184- for (Map .Entry <String , RemoteEval .Parameter > paramEntry :
185- eval .getParameters ().entrySet ()) {
186- String paramName = paramEntry .getKey ();
187- RemoteEval .Parameter param = paramEntry .getValue ();
184+ // Serialize parameters in the container format
185+ if (eval .getParameters ().isEmpty ()) {
186+ metadata .put ("parameters" , NullNode .getInstance ());
187+ } else {
188+ Map <String , Map <String , Object >> schemaMap = new LinkedHashMap <>();
189+ for (ParameterDef <?> param : eval .getParameters ()) {
190+ Map <String , Object > paramMetadata = new LinkedHashMap <>();
191+ paramMetadata .put ("type" , param .type ().toString ().toLowerCase ());
188192
189- Map <String , Object > paramMetadata = new LinkedHashMap <>();
190- paramMetadata .put ("type" , param .getType ().getValue ());
193+ if (param .schema () != null ) {
194+ paramMetadata .put ("schema" , param .schema ());
195+ }
191196
192- if (param .getDescription () != null ) {
193- paramMetadata .put ("description " , param .getDescription ());
194- }
197+ if (param .defaultValue () != null ) {
198+ paramMetadata .put ("default " , param .defaultValue ());
199+ }
195200
196- if (param .getDefaultValue () != null ) {
197- paramMetadata .put ("default " , param .getDefaultValue ());
198- }
201+ if (param .description () != null ) {
202+ paramMetadata .put ("description " , param .description ());
203+ }
199204
200- // Only include schema for data type parameters
201- if (param .getType () == RemoteEval .ParameterType .DATA
202- && param .getSchema () != null ) {
203- paramMetadata .put ("schema" , param .getSchema ());
205+ schemaMap .put (param .name (), paramMetadata );
204206 }
205207
206- parametersMap .put (paramName , paramMetadata );
208+ Map <String , Object > parametersContainer = new LinkedHashMap <>();
209+ parametersContainer .put ("type" , "braintrust.staticParameters" );
210+ parametersContainer .put ("schema" , schemaMap );
211+ parametersContainer .put ("source" , NullNode .getInstance ());
212+ metadata .put ("parameters" , parametersContainer );
207213 }
208- metadata .put ("parameters" , parametersMap );
209214
210215 // Add scores (list of scorer names)
211216 List <Map <String , String >> scores = new ArrayList <>();
@@ -245,7 +250,14 @@ private void handleEval(HttpExchange exchange) throws IOException {
245250 try {
246251 InputStream requestBody = exchange .getRequestBody ();
247252 var requestBodyString = new String (requestBody .readAllBytes (), StandardCharsets .UTF_8 );
248- EvalRequest request = fromJson (requestBodyString , EvalRequest .class );
253+ EvalRequest request ;
254+ try {
255+ request = fromJson (requestBodyString , EvalRequest .class );
256+ } catch (Exception e ) {
257+ sendResponse (
258+ exchange , 400 , "text/plain" , "Invalid request body: " + e .getMessage ());
259+ return ;
260+ }
249261
250262 // Validate evaluator exists
251263 RemoteEval eval = evals .get (request .getName ());
@@ -376,6 +388,9 @@ private <I, O> void handleStreamingEval(
376388
377389 var tracer = BraintrustTracing .getTracer ();
378390
391+ // Merge parameters: evaluator defaults + request overrides
392+ final Parameters mergedParameters = mergeParameters (eval , request );
393+
379394 // Execute task and scorers for each case
380395 final Map <String , List <Double >> scoresByName = new ConcurrentHashMap <>();
381396 final var parentInfo = extractParentInfo (request );
@@ -414,7 +429,9 @@ private <I, O> void handleStreamingEval(
414429 .makeCurrent ()) {
415430 var task = eval .getTask ();
416431 try {
417- taskResult = task .apply (datasetCase );
432+ taskResult =
433+ task .apply (
434+ datasetCase , mergedParameters );
418435 } catch (Exception e ) {
419436 taskSpan .setStatus (
420437 StatusCode .ERROR , e .getMessage ());
@@ -431,6 +448,21 @@ private <I, O> void handleStreamingEval(
431448 "Task threw exception for input: "
432449 + datasetCase .input (),
433450 e );
451+ // Set eval span attributes so Braintrust can
452+ // resolve the trace
453+ setEvalSpanAttributesForError (
454+ evalSpan ,
455+ braintrustParent ,
456+ braintrustGeneration ,
457+ datasetCase );
458+ // Send progress event even on error so the
459+ // Playground can link to the trace
460+ sendProgressEvent (
461+ os ,
462+ evalSpan .getSpanContext ().getSpanId (),
463+ datasetCase .origin (),
464+ eval .getName (),
465+ null );
434466 // run scoreForTaskException on each scorer
435467 List <Scorer <I , O >> allScorersForError =
436468 new ArrayList <>(eval .getScorers ());
@@ -578,6 +610,38 @@ private void setEvalSpanAttributes(
578610 "braintrust.output_json" , toJson (Map .of ("output" , taskResult .result ())));
579611 }
580612
613+ /**
614+ * Sets eval span attributes when the task threw an exception. Similar to {@link
615+ * #setEvalSpanAttributes} but does not require a TaskResult.
616+ */
617+ private void setEvalSpanAttributesForError (
618+ Span evalSpan ,
619+ BraintrustUtils .Parent braintrustParent ,
620+ String braintrustGeneration ,
621+ DatasetCase <?, ?> datasetCase ) {
622+ var spanAttrs = new LinkedHashMap <>();
623+ spanAttrs .put ("type" , "eval" );
624+ spanAttrs .put ("name" , "eval" );
625+ if (braintrustGeneration != null ) {
626+ spanAttrs .put ("generation" , braintrustGeneration );
627+ }
628+ evalSpan .setAttribute (PARENT , braintrustParent .toParentValue ())
629+ .setAttribute ("braintrust.span_attributes" , toJson (spanAttrs ))
630+ .setAttribute ("braintrust.input_json" , toJson (Map .of ("input" , datasetCase .input ())))
631+ .setAttribute ("braintrust.expected_json" , toJson (datasetCase .expected ()));
632+
633+ if (datasetCase .origin ().isPresent ()) {
634+ evalSpan .setAttribute ("braintrust.origin" , toJson (datasetCase .origin ().get ()));
635+ }
636+ if (!datasetCase .tags ().isEmpty ()) {
637+ evalSpan .setAttribute (
638+ AttributeKey .stringArrayKey ("braintrust.tags" ), datasetCase .tags ());
639+ }
640+ if (!datasetCase .metadata ().isEmpty ()) {
641+ evalSpan .setAttribute ("braintrust.metadata" , toJson (datasetCase .metadata ()));
642+ }
643+ }
644+
581645 private void setTaskSpanAttributes (
582646 Span taskSpan ,
583647 BraintrustUtils .Parent braintrustParent ,
@@ -1083,6 +1147,33 @@ private static ParentInfo extractParentInfo(EvalRequest request) {
10831147 return new ParentInfo (BraintrustUtils .parseParent (parentSpec ), generation );
10841148 }
10851149
1150+ /**
1151+ * Merges evaluator default parameter values with request overrides.
1152+ *
1153+ * <p>Request values take precedence over evaluator defaults. Unknown parameters from the
1154+ * request are passed through.
1155+ */
1156+ @ SuppressWarnings ("rawtypes" )
1157+ private static Parameters mergeParameters (RemoteEval eval , EvalRequest request ) {
1158+ List <ParameterDef <?>> definitions = eval .getParameters ();
1159+
1160+ // Collect defaults from evaluator definitions
1161+ Map <String , Object > merged = new LinkedHashMap <>();
1162+ for (ParameterDef <?> def : definitions ) {
1163+ if (def .defaultValue () != null ) {
1164+ merged .put (def .name (), def .defaultValue ());
1165+ }
1166+ }
1167+
1168+ // Overlay request parameters (request wins)
1169+ Map <String , Object > requestParams = request .getParameters ();
1170+ if (requestParams != null ) {
1171+ merged .putAll (requestParams );
1172+ }
1173+
1174+ return new Parameters (merged , definitions );
1175+ }
1176+
10861177 /**
10871178 * Extracts and loads the dataset from the eval request.
10881179 *
0 commit comments