mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
Fix Combo
This commit is contained in:
@@ -76,7 +76,6 @@ export const PROVIDERS = {
|
|||||||
baseUrls: [
|
baseUrls: [
|
||||||
"https://daily-cloudcode-pa.googleapis.com",
|
"https://daily-cloudcode-pa.googleapis.com",
|
||||||
"https://cloudcode-pa.googleapis.com",
|
"https://cloudcode-pa.googleapis.com",
|
||||||
"https://daily-cloudcode-pa.sandbox.googleapis.com"
|
|
||||||
],
|
],
|
||||||
format: "antigravity",
|
format: "antigravity",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -189,21 +188,21 @@ export const DEFAULT_MIN_TOKENS = 32000;
|
|||||||
// Exponential backoff config for rate limits (like CLIProxyAPI)
|
// Exponential backoff config for rate limits (like CLIProxyAPI)
|
||||||
export const BACKOFF_CONFIG = {
|
export const BACKOFF_CONFIG = {
|
||||||
base: 1000, // 1 second base
|
base: 1000, // 1 second base
|
||||||
max: 30 * 60 * 1000, // 30 minutes max
|
max: 2 * 60 * 1000, // 2 minutes max
|
||||||
maxLevel: 15 // Cap backoff level
|
maxLevel: 15 // Cap backoff level
|
||||||
};
|
};
|
||||||
|
|
||||||
// Error-based cooldown times (aligned with CLIProxyAPI)
|
// Error-based cooldown times (aligned with CLIProxyAPI)
|
||||||
export const COOLDOWN_MS = {
|
export const COOLDOWN_MS = {
|
||||||
unauthorized: 30 * 60 * 1000, // 401 → 30 min
|
unauthorized: 2 * 60 * 1000, // 401 → 30 min
|
||||||
paymentRequired: 30 * 60 * 1000, // 402/403 → 30 min
|
paymentRequired: 2 * 60 * 1000, // 402/403 → 30 min
|
||||||
notFound: 12 * 60 * 60 * 1000, // 404 → 12 hours
|
notFound: 2 * 60 * 60 * 1000, // 404 → 12 hours
|
||||||
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
|
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
|
||||||
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
|
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
|
||||||
// Legacy aliases for backward compatibility
|
// Legacy aliases for backward compatibility
|
||||||
rateLimit: 15 * 60 * 1000,
|
rateLimit: 2 * 60 * 1000,
|
||||||
serviceUnavailable: 60 * 1000,
|
serviceUnavailable: 2 * 1000,
|
||||||
authExpired: 30 * 60 * 1000
|
authExpired: 2 * 60 * 1000
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip patterns - requests containing these texts will bypass provider
|
// Skip patterns - requests containing these texts will bypass provider
|
||||||
|
|||||||
@@ -121,6 +121,22 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse retry time from Antigravity error message body
|
||||||
|
// Format: "Your quota will reset after 2h7m23s" or "1h30m" or "45m" or "30s"
|
||||||
|
parseRetryFromErrorMessage(errorMessage) {
|
||||||
|
if (!errorMessage || typeof errorMessage !== "string") return null;
|
||||||
|
|
||||||
|
const match = errorMessage.match(/reset after (\d+h)?(\d+m)?(\d+s)?/i);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
let totalMs = 0;
|
||||||
|
if (match[1]) totalMs += parseInt(match[1]) * 3600 * 1000; // hours
|
||||||
|
if (match[2]) totalMs += parseInt(match[2]) * 60 * 1000; // minutes
|
||||||
|
if (match[3]) totalMs += parseInt(match[3]) * 1000; // seconds
|
||||||
|
|
||||||
|
return totalMs > 0 ? totalMs : null;
|
||||||
|
}
|
||||||
|
|
||||||
async execute({ model, body, stream, credentials, signal, log }) {
|
async execute({ model, body, stream, credentials, signal, log }) {
|
||||||
const fallbackCount = this.getFallbackCount();
|
const fallbackCount = this.getFallbackCount();
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
@@ -147,7 +163,20 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 429 || response.status === 503) {
|
if (response.status === 429 || response.status === 503) {
|
||||||
const retryMs = this.parseRetryHeaders(response.headers);
|
// Try to get retry time from headers first
|
||||||
|
let retryMs = this.parseRetryHeaders(response.headers);
|
||||||
|
|
||||||
|
// If no retry time in headers, try to parse from error message body
|
||||||
|
if (!retryMs) {
|
||||||
|
try {
|
||||||
|
const errorBody = await response.clone().text();
|
||||||
|
const errorJson = JSON.parse(errorBody);
|
||||||
|
const errorMessage = errorJson?.error?.message || errorJson?.message || "";
|
||||||
|
retryMs = this.parseRetryFromErrorMessage(errorMessage);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse errors, will fall back to exponential backoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (retryMs && retryMs <= MAX_RETRY_AFTER_MS) {
|
if (retryMs && retryMs <= MAX_RETRY_AFTER_MS) {
|
||||||
log?.debug?.("RETRY", `${response.status} with Retry-After: ${Math.ceil(retryMs/1000)}s, waiting...`);
|
log?.debug?.("RETRY", `${response.status} with Retry-After: ${Math.ceil(retryMs/1000)}s, waiting...`);
|
||||||
@@ -160,7 +189,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
if (response.status === 429 && (!retryMs || retryMs === 0) && retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES) {
|
if (response.status === 429 && (!retryMs || retryMs === 0) && retryAttemptsByUrl[urlIndex] < MAX_AUTO_RETRIES) {
|
||||||
retryAttemptsByUrl[urlIndex]++;
|
retryAttemptsByUrl[urlIndex]++;
|
||||||
// Exponential backoff: 2s, 4s, 8s...
|
// Exponential backoff: 2s, 4s, 8s...
|
||||||
const backoffMs = Math.min(1000 * Math.pow(2, retryAttemptsByUrl[urlIndex]), MAX_RETRY_AFTER_MS);
|
const backoffMs = Math.min(1000 * (2 ** retryAttemptsByUrl[urlIndex]), MAX_RETRY_AFTER_MS);
|
||||||
log?.debug?.("RETRY", `429 auto retry ${retryAttemptsByUrl[urlIndex]}/${MAX_AUTO_RETRIES} after ${backoffMs/1000}s`);
|
log?.debug?.("RETRY", `429 auto retry ${retryAttemptsByUrl[urlIndex]}/${MAX_AUTO_RETRIES} after ${backoffMs/1000}s`);
|
||||||
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||||||
urlIndex--;
|
urlIndex--;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { COOLDOWN_MS, BACKOFF_CONFIG } from "../config/constants.js";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate exponential backoff cooldown for rate limits (429)
|
* Calculate exponential backoff cooldown for rate limits (429)
|
||||||
* Level 0: 1s, Level 1: 2s, Level 2: 4s... → max 30 min
|
* Level 0: 1s, Level 1: 2s, Level 2: 4s... → max 2 min
|
||||||
* @param {number} backoffLevel - Current backoff level
|
* @param {number} backoffLevel - Current backoff level
|
||||||
* @returns {number} Cooldown in milliseconds
|
* @returns {number} Cooldown in milliseconds
|
||||||
*/
|
*/
|
||||||
@@ -22,12 +22,12 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
|||||||
// Check error message FIRST - specific patterns take priority over status codes
|
// Check error message FIRST - specific patterns take priority over status codes
|
||||||
if (errorText) {
|
if (errorText) {
|
||||||
const lowerError = errorText.toLowerCase();
|
const lowerError = errorText.toLowerCase();
|
||||||
|
|
||||||
// "Request not allowed" - short cooldown (5s), takes priority over status code
|
// "Request not allowed" - short cooldown (5s), takes priority over status code
|
||||||
if (lowerError.includes("request not allowed")) {
|
if (lowerError.includes("request not allowed")) {
|
||||||
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed };
|
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit keywords - exponential backoff
|
// Rate limit keywords - exponential backoff
|
||||||
if (
|
if (
|
||||||
lowerError.includes("rate limit") ||
|
lowerError.includes("rate limit") ||
|
||||||
@@ -37,8 +37,8 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
|||||||
lowerError.includes("overloaded")
|
lowerError.includes("overloaded")
|
||||||
) {
|
) {
|
||||||
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
||||||
return {
|
return {
|
||||||
shouldFallback: true,
|
shouldFallback: true,
|
||||||
cooldownMs: getQuotaCooldown(backoffLevel),
|
cooldownMs: getQuotaCooldown(backoffLevel),
|
||||||
newBackoffLevel: newLevel
|
newBackoffLevel: newLevel
|
||||||
};
|
};
|
||||||
@@ -63,8 +63,8 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
|||||||
// 429 - Rate limit with exponential backoff
|
// 429 - Rate limit with exponential backoff
|
||||||
if (status === 429) {
|
if (status === 429) {
|
||||||
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
||||||
return {
|
return {
|
||||||
shouldFallback: true,
|
shouldFallback: true,
|
||||||
cooldownMs: getQuotaCooldown(backoffLevel),
|
cooldownMs: getQuotaCooldown(backoffLevel),
|
||||||
newBackoffLevel: newLevel
|
newBackoffLevel: newLevel
|
||||||
};
|
};
|
||||||
@@ -134,10 +134,10 @@ export function resetAccountState(account) {
|
|||||||
*/
|
*/
|
||||||
export function applyErrorState(account, status, errorText) {
|
export function applyErrorState(account, status, errorText) {
|
||||||
if (!account) return account;
|
if (!account) return account;
|
||||||
|
|
||||||
const backoffLevel = account.backoffLevel || 0;
|
const backoffLevel = account.backoffLevel || 0;
|
||||||
const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel);
|
const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null,
|
rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
* Shared combo (model combo) handling with fallback support
|
* Shared combo (model combo) handling with fallback support
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { checkFallbackError } from "./accountFallback.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get combo models from combos data
|
* Get combo models from combos data
|
||||||
* @param {string} modelStr - Model string to check
|
* @param {string} modelStr - Model string to check
|
||||||
@@ -42,20 +44,34 @@ export async function handleComboChat({ body, models, handleSingleModel, log })
|
|||||||
|
|
||||||
// Success (2xx) - return response
|
// Success (2xx) - return response
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
log.info("COMBO", `Model ${modelStr} succeeded`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 unauthorized - return immediately (auth error)
|
// Extract error message from response
|
||||||
if (result.status === 401) {
|
let errorText = result.statusText || "";
|
||||||
|
try {
|
||||||
|
const errorBody = await result.clone().json();
|
||||||
|
errorText = errorBody.error || errorBody.message || errorText;
|
||||||
|
} catch {
|
||||||
|
// Ignore JSON parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if should fallback to next model
|
||||||
|
const { shouldFallback } = checkFallbackError(result.status, errorText);
|
||||||
|
|
||||||
|
if (!shouldFallback) {
|
||||||
|
// Don't fallback - return error immediately (e.g. 401 auth errors)
|
||||||
|
log.warn("COMBO", `Model ${modelStr} failed (no fallback)`, { status: result.status });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4xx/5xx - try next model
|
// Fallback to next model
|
||||||
lastError = `${modelStr}: ${result.statusText || result.status}`;
|
lastError = `${modelStr}: ${errorText || result.status}`;
|
||||||
log.warn("COMBO", `Model failed, trying next`, { model: modelStr, status: result.status });
|
log.warn("COMBO", `Model ${modelStr} failed, trying next`, { status: result.status, error: errorText.slice(0, 100) });
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn("COMBO", "All models failed");
|
log.warn("COMBO", "All combo models failed");
|
||||||
|
|
||||||
// Return 503 with last error
|
// Return 503 with last error
|
||||||
return new Response(
|
return new Response(
|
||||||
|
|||||||
+44
-1
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Card, Button, Input } from "@/shared/components";
|
import { Card, Button, Input } from "@/shared/components";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
@@ -8,8 +8,39 @@ export default function LoginPage() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [hasPassword, setHasPassword] = useState(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Check if password is set on mount
|
||||||
|
useEffect(() => {
|
||||||
|
async function checkPassword() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings");
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.password) {
|
||||||
|
// No password set - auto login
|
||||||
|
const loginRes = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ password: "123456" }),
|
||||||
|
});
|
||||||
|
if (loginRes.ok) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHasPassword(!!data.password);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to check password status:", err);
|
||||||
|
setHasPassword(true); // Default to showing login form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkPassword();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -36,6 +67,18 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading state while checking password
|
||||||
|
if (hasPassword === null) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<p className="text-text-muted mt-4">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user