mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix Combo
This commit is contained in:
@@ -76,7 +76,6 @@ export const PROVIDERS = {
|
||||
baseUrls: [
|
||||
"https://daily-cloudcode-pa.googleapis.com",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
"https://daily-cloudcode-pa.sandbox.googleapis.com"
|
||||
],
|
||||
format: "antigravity",
|
||||
headers: {
|
||||
@@ -189,21 +188,21 @@ export const DEFAULT_MIN_TOKENS = 32000;
|
||||
// Exponential backoff config for rate limits (like CLIProxyAPI)
|
||||
export const BACKOFF_CONFIG = {
|
||||
base: 1000, // 1 second base
|
||||
max: 30 * 60 * 1000, // 30 minutes max
|
||||
max: 2 * 60 * 1000, // 2 minutes max
|
||||
maxLevel: 15 // Cap backoff level
|
||||
};
|
||||
|
||||
// Error-based cooldown times (aligned with CLIProxyAPI)
|
||||
export const COOLDOWN_MS = {
|
||||
unauthorized: 30 * 60 * 1000, // 401 → 30 min
|
||||
paymentRequired: 30 * 60 * 1000, // 402/403 → 30 min
|
||||
notFound: 12 * 60 * 60 * 1000, // 404 → 12 hours
|
||||
unauthorized: 2 * 60 * 1000, // 401 → 30 min
|
||||
paymentRequired: 2 * 60 * 1000, // 402/403 → 30 min
|
||||
notFound: 2 * 60 * 60 * 1000, // 404 → 12 hours
|
||||
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
|
||||
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
|
||||
// Legacy aliases for backward compatibility
|
||||
rateLimit: 15 * 60 * 1000,
|
||||
serviceUnavailable: 60 * 1000,
|
||||
authExpired: 30 * 60 * 1000
|
||||
rateLimit: 2 * 60 * 1000,
|
||||
serviceUnavailable: 2 * 1000,
|
||||
authExpired: 2 * 60 * 1000
|
||||
};
|
||||
|
||||
// Skip patterns - requests containing these texts will bypass provider
|
||||
|
||||
@@ -121,6 +121,22 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
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 }) {
|
||||
const fallbackCount = this.getFallbackCount();
|
||||
let lastError = null;
|
||||
@@ -147,7 +163,20 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
});
|
||||
|
||||
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) {
|
||||
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) {
|
||||
retryAttemptsByUrl[urlIndex]++;
|
||||
// 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`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
||||
urlIndex--;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { COOLDOWN_MS, BACKOFF_CONFIG } from "../config/constants.js";
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
if (errorText) {
|
||||
const lowerError = errorText.toLowerCase();
|
||||
|
||||
|
||||
// "Request not allowed" - short cooldown (5s), takes priority over status code
|
||||
if (lowerError.includes("request not allowed")) {
|
||||
return { shouldFallback: true, cooldownMs: COOLDOWN_MS.requestNotAllowed };
|
||||
}
|
||||
|
||||
|
||||
// Rate limit keywords - exponential backoff
|
||||
if (
|
||||
lowerError.includes("rate limit") ||
|
||||
@@ -37,8 +37,8 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
||||
lowerError.includes("overloaded")
|
||||
) {
|
||||
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
||||
return {
|
||||
shouldFallback: true,
|
||||
return {
|
||||
shouldFallback: true,
|
||||
cooldownMs: getQuotaCooldown(backoffLevel),
|
||||
newBackoffLevel: newLevel
|
||||
};
|
||||
@@ -63,8 +63,8 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
|
||||
// 429 - Rate limit with exponential backoff
|
||||
if (status === 429) {
|
||||
const newLevel = Math.min(backoffLevel + 1, BACKOFF_CONFIG.maxLevel);
|
||||
return {
|
||||
shouldFallback: true,
|
||||
return {
|
||||
shouldFallback: true,
|
||||
cooldownMs: getQuotaCooldown(backoffLevel),
|
||||
newBackoffLevel: newLevel
|
||||
};
|
||||
@@ -134,10 +134,10 @@ export function resetAccountState(account) {
|
||||
*/
|
||||
export function applyErrorState(account, status, errorText) {
|
||||
if (!account) return account;
|
||||
|
||||
|
||||
const backoffLevel = account.backoffLevel || 0;
|
||||
const { cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel);
|
||||
|
||||
|
||||
return {
|
||||
...account,
|
||||
rateLimitedUntil: cooldownMs > 0 ? getUnavailableUntil(cooldownMs) : null,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Shared combo (model combo) handling with fallback support
|
||||
*/
|
||||
|
||||
import { checkFallbackError } from "./accountFallback.js";
|
||||
|
||||
/**
|
||||
* Get combo models from combos data
|
||||
* @param {string} modelStr - Model string to check
|
||||
@@ -42,20 +44,34 @@ export async function handleComboChat({ body, models, handleSingleModel, log })
|
||||
|
||||
// Success (2xx) - return response
|
||||
if (result.ok) {
|
||||
log.info("COMBO", `Model ${modelStr} succeeded`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 401 unauthorized - return immediately (auth error)
|
||||
if (result.status === 401) {
|
||||
// Extract error message from response
|
||||
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;
|
||||
}
|
||||
|
||||
// 4xx/5xx - try next model
|
||||
lastError = `${modelStr}: ${result.statusText || result.status}`;
|
||||
log.warn("COMBO", `Model failed, trying next`, { model: modelStr, status: result.status });
|
||||
// Fallback to next model
|
||||
lastError = `${modelStr}: ${errorText || 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 new Response(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Input } from "@/shared/components";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -8,8 +8,39 @@ export default function LoginPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasPassword, setHasPassword] = useState(null);
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
||||
<div className="w-full max-w-md">
|
||||
|
||||
Reference in New Issue
Block a user