diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 813483cc87a..613a724b24c 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -265,6 +265,7 @@ describe("handle_agent_failure", () => { it("includes AIC and ambient context metrics in the generated failure issue footer", async () => { process.env.GH_AW_AIC = "1.25"; process.env.GH_AW_AMBIENT_CONTEXT = "900"; + process.env.GH_AW_ENGINE_MODEL = "claude-sonnet-4.6"; /** @type {string} */ let capturedIssueBody = ""; @@ -296,10 +297,11 @@ describe("handle_agent_failure", () => { try { await main(); - expect(capturedIssueBody).toContain("> Generated from [Test Workflow](https://github.com/owner/repo/actions/runs/123456) · 1.25 AIC · ⊞ 900"); + expect(capturedIssueBody).toContain("> Generated from [Test Workflow](https://github.com/owner/repo/actions/runs/123456) · sonnet46 1.25 AIC · ⊞ 900"); } finally { delete process.env.GH_AW_AIC; delete process.env.GH_AW_AMBIENT_CONTEXT; + delete process.env.GH_AW_ENGINE_MODEL; } }); diff --git a/actions/setup/js/handle_noop_message.cjs b/actions/setup/js/handle_noop_message.cjs index d435e3fd2cb..14633486c27 100644 --- a/actions/setup/js/handle_noop_message.cjs +++ b/actions/setup/js/handle_noop_message.cjs @@ -11,6 +11,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { isStagedMode } = require("./safe_output_helpers.cjs"); const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { formatAIC } = require("./model_costs.cjs"); +const { reduceModelNameToIdentifier } = require("./model_aliases.cjs"); /** * Search for or create the parent issue for all agentic workflow no-op runs * @returns {Promise<{number: number, node_id: string}>} Parent issue number and node ID @@ -87,11 +88,12 @@ function buildAICSuffix() { const detectionRaw = process.env.GH_AW_THREAT_DETECTION_AIC; const agentAIC = agentRaw ? Number.parseFloat(agentRaw) : NaN; const detectionAIC = detectionRaw ? Number.parseFloat(detectionRaw) : NaN; + const compressedModelName = reduceModelNameToIdentifier(process.env.GH_AW_PRIMARY_MODEL || process.env.GH_AW_ENGINE_MODEL); const totalAIC = (Number.isFinite(agentAIC) && agentAIC > 0 ? agentAIC : 0) + (Number.isFinite(detectionAIC) && detectionAIC > 0 ? detectionAIC : 0); if (totalAIC <= 0) { return ""; } - return ` · ${formatAIC(totalAIC)} AIC`; + return ` · ${compressedModelName ? `${compressedModelName} ` : ""}${formatAIC(totalAIC)} AIC`; } /** diff --git a/actions/setup/js/handle_noop_message.test.cjs b/actions/setup/js/handle_noop_message.test.cjs index 5f21d0a07e6..3bde070351e 100644 --- a/actions/setup/js/handle_noop_message.test.cjs +++ b/actions/setup/js/handle_noop_message.test.cjs @@ -763,6 +763,7 @@ This issue helps you: process.env.GH_AW_AGENT_CONCLUSION = "success"; process.env.GH_AW_AIC = "0.100"; process.env.GH_AW_THREAT_DETECTION_AIC = "0.025"; + process.env.GH_AW_ENGINE_MODEL = "claude-sonnet-4.6"; const outputFile = path.join(tempDir, "agent_output.json"); fs.writeFileSync(outputFile, JSON.stringify({ items: [{ type: "noop", message: "No action needed" }] })); @@ -777,7 +778,7 @@ This issue helps you: await main(); const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0]; - expect(commentCall.body).toContain("0.125 AIC"); + expect(commentCall.body).toContain("sonnet46 0.125 AIC"); }); it("should not include AIC suffix in comment footer when GH_AW_AIC is not set", async () => { diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs index 123ab252278..5de160f29ab 100644 --- a/actions/setup/js/messages.test.cjs +++ b/actions/setup/js/messages.test.cjs @@ -32,6 +32,7 @@ describe("messages.cjs", () => { delete process.env.GH_AW_ENGINE_ID; delete process.env.GH_AW_ENGINE_VERSION; delete process.env.GH_AW_ENGINE_MODEL; + delete process.env.GH_AW_PRIMARY_MODEL; delete process.env.GH_AW_TRACKER_ID; delete process.env.GITHUB_RUN_ID; delete process.env.GH_AW_WORKFLOW_ID; @@ -403,6 +404,7 @@ describe("messages.cjs", () => { it("should include AI Credits without effective tokens when GH_AW_AIC is set", async () => { process.env.GH_AW_EFFECTIVE_TOKENS = "5000"; process.env.GH_AW_AIC = "0.125"; + process.env.GH_AW_ENGINE_MODEL = "claude-sonnet-4.6"; const { getFooterMessage } = await import("./messages.cjs"); @@ -411,7 +413,7 @@ describe("messages.cjs", () => { runUrl: "https://github.com/test/repo/actions/runs/123", }); - expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · 0.125 AIC"); + expect(result).toBe("> Generated by [Test Workflow](https://github.com/test/repo/actions/runs/123) · sonnet46 0.125 AIC"); }); it("should include ambient context in the default footer when GH_AW_AMBIENT_CONTEXT is set", async () => { @@ -1142,6 +1144,7 @@ describe("messages.cjs", () => { it("should include AIC and ambient context in default footer when available", async () => { process.env.GH_AW_AIC = "1.25"; process.env.GH_AW_AMBIENT_CONTEXT = "900"; + process.env.GH_AW_ENGINE_MODEL = "claude-sonnet-4.6"; const { getFooterAgentFailureIssueMessage } = await import("./messages.cjs"); @@ -1150,7 +1153,7 @@ describe("messages.cjs", () => { runUrl: "https://github.com/test/repo/actions/runs/123", }); - expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · 1.25 AIC · ⊞ 900"); + expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · sonnet46 1.25 AIC · ⊞ 900"); }); it("should suppress env AIC fallback when explicit context AIC is zero", async () => { @@ -1241,6 +1244,7 @@ describe("messages.cjs", () => { it("should include ambient context in the default footer when available", async () => { process.env.GH_AW_AIC = "1.25"; process.env.GH_AW_AMBIENT_CONTEXT = "900"; + process.env.GH_AW_ENGINE_MODEL = "claude-sonnet-4.6"; const { getFooterAgentFailureCommentMessage } = await import("./messages.cjs"); @@ -1249,7 +1253,7 @@ describe("messages.cjs", () => { runUrl: "https://github.com/test/repo/actions/runs/123", }); - expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · 1.25 AIC · ⊞ 900"); + expect(result).toBe("> Generated from [Test Workflow](https://github.com/test/repo/actions/runs/123) · sonnet46 1.25 AIC · ⊞ 900"); }); it("should expose ai_credits_suffix in custom comment footer templates", async () => { diff --git a/actions/setup/js/messages_footer.cjs b/actions/setup/js/messages_footer.cjs index 71a72ef0a76..5956bfd75eb 100644 --- a/actions/setup/js/messages_footer.cjs +++ b/actions/setup/js/messages_footer.cjs @@ -14,6 +14,7 @@ const { getBlockedDomains, generateBlockedDomainsSection } = require("./firewall const { getDifcFilteredEvents, generateDifcFilteredSection } = require("./gateway_difc_filtered.cjs"); const { formatCompactInteger } = require("./compact_numbers.cjs"); const { formatAIC } = require("./model_costs.cjs"); +const { reduceModelNameToIdentifier } = require("./model_aliases.cjs"); const { getDetectionWarningMessage } = require("./messages_run_status.cjs"); /** @@ -76,15 +77,16 @@ function getAmbientContextFromEnv() { /** * @param {string} label * @param {number|undefined} value + * @param {string|undefined} [modelAlias] * @returns {{ value: number|undefined, formatted: string|undefined, suffix: string }} */ -function buildAICEntry(label, value) { +function buildAICEntry(label, value, modelAlias) { const formatted = typeof value === "number" ? formatAIC(value) : undefined; - const labelPrefix = label ? `${label} ` : ""; + const prefix = [label, modelAlias].filter(Boolean).join(" "); return { value, formatted, - suffix: formatted ? ` · ${labelPrefix}${formatted} AIC` : "", + suffix: formatted ? ` · ${prefix ? `${prefix} ` : ""}${formatted} AIC` : "", }; } @@ -95,6 +97,7 @@ function buildAICEntry(label, value) { * aiCredits: number|undefined, * aiCreditsFormatted: string|undefined, * aiCreditsSuffix: string, + * compressedModelName: string|undefined, * agentAiCredits: number|undefined, * agentAiCreditsFormatted: string|undefined, * agentAiCreditsSuffix: string, @@ -104,21 +107,23 @@ function buildAICEntry(label, value) { * }} */ function getAICFromEnv() { + const compressedModelName = reduceModelNameToIdentifier(process.env.GH_AW_PRIMARY_MODEL || process.env.GH_AW_ENGINE_MODEL); const totalAIC = parsePositiveAIC(process.env.GH_AW_AIC); const explicitAgentAIC = parsePositiveAIC(process.env.GH_AW_AGENT_AIC); const threatDetectionAIC = parsePositiveAIC(process.env.GH_AW_THREAT_DETECTION_AIC); const agentAIC = typeof explicitAgentAIC === "number" ? explicitAgentAIC : totalAIC; - const agentEntry = buildAICEntry("", agentAIC); + const agentEntry = buildAICEntry("", agentAIC, compressedModelName); const threatDetectionEntry = buildAICEntry("⌖", threatDetectionAIC); const useBreakdown = threatDetectionEntry.suffix.length > 0; const aiCredits = useBreakdown ? (agentAIC || 0) + (threatDetectionAIC || 0) : typeof totalAIC === "number" ? totalAIC : agentAIC; const aiCreditsFormatted = typeof aiCredits === "number" ? formatAIC(aiCredits) : undefined; - const aiCreditsSuffix = useBreakdown ? `${agentEntry.suffix}${threatDetectionEntry.suffix}` : aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : ""; + const aiCreditsSuffix = useBreakdown ? `${agentEntry.suffix}${threatDetectionEntry.suffix}` : buildAICEntry("", aiCredits, compressedModelName).suffix; return { aiCredits, aiCreditsFormatted, aiCreditsSuffix, + compressedModelName, agentAiCredits: agentEntry.value, agentAiCreditsFormatted: agentEntry.formatted, agentAiCreditsSuffix: agentEntry.suffix, @@ -158,6 +163,7 @@ function getFooterMessage(ctx) { aiCredits: envAIC, aiCreditsFormatted: envAICFormatted, aiCreditsSuffix: envAICSuffix, + compressedModelName, agentAiCredits, agentAiCreditsFormatted, agentAiCreditsSuffix, @@ -181,7 +187,7 @@ function getFooterMessage(ctx) { let aiCreditsSuffix = envAICSuffix; if (hasExplicitContextAIC) { aiCreditsFormatted = explicitContextAIC ? formatAIC(explicitContextAIC) : undefined; - aiCreditsSuffix = aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : ""; + aiCreditsSuffix = buildAICEntry("", explicitContextAIC, compressedModelName).suffix; } const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${envAmbientContextSuffix}`; @@ -377,6 +383,7 @@ function getFooterAgentFailureIssueMessage(ctx) { aiCredits: envAIC, aiCreditsFormatted: envAICFormatted, aiCreditsSuffix: envAICSuffix, + compressedModelName, agentAiCredits, agentAiCreditsFormatted, agentAiCreditsSuffix, @@ -389,7 +396,7 @@ function getFooterAgentFailureIssueMessage(ctx) { const explicitContextAIC = parseExplicitContextAIC(ctx.aiCredits); const aiCredits = hasExplicitContextAIC ? explicitContextAIC : envAIC; const aiCreditsFormatted = hasExplicitContextAIC ? (explicitContextAIC ? formatAIC(explicitContextAIC) : undefined) : envAICFormatted; - const aiCreditsSuffix = hasExplicitContextAIC ? (aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : "") : envAICSuffix; + const aiCreditsSuffix = hasExplicitContextAIC ? buildAICEntry("", explicitContextAIC, compressedModelName).suffix : envAICSuffix; const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${ambientContextSuffix}`; // Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url @@ -452,6 +459,7 @@ function getFooterAgentFailureCommentMessage(ctx) { aiCredits: envAIC, aiCreditsFormatted: envAICFormatted, aiCreditsSuffix: envAICSuffix, + compressedModelName, agentAiCredits, agentAiCreditsFormatted, agentAiCreditsSuffix, @@ -464,7 +472,7 @@ function getFooterAgentFailureCommentMessage(ctx) { const explicitContextAIC = parseExplicitContextAIC(ctx.aiCredits); const aiCredits = hasExplicitContextAIC ? explicitContextAIC : envAIC; const aiCreditsFormatted = hasExplicitContextAIC ? (explicitContextAIC ? formatAIC(explicitContextAIC) : undefined) : envAICFormatted; - const aiCreditsSuffix = hasExplicitContextAIC ? (aiCreditsFormatted ? ` · ${aiCreditsFormatted} AIC` : "") : envAICSuffix; + const aiCreditsSuffix = hasExplicitContextAIC ? buildAICEntry("", explicitContextAIC, compressedModelName).suffix : envAICSuffix; const aiCreditsSuffixForTemplate = `${aiCreditsSuffix}${ambientContextSuffix}`; // Create context with both camelCase and snake_case keys, including computed history_link and agentic_workflow_url diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs index 2e71f2b8052..5701cf10028 100644 --- a/actions/setup/js/parse_token_usage.cjs +++ b/actions/setup/js/parse_token_usage.cjs @@ -200,6 +200,11 @@ async function main() { }; fs.writeFileSync(AGENT_USAGE_PATH, JSON.stringify(agentUsage) + "\n"); + if (primaryModel) { + core.exportVariable("GH_AW_PRIMARY_MODEL", primaryModel); + core.setOutput("primary_model", primaryModel); + core.info(`Primary model: ${primaryModel}`); + } if (summary.totalAIC > 0) { const aic = summary.totalAIC.toFixed(3); core.exportVariable("GH_AW_AIC", aic); diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs index dffdeb99c7e..49ec46798eb 100644 --- a/actions/setup/js/parse_token_usage.test.cjs +++ b/actions/setup/js/parse_token_usage.test.cjs @@ -299,6 +299,9 @@ describe("parse_token_usage", () => { expect(typeof agentUsage.ai_credits).toBe("number"); // primary_model is the actual model from token-usage data (not a user alias) expect(agentUsage.primary_model).toBe("claude-sonnet-4-6"); + // GH_AW_PRIMARY_MODEL is exported so footer attribution can use the real model name + expect(mockCore.exportVariable).toHaveBeenCalledWith("GH_AW_PRIMARY_MODEL", "claude-sonnet-4-6"); + expect(mockCore.setOutput).toHaveBeenCalledWith("primary_model", "claude-sonnet-4-6"); }); test("handles multiple model entries", async () => {