mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
fix(kimi): recupera limite temporario sem bloquear conta (#10058)
This commit is contained in:
@@ -214,6 +214,7 @@ import { stageTrace } from "./chatCore/stageTrace.ts";
|
||||
import { attachCompressionUsageReceiptAfterAnalytics as attachCompressionUsageReceiptAfterAnalyticsFor } from "./chatCore/compressionUsageReceipt.ts";
|
||||
import { prepareUpstreamBody } from "./chatCore/upstreamBody.ts";
|
||||
import { getQuotaScopeLabelForProvider } from "../services/antigravityQuotaFamily.ts";
|
||||
import { getKimiTemporaryRateLimitResetAt } from "./chatCore/kimiQuotaRecovery.ts";
|
||||
import {
|
||||
getCallLogPipelineCaptureStreamChunks,
|
||||
getCallLogPipelineMaxSizeBytes,
|
||||
@@ -3734,8 +3735,25 @@ export async function handleChatCore({
|
||||
);
|
||||
}
|
||||
} else if (errorType === PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED) {
|
||||
// Kimi's 403 says "billing cycle" for both an exhausted subscription and a
|
||||
// temporary request window. Read its official usage endpoint before making
|
||||
// the connection terminal: a non-zero Weekly quota plus an empty Ratelimit
|
||||
// window must recover automatically at the reported reset time.
|
||||
let kimiRateLimitResetAt: string | null = null;
|
||||
if (provider === "kimi-coding") {
|
||||
try {
|
||||
const { fetchAndPersistProviderLimits } = await import("@/lib/usage/providerLimits");
|
||||
const { usage } = await fetchAndPersistProviderLimits(errorConnectionId, "manual");
|
||||
kimiRateLimitResetAt = getKimiTemporaryRateLimitResetAt(usage);
|
||||
} catch {
|
||||
// Preserve the existing quota handling when Kimi's usage endpoint is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
// Providers with per-model quotas — lock the model only, not the connection
|
||||
const quotaCooldownMs = retryAfterMs || COOLDOWN_MS.rateLimit;
|
||||
const quotaCooldownMs = kimiRateLimitResetAt
|
||||
? Math.max(new Date(kimiRateLimitResetAt).getTime() - Date.now(), 0)
|
||||
: retryAfterMs || COOLDOWN_MS.rateLimit;
|
||||
const accountSemaphoreKey = resolveAccountSemaphoreKey({
|
||||
provider,
|
||||
model: currentModel,
|
||||
@@ -3745,7 +3763,19 @@ export async function handleChatCore({
|
||||
if (accountSemaphoreKey) {
|
||||
markAccountSemaphoreBlocked(accountSemaphoreKey, quotaCooldownMs);
|
||||
}
|
||||
if (isModelScope() && errorConnectionId) {
|
||||
if (kimiRateLimitResetAt) {
|
||||
await updateProviderConnection(errorConnectionId, {
|
||||
testStatus: "unavailable",
|
||||
rateLimitedUntil: kimiRateLimitResetAt,
|
||||
backoffLevel: 0,
|
||||
lastErrorType: PROVIDER_ERROR_TYPES.RATE_LIMITED,
|
||||
lastError: message,
|
||||
errorCode: statusCode,
|
||||
});
|
||||
console.warn(
|
||||
`[provider] Node ${errorConnectionId} Kimi request window exhausted (${statusCode}) — retrying after ${kimiRateLimitResetAt}`
|
||||
);
|
||||
} else if (isModelScope() && errorConnectionId) {
|
||||
const lockFn = provider === "antigravity" ? lockExactModel : lockModel;
|
||||
lockFn(provider, errorConnectionId, model, "quota_exhausted", quotaCooldownMs);
|
||||
console.warn(
|
||||
|
||||
42
open-sse/handlers/chatCore/kimiQuotaRecovery.ts
Normal file
42
open-sse/handlers/chatCore/kimiQuotaRecovery.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function asRecord(value: unknown): JsonRecord | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
|
||||
}
|
||||
|
||||
function remaining(value: JsonRecord | null): number | null {
|
||||
if (!value) return null;
|
||||
const candidate = value.remaining ?? value.remainingPercentage;
|
||||
const parsed = typeof candidate === "number" ? candidate : Number(candidate);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kimi uses the same 403 wording for two different conditions:
|
||||
* a depleted weekly subscription and a temporary request window. The latter
|
||||
* must stay recoverable, otherwise a healthy subscription is marked terminal.
|
||||
*/
|
||||
export function getKimiTemporaryRateLimitResetAt(
|
||||
usage: unknown,
|
||||
nowMs = Date.now()
|
||||
): string | null {
|
||||
const quotas = asRecord(asRecord(usage)?.quotas);
|
||||
const rateLimit = asRecord(quotas?.Ratelimit);
|
||||
const weekly = asRecord(quotas?.Weekly);
|
||||
const rateLimitRemaining = remaining(rateLimit);
|
||||
const weeklyRemaining = remaining(weekly);
|
||||
const resetAt = typeof rateLimit?.resetAt === "string" ? rateLimit.resetAt : null;
|
||||
const resetMs = resetAt ? new Date(resetAt).getTime() : NaN;
|
||||
|
||||
if (
|
||||
rateLimitRemaining !== 0 ||
|
||||
weeklyRemaining === null ||
|
||||
weeklyRemaining <= 0 ||
|
||||
!Number.isFinite(resetMs) ||
|
||||
resetMs <= nowMs
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resetAt;
|
||||
}
|
||||
34
tests/unit/kimi-temporary-rate-limit.test.ts
Normal file
34
tests/unit/kimi-temporary-rate-limit.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { getKimiTemporaryRateLimitResetAt } from "../../open-sse/handlers/chatCore/kimiQuotaRecovery.ts";
|
||||
|
||||
const NOW = Date.parse("2026-08-11T01:00:00.000Z");
|
||||
const RESET_AT = "2026-08-11T04:49:07.783Z";
|
||||
|
||||
test("Kimi temporary request limit stays recoverable when weekly quota remains", () => {
|
||||
const resetAt = getKimiTemporaryRateLimitResetAt(
|
||||
{
|
||||
quotas: {
|
||||
Ratelimit: { remaining: 0, resetAt: RESET_AT },
|
||||
Weekly: { remaining: 14 },
|
||||
},
|
||||
},
|
||||
NOW
|
||||
);
|
||||
|
||||
assert.equal(resetAt, RESET_AT);
|
||||
});
|
||||
|
||||
test("Kimi depleted weekly quota is not mistaken for a temporary request limit", () => {
|
||||
const resetAt = getKimiTemporaryRateLimitResetAt(
|
||||
{
|
||||
quotas: {
|
||||
Ratelimit: { remaining: 0, resetAt: RESET_AT },
|
||||
Weekly: { remaining: 0 },
|
||||
},
|
||||
},
|
||||
NOW
|
||||
);
|
||||
|
||||
assert.equal(resetAt, null);
|
||||
});
|
||||
Reference in New Issue
Block a user