Skip to content

Commit 2f84642

Browse files
arturoliduenaclaude
andcommitted
[kbn-evals] Simplify CI triage to two paths: tool call parses, or generic failure
Drop the speculative branches (finish_reason=length, missing tool call, prose fallback, object arguments, group cap) and their constants. The structured triage now either parses the forced tool call arguments or throws the existing generic error, with the redacted raw reply logged once in the notify step so the next failure is diagnosable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 74fdadc commit 2f84642

4 files changed

Lines changed: 91 additions & 234 deletions

File tree

x-pack/platform/packages/shared/kbn-evals/scripts/ci/ai_connectors.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ function parseVaultConfig() {
5252
}
5353

5454
// CI notify triage uses this OpenRouter model.
55-
// Model must support OpenAI-style tool calling with `tool_choice` on OpenRouter.
56-
// Models without it fail every triage with "did not call the report_triage tool".
55+
// The per-suite triage is a forced tool call, so the model must support OpenAI-style
56+
// tool calling with `tool_choice` on OpenRouter. Without it every triage falls back
57+
// to "did not return valid JSON" (the raw reply is logged in the notify step).
5758
const TRIAGE_OPENROUTER_MODEL = 'google/gemini-3.7-flash';
5859

5960
/**

x-pack/platform/packages/shared/kbn-evals/scripts/ci/build_suite_owner_slack_message.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,6 @@ async function main() {
146146
} catch (error) {
147147
const message = formatTriageError(error);
148148
console.error(`--- Triage summary failed: ${message}`);
149-
if (error && typeof error === 'object' && typeof error.details === 'string') {
150-
console.error(error.details);
151-
}
152149
triage = { modelId: TRIAGE_OPENROUTER_CONNECTOR_ID, error: message };
153150
}
154151

x-pack/platform/packages/shared/kbn-evals/scripts/ci/failure_context_helpers.js

Lines changed: 35 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,12 @@ const MAX_CONTEXT_JSON_BYTES = 30 * 1024;
1515
// Triage/summary text is always generated with a small, low-cost OpenRouter model.
1616
const TRIAGE_OPENROUTER_CONNECTOR_ID = `openrouter-${slugifyId(TRIAGE_OPENROUTER_MODEL)}`;
1717

18-
// Output budget for the free-text weekly rollup (bullets, < 900 chars).
19-
const DEFAULT_MAX_TOKENS = 800;
20-
21-
// Output budget for the per-suite structured triage.
18+
// Output budget for the per-suite structured triage. One group per distinct error,
19+
// each quoting an error line; a sharded suite with several failing models needs
20+
// more than the 800 tokens the free-text weekly rollup uses.
2221
const TRIAGE_MAX_TOKENS = 4000;
23-
24-
// Bounds enforced through the prompt so the structured output stays well inside
25-
// TRIAGE_MAX_TOKENS regardless of how many models failed.
26-
const TRIAGE_MAX_GROUPS = 6;
2722
const TRIAGE_MAX_ERROR_CHARS = 200;
2823

29-
// How much of a malformed model reply to surface in the Buildkite step log.
30-
const TRIAGE_RAW_PREVIEW_CHARS = 300;
31-
3224
const TRIAGE_SYSTEM_PROMPT =
3325
'You are an SRE assistant triaging failed LLM evaluation CI runs. Be concise and factual, and base every statement on the provided context.';
3426

@@ -83,7 +75,6 @@ const TRIAGE_OUTPUT_INSTRUCTIONS = `Report the result by calling the \`${TRIAGE_
8375
8476
Rules:
8577
- One group per distinct error. Merge the same failure (same message/location) into one group and list all its affected models.
86-
- Report at most ${TRIAGE_MAX_GROUPS} groups; if there are more distinct errors, keep the ones affecting the most models.
8778
- Quote "error" verbatim from the excerpts, trimmed to a single line of at most ${TRIAGE_MAX_ERROR_CHARS} characters; do not invent errors, file names, line numbers, or causes.
8879
- Set "location" only if the excerpts show it; otherwise use an empty string.
8980
- Keep "rootCause" to one short sentence plus one short action.
@@ -305,49 +296,35 @@ function buildOpenrouterChatRequest(connector, messages, options = {}) {
305296
throw new Error('OpenRouter connector is missing apiUrl, defaultModel, or apiKey');
306297
}
307298

308-
const { maxTokens = DEFAULT_MAX_TOKENS, tools, toolChoice } = options;
309-
310-
const body = {
311-
model: defaultModel,
312-
messages,
313-
temperature: 0.2,
314-
max_tokens: maxTokens,
315-
};
316-
if (Array.isArray(tools) && tools.length > 0) {
317-
body.tools = tools;
318-
if (toolChoice) {
319-
body.tool_choice = toolChoice;
320-
}
321-
}
299+
const { maxTokens = 800, tools, toolChoice } = options;
322300

323301
return {
324302
url: apiUrl,
325303
headers: {
326304
'content-type': 'application/json',
327305
authorization: `Bearer ${apiKey}`,
328306
},
329-
body,
307+
body: {
308+
model: defaultModel,
309+
messages,
310+
temperature: 0.2,
311+
max_tokens: maxTokens,
312+
...(tools ? { tools, tool_choice: toolChoice } : {}),
313+
},
330314
};
331315
}
332316

333-
function parseOpenrouterChatChoice(responseJson) {
317+
function parseOpenrouterChatContent(responseJson) {
334318
if (!responseJson || typeof responseJson !== 'object') {
335319
throw new Error('OpenRouter response was not JSON');
336320
}
337321

338-
const choice = Array.isArray(responseJson.choices) ? responseJson.choices[0] : undefined;
339-
const message = choice?.message && typeof choice.message === 'object' ? choice.message : {};
322+
const choices = /** @type {{ choices?: Array<{ message?: { content?: string } }> }} */ (
323+
responseJson
324+
).choices;
340325

341-
return {
342-
content: typeof message.content === 'string' ? message.content : '',
343-
toolCalls: Array.isArray(message.tool_calls) ? message.tool_calls : [],
344-
finishReason: typeof choice?.finish_reason === 'string' ? choice.finish_reason : '',
345-
};
346-
}
347-
348-
function parseOpenrouterChatContent(responseJson) {
349-
const { content } = parseOpenrouterChatChoice(responseJson);
350-
if (!content.trim()) {
326+
const content = choices?.[0]?.message?.content;
327+
if (typeof content !== 'string' || !content.trim()) {
351328
throw new Error('OpenRouter response did not include message content');
352329
}
353330

@@ -404,21 +381,6 @@ function resolveTriageConnector() {
404381
};
405382
}
406383

407-
function triageError(message, raw) {
408-
const error = /** @type {Error & { details?: string }} */ (new Error(message));
409-
const preview = redactSecrets(String(raw ?? ''))
410-
.replace(/\s+/g, ' ')
411-
.trim();
412-
if (preview) {
413-
const clipped =
414-
preview.length > TRIAGE_RAW_PREVIEW_CHARS
415-
? `${preview.slice(0, TRIAGE_RAW_PREVIEW_CHARS)}…`
416-
: preview;
417-
error.details = `Raw model output (${preview.length} chars): ${clipped}`;
418-
}
419-
return error;
420-
}
421-
422384
/**
423385
* Parse the structured triage groups returned by the model.
424386
*/
@@ -433,19 +395,7 @@ function parseTriageGroups(rawText) {
433395
try {
434396
parsed = JSON.parse(unfenced);
435397
} catch {
436-
// Prose around the object: fall back to the outermost `{ ... }`.
437-
const start = unfenced.indexOf('{');
438-
const end = unfenced.lastIndexOf('}');
439-
if (start >= 0 && end > start) {
440-
try {
441-
parsed = JSON.parse(unfenced.slice(start, end + 1));
442-
} catch {
443-
// fall through
444-
}
445-
}
446-
if (parsed === undefined) {
447-
throw triageError('Triage model did not return valid JSON', text);
448-
}
398+
throw new Error('Triage model did not return valid JSON');
449399
}
450400

451401
const groups = Array.isArray(parsed?.groups) ? parsed.groups : [];
@@ -482,46 +432,11 @@ async function runTriageModel(userPrompt, { maxChars = 1500 } = {}) {
482432
return { summary: summary.trim(), modelId };
483433
}
484434

485-
/**
486-
* Extract the triage groups from a forced `report_triage` tool call.
487-
*/
488-
function parseTriageToolCall(responseJson) {
489-
const { content, toolCalls, finishReason } = parseOpenrouterChatChoice(responseJson);
490-
491-
const call =
492-
toolCalls.find((candidate) => candidate?.function?.name === TRIAGE_TOOL_NAME) ?? toolCalls[0];
493-
const rawArguments = call?.function?.arguments;
494-
// The OpenAI-compatible contract is a JSON string, but some providers inline the object.
495-
let args = '';
496-
if (typeof rawArguments === 'string') {
497-
args = rawArguments;
498-
} else if (rawArguments && typeof rawArguments === 'object') {
499-
args = JSON.stringify(rawArguments);
500-
}
501-
502-
if (finishReason === 'length') {
503-
throw triageError(
504-
`Triage model output was truncated at the ${TRIAGE_MAX_TOKENS}-token limit`,
505-
args || content
506-
);
507-
}
508-
509-
if (!args.trim()) {
510-
throw triageError(
511-
`Triage model did not call the ${TRIAGE_TOOL_NAME} tool (finish_reason: ${
512-
finishReason || 'unknown'
513-
})`,
514-
content
515-
);
516-
}
517-
518-
return parseTriageGroups(args);
519-
}
520-
521435
/**
522436
* Resolve the OpenRouter connector, send the shared system prompt + the given user
523-
* prompt, and return the parsed structured triage groups and the model id used.
524-
* Used by the per-suite triage, which renders the message deterministically.
437+
* prompt as a forced `report_triage` tool call, and return the parsed groups and
438+
* the model id used. Used by the per-suite triage, which renders the message
439+
* deterministically.
525440
*/
526441
async function runTriageModelStructured(userPrompt) {
527442
const { connector, modelId } = resolveTriageConnector();
@@ -539,16 +454,26 @@ async function runTriageModelStructured(userPrompt) {
539454
})
540455
);
541456

542-
return { groups: parseTriageToolCall(responseJson), modelId };
457+
const message = responseJson?.choices?.[0]?.message;
458+
try {
459+
return { groups: parseTriageGroups(message?.tool_calls?.[0]?.function?.arguments), modelId };
460+
} catch (error) {
461+
// The Buildkite step log is the only place the raw reply is visible; the
462+
// Slack/GitHub fallback line stays generic.
463+
console.error(
464+
`Raw triage model reply: ${redactSecrets(JSON.stringify(message ?? responseJson)).slice(
465+
0,
466+
500
467+
)}`
468+
);
469+
throw error;
470+
}
543471
}
544472

545473
module.exports = {
546474
MAX_LOG_EXCERPT_CHARS,
547475
MAX_CONTEXT_JSON_BYTES,
548-
DEFAULT_MAX_TOKENS,
549476
TRIAGE_MAX_TOKENS,
550-
TRIAGE_MAX_GROUPS,
551-
TRIAGE_MAX_ERROR_CHARS,
552477
TRIAGE_SYSTEM_PROMPT,
553478
TRIAGE_OPENROUTER_CONNECTOR_ID,
554479
TRIAGE_TOOL_NAME,
@@ -563,13 +488,11 @@ module.exports = {
563488
buildWeeklyRollupUserPrompt,
564489
extractSuiteRootCauseLine,
565490
buildOpenrouterChatRequest,
566-
parseOpenrouterChatChoice,
567491
parseOpenrouterChatContent,
568492
postOpenrouterChat,
569493
postOpenrouterChatRequest,
570494
resolveTriageConnector,
571495
parseTriageGroups,
572-
parseTriageToolCall,
573496
runTriageModel,
574497
runTriageModelStructured,
575498
};

0 commit comments

Comments
 (0)