Skip to content

Commit a1bedeb

Browse files
cquil11claude
andauthored
fix(api): support agentic-only runs in unofficial-run previews (#537)
Agentic-only sweeps never run the collect-results merge job (run-sweep.yml only triggers it for fixed-seq sweeps), so they upload per-config bmk_agentic_<config> artifacts with no merged results_bmk. The /api/unofficial-run route only looked for results_bmk and returned 404, breaking ?unofficialrun= chart overlays for agentic PRs. Fall back to downloading every per-config bmk_* artifact (deduped by name, newest id wins) when results_bmk is absent. The fallback stays off when the merged artifact exists since it already contains all per-config rows. Downloads run in bounded batches of 8 to stay clear of GitHub secondary rate limits. 中文:agentic 专属 sweep 不会触发 collect-results 合并任务(run-sweep.yml 仅为固定序列 sweep 触发),因此只上传按配置拆分的 bmk_agentic_<config> 产物,没有合并的 results_bmk。/api/unofficial-run 路由此前只查找 results_bmk 并返回 404,导致 agentic PR 的 ?unofficialrun= 图表叠加预览 失效。现在当 results_bmk 缺失时回退为逐个下载 bmk_* 产物(按名称去重, 保留最新 id),合并产物存在时不启用回退以避免重复计数;下载以每批 8 个 的有界批次执行,避免触发 GitHub 二级速率限制。 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 53b35da commit a1bedeb

2 files changed

Lines changed: 210 additions & 2 deletions

File tree

packages/app/src/app/api/unofficial-run/route.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ function rawRow(overrides: Record<string, unknown> = {}): Record<string, unknown
6565
};
6666
}
6767

68+
/** Minimal valid agentic row: scenario_type triggers the agentic path; `users` → conc. */
69+
function rawAgenticRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
70+
return {
71+
infmax_model_prefix: 'dsv4',
72+
hw: 'mi355x-amds',
73+
framework: 'vllm',
74+
precision: 'fp4',
75+
scenario_type: 'agentic-coding',
76+
users: 72,
77+
tput_per_gpu: 20000,
78+
...overrides,
79+
};
80+
}
81+
6882
function rawEvalRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
6983
return {
7084
hw: 'gb300-nv',
@@ -437,6 +451,158 @@ describe('GET /api/unofficial-run', () => {
437451
expect(body.evaluations).toEqual([]);
438452
});
439453

454+
it('falls back to per-config bmk_* artifacts when results_bmk is missing (agentic-only sweeps)', async () => {
455+
// Run metadata
456+
mockFetch.mockResolvedValueOnce({
457+
ok: true,
458+
json: () =>
459+
Promise.resolve({
460+
id: 777,
461+
name: 'agentic-run',
462+
head_branch: 'amd/agentx',
463+
head_sha: 'ccc',
464+
created_at: '2026-07-07T00:00:00Z',
465+
html_url: 'http://github.com/run/777',
466+
conclusion: 'success',
467+
status: 'completed',
468+
}),
469+
});
470+
// Artifacts: no results_bmk — only per-config agentic artifacts (plus the
471+
// big sibling `agentic_*` trace blobs the route must NOT download).
472+
mockFetch.mockResolvedValueOnce({
473+
ok: true,
474+
json: () =>
475+
Promise.resolve({
476+
artifacts: [
477+
{ name: 'bmk_agentic_dsv4_conc72', id: 30, archive_download_url: 'http://dl-a' },
478+
{ name: 'bmk_agentic_dsv4_conc56', id: 31, archive_download_url: 'http://dl-b' },
479+
{ name: 'agentic_dsv4_conc72', id: 32, archive_download_url: 'http://dl-blob' },
480+
{ name: 'server_logs_dsv4_conc72', id: 33, archive_download_url: 'http://dl-log' },
481+
],
482+
}),
483+
});
484+
// Two per-config downloads (Promise.all order matches artifact order)
485+
mockFetch.mockResolvedValueOnce({
486+
ok: true,
487+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
488+
});
489+
mockFetch.mockResolvedValueOnce({
490+
ok: true,
491+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
492+
});
493+
mockGetEntries
494+
.mockReturnValueOnce([
495+
{
496+
entryName: 'dsv4_conc72.json',
497+
getData: () => Buffer.from(JSON.stringify(rawAgenticRow())),
498+
},
499+
])
500+
.mockReturnValueOnce([
501+
{
502+
entryName: 'dsv4_conc56.json',
503+
getData: () => Buffer.from(JSON.stringify(rawAgenticRow({ users: 56 }))),
504+
},
505+
]);
506+
507+
const res = await GET(makeRequest('runId=777'));
508+
expect(res.status).toBe(200);
509+
const body = await res.json();
510+
expect(body.benchmarks).toHaveLength(2);
511+
expect(
512+
body.benchmarks
513+
.map((b: { conc: number }) => b.conc)
514+
.toSorted((a: number, b: number) => a - b),
515+
).toEqual([56, 72]);
516+
expect(body.benchmarks[0].benchmark_type).toBe('agentic_traces');
517+
expect(body.benchmarks[0].hardware).toBe('mi355x');
518+
expect(body.benchmarks[0].run_url).toBe('http://github.com/run/777');
519+
// Only the two bmk_* artifacts were downloaded: run + artifacts + 2 downloads.
520+
expect(mockFetch).toHaveBeenCalledTimes(4);
521+
});
522+
523+
it('does not download per-config bmk_* artifacts when results_bmk exists', async () => {
524+
mockFetch.mockResolvedValueOnce({
525+
ok: true,
526+
json: () =>
527+
Promise.resolve({
528+
id: 888,
529+
head_branch: 'main',
530+
html_url: 'http://github.com/run/888',
531+
created_at: '2026-07-07T00:00:00Z',
532+
}),
533+
});
534+
// Merged artifact alongside per-config ones (mixed fixed-seq + agentic
535+
// sweep) — the merged artifact already contains the per-config rows.
536+
mockFetch.mockResolvedValueOnce({
537+
ok: true,
538+
json: () =>
539+
Promise.resolve({
540+
artifacts: [
541+
{ name: 'results_bmk', id: 40, archive_download_url: 'http://dl-merged' },
542+
{ name: 'bmk_agentic_dsv4_conc72', id: 41, archive_download_url: 'http://dl-a' },
543+
],
544+
}),
545+
});
546+
mockFetch.mockResolvedValueOnce({
547+
ok: true,
548+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
549+
});
550+
mockGetEntries.mockReturnValueOnce([
551+
{
552+
entryName: 'agg_bmk.json',
553+
getData: () => Buffer.from(JSON.stringify([rawRow(), rawAgenticRow()])),
554+
},
555+
]);
556+
557+
const res = await GET(makeRequest('runId=888'));
558+
expect(res.status).toBe(200);
559+
const body = await res.json();
560+
expect(body.benchmarks).toHaveLength(2);
561+
// Exactly one download (the merged artifact) — per-config was skipped.
562+
expect(mockFetch).toHaveBeenCalledTimes(3);
563+
});
564+
565+
it('keeps only the newest per-config artifact when names repeat', async () => {
566+
mockFetch.mockResolvedValueOnce({
567+
ok: true,
568+
json: () =>
569+
Promise.resolve({
570+
id: 999,
571+
head_branch: 'feature/agentic',
572+
html_url: 'http://github.com/run/999',
573+
created_at: '2026-07-07T00:00:00Z',
574+
}),
575+
});
576+
mockFetch.mockResolvedValueOnce({
577+
ok: true,
578+
json: () =>
579+
Promise.resolve({
580+
artifacts: [
581+
{ name: 'bmk_agentic_dsv4_conc72', id: 50, archive_download_url: 'http://dl-old' },
582+
{ name: 'bmk_agentic_dsv4_conc72', id: 51, archive_download_url: 'http://dl-new' },
583+
],
584+
}),
585+
});
586+
mockFetch.mockResolvedValueOnce({
587+
ok: true,
588+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
589+
});
590+
mockGetEntries.mockReturnValueOnce([
591+
{
592+
entryName: 'dsv4_conc72.json',
593+
getData: () => Buffer.from(JSON.stringify(rawAgenticRow())),
594+
},
595+
]);
596+
597+
const res = await GET(makeRequest('runId=999'));
598+
expect(res.status).toBe(200);
599+
const body = await res.json();
600+
expect(body.benchmarks).toHaveLength(1);
601+
// Three fetches total; the single download hit the newest artifact URL.
602+
expect(mockFetch).toHaveBeenCalledTimes(3);
603+
expect(mockFetch.mock.calls[2][0]).toBe('http://dl-new');
604+
});
605+
440606
it('returns evaluations for eval-only runs', async () => {
441607
const evalData = [rawEvalRow()];
442608

packages/app/src/app/api/unofficial-run/route.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,17 @@ import {
1717
getGithubToken,
1818
getRunDate,
1919
normalizeGithubRunInfo,
20+
type GithubArtifact,
2021
type GithubWorkflowRun,
2122
} from '@/lib/github-artifacts';
2223

24+
/**
25+
* Batch size for downloading per-config `bmk_*` artifacts. Big agentic sweeps
26+
* upload one artifact per config (dozens per run); a bounded batch keeps us
27+
* clear of GitHub's secondary rate limits while still parallelizing.
28+
*/
29+
const PER_CONFIG_DOWNLOAD_BATCH_SIZE = 8;
30+
2331
/** Normalize raw artifact rows into the BenchmarkRow shape the frontend expects. */
2432
export function normalizeArtifactRows(
2533
rawRows: Record<string, unknown>[],
@@ -234,11 +242,32 @@ async function processSingleRun(
234242
.filter((a) => a.name === 'eval_results_all')
235243
.toSorted((a, b) => b.id - a.id)[0];
236244

237-
if (!bmkArtifact && !evalArtifact) {
245+
// Agentic-only sweeps never run the collect-results merge job (run-sweep.yml
246+
// only triggers it for fixed-seq sweeps), so they upload per-config
247+
// `bmk_agentic_<config>` artifacts with no merged `results_bmk`. Fall back to
248+
// downloading those directly. When `results_bmk` exists it already contains
249+
// every `bmk_*` row (the merge job downloads the `bmk_*` pattern), so the
250+
// fallback must stay off in that case — running both would double-count.
251+
// Same-name re-uploads keep only the newest (highest id), mirroring the
252+
// merged-artifact selection above.
253+
const perConfigBmkArtifacts: GithubArtifact[] = bmkArtifact
254+
? []
255+
: [
256+
...artifacts
257+
.filter((a) => a.name.startsWith('bmk_'))
258+
.reduce((byName, a) => {
259+
const prev = byName.get(a.name);
260+
if (!prev || a.id > prev.id) byName.set(a.name, a);
261+
return byName;
262+
}, new Map<string, GithubArtifact>())
263+
.values(),
264+
];
265+
266+
if (!bmkArtifact && perConfigBmkArtifacts.length === 0 && !evalArtifact) {
238267
return {
239268
errorResponse: NextResponse.json(
240269
{
241-
error: `No results_bmk or eval_results_all artifact found for runId ${runId}`,
270+
error: `No results_bmk, per-config bmk_*, or eval_results_all artifact found for runId ${runId}`,
242271
},
243272
{ status: 404 },
244273
),
@@ -259,6 +288,19 @@ async function processSingleRun(
259288
);
260289
if (errorResponse) return { errorResponse };
261290
benchmarks = normalizeArtifactRows(rows, date, runUrl || null);
291+
} else if (perConfigBmkArtifacts.length > 0) {
292+
const rawRows: Record<string, unknown>[] = [];
293+
for (let i = 0; i < perConfigBmkArtifacts.length; i += PER_CONFIG_DOWNLOAD_BATCH_SIZE) {
294+
const batch = perConfigBmkArtifacts.slice(i, i + PER_CONFIG_DOWNLOAD_BATCH_SIZE);
295+
const results = await Promise.all(
296+
batch.map((a) => downloadArtifactRows(a.archive_download_url, githubToken)),
297+
);
298+
for (const result of results) {
299+
if (result.errorResponse) return { errorResponse: result.errorResponse };
300+
rawRows.push(...result.rows);
301+
}
302+
}
303+
benchmarks = normalizeArtifactRows(rawRows, date, runUrl || null);
262304
}
263305

264306
if (evalArtifact) {

0 commit comments

Comments
 (0)