fix(providers): enrich model_cooldown 429 body with retry_after ISO + credential count (#6460) (#6523)

fix(providers): enrich model_cooldown 429 body with retry_after ISO + credential count (#6460). Integrated into release/v3.8.47. (thanks @chirag127)
This commit is contained in:
Chirag Singhal
2026-07-08 04:19:57 +05:30
committed by GitHub
parent 5e5fb61fcc
commit 07eb2ecdd9
6 changed files with 108 additions and 4 deletions

View File

@@ -259,8 +259,8 @@
"src/shared/validation/schemas.ts": 2523,
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
"src/sse/handlers/chat.ts": 1751,
"src/sse/handlers/chatHelpers.ts": 860,
"src/sse/services/auth.ts": 2447,
"src/sse/handlers/chatHelpers.ts": 866,
"src/sse/services/auth.ts": 2448,
"open-sse/executors/default.ts": 877,
"open-sse/translator/request/openai-responses.ts": 902,
"open-sse/executors/kiro.ts": 944,
@@ -381,5 +381,6 @@
"_rebaseline_2026_07_06_v3845_release_close": "Release v3.8.45 cycle-close rebaseline (captain, sess ce897453): 13 files grown by the cycle's merged fix/feature PRs (#6216 streaming fixes + request-logger UI grew RequestLoggerV2/chat/chatHelpers/auth/stream/response-sanitizer.test; #6251/#6253 dashboard UX grew combos page/modals/wizard/ComboDefaultsTab/ProxyRegistryManager/providerPageHelpers). Growth is legitimate merged-feature code, absorbed at release per Phase 0 drift policy; all remain frozen (cannot grow further).",
"_rebaseline_2026_07_06_6118_zed_oauthmodal": "PR #6118 own growth: OAuthModal.tsx 989->993 (+4 = Zed hosted native-app sign-in modal branch). Cohesive UI growth for the zed-hosted OAuth provider; not extractable. The prior 6118 comment set the note but left the frozen value at 989.",
"_rebaseline_2026_07_06_6351_glm_team_quota": "PR #6351 own growth (GLM team-plan quota fields threaded through the connection modals; new GlmTeamQuotaFields.tsx extracted): AddApiKeyModal.tsx ->951 (+9), EditConnectionModal.tsx ->1277 (+18). Absorbs the pre-existing session base-red on these frozen modals; release captain rebaseline-at-release supersedes.",
"_rebaseline_2026_07_06_6499_unique_default_name": "PR #6499 own growth: AddApiKeyModal.tsx 952->959 (+7 = a unique default connection name so a second API key for the same provider does not reuse 'main' and trigger the backend name-based upsert that silently overwrote the first connection). The pure name derivation was extracted to computeConnectionDefaultName.ts (unit-tested) to keep the growth minimal; the contributor's original full-form-reset rewrite was trimmed to a spread reset to avoid dropping the GLM team-quota fields #6351 added and to hold the frozen god-file growth down. Release captain rebaseline-at-release supersedes."
}
"_rebaseline_2026_07_06_6499_unique_default_name": "PR #6499 own growth: AddApiKeyModal.tsx 952->959 (+7 = a unique default connection name so a second API key for the same provider does not reuse 'main' and trigger the backend name-based upsert that silently overwrote the first connection). The pure name derivation was extracted to computeConnectionDefaultName.ts (unit-tested) to keep the growth minimal; the contributor's original full-form-reset rewrite was trimmed to a spread reset to avoid dropping the GLM team-quota fields #6351 added and to hold the frozen god-file growth down. Release captain rebaseline-at-release supersedes.",
"_rebaseline_2026_07_07_6523_chirag_cooldown_body": "PR #6523 (@chirag127, #6460) own growth: chatHelpers.ts 860->866 (+6 = retryAfterAt/credentialsCoolingCount fields on modelCooldownResponse) and auth.ts 2447->2448 (+1 = connectionsCount threaded through no-credentials fallback). Owner-approved rebaseline (file-size cap for contributor PR). Frozen (cannot grow further); release captain's rebaseline-at-release supersedes."
}

View File

@@ -408,11 +408,23 @@ export function providerCircuitOpenResponse(
export function buildModelCooldownBody({
model,
retryAfterSec,
retryAfterAt,
credentialsCoolingCount,
}: {
model?: string | null;
retryAfterSec: number;
retryAfterAt?: string | null;
credentialsCoolingCount?: number | null;
}): ModelCooldownErrorPayload {
const resolvedModel = typeof model === "string" && model.trim().length > 0 ? model.trim() : null;
const resolvedRetryAfterAt =
typeof retryAfterAt === "string" && retryAfterAt.length > 0 ? retryAfterAt : null;
const resolvedCoolingCount =
typeof credentialsCoolingCount === "number" &&
Number.isFinite(credentialsCoolingCount) &&
credentialsCoolingCount > 0
? Math.floor(credentialsCoolingCount)
: null;
return {
error: {
@@ -423,6 +435,8 @@ export function buildModelCooldownBody({
code: "model_cooldown",
...(resolvedModel ? { model: resolvedModel } : {}),
reset_seconds: Math.max(Math.ceil(retryAfterSec), 1),
...(resolvedRetryAfterAt ? { retry_after: resolvedRetryAfterAt } : {}),
...(resolvedCoolingCount ? { credentials_cooling: resolvedCoolingCount } : {}),
},
};
}
@@ -430,16 +444,28 @@ export function buildModelCooldownBody({
export function modelCooldownResponse({
model,
retryAfter,
retryAfterAt,
credentialsCoolingCount,
}: {
model?: string | null;
retryAfter?: string | number | Date | null;
retryAfterAt?: string | null;
credentialsCoolingCount?: number | null;
}) {
const retryAfterSec = normalizeRetryAfterSeconds(retryAfter);
const resolvedRetryAfterAt =
typeof retryAfterAt === "string" && retryAfterAt.length > 0
? retryAfterAt
: typeof retryAfter === "string" && retryAfter.length > 0
? retryAfter
: null;
return new Response(
JSON.stringify(
buildModelCooldownBody({
model,
retryAfterSec,
retryAfterAt: resolvedRetryAfterAt,
credentialsCoolingCount,
})
),
{

View File

@@ -577,6 +577,12 @@ export function handleNoCredentials(
return modelCooldownResponse({
model: cooldownModel,
retryAfter: credentials.retryAfter,
retryAfterAt:
typeof credentials.retryAfter === "string" ? credentials.retryAfter : null,
credentialsCoolingCount:
typeof credentials.connectionsCount === "number"
? credentials.connectionsCount
: null,
});
}

View File

@@ -1377,6 +1377,7 @@ export async function getProviderCredentials(
lastErrorCode: allBlockedByModelCooldown ? 429 : earliestConn?.errorCode || null,
cooldownScope: allBlockedByModelCooldown ? "model" : "connection",
cooldownModel: allBlockedByModelCooldown ? requestedModel : null,
connectionsCount: connections.length,
};
}
const syntheticFallback = await maybeSyntheticNoAuthFallback(

View File

@@ -5,5 +5,7 @@ export interface ModelCooldownErrorPayload {
code: "model_cooldown";
model?: string;
reset_seconds: number;
retry_after?: string;
credentials_cooling?: number;
};
}

View File

@@ -14,3 +14,71 @@ test("types barrel supports the model cooldown error payload consumer", async ()
},
});
});
test("model cooldown body includes optional retry_after ISO + credentials_cooling count", async () => {
const { buildModelCooldownBody } = await import("../../open-sse/utils/error.ts");
const iso = "2026-07-07T12:34:56.000Z";
assert.deepEqual(
buildModelCooldownBody({
model: "openrouter/fusion",
retryAfterSec: 30,
retryAfterAt: iso,
credentialsCoolingCount: 3,
}),
{
error: {
message: "All credentials for model openrouter/fusion are cooling down",
type: "rate_limit_error",
code: "model_cooldown",
model: "openrouter/fusion",
reset_seconds: 30,
retry_after: iso,
credentials_cooling: 3,
},
}
);
});
test("model cooldown body omits retry_after / credentials_cooling when absent or invalid", async () => {
const { buildModelCooldownBody } = await import("../../open-sse/utils/error.ts");
assert.deepEqual(
buildModelCooldownBody({
model: "x",
retryAfterSec: 5,
retryAfterAt: null,
credentialsCoolingCount: 0,
}),
{
error: {
message: "All credentials for model x are cooling down",
type: "rate_limit_error",
code: "model_cooldown",
model: "x",
reset_seconds: 5,
},
}
);
});
test("modelCooldownResponse emits HTTP 429 with Retry-After header and retry_after ISO in body (#6460)", async () => {
const { modelCooldownResponse } = await import("../../open-sse/utils/error.ts");
const iso = "2026-07-07T12:34:56.000Z";
const res = modelCooldownResponse({
model: "openrouter/fusion",
retryAfter: iso,
credentialsCoolingCount: 4,
});
assert.equal(res.status, 429);
assert.ok(res.headers.get("Retry-After"), "Retry-After header must be set");
const body = await res.json();
assert.equal(body.error.code, "model_cooldown");
assert.equal(body.error.type, "rate_limit_error");
assert.equal(body.error.model, "openrouter/fusion");
assert.equal(body.error.retry_after, iso);
assert.equal(body.error.credentials_cooling, 4);
assert.equal(typeof body.error.reset_seconds, "number");
});