diff --git a/actions/setup/js/ai_credits_context.cjs b/actions/setup/js/ai_credits_context.cjs
index b2134d2630b..70ae4b3a79a 100644
--- a/actions/setup/js/ai_credits_context.cjs
+++ b/actions/setup/js/ai_credits_context.cjs
@@ -43,28 +43,12 @@ function parsePositiveNumberString(value) {
return "";
}
-/**
- * @param {string} left
- * @param {string} right
- * @returns {boolean}
- */
-function isNumberStringGreaterThanOrEqual(left, right) {
- if (!left || !right) return false;
- const leftNumber = Number.parseFloat(left);
- const rightNumber = Number.parseFloat(right);
- return Number.isFinite(leftNumber) && Number.isFinite(rightNumber) && leftNumber >= rightNumber;
-}
-
/**
* @param {boolean} hasRateLimitSignal
- * @param {string} aiCredits
- * @param {string} maxAICredits
* @returns {boolean}
*/
-function shouldReportAICreditsRateLimitError(hasRateLimitSignal, aiCredits, maxAICredits) {
- if (!hasRateLimitSignal) return false;
- if (!aiCredits || !maxAICredits) return true;
- return isNumberStringGreaterThanOrEqual(aiCredits, maxAICredits);
+function shouldReportAICreditsRateLimitError(hasRateLimitSignal) {
+ return hasRateLimitSignal;
}
/**
@@ -525,7 +509,7 @@ function resolveAICreditsFailureState({ logProvenance = true } = {}) {
const aiCredits = auditAICredits || stdioSignals.aiCredits || envAICredits || "";
const maxAICredits = auditMaxAICredits || stdioSignals.maxAICredits || envMaxAICredits || "";
const rawAICreditsRateLimitError = auditRateLimitError || stdioSignals.rateLimitError || envRateLimitSignalHasEvidence;
- const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError, aiCredits, maxAICredits);
+ const aiCreditsRateLimitError = shouldReportAICreditsRateLimitError(rawAICreditsRateLimitError);
return { aiCredits, maxAICredits, aiCreditsRateLimitError, maxAICreditsExceeded: auditMaxAICreditsExceeded || stdioSignals.maxAICreditsExceeded };
}
diff --git a/actions/setup/js/ai_credits_context.test.cjs b/actions/setup/js/ai_credits_context.test.cjs
index 6b7b5890a48..de8fc0cd29d 100644
--- a/actions/setup/js/ai_credits_context.test.cjs
+++ b/actions/setup/js/ai_credits_context.test.cjs
@@ -174,6 +174,28 @@ describe("ai_credits_context max_ai_credits_exceeded detection", () => {
expect(result.aiCredits).toBe("");
expect(result.maxAICredits).toBe("1000");
});
+
+ it("reports aiCreditsRateLimitError when rate-limit signal present but under budget", () => {
+ // Reproduces the failing run scenario: gateway log detected a 429 while the agent
+ // was under budget (236 AIC used vs 1000 max). Before the fix, the check
+ // `aiCredits >= maxAICredits` suppressed this signal, causing the conclusion handler
+ // to fall through to the generic "unexpected engine termination" path.
+ writeAuditLog([{ type: "response", ai_credits_rate_limit_error: true, ai_credits: 236, max_ai_credits: 1000 }]);
+ const result = resolveAICreditsFailureState();
+ expect(result.aiCreditsRateLimitError).toBe(true);
+ expect(result.maxAICreditsExceeded).toBe(false);
+ });
+
+ it("reports aiCreditsRateLimitError from env signal combined with env AIC evidence when under budget", () => {
+ writeAuditLog([{ type: "response", status: 200 }]);
+ process.env.GH_AW_AI_CREDITS_RATE_LIMIT_ERROR = "true";
+ process.env.GH_AW_AIC = "236.079";
+ process.env.GH_AW_MAX_AI_CREDITS = "1000";
+ const result = resolveAICreditsFailureState();
+ expect(result.aiCreditsRateLimitError).toBe(true);
+ expect(result.maxAICreditsExceeded).toBe(false);
+ expect(result.aiCredits).toBe("236.079");
+ });
});
});
diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs
index 9648332e61f..b1e56e2ee12 100644
--- a/actions/setup/js/handle_agent_failure.cjs
+++ b/actions/setup/js/handle_agent_failure.cjs
@@ -2050,9 +2050,10 @@ function readTokenUsageMarkdown() {
* @param {string} aiCredits
* @param {string} maxAICredits
* @param {string} runUrl
+ * @param {boolean} [isBudgetExceeded] - true when the agent exceeded the configured max-ai-credits budget; false when the 429 was a throughput throttle
* @returns {string}
*/
-function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredits, maxAICredits, runUrl) {
+function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredits, maxAICredits, runUrl, isBudgetExceeded = false) {
if (!hasAICreditsRateLimitError) {
return "";
}
@@ -2076,11 +2077,9 @@ function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredi
metricsSummary = ` Used \`${formattedAICredits}\`.`;
}
- // Suggest a new limit: 2x current max, or 2x actual usage if max is unknown, or a reasonable default
- const baseForSuggestion = Number.isFinite(numericMaxAICredits) && numericMaxAICredits > 0 ? numericMaxAICredits : Number.isFinite(numericAICredits) && numericAICredits > 0 ? numericAICredits : 0;
- const suggestedCredits = baseForSuggestion > 0 ? Math.ceil(baseForSuggestion * 2) : 2000;
-
- const templateName = "ai_credits_rate_limit_error.md";
+ // Use the budget-exceeded template when the agent exhausted its configured limit;
+ // use the throughput-throttle template when the 429 arrived before the budget was spent.
+ const templateName = isBudgetExceeded ? "ai_credits_rate_limit_error.md" : "ai_credits_rate_limit_throttle.md";
let templatePath = "";
try {
templatePath = getPromptPath(templateName);
@@ -2088,6 +2087,13 @@ function buildAICreditsRateLimitErrorContext(hasAICreditsRateLimitError, aiCredi
throw new Error(`failed to resolve template path for ${templateName} (${getErrorMessage(error)}); ensure RUNNER_TEMP or GH_AW_PROMPTS_DIR is set and the template file exists`, { cause: error });
}
+ let suggestedCredits;
+ if (isBudgetExceeded) {
+ // Suggest a new limit: 2x current max, or 2x actual usage if max is unknown, or a reasonable default.
+ const baseForSuggestion = Number.isFinite(numericMaxAICredits) && numericMaxAICredits > 0 ? numericMaxAICredits : Number.isFinite(numericAICredits) && numericAICredits > 0 ? numericAICredits : 0;
+ suggestedCredits = baseForSuggestion > 0 ? Math.ceil(baseForSuggestion * 2) : 2000;
+ }
+
try {
return (
"\n" +
@@ -3733,7 +3739,7 @@ async function main() {
// Build model not supported error context
const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError);
const http400ResponseErrorContext = buildHTTP400ResponseErrorContext(http400ResponseError);
- const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl);
+ const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl, maxAICreditsExceeded);
const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits);
// Build GitHub App token minting failure context
@@ -3955,7 +3961,7 @@ async function main() {
// Build model not supported error context
const modelNotSupportedErrorContext = buildModelNotSupportedErrorContext(modelNotSupportedError);
const http400ResponseErrorContext = buildHTTP400ResponseErrorContext(http400ResponseError);
- const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl);
+ const aiCreditsRateLimitErrorContext = buildAICreditsRateLimitErrorContext(aiCreditsRateLimitError || maxAICreditsExceeded, aiCredits, maxAICredits, runUrl, maxAICreditsExceeded);
const unknownModelAICreditsContext = buildUnknownModelAICreditsContext(unknownModelAICredits);
// Build GitHub App token minting failure context
diff --git a/actions/setup/js/handle_agent_failure_ai_credits_rate_limit.test.cjs b/actions/setup/js/handle_agent_failure_ai_credits_rate_limit.test.cjs
index 367032b61a5..34508e59176 100644
--- a/actions/setup/js/handle_agent_failure_ai_credits_rate_limit.test.cjs
+++ b/actions/setup/js/handle_agent_failure_ai_credits_rate_limit.test.cjs
@@ -20,7 +20,7 @@ describe("handle_agent_failure AI Credits rate-limit context", () => {
});
it("shows inline usage and overage without table, no run URL, with suggested limit snippet", () => {
- const rendered = buildAICreditsRateLimitErrorContext(true, "17.329230000000003", "10.1", "https://github.com/octo/repo/actions/runs/123");
+ const rendered = buildAICreditsRateLimitErrorContext(true, "17.329230000000003", "10.1", "https://github.com/octo/repo/actions/runs/123", true);
expect(rendered).toContain("AI Credits Budget Exceeded");
// inline metrics
@@ -41,6 +41,35 @@ describe("handle_agent_failure AI Credits rate-limit context", () => {
expect(rendered).not.toContain("Consult the billing dashboards for accurate usage and charges.");
});
+ it("shows throughput rate-limit message when under budget (isBudgetExceeded=false)", () => {
+ const rendered = buildAICreditsRateLimitErrorContext(true, "236.079", "1000", "https://github.com/octo/repo/actions/runs/123", false);
+
+ expect(rendered).toContain("AI Credits Rate Limit");
+ expect(rendered).not.toContain("AI Credits Budget Exceeded");
+ // inline metrics show usage without an overage
+ expect(rendered).toContain("Used `236.1` of `1K` max");
+ expect(rendered).not.toContain("over by");
+ // no "Increase the limit" section
+ expect(rendered).not.toContain("Increase the limit");
+ // tips section is still present
+ expect(rendered).toContain("Tips for reducing rate limit issues");
+ expect(rendered).toContain("https://github.github.com/gh-aw/reference/cost-management/");
+ });
+
+ it("defaults to throughput template when isBudgetExceeded is omitted", () => {
+ const rendered = buildAICreditsRateLimitErrorContext(true, "236.079", "1000", "");
+
+ expect(rendered).toContain("AI Credits Rate Limit");
+ expect(rendered).not.toContain("AI Credits Budget Exceeded");
+ });
+
+ it("shows throughput message without metrics when no credit data is available", () => {
+ const rendered = buildAICreditsRateLimitErrorContext(true, "", "", "", false);
+
+ expect(rendered).toContain("AI Credits Rate Limit");
+ expect(rendered).not.toContain("Used");
+ });
+
it("returns empty string when the AI Credits rate-limit did not trigger", () => {
expect(buildAICreditsRateLimitErrorContext(false, "17.3", "10", "")).toBe("");
});
diff --git a/actions/setup/js/handle_agent_failure_max_ai_credits_exceeded.test.cjs b/actions/setup/js/handle_agent_failure_max_ai_credits_exceeded.test.cjs
index a1e0f60b998..d35fba00017 100644
--- a/actions/setup/js/handle_agent_failure_max_ai_credits_exceeded.test.cjs
+++ b/actions/setup/js/handle_agent_failure_max_ai_credits_exceeded.test.cjs
@@ -21,7 +21,7 @@ describe("handle_agent_failure Max AI Credits exceeded context", () => {
});
it("shows budget exhaustion message with inline usage, limit, and overage — no table, no run URL", () => {
- const rendered = buildAICreditsRateLimitErrorContext(true, "105000", "100000", "https://github.com/octo/repo/actions/runs/456");
+ const rendered = buildAICreditsRateLimitErrorContext(true, "105000", "100000", "https://github.com/octo/repo/actions/runs/456", true);
expect(rendered).toContain("AI Credits Budget Exceeded");
expect(rendered).toContain("hit the configured `max-ai-credits` guardrail");
@@ -42,7 +42,7 @@ describe("handle_agent_failure Max AI Credits exceeded context", () => {
});
it("shows message without metrics when no credit data is available, still shows snippet with default limit", () => {
- const rendered = buildAICreditsRateLimitErrorContext(true, "", "", "");
+ const rendered = buildAICreditsRateLimitErrorContext(true, "", "", "", true);
expect(rendered).toContain("AI Credits Budget Exceeded");
expect(rendered).not.toContain("Used `");
@@ -53,7 +53,10 @@ describe("handle_agent_failure Max AI Credits exceeded context", () => {
});
it("does not show overage when usage does not exceed limit", () => {
- const rendered = buildAICreditsRateLimitErrorContext(true, "50000", "100000", "");
+ // isBudgetExceeded=true because the maxAICreditsExceeded signal was set (the credit amounts
+ // in the log may be sampled before the final over-budget request, so usage < max is possible
+ // even when the budget was truly exceeded).
+ const rendered = buildAICreditsRateLimitErrorContext(true, "50000", "100000", "", true);
expect(rendered).toContain("AI Credits Budget Exceeded");
expect(rendered).toContain("Used `50K` of `100K` max");
@@ -65,6 +68,6 @@ describe("handle_agent_failure Max AI Credits exceeded context", () => {
});
it("returns empty string when max_ai_credits_exceeded is false", () => {
- expect(buildAICreditsRateLimitErrorContext(false, "105000", "100000", "https://github.com/octo/repo/actions/runs/456")).toBe("");
+ expect(buildAICreditsRateLimitErrorContext(false, "105000", "100000", "https://github.com/octo/repo/actions/runs/456", true)).toBe("");
});
});
diff --git a/actions/setup/md/ai_credits_rate_limit_throttle.md b/actions/setup/md/ai_credits_rate_limit_throttle.md
new file mode 100644
index 00000000000..b69c3f99487
--- /dev/null
+++ b/actions/setup/md/ai_credits_rate_limit_throttle.md
@@ -0,0 +1,14 @@
+> [!WARNING]
+> **AI Credits Rate Limit**
+>
+> The Copilot API returned a rate limit response (HTTP 429), but the workflow did not report the explicit AI credits budget-exceeded guardrail signal.{metrics_summary}
+
+
+Tips for reducing rate limit issues
+
+- Review the [cost optimization guidance](https://github.github.com/gh-aw/reference/cost-management/).
+- Reduce unnecessary model or tool calls in the prompt.
+- Trim large inputs or excess context that does not change the outcome.
+- Split large tasks across smaller runs when possible.
+
+