@vitest-evals/github-reporter 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -93,7 +93,8 @@ function collectEvalReport(input, options = {}) {
93
93
  const cases = workspace.cases.map(collectEvalCase);
94
94
  const failures = cases.filter((testCase) => testCase.status === "failed");
95
95
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
96
- const usage = sumUsage(cases);
96
+ const usage = sumAppUsage(cases);
97
+ const judgeUsage = sumJudgeUsage(cases);
97
98
  const durationMs = workspace.runs[0]?.durationMs;
98
99
  return {
99
100
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -113,6 +114,7 @@ function collectEvalReport(input, options = {}) {
113
114
  minimum: Math.min(...evalScores)
114
115
  } : void 0,
115
116
  usage,
117
+ judgeUsage,
116
118
  cases,
117
119
  failures
118
120
  };
@@ -237,24 +239,67 @@ function stringifyReason(value) {
237
239
  }
238
240
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
239
241
  }
240
- function sumUsage(cases) {
241
- const usage = {
242
+ function emptyUsage() {
243
+ return {
242
244
  inputTokens: 0,
243
245
  outputTokens: 0,
244
246
  reasoningTokens: 0,
245
247
  totalTokens: 0,
246
248
  toolCalls: 0
247
249
  };
250
+ }
251
+ function addRunUsage(total, usage) {
252
+ total.inputTokens += usage?.inputTokens ?? 0;
253
+ total.outputTokens += usage?.outputTokens ?? 0;
254
+ total.reasoningTokens += usage?.reasoningTokens ?? 0;
255
+ total.totalTokens += usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0);
256
+ if (usage?.costUsd !== void 0) {
257
+ total.costUsd = (total.costUsd ?? 0) + usage.costUsd;
258
+ }
259
+ total.toolCalls += usage?.toolCalls ?? 0;
260
+ }
261
+ function sumAppUsage(cases) {
262
+ const usage = emptyUsage();
263
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
264
+ for (const runUsage of runUsages) {
265
+ addRunUsage(usage, runUsage);
266
+ }
267
+ omitPartialCost(usage, runUsages);
268
+ return usage;
269
+ }
270
+ function appUsageForCase(testCase) {
271
+ const usage = testCase.harness?.usage;
272
+ const effectiveToolCalls = toolCallCount(testCase);
273
+ if (!usage && effectiveToolCalls === 0) {
274
+ return void 0;
275
+ }
276
+ return {
277
+ ...usage,
278
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
279
+ };
280
+ }
281
+ function sumJudgeUsage(cases) {
282
+ const usage = emptyUsage();
283
+ const runUsages = [];
248
284
  for (const testCase of cases) {
249
- const caseUsage = testCase.harness?.usage;
250
- usage.inputTokens += caseUsage?.inputTokens ?? 0;
251
- usage.outputTokens += caseUsage?.outputTokens ?? 0;
252
- usage.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
253
- usage.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
254
- usage.toolCalls += toolCallCount(testCase);
285
+ for (const score of testCase.eval?.scores ?? []) {
286
+ for (const run of score.judgeRuns ?? []) {
287
+ runUsages.push(run.usage);
288
+ addRunUsage(usage, run.usage);
289
+ }
290
+ }
255
291
  }
292
+ omitPartialCost(usage, runUsages);
256
293
  return usage;
257
294
  }
295
+ function omitPartialCost(total, usages) {
296
+ if (usages.some(hasUsageWithoutCost)) {
297
+ total.costUsd = void 0;
298
+ }
299
+ }
300
+ function hasUsageWithoutCost(usage) {
301
+ return usage?.costUsd === void 0 && ((usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0)) > 0 || (usage?.toolCalls ?? 0) > 0);
302
+ }
258
303
  function toolCallCount(testCase) {
259
304
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
260
305
  if (usageToolCalls !== void 0) {
@@ -398,8 +443,8 @@ function evaluateEvalGate(report, policy = {}) {
398
443
  status: "failed",
399
444
  enforced: true,
400
445
  passRate,
401
- title: "Eval report hard failure",
402
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
446
+ title: "Eval run failed",
447
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
403
448
  };
404
449
  }
405
450
  if (report.totals.evalTotal === 0) {
@@ -408,7 +453,7 @@ function evaluateEvalGate(report, policy = {}) {
408
453
  status: "failed",
409
454
  enforced: true,
410
455
  passRate: null,
411
- title: "Eval report hard failure",
456
+ title: "Eval run failed",
412
457
  message: "no eval cases were reported"
413
458
  };
414
459
  }
@@ -418,8 +463,8 @@ function evaluateEvalGate(report, policy = {}) {
418
463
  status: "failed",
419
464
  enforced: true,
420
465
  passRate,
421
- title: "Eval report hard failure",
422
- message: `vitest run failed without counted test failures; ${counts}`
466
+ title: "Eval run failed",
467
+ message: `Vitest failed without reporting a failed test; ${counts}`
423
468
  };
424
469
  }
425
470
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -429,7 +474,7 @@ function evaluateEvalGate(report, policy = {}) {
429
474
  enforced: true,
430
475
  passRate,
431
476
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
432
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
477
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
433
478
  };
434
479
  }
435
480
  if (minScoreAverage !== void 0) {
@@ -440,8 +485,8 @@ function evaluateEvalGate(report, policy = {}) {
440
485
  status: "failed",
441
486
  enforced: true,
442
487
  passRate,
443
- title: "Eval score gate failed",
444
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
488
+ title: "Average score unavailable",
489
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
445
490
  };
446
491
  }
447
492
  if (average + Number.EPSILON < minScoreAverage) {
@@ -450,8 +495,8 @@ function evaluateEvalGate(report, policy = {}) {
450
495
  status: "failed",
451
496
  enforced: true,
452
497
  passRate,
453
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
454
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
498
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
499
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
455
500
  };
456
501
  }
457
502
  }
@@ -461,7 +506,7 @@ function evaluateEvalGate(report, policy = {}) {
461
506
  enforced: true,
462
507
  passRate,
463
508
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
464
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
509
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
465
510
  };
466
511
  }
467
512
  function computePassRate(report) {
@@ -494,7 +539,7 @@ function resolveMinPassRate(policy) {
494
539
  }
495
540
  function defaultCheckTitle(report) {
496
541
  if (report.failures.length === 0 && report.status === "passed") {
497
- return "No eval failures";
542
+ return "No eval cases failed";
498
543
  }
499
544
  if (report.failures.length === 0) {
500
545
  return "Vitest run failed";
@@ -503,10 +548,10 @@ function defaultCheckTitle(report) {
503
548
  }
504
549
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
505
550
  if (minPassRate !== void 0) {
506
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
551
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
507
552
  }
508
553
  if (minScoreAverage !== void 0) {
509
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
554
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
510
555
  }
511
556
  return defaultCheckTitle(report);
512
557
  }
@@ -515,19 +560,22 @@ function formatEvalCounts(report, passRate) {
515
560
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
516
561
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
517
562
  report.totals.evalTotal
518
- )} passed (${passRateText}), avg score ${scoreText}`;
563
+ )} passed (${passRateText}), average score ${scoreText}`;
519
564
  }
520
- function formatFloorSuffix(minPassRate, minScoreAverage) {
565
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
521
566
  const parts = [];
522
567
  if (minPassRate !== void 0) {
523
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
568
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
524
569
  }
525
570
  if (minScoreAverage !== void 0) {
526
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
571
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
527
572
  }
528
573
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
529
574
  }
530
575
 
576
+ // src/github.ts
577
+ import { readFileSync } from "fs";
578
+
531
579
  // src/summary.ts
532
580
  var DEFAULT_MAX_FAILURES = 20;
533
581
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -555,23 +603,23 @@ function renderJobSummary(report, options = {}) {
555
603
  ""
556
604
  ];
557
605
  if (report.failures.length > 0) {
558
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
606
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
559
607
  lines.push(failureHeading, "");
560
608
  failures.forEach((testCase, index) => {
561
609
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
562
610
  });
563
611
  if (report.failures.length > failures.length) {
564
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
612
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
565
613
  lines.push(
566
614
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
567
615
  ""
568
616
  );
569
617
  }
570
618
  } else if (report.totals.evalTotal > 0) {
571
- lines.push("### Failures", "", "No eval failures.", "");
619
+ lines.push("### Failures", "", "No eval cases failed.", "");
572
620
  }
573
621
  if (report.totals.evalTotal === 0) {
574
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
622
+ lines.push("No eval results were found in the Vitest JSON report.", "");
575
623
  }
576
624
  return `${lines.join("\n")}
577
625
  `;
@@ -581,9 +629,9 @@ function formatCountLine(passed, failed, total) {
581
629
  }
582
630
  function renderSummaryTable(report, nonEvalFailures, gate) {
583
631
  const rows = [
584
- ["Status", gate?.status ?? report.status],
632
+ ["Status", capitalize(gate?.status ?? report.status)],
585
633
  [
586
- "Evals",
634
+ "Eval cases",
587
635
  formatCountLine(
588
636
  report.totals.evalPassed,
589
637
  report.totals.evalFailed,
@@ -592,23 +640,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
592
640
  ]
593
641
  ];
594
642
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
595
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
643
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
596
644
  } else if (report.totals.evalTotal > 0) {
597
645
  rows.push([
598
- "Pass Rate",
646
+ "Pass rate",
599
647
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
600
648
  ]);
601
649
  }
602
650
  if (report.score) {
603
651
  rows.push(["Score", formatScoreSummary(report.score)]);
604
652
  }
653
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
654
+ rows.push(["App usage", formatUsage(report.usage)]);
655
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
656
+ rows.push([
657
+ "Total usage",
658
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
659
+ ]);
660
+ }
605
661
  if (gate?.enforced) {
606
- rows.push(["Gate", gate.message]);
662
+ rows.push(["Requirements", gate.message]);
607
663
  }
608
664
  if (nonEvalFailures > 0) {
609
665
  rows.push([
610
- "Other Failures",
611
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
666
+ "Other test failures",
667
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
612
668
  ]);
613
669
  }
614
670
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -621,7 +677,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
621
677
  ];
622
678
  }
623
679
  function formatScoreSummary(score) {
624
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
680
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
681
+ }
682
+ function capitalize(value) {
683
+ return value.charAt(0).toUpperCase() + value.slice(1);
684
+ }
685
+ function hasUsage(usage) {
686
+ return usage.totalTokens > 0 || usage.costUsd !== void 0 || usage.toolCalls > 0;
687
+ }
688
+ function sumUsage(app, judge) {
689
+ const appCostKnown = !hasUsageWithoutCost2(app);
690
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
691
+ return {
692
+ inputTokens: app.inputTokens + judge.inputTokens,
693
+ outputTokens: app.outputTokens + judge.outputTokens,
694
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
695
+ totalTokens: app.totalTokens + judge.totalTokens,
696
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
697
+ toolCalls: app.toolCalls + judge.toolCalls
698
+ };
699
+ }
700
+ function hasUsageWithoutCost2(usage) {
701
+ return usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0);
702
+ }
703
+ function formatUsage(usage) {
704
+ const parts = [];
705
+ if (usage.totalTokens > 0) {
706
+ parts.push(`${formatNumber(usage.totalTokens)} tokens`);
707
+ }
708
+ if (usage.costUsd !== void 0) {
709
+ parts.push(
710
+ usage.costUsd.toLocaleString("en-US", {
711
+ style: "currency",
712
+ currency: "USD",
713
+ minimumFractionDigits: 2,
714
+ maximumFractionDigits: 6
715
+ })
716
+ );
717
+ }
718
+ if (hasUsageWithoutCost2(usage)) {
719
+ parts.push("cost unavailable");
720
+ }
721
+ if (usage.toolCalls > 0) {
722
+ parts.push(
723
+ `${formatNumber(usage.toolCalls)} tool call${usage.toolCalls === 1 ? "" : "s"}`
724
+ );
725
+ }
726
+ return parts.join(", ") || "none";
625
727
  }
626
728
  function escapeTableCell(value) {
627
729
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -701,7 +803,7 @@ function renderFailureBlock(testCase, {
701
803
  ["Case", `${number}. ${testCase.displayName}`],
702
804
  ["Status", testCase.status],
703
805
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
704
- ["Harness", testCase.harness?.name ?? "n/a"],
806
+ ["App runner", testCase.harness?.name ?? "n/a"],
705
807
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
706
808
  ["Judge", failure?.judgeName ?? "n/a"]
707
809
  ];
@@ -739,7 +841,7 @@ function renderFailureBlock(testCase, {
739
841
  if (finalOutput !== void 0) {
740
842
  lines.push(
741
843
  ...renderAsciiSection(
742
- "Final Output",
844
+ "Output",
743
845
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
744
846
  ),
745
847
  ""
@@ -767,7 +869,7 @@ function renderFailureBlock(testCase, {
767
869
  if (testCase.harness?.errors.length) {
768
870
  lines.push(
769
871
  ...renderAsciiSection(
770
- "Harness Errors",
872
+ "App errors",
771
873
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
772
874
  ),
773
875
  ""
@@ -816,7 +918,9 @@ function formatCaseUsage(testCase) {
816
918
  parts.push(`${formatNumber(totalTokens)} tokens`);
817
919
  }
818
920
  if (toolCalls > 0) {
819
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
921
+ parts.push(
922
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
923
+ );
820
924
  }
821
925
  if (testCase.harness?.timingMs !== void 0) {
822
926
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -828,10 +932,44 @@ function formatCaseUsage(testCase) {
828
932
  var DEFAULT_CHECK_NAME = "vitest-evals";
829
933
  var MAX_CHECK_SUMMARY_LENGTH = 64e3;
830
934
  var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
935
+ function resolveCheckSha(env = process.env, options = {}) {
936
+ const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
937
+ if (explicit) {
938
+ return explicit;
939
+ }
940
+ const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
941
+ if (eventPath) {
942
+ try {
943
+ const event = JSON.parse(readFileSync(eventPath, "utf8"));
944
+ const headSha = event.pull_request?.head?.sha;
945
+ if (typeof headSha === "string" && headSha.trim()) {
946
+ return headSha.trim();
947
+ }
948
+ } catch {
949
+ }
950
+ }
951
+ return env.GITHUB_SHA?.trim() || void 0;
952
+ }
953
+ function resolveCheckDetailsUrl(env = process.env, options = {}) {
954
+ const explicit = options.detailsUrl?.trim();
955
+ if (explicit) {
956
+ return explicit;
957
+ }
958
+ const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
959
+ const repository = env.GITHUB_REPOSITORY?.trim();
960
+ const runId = env.GITHUB_RUN_ID?.trim();
961
+ if (!server || !repository || !runId) {
962
+ return void 0;
963
+ }
964
+ return `${server}/${repository}/actions/runs/${runId}`;
965
+ }
831
966
  async function publishCheckRun(report, options = {}) {
832
967
  const token = options.token ?? process.env.GITHUB_TOKEN;
833
968
  const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
834
- const sha = options.sha ?? process.env.GITHUB_SHA;
969
+ const sha = resolveCheckSha(process.env, { sha: options.sha });
970
+ const detailsUrl = resolveCheckDetailsUrl(process.env, {
971
+ detailsUrl: options.detailsUrl
972
+ });
835
973
  if (!token) {
836
974
  return { status: "skipped", reason: "missing GITHUB_TOKEN" };
837
975
  }
@@ -839,7 +977,10 @@ async function publishCheckRun(report, options = {}) {
839
977
  return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
840
978
  }
841
979
  if (!sha && options.checkRunId === void 0) {
842
- return { status: "skipped", reason: "missing GITHUB_SHA" };
980
+ return {
981
+ status: "skipped",
982
+ reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
983
+ };
843
984
  }
844
985
  const [owner, repo] = repository.split("/");
845
986
  if (!owner || !repo) {
@@ -848,7 +989,7 @@ async function publishCheckRun(report, options = {}) {
848
989
  reason: `invalid GitHub repository: ${repository}`
849
990
  };
850
991
  }
851
- const payload = buildCheckRunPayload(report, options);
992
+ const payload = buildCheckRunPayload(report, options, detailsUrl);
852
993
  const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
853
994
  const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
854
995
  const response = await fetch(requestUrl, {
@@ -863,6 +1004,7 @@ async function publishCheckRun(report, options = {}) {
863
1004
  options.checkRunId === void 0 ? {
864
1005
  name: options.name ?? DEFAULT_CHECK_NAME,
865
1006
  head_sha: sha,
1007
+ ...options.externalId ? { external_id: options.externalId } : {},
866
1008
  ...payload
867
1009
  } : payload
868
1010
  )
@@ -877,10 +1019,11 @@ async function publishCheckRun(report, options = {}) {
877
1019
  return {
878
1020
  status: options.checkRunId === void 0 ? "created" : "updated",
879
1021
  id: data.id,
880
- htmlUrl: data.html_url
1022
+ htmlUrl: data.html_url,
1023
+ sha
881
1024
  };
882
1025
  }
883
- function buildCheckRunPayload(report, options) {
1026
+ function buildCheckRunPayload(report, options, detailsUrl) {
884
1027
  const gate = options.gate ?? evaluateEvalGate(report);
885
1028
  const annotations = buildCheckAnnotations(report, {
886
1029
  maxAnnotations: options.maxAnnotations,
@@ -890,6 +1033,7 @@ function buildCheckRunPayload(report, options) {
890
1033
  status: "completed",
891
1034
  conclusion: gate.ok ? "success" : "failure",
892
1035
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1036
+ ...detailsUrl ? { details_url: detailsUrl } : {},
893
1037
  output: {
894
1038
  title: gate.title,
895
1039
  summary: truncateCheckSummary(
@@ -949,16 +1093,22 @@ function mergeEvalReports(reports) {
949
1093
  minimum: Math.min(...scoredCases)
950
1094
  } : void 0,
951
1095
  usage: mergeUsage(reports.map((report) => report.usage)),
1096
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
952
1097
  cases,
953
1098
  failures
954
1099
  };
955
1100
  }
956
1101
  function mergeUsage(usages) {
1102
+ const costs = usages.map((usage) => usage.costUsd).filter((cost) => cost !== void 0);
1103
+ const costComplete = !usages.some(
1104
+ (usage) => usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0)
1105
+ );
957
1106
  return {
958
1107
  inputTokens: sum(usages, (usage) => usage.inputTokens),
959
1108
  outputTokens: sum(usages, (usage) => usage.outputTokens),
960
1109
  reasoningTokens: sum(usages, (usage) => usage.reasoningTokens),
961
1110
  totalTokens: sum(usages, (usage) => usage.totalTokens),
1111
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
962
1112
  toolCalls: sum(usages, (usage) => usage.toolCalls)
963
1113
  };
964
1114
  }
@@ -1050,11 +1200,17 @@ async function publishEvalReport(options) {
1050
1200
  name: options.checkName,
1051
1201
  repository: options.repository,
1052
1202
  sha: options.sha,
1203
+ detailsUrl: options.detailsUrl,
1204
+ externalId: options.externalId,
1053
1205
  token: options.token,
1054
1206
  gate
1055
1207
  });
1056
1208
  if (checkRun.status === "skipped") {
1057
1209
  options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
1210
+ } else if (checkRun.htmlUrl) {
1211
+ console.log(`published check run: ${checkRun.htmlUrl}`);
1212
+ } else if (checkRun.id !== void 0) {
1213
+ console.log(`published check run id: ${checkRun.id}`);
1058
1214
  }
1059
1215
  } catch (error) {
1060
1216
  const message = error instanceof Error ? error.message : String(error);
@@ -1064,14 +1220,21 @@ async function publishEvalReport(options) {
1064
1220
  options.warn?.(message);
1065
1221
  }
1066
1222
  }
1223
+ const gateFailed = gate.enforced && !gate.ok;
1224
+ const wantsSoftFail = options.softFail ?? options.checkRun === true;
1225
+ const softFail = wantsSoftFail && checkRunPublished(checkRun);
1226
+ const shouldFail = gateFailed && !softFail;
1067
1227
  return {
1068
1228
  report,
1069
1229
  resultFiles,
1070
1230
  gate,
1071
- shouldFail: gate.enforced && !gate.ok,
1231
+ shouldFail,
1072
1232
  checkRun
1073
1233
  };
1074
1234
  }
1235
+ function checkRunPublished(checkRun) {
1236
+ return checkRun?.status === "created" || checkRun?.status === "updated";
1237
+ }
1075
1238
  export {
1076
1239
  buildCheckAnnotations,
1077
1240
  collectEvalReport,
@@ -1082,6 +1245,8 @@ export {
1082
1245
  publishEvalReport,
1083
1246
  renderGateWorkflowCommand,
1084
1247
  renderJobSummary,
1085
- renderWorkflowCommands
1248
+ renderWorkflowCommands,
1249
+ resolveCheckDetailsUrl,
1250
+ resolveCheckSha
1086
1251
  };
1087
1252
  //# sourceMappingURL=index.mjs.map