From ae3d55b79c54c845a488604a7b617325b88daf64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:54:09 +0000 Subject: [PATCH] chore: sync actions from gh-aw@v0.79.6 --- setup/js/ai_credits_context.cjs | 80 +++++++++++++++------ setup/js/handle_agent_failure.cjs | 88 ++++++++++++++++++++++-- setup/js/parse_mcp_gateway_log.cjs | 38 +++++++++- setup/js/send_otlp_span.cjs | 56 ++++++++++++++- setup/js/write_large_content_to_file.cjs | 12 +--- setup/md/agent_failure_comment.md | 2 +- setup/md/agent_failure_issue.md | 2 +- setup/md/unknown_model_ai_credits.md | 43 ++++++++++++ 8 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 setup/md/unknown_model_ai_credits.md diff --git a/setup/js/ai_credits_context.cjs b/setup/js/ai_credits_context.cjs index f1b6ef24..14f9fea0 100644 --- a/setup/js/ai_credits_context.cjs +++ b/setup/js/ai_credits_context.cjs @@ -12,6 +12,9 @@ const AI_CREDITS_RATE_LIMIT_TEXT_FIELDS = new Set(["error", "message", "reason", const AI_CREDITS_RATE_LIMIT_PATTERNS = [/ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i, /(?:rate[\s-]*limit|too many requests).*(?:ai[\s_-]*credits?)/i, /\bai_credits_limit_exceeded\b/i]; const MAX_AI_CREDITS_EXCEEDED_FIELDS = new Set(["max_ai_credits_exceeded", "maxAiCreditsExceeded"]); const BUDGET_EXCEEDED_EVENT = "budget_exceeded"; +// The literal error type emitted by the AWF API proxy (HTTP 400) when maxAiCredits is active +// and the requested model is not in the built-in pricing table. +const UNKNOWN_MODEL_AI_CREDITS_TYPE = "unknown_model_ai_credits"; const MAX_AI_CREDITS_EXCEEDED_STDIO_RE = /maximum ai credits exceeded(?:\s*\((\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)\))?/i; const DEFAULT_AGENT_STDIO_LOG = "/tmp/gh-aw/agent-stdio.log"; const AGENT_STDIO_LOG_MAX_TAIL = 64 * 1024; // 64 KB — sufficient for any realistic error block @@ -251,6 +254,37 @@ function parseMaxAICreditsExceededFromAuditLog(auditJsonlPathOverride) { ); } +/** + * Detects an `unknown_model_ai_credits` error from the firewall audit log. + * This HTTP 400 error is emitted by the AWF API proxy when `maxAiCredits` is active and + * the requested model is not in the built-in pricing table and no `defaultAiCreditsPricing` + * fallback is configured. + * + * @param {string} [auditJsonlPathOverride] + * @returns {boolean} + */ +function parseUnknownModelAICreditsFromAuditLog(auditJsonlPathOverride) { + return iterateAuditEntries( + auditJsonlPathOverride, + false, + content => content.includes(UNKNOWN_MODEL_AI_CREDITS_TYPE), + (acc, entry) => { + if (acc) return true; + if (!entry || typeof entry !== "object") return false; + const stack = [entry]; + while (stack.length > 0) { + const node = stack.pop(); + if (!node || typeof node !== "object") continue; + for (const [, value] of Object.entries(node)) { + if (value === UNKNOWN_MODEL_AI_CREDITS_TYPE) return true; + if (value && typeof value === "object") stack.push(value); + } + } + return false; + } + ); +} + /** * Single-pass combined read of the audit log, returning all AI credits fields at once. * Used by resolveAICreditsFailureState to avoid reading the same file twice. @@ -277,37 +311,40 @@ function parseAuditLogCombined(auditJsonlPathOverride) { } /** + * @param {{ logProvenance?: boolean }} [options] * @returns {{ aiCredits: string, maxAICredits: string, aiCreditsRateLimitError: boolean, maxAICreditsExceeded: boolean }} */ -function resolveAICreditsFailureState() { +function resolveAICreditsFailureState({ logProvenance = true } = {}) { const stdioSignals = parseAICreditsExceededFromAgentStdio(); const { aiCredits: auditAICredits, maxAICredits: auditMaxAICredits, rateLimitError: auditRateLimitError, maxAICreditsExceeded: auditMaxAICreditsExceeded } = parseAuditLogCombined(); const envAICredits = parsePositiveNumberString(process.env.GH_AW_AIC); const envMaxAICredits = parsePositiveNumberString(process.env.GH_AW_MAX_AI_CREDITS); // Log provenance so failing issues can be diagnosed when credit data is missing. - if (auditAICredits) { - console.log(`[ai-credits] aiCredits source=audit_log value=${auditAICredits}`); - } else if (stdioSignals.aiCredits) { - console.log(`[ai-credits] aiCredits source=agent_stdio value=${stdioSignals.aiCredits}`); - } else if (envAICredits) { - console.log(`[ai-credits] aiCredits source=env(GH_AW_AIC) value=${envAICredits}`); - } else { - console.log(`[ai-credits] aiCredits source=none GH_AW_AIC=${process.env.GH_AW_AIC || "(unset)"}`); - } + if (logProvenance) { + if (auditAICredits) { + console.log(`[ai-credits] aiCredits source=audit_log value=${auditAICredits}`); + } else if (stdioSignals.aiCredits) { + console.log(`[ai-credits] aiCredits source=agent_stdio value=${stdioSignals.aiCredits}`); + } else if (envAICredits) { + console.log(`[ai-credits] aiCredits source=env(GH_AW_AIC) value=${envAICredits}`); + } else { + console.log(`[ai-credits] aiCredits source=none GH_AW_AIC=${process.env.GH_AW_AIC || "(unset)"}`); + } - if (auditMaxAICredits) { - console.log(`[ai-credits] maxAICredits source=audit_log value=${auditMaxAICredits}`); - } else if (stdioSignals.maxAICredits) { - console.log(`[ai-credits] maxAICredits source=agent_stdio value=${stdioSignals.maxAICredits}`); - } else if (envMaxAICredits) { - console.log(`[ai-credits] maxAICredits source=env(GH_AW_MAX_AI_CREDITS) value=${envMaxAICredits}`); - } else { - console.log(`[ai-credits] maxAICredits source=none GH_AW_MAX_AI_CREDITS=${process.env.GH_AW_MAX_AI_CREDITS || "(unset)"}`); - } + if (auditMaxAICredits) { + console.log(`[ai-credits] maxAICredits source=audit_log value=${auditMaxAICredits}`); + } else if (stdioSignals.maxAICredits) { + console.log(`[ai-credits] maxAICredits source=agent_stdio value=${stdioSignals.maxAICredits}`); + } else if (envMaxAICredits) { + console.log(`[ai-credits] maxAICredits source=env(GH_AW_MAX_AI_CREDITS) value=${envMaxAICredits}`); + } else { + console.log(`[ai-credits] maxAICredits source=none GH_AW_MAX_AI_CREDITS=${process.env.GH_AW_MAX_AI_CREDITS || "(unset)"}`); + } - const rawRateLimitSignalSource = auditRateLimitError ? "audit_log" : stdioSignals.rateLimitError ? "agent_stdio" : process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true" ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)" : "none"; - console.log(`[ai-credits] rateLimitSignal source=${rawRateLimitSignalSource}`); + const rawRateLimitSignalSource = auditRateLimitError ? "audit_log" : stdioSignals.rateLimitError ? "agent_stdio" : process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR === "true" ? "env(GH_AW_AI_CREDITS_RATE_LIMIT_ERROR)" : "none"; + console.log(`[ai-credits] rateLimitSignal source=${rawRateLimitSignalSource}`); + } const aiCredits = auditAICredits || stdioSignals.aiCredits || envAICredits || ""; const maxAICredits = auditMaxAICredits || stdioSignals.maxAICredits || envMaxAICredits || ""; @@ -366,5 +403,6 @@ module.exports = { parseMaxAICreditsFromAuditLog, parseAICreditsErrorInfoFromAuditLog, parseMaxAICreditsExceededFromAuditLog, + parseUnknownModelAICreditsFromAuditLog, resolveAICreditsFailureState, }; diff --git a/setup/js/handle_agent_failure.cjs b/setup/js/handle_agent_failure.cjs index c37ddcf4..47e50387 100644 --- a/setup/js/handle_agent_failure.cjs +++ b/setup/js/handle_agent_failure.cjs @@ -35,7 +35,7 @@ const COPILOT_SESSION_STATE_DIR = path.join(os.tmpdir(), "gh-aw", "sandbox", "ag // - Copilot/CAPI "CAPIError: 429" and utility-model quota text // - retry wrapper text that includes the canonical "Failed to get response..." phrase const ENGINE_RATE_LIMIT_429_RE = - /(?:\b429\b[\s\S]{0,120}(?:too many requests|rate[\s-]*limit)|rate_limit_(?:error|exceeded)|capierror:\s*429|failed to get response from the ai model[\s\S]{0,120}\b429\b|exceeded your rate limit for utility models)/i; + /(?:\b429\b[\s\S]{0,120}(?:too many requests|rate[\s-]*limit)|\brate_limit_(?:error|exceeded)\b|capierror:\s*429|failed to get response from the ai model[\s\S]{0,120}\b429\b|exceeded your rate limit for utility models)/i; /** * Parse action failure issue expiration from environment. @@ -195,6 +195,7 @@ function buildFailureMatchCategories(options) { if (options.mcpPolicyError) categories.push("mcp_policy_error"); if (options.modelNotSupportedError) categories.push("model_not_supported_error"); if (options.aiCreditsRateLimitError) categories.push("ai_credits_rate_limit_error"); + if (options.unknownModelAICredits) categories.push("unknown_model_ai_credits"); if (options.maxAICreditsExceeded) categories.push("max_ai_credits_exceeded"); if (options.hasAppTokenMintingFailed) categories.push("app_token_minting_failed"); if (options.hasLockdownCheckFailed) categories.push("lockdown_check_failed"); @@ -208,6 +209,44 @@ function buildFailureMatchCategories(options) { return categories.sort(); } +/** + * Build a precise failure issue title for known failure classes. + * Falls back to the generic failure title when no specific class matches. + * @param {Object} options + * @param {string} options.workflowName + * @param {boolean} options.isTimedOut + * @param {boolean} options.hasMissingSafeOutputs + * @param {boolean} options.hasReportIncomplete + * @param {boolean} options.hasMissingTool + * @param {boolean} options.hasMissingData + * @param {boolean} options.hasCacheMissMisconfiguration + * @param {boolean} options.hasToolDenialsExceeded + * @param {boolean} options.hasAppTokenMintingFailed + * @param {boolean} options.hasLockdownCheckFailed + * @param {boolean} options.hasStaleLockFileFailed + * @param {boolean} options.hasDailyAICExceeded + * @param {boolean} options.aiCreditsRateLimitError + * @param {boolean} options.maxAICreditsExceeded + * @returns {string} + */ +function buildFailureIssueTitle(options) { + const { workflowName } = options; + if (options.hasDailyAICExceeded) return `[aw] ${workflowName} exceeded daily effective workflow budget`; + if (options.maxAICreditsExceeded) return `[aw] ${workflowName} exceeded max AI credits`; + if (options.aiCreditsRateLimitError) return `[aw] ${workflowName} hit AI credits rate limit`; + if (options.hasAppTokenMintingFailed) return `[aw] ${workflowName} failed to mint GitHub App token`; + if (options.hasLockdownCheckFailed) return `[aw] ${workflowName} failed lockdown check`; + if (options.hasStaleLockFileFailed) return `[aw] ${workflowName} has stale lock file`; + if (options.isTimedOut) return `[aw] ${workflowName} timed out`; + if (options.hasToolDenialsExceeded) return `[aw] ${workflowName} exceeded tool denial limit`; + if (options.hasCacheMissMisconfiguration) return `[aw] ${workflowName} has cache-memory miss misconfiguration`; + if (options.hasReportIncomplete) return `[aw] ${workflowName} reported incomplete result`; + if (options.hasMissingSafeOutputs) return `[aw] ${workflowName} produced no safe outputs`; + if (options.hasMissingTool) return `[aw] ${workflowName} is missing required tool`; + if (options.hasMissingData) return `[aw] ${workflowName} is missing required data`; + return `[aw] ${workflowName} failed`; +} + /** * Generate a precise failure-match marker for failure issue bodies. * @param {Object} options - Marker options @@ -1417,6 +1456,19 @@ function buildModelNotSupportedErrorContext(hasModelNotSupportedError) { return "\n" + renderPromptTemplate("model_not_supported_error.md"); } +/** + * Builds the unknown_model_ai_credits failure context block for templates. + * @param {boolean} hasUnknownModelAICreditsError + * @returns {string} + */ +function buildUnknownModelAICreditsContext(hasUnknownModelAICreditsError) { + if (!hasUnknownModelAICreditsError) { + return ""; + } + + return "\n" + renderPromptTemplate("unknown_model_ai_credits.md"); +} + /** * Detect HTTP 429/rate-limit engine failures in text payloads. * @param {string} content @@ -2030,8 +2082,8 @@ const CASCADE_ROLLUP_LABEL = "cascade-rollup"; /** Daily-cap rollup constants */ const DAILY_CAP_ROLLUP_TITLE = "[aw] Daily failure issue cap exceeded"; const DAILY_CAP_ROLLUP_LABEL = "daily-cap-exceeded"; -/** Matches the exact title pattern produced by handle_agent_failure for individual failure issues */ -const FAILURE_TITLE_PATTERN = /^\[aw\] .+ failed$/; +/** Matches individual failure issue titles produced by handle_agent_failure */ +const FAILURE_TITLE_PATTERN = /^\[aw\] \S.*$/; /** * Ensure a GitHub label exists in the repository, creating it with a deterministic @@ -2070,7 +2122,7 @@ async function ensureLabelExists(owner, repo, labelName) { } /** - * Detect whether a failure cascade is active by counting `[aw] * failed` issues + * Detect whether a failure cascade is active by counting `[aw] *` failure issues * created within the last CASCADE_WINDOW_MINUTES minutes. * * @param {string} owner @@ -2083,7 +2135,7 @@ async function findRecentFailureIssues(owner, repo) { const since = windowStart.toISOString().slice(0, 19) + "Z"; // e.g. "2026-05-22T02:00:00Z" // GitHub search API supports `created:>=YYYY-MM-DDTHH:MM:SSZ` - const searchQuery = `repo:${owner}/${repo} is:issue is:open label:agentic-workflows "[aw]" "failed" in:title created:>=${since}`; + const searchQuery = `repo:${owner}/${repo} is:issue is:open label:agentic-workflows "[aw]" in:title created:>=${since}`; try { const result = await github.rest.search.issuesAndPullRequests({ @@ -2303,6 +2355,7 @@ async function main() { const mcpPolicyError = process.env.GH_AW_MCP_POLICY_ERROR === "true"; const agenticEngineTimeout = process.env.GH_AW_AGENTIC_ENGINE_TIMEOUT === "true"; const modelNotSupportedError = process.env.GH_AW_MODEL_NOT_SUPPORTED_ERROR === "true"; + const unknownModelAICredits = process.env.GH_AW_UNKNOWN_MODEL_AI_CREDITS === "true"; const pushRepoMemoryResult = process.env.GH_AW_PUSH_REPO_MEMORY_RESULT || ""; const reportFailureAsIssue = process.env.GH_AW_FAILURE_REPORT_AS_ISSUE !== "false"; // Default to true // Feature flags: control whether missing_tool/missing_data signals trigger agent failure handling. @@ -2369,6 +2422,7 @@ async function main() { core.info(`MCP policy error: ${mcpPolicyError}`); core.info(`Agentic engine timeout: ${agenticEngineTimeout}`); core.info(`Model not supported error: ${modelNotSupportedError}`); + core.info(`Unknown model AI credits error: ${unknownModelAICredits}`); core.info(`Push repo-memory result: ${pushRepoMemoryResult}`); core.info(`App token minting failed (safe_outputs/conclusion/activation): ${safeOutputsAppTokenMintingFailed}/${conclusionAppTokenMintingFailed}/${activationAppTokenMintingFailed}`); core.info(`Lockdown check failed: ${hasLockdownCheckFailed}`); @@ -2622,7 +2676,22 @@ async function main() { // Sanitize workflow name for title const sanitizedWorkflowName = sanitizeContent(workflowName, { maxLength: 100 }); - const issueTitle = `[aw] ${sanitizedWorkflowName} failed`; + const issueTitle = buildFailureIssueTitle({ + workflowName: sanitizedWorkflowName, + isTimedOut, + hasMissingSafeOutputs, + hasReportIncomplete, + hasMissingTool, + hasMissingData, + hasCacheMissMisconfiguration, + hasToolDenialsExceeded, + hasAppTokenMintingFailed, + hasLockdownCheckFailed, + hasStaleLockFileFailed, + hasDailyAICExceeded, + aiCreditsRateLimitError, + maxAICreditsExceeded, + }); const failureCategories = buildFailureMatchCategories({ agentConclusion, isTimedOut, @@ -2643,6 +2712,7 @@ async function main() { mcpPolicyError, modelNotSupportedError, aiCreditsRateLimitError, + unknownModelAICredits, maxAICreditsExceeded, hasAppTokenMintingFailed, hasLockdownCheckFailed, @@ -2774,6 +2844,7 @@ async function main() { // Build model not supported error context const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError); const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl); + const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits); // Build GitHub App token minting failure context const appTokenMintingFailedContext = buildAppTokenMintingFailedContext(hasAppTokenMintingFailed); @@ -2824,6 +2895,7 @@ async function main() { mcp_policy_error_context: mcpPolicyErrorContext, model_not_supported_error_context: modelNotSupportedErrorContext, ai_credits_rate_limit_error_context: aiCreditsRateLimitErrorContext, + unknown_model_ai_credits_context: unknownModelAICreditsContext, app_token_minting_failed_context: appTokenMintingFailedContext, lockdown_check_failed_context: lockdownCheckFailedContext, stale_lock_file_failed_context: staleLockFileFailedContext, @@ -3000,6 +3072,7 @@ async function main() { // Build model not supported error context const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError); const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl); + const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits); // Build GitHub App token minting failure context const appTokenMintingFailedContext = buildAppTokenMintingFailedContext(hasAppTokenMintingFailed); @@ -3051,6 +3124,7 @@ async function main() { mcp_policy_error_context: mcpPolicyErrorContext, model_not_supported_error_context: modelNotSupportedErrorContext, ai_credits_rate_limit_error_context: aiCreditsRateLimitErrorContext, + unknown_model_ai_credits_context: unknownModelAICreditsContext, app_token_minting_failed_context: appTokenMintingFailedContext, lockdown_check_failed_context: lockdownCheckFailedContext, stale_lock_file_failed_context: staleLockFileFailedContext, @@ -3154,6 +3228,7 @@ module.exports = { buildToolDenialsExceededContext, buildCredentialAuthErrorContext, buildAICreditsRateLimitErrorContext, + buildUnknownModelAICreditsContext, hasEngineRateLimit429Signal, hasEngineRateLimit429InOTELMirror, buildEngineRateLimit429Context, @@ -3173,5 +3248,6 @@ module.exports = { CASCADE_ROLLUP_TITLE, FAILURE_TITLE_PATTERN, buildFailureMatchCategories, + buildFailureIssueTitle, FAILURE_CATEGORIES_PATH, }; diff --git a/setup/js/parse_mcp_gateway_log.cjs b/setup/js/parse_mcp_gateway_log.cjs index f6c7b97a..fbbc8819 100644 --- a/setup/js/parse_mcp_gateway_log.cjs +++ b/setup/js/parse_mcp_gateway_log.cjs @@ -8,6 +8,7 @@ const { ERR_PARSE, ERR_SYSTEM } = require("./error_codes.cjs"); const { formatModelEmojiAlias } = require("./model_aliases.cjs"); const { computeInferenceAIC, formatAIC } = require("./model_costs.cjs"); const { generateUnifiedTimelineSummary } = require("./unified_timeline.cjs"); +const { parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context.cjs"); /** * Parses MCP gateway logs and creates a step summary @@ -29,6 +30,9 @@ const AI_CREDITS_RATE_LIMIT_PATTERNS = [ /(?:rate[\s-]*limit|too many requests).*(?:ai[\s_-]*credits?)/i, /\b429\b.*(?:rate[\s-]*limit|too many requests|ai[\s_-]*credits?)/i, ]; +// Detects the AWF API proxy HTTP 400 error emitted when maxAiCredits is active and +// the requested model is not in the built-in pricing table. +const UNKNOWN_MODEL_AI_CREDITS_PATTERNS = [/\bunknown_model_ai_credits\b/i]; /** * Formats milliseconds as a human-readable duration string. @@ -253,6 +257,27 @@ function setAICreditsRateLimitOutput(coreObj, value) { coreObj.setOutput("ai_credits_rate_limit_error", strValue); } +/** + * Detects `unknown_model_ai_credits` errors from gateway log text content. + * Also checks the firewall audit log via ai_credits_context. + * @param {string[]} contents + * @returns {boolean} + */ +function hasUnknownModelAICreditsError(contents) { + const joined = contents.filter(Boolean).join("\n"); + if (joined && UNKNOWN_MODEL_AI_CREDITS_PATTERNS.some(pattern => pattern.test(joined))) return true; + return parseUnknownModelAICreditsFromAuditLog(); +} + +/** + * Exports unknown_model_ai_credits output. + * @param {typeof import('@actions/core')} coreObj + * @param {boolean} value + */ +function setUnknownModelAICreditsOutput(coreObj, value) { + coreObj.setOutput("unknown_model_ai_credits", value ? "true" : "false"); +} + /** * Prints all gateway-related files to core.info for debugging */ @@ -797,6 +822,7 @@ async function main() { const gatewayLogPath = "/tmp/gh-aw/mcp-logs/gateway.log"; const stderrLogPath = "/tmp/gh-aw/mcp-logs/stderr.log"; let aiCreditsRateLimitError = false; + let unknownModelAICredits = false; // Parse DIFC_FILTERED events from gateway.jsonl (preferred) or rpc-messages.jsonl (fallback). // Both files use the same JSONL format with DIFC_FILTERED entries interleaved. @@ -811,6 +837,7 @@ async function main() { tokenSteeringEvents = parseGatewayJsonlForTokenSteering(jsonlContent); modelAliasResolutionEvents = parseGatewayJsonlForModelAliasResolution(jsonlContent); aiCreditsRateLimitError ||= hasAICreditsRateLimitError([jsonlContent]); + unknownModelAICredits ||= hasUnknownModelAICreditsError([jsonlContent]); if (difcFilteredEvents.length > 0) { core.info(`Found ${difcFilteredEvents.length} DIFC_FILTERED event(s) in gateway.jsonl`); } @@ -830,6 +857,7 @@ async function main() { tokenSteeringEvents = parseGatewayJsonlForTokenSteering(rpcMessagesContent); modelAliasResolutionEvents = parseGatewayJsonlForModelAliasResolution(rpcMessagesContent); aiCreditsRateLimitError ||= hasAICreditsRateLimitError([rpcMessagesContent]); + unknownModelAICredits ||= hasUnknownModelAICreditsError([rpcMessagesContent]); if (difcFilteredEvents.length > 0) { core.info(`Found ${difcFilteredEvents.length} DIFC_FILTERED event(s) in rpc-messages.jsonl`); } @@ -849,6 +877,7 @@ async function main() { if (gatewayMdContent && gatewayMdContent.trim().length > 0) { core.info(`Found gateway.md (${gatewayMdContent.length} bytes)`); aiCreditsRateLimitError ||= hasAICreditsRateLimitError([gatewayMdContent]); + unknownModelAICredits ||= hasUnknownModelAICreditsError([gatewayMdContent]); // Write the markdown directly to the step summary core.summary.addRaw(gatewayMdContent.endsWith("\n") ? gatewayMdContent : gatewayMdContent + "\n"); @@ -870,6 +899,7 @@ async function main() { } setAICreditsRateLimitOutput(core, aiCreditsRateLimitError); + setUnknownModelAICreditsOutput(core, unknownModelAICredits); writeStepSummaryWithTokenUsage(core); return; } @@ -900,6 +930,7 @@ async function main() { core.info("rpc-messages.jsonl is present but contains no renderable messages"); } setAICreditsRateLimitOutput(core, aiCreditsRateLimitError); + setUnknownModelAICreditsOutput(core, unknownModelAICredits); writeStepSummaryWithTokenUsage(core); return; } @@ -913,6 +944,7 @@ async function main() { gatewayLogContent = fs.readFileSync(gatewayLogPath, "utf8"); core.info(`Found gateway.log (${gatewayLogContent.length} bytes)`); aiCreditsRateLimitError ||= hasAICreditsRateLimitError([gatewayLogContent]); + unknownModelAICredits ||= hasUnknownModelAICreditsError([gatewayLogContent]); } else { core.info(`No gateway.log found at: ${gatewayLogPath}`); } @@ -922,6 +954,7 @@ async function main() { stderrLogContent = fs.readFileSync(stderrLogPath, "utf8"); core.info(`Found stderr.log (${stderrLogContent.length} bytes)`); aiCreditsRateLimitError ||= hasAICreditsRateLimitError([stderrLogContent]); + unknownModelAICredits ||= hasUnknownModelAICreditsError([stderrLogContent]); } else { core.info(`No stderr.log found at: ${stderrLogPath}`); } @@ -936,6 +969,7 @@ async function main() { ) { core.info("MCP gateway log files are empty or missing"); setAICreditsRateLimitOutput(core, aiCreditsRateLimitError); + setUnknownModelAICreditsOutput(core, unknownModelAICredits); writeStepSummaryWithTokenUsage(core); return; } @@ -957,7 +991,7 @@ async function main() { core.summary.addRaw(fullSummary); } setAICreditsRateLimitOutput(core, aiCreditsRateLimitError); - writeStepSummaryWithTokenUsage(core); + setUnknownModelAICreditsOutput(core, unknownModelAICredits); } catch (error) { core.setFailed(`${ERR_PARSE}: ${getErrorMessage(error)}`); } @@ -1086,6 +1120,8 @@ if (typeof module !== "undefined" && module.exports) { generateTokenUsageSummary, formatDurationMs, hasAICreditsRateLimitError, + hasUnknownModelAICreditsError, + setUnknownModelAICreditsOutput, }; } diff --git a/setup/js/send_otlp_span.cjs b/setup/js/send_otlp_span.cjs index 04ec3f1a..83793c36 100644 --- a/setup/js/send_otlp_span.cjs +++ b/setup/js/send_otlp_span.cjs @@ -11,6 +11,7 @@ const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { readExperimentAssignments, EXPERIMENT_ASSIGNMENTS_PATH } = require("./experiment_helpers.cjs"); const { parseJsonlContent } = require("./jsonl_helpers.cjs"); const { countSteeringEventsInApiProxyJsonl } = require("./steering_helpers.cjs"); +const { resolveAICreditsFailureState } = require("./ai_credits_context.cjs"); /** * send_otlp_span.cjs @@ -102,6 +103,22 @@ function buildAttr(key, value) { return { key, value: { stringValue: String(value) } }; } +/** + * Build an OTLP key-value attribute whose wire type is always `doubleValue`. + * Use for OTel attributes declared as `double` in the observability spec + * (e.g. `gh-aw.aic`) so that Sentry EAP infers the field schema as float + * rather than int — enabling sum()/avg()/percentile() rollups even when the + * value is 0, which JavaScript treats as an integer and `buildAttr` would + * encode as `intValue`, potentially creating a type mismatch across spans. + * + * @param {string} key + * @param {number} value + * @returns {{ key: string, value: { doubleValue: number } }} + */ +function buildDoubleAttr(key, value) { + return { key, value: { doubleValue: typeof value === "number" && Number.isFinite(value) ? value : 0 } }; +} + /** * Build an OTLP key-value attribute with an array of string values. * Used for OTel attributes whose type is `string[]`, such as @@ -139,6 +156,25 @@ function parseBooleanEnv(value) { return undefined; } +/** + * Resolve the job name for conclusion spans. + * + * Normally this comes from INPUT_JOB_NAME (job-name action input), but some + * deployment paths can miss that env var in the post step. In that case, fall + * back to parsing the conclusion span name ("gh-aw..conclusion"). + * + * @param {string} spanName + * @returns {string} + */ +function resolveConclusionJobName(spanName) { + const inputJobName = (process.env.INPUT_JOB_NAME || "").trim(); + if (inputJobName) { + return inputJobName; + } + const match = /^gh-aw\.([^.]+)\.conclusion$/.exec(spanName); + return match ? match[1] : ""; +} + /** * Parse setup-time aw_context passed via environment before aw_info.json exists. * @@ -1937,7 +1973,7 @@ async function sendJobConclusionSpan(spanName, options = {}) { const awmgVersion = (typeof awInfo.awmg_version === "string" ? awInfo.awmg_version : "") || process.env.GH_AW_INFO_AWMG_VERSION || ""; const bodyModified = typeof awInfo.body_modified === "boolean" ? awInfo.body_modified : parseBooleanEnv(process.env.GH_AW_INFO_BODY_MODIFIED); const trackerId = process.env.GH_AW_TRACKER_ID || awInfo.tracker_id || ""; - const jobName = process.env.INPUT_JOB_NAME || ""; + const jobName = resolveConclusionJobName(spanName); const jobEmitsOwnTokenUsage = jobName === "agent" || jobName === "detection" || (!!engineId && jobName === engineId); const runId = process.env.GITHUB_RUN_ID || ""; const runAttempt = awInfo.run_attempt || process.env.GITHUB_RUN_ATTEMPT || "1"; @@ -2079,7 +2115,10 @@ async function sendJobConclusionSpan(spanName, options = {}) { const aiCreditsFromMetrics = runtimeMetrics.tokenUsage?.ai_credits; const aiCredits = jobEmitsOwnTokenUsage ? (aiCreditsFromEnv ?? ((aiCreditsFromFile ?? 0) > 0 ? aiCreditsFromFile : (aiCreditsFromMetrics ?? aiCreditsFromFile ?? 0))) : undefined; if (typeof aiCredits === "number") { - attributes.push(buildAttr("gh-aw.aic", aiCredits)); + // Always encode gh-aw.aic as doubleValue (not intValue) so that Sentry EAP + // infers the field schema as float on first emission, enabling sum()/avg()/ + // percentile() aggregations even when the value is 0 (an integer in JS). + attributes.push(buildDoubleAttr("gh-aw.aic", aiCredits)); } if (typeof runtimeMetrics.turns === "number") { attributes.push(buildAttr("gh-aw.turns", runtimeMetrics.turns)); @@ -2340,6 +2379,18 @@ async function sendJobConclusionSpan(spanName, options = {}) { attributes.push(...usageAttrs); } + const { maxAICredits, aiCreditsRateLimitError, maxAICreditsExceeded } = resolveAICreditsFailureState({ logProvenance: false }); + const maxAICreditsValue = normalizeNonNegativeNumber(maxAICredits); + if (typeof maxAICreditsValue === "number") { + attributes.push(buildAttr("gh-aw.max_ai_credits", maxAICreditsValue)); + } + if (typeof maxAICreditsExceeded === "boolean") { + attributes.push(buildAttr("gh-aw.max_ai_credits_exceeded", maxAICreditsExceeded)); + } + if (typeof aiCreditsRateLimitError === "boolean") { + attributes.push(buildAttr("gh-aw.ai_credits_rate_limit_error", aiCreditsRateLimitError)); + } + const payload = buildOTLPPayload({ traceId, spanId: conclusionSpanId, @@ -2379,6 +2430,7 @@ module.exports = { generateSpanId, toNanoString, buildAttr, + buildDoubleAttr, buildArrayAttr, buildGitHubActionsResourceAttributes, buildOTLPSpan, diff --git a/setup/js/write_large_content_to_file.cjs b/setup/js/write_large_content_to_file.cjs index 89561cf2..317f6bf0 100644 --- a/setup/js/write_large_content_to_file.cjs +++ b/setup/js/write_large_content_to_file.cjs @@ -15,10 +15,7 @@ const { generateCompactSchema } = require("./generate_compact_schema.cjs"); function writeLargeContentToFile(content) { const logsDir = "/tmp/gh-aw/safeoutputs"; - // Ensure directory exists - if (!fs.existsSync(logsDir)) { - fs.mkdirSync(logsDir, { recursive: true }); - } + fs.mkdirSync(logsDir, { recursive: true }); // Generate SHA256 hash of content const hash = crypto.createHash("sha256").update(content).digest("hex"); @@ -27,16 +24,11 @@ function writeLargeContentToFile(content) { const filename = `${hash}.json`; const filepath = path.join(logsDir, filename); - // Write content to file fs.writeFileSync(filepath, content, "utf8"); - // Generate compact schema description for jq/agent const description = generateCompactSchema(content); - return { - filename: filename, - description: description, - }; + return { filename, description }; } module.exports = { diff --git a/setup/md/agent_failure_comment.md b/setup/md/agent_failure_comment.md index 9ca9cc47..38ed0133 100644 --- a/setup/md/agent_failure_comment.md +++ b/setup/md/agent_failure_comment.md @@ -1,3 +1,3 @@ Agent job [{run_id}]({run_url}) failed. -{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context} +{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context} diff --git a/setup/md/agent_failure_issue.md b/setup/md/agent_failure_issue.md index c3968011..345c7429 100644 --- a/setup/md/agent_failure_issue.md +++ b/setup/md/agent_failure_issue.md @@ -4,7 +4,7 @@ **Branch:** {branch} **Run:** {run_url}{pull_request_info} -{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context} +{secret_verification_context}{credential_auth_error_context}{inference_access_error_context}{mcp_policy_error_context}{model_not_supported_error_context}{ai_credits_rate_limit_error_context}{unknown_model_ai_credits_context}{app_token_minting_failed_context}{lockdown_check_failed_context}{stale_lock_file_failed_context}{daily_effective_workflow_exceeded_context}{assignment_errors_context}{assign_copilot_failure_context}{create_discussion_errors_context}{code_push_failure_context}{repo_memory_validation_context}{push_repo_memory_failure_context}{missing_data_context}{missing_tool_context}{permission_denied_context}{tool_denials_exceeded_context}{report_incomplete_context}{missing_safe_outputs_context}{engine_failure_context}{timeout_context}{fork_context} ### Action Required diff --git a/setup/md/unknown_model_ai_credits.md b/setup/md/unknown_model_ai_credits.md new file mode 100644 index 00000000..44235329 --- /dev/null +++ b/setup/md/unknown_model_ai_credits.md @@ -0,0 +1,43 @@ +> [!WARNING] +> **Unknown Model for AI Credits Pricing**: The agent failed because the requested model is not in the built-in AI credits pricing table and `max-ai-credits` is active. The AWF API proxy rejected the request with an HTTP 400 error. + +This is a **configuration issue**, not a transient error — retrying will not help. + +
+How to fix this + +Choose one of the following options: + +**Option 1 — Map the model to a known model using the `models` field:** + +Use the `models` frontmatter field to provide an alias from your custom model name to a model that exists in the built-in pricing table: + +```yaml +--- +model: my-custom-model +max-ai-credits: 500 +models: + my-custom-model: + model: gpt-4.1 +--- +``` + +**Option 2 — Use a model already in the built-in pricing table:** + +Switch to a model name that the AWF pricing system recognizes directly (e.g. `gpt-4.1`, `claude-sonnet-4-5`, `gemini-2.0-flash`). + +**Option 3 — Set a default AI credits price as a fallback:** + +Add `defaultAiCreditsPricing` to supply a price for any unrecognized models: + +```yaml +--- +model: my-custom-model +max-ai-credits: 500 +models: + my-custom-model: + defaultAiCreditsPricing: 3.0 +--- +``` + +