From c39eca6d4e6731aef20245fde11c2e607ca16211 Mon Sep 17 00:00:00 2001 From: decolua Date: Wed, 14 Jan 2026 14:55:47 +0700 Subject: [PATCH] Fix Combo --- open-sse/config/constants.js | 15 +++++----- open-sse/executors/antigravity.js | 33 ++++++++++++++++++-- open-sse/services/accountFallback.js | 18 +++++------ open-sse/services/combo.js | 28 +++++++++++++---- src/app/login/page.js | 45 +++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 26 deletions(-) diff --git a/open-sse/config/constants.js b/open-sse/config/constants.js index db845f7..1b0e7ee 100644 --- a/open-sse/config/constants.js +++ b/open-sse/config/constants.js @@ -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 diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index 90c85c1..e9cc438 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -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--; diff --git a/open-sse/services/accountFallback.js b/open-sse/services/accountFallback.js index f58aa27..7b813eb 100644 --- a/open-sse/services/accountFallback.js +++ b/open-sse/services/accountFallback.js @@ -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, diff --git a/open-sse/services/combo.js b/open-sse/services/combo.js index a3597de..85a874b 100644 --- a/open-sse/services/combo.js +++ b/open-sse/services/combo.js @@ -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( diff --git a/src/app/login/page.js b/src/app/login/page.js index 81bb749..e63511c 100644 --- a/src/app/login/page.js +++ b/src/app/login/page.js @@ -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 ( +
+
+
+

Loading...

+
+
+ ); + } + return (