mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- Cap maximum cooldown for rate limit handling in account unavailability and single-model chat flows
- Dynamic custom model fetching for model selection
This commit is contained in:
@@ -174,6 +174,7 @@ model = "${effectiveSubagentModel}"
|
||||
`;
|
||||
|
||||
const authContent = JSON.stringify({
|
||||
auth_mode: "apikey",
|
||||
OPENAI_API_KEY: keyToUse
|
||||
}, null, 2);
|
||||
|
||||
|
||||
@@ -159,7 +159,9 @@ export async function POST(request) {
|
||||
authData = JSON.parse(existingAuth);
|
||||
} catch { /* No existing auth */ }
|
||||
|
||||
// Force apikey mode (keep existing tokens untouched for ChatGPT login reuse)
|
||||
authData.OPENAI_API_KEY = apiKey;
|
||||
authData.auth_mode = "apikey";
|
||||
await fs.writeFile(authPath, JSON.stringify(authData, null, 2));
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -215,7 +217,8 @@ export async function DELETE() {
|
||||
const existingAuth = await fs.readFile(authPath, "utf-8");
|
||||
const authData = JSON.parse(existingAuth);
|
||||
delete authData.OPENAI_API_KEY;
|
||||
|
||||
delete authData.auth_mode;
|
||||
|
||||
// Write back or delete if empty
|
||||
if (Object.keys(authData).length === 0) {
|
||||
await fs.unlink(authPath);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getComboById, updateCombo, deleteCombo, getComboByName } from "@/lib/localDb";
|
||||
import { resetComboRotation } from "open-sse/services/combo.js";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
|
||||
@@ -40,12 +41,18 @@ export async function PUT(request, { params }) {
|
||||
}
|
||||
}
|
||||
|
||||
// Capture previous name to invalidate rotation state on rename
|
||||
const prev = await getComboById(id);
|
||||
const combo = await updateCombo(id, body);
|
||||
|
||||
if (!combo) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Invalidate rotation state (models/strategy/name may have changed)
|
||||
if (prev?.name) resetComboRotation(prev.name);
|
||||
if (combo.name && combo.name !== prev?.name) resetComboRotation(combo.name);
|
||||
|
||||
return NextResponse.json(combo);
|
||||
} catch (error) {
|
||||
console.log("Error updating combo:", error);
|
||||
@@ -57,11 +64,14 @@ export async function PUT(request, { params }) {
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const prev = await getComboById(id);
|
||||
const success = await deleteCombo(id);
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (prev?.name) resetComboRotation(prev.name);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy";
|
||||
import { setRtkEnabled } from "open-sse/rtk/flag.js";
|
||||
import { resetComboRotation } from "open-sse/services/combo.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function GET() {
|
||||
@@ -67,10 +67,14 @@ export async function PATCH(request) {
|
||||
applyOutboundProxyEnv(settings);
|
||||
}
|
||||
|
||||
// Sync RTK toggle immediately (sync cache for request hot path)
|
||||
if (Object.prototype.hasOwnProperty.call(body, "rtkEnabled")) {
|
||||
setRtkEnabled(settings.rtkEnabled);
|
||||
// Invalidate combo rotation state when strategy settings change
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(body, "comboStrategy") ||
|
||||
Object.prototype.hasOwnProperty.call(body, "comboStrategies")
|
||||
) {
|
||||
resetComboRotation();
|
||||
}
|
||||
|
||||
const { password, ...safeSettings } = settings;
|
||||
return NextResponse.json(safeSettings);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import "./globals.css";
|
||||
import { ThemeProvider } from "@/shared/components/ThemeProvider";
|
||||
import "@/lib/initCloudSync"; // Auto-initialize cloud sync
|
||||
import "@/lib/network/initOutboundProxy"; // Auto-initialize outbound proxy env
|
||||
import "@/lib/rtk/initRtk"; // Auto-initialize RTK toggle from DB
|
||||
import { initConsoleLogCapture } from "@/lib/consoleLogBuffer";
|
||||
import { RuntimeI18nProvider } from "@/i18n/RuntimeI18nProvider";
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
import { setRtkEnabled } from "open-sse/rtk/flag.js";
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export async function ensureRtkInitialized() {
|
||||
if (initialized) return true;
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
setRtkEnabled(settings.rtkEnabled === true);
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error("[ServerInit] Error initializing RTK flag:", error);
|
||||
}
|
||||
return initialized;
|
||||
}
|
||||
|
||||
ensureRtkInitialized().catch(console.log);
|
||||
|
||||
export default ensureRtkInitialized;
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Modal from "./Modal";
|
||||
import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, FREE_PROVIDERS, FREE_TIER_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, getProviderAlias } from "@/shared/constants/providers";
|
||||
|
||||
// Provider order: OAuth first, then Free Tier, then API Key (matches dashboard/providers)
|
||||
const PROVIDER_ORDER = [
|
||||
@@ -29,6 +29,7 @@ export default function ModelSelectModal({
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [combos, setCombos] = useState([]);
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
|
||||
const fetchCombos = async () => {
|
||||
try {
|
||||
@@ -62,6 +63,22 @@ export default function ModelSelectModal({
|
||||
if (isOpen) fetchProviderNodes();
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchCustomModels = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/models/custom");
|
||||
if (!res.ok) throw new Error(`Failed to fetch custom models: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setCustomModels(data.models || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching custom models:", error);
|
||||
setCustomModels([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchCustomModels();
|
||||
}, [isOpen]);
|
||||
|
||||
const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...FREE_PROVIDERS, ...FREE_TIER_PROVIDERS, ...APIKEY_PROVIDERS }), []);
|
||||
|
||||
// Group models by provider with priority order
|
||||
@@ -85,7 +102,7 @@ export default function ModelSelectModal({
|
||||
});
|
||||
|
||||
sortedProviderIds.forEach((providerId) => {
|
||||
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
|
||||
const alias = getProviderAlias(providerId);
|
||||
const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" };
|
||||
const isCustomProvider = isOpenAICompatibleProvider(providerId) || isAnthropicCompatibleProvider(providerId);
|
||||
|
||||
@@ -151,7 +168,7 @@ export default function ModelSelectModal({
|
||||
// Custom models: if no hardcoded models (e.g. openrouter), show all aliases for this provider
|
||||
// Otherwise only show aliases where aliasName === modelId ("Add Model" button pattern)
|
||||
const hasHardcoded = hardcodedModels.length > 0;
|
||||
const customModels = Object.entries(modelAliases)
|
||||
const customAliasModels = Object.entries(modelAliases)
|
||||
.filter(([aliasName, fullModel]) =>
|
||||
fullModel.startsWith(`${alias}/`) &&
|
||||
(hasHardcoded ? aliasName === fullModel.replace(`${alias}/`, "") : true) &&
|
||||
@@ -162,9 +179,16 @@ export default function ModelSelectModal({
|
||||
return { id: modelId, name: aliasName, value: fullModel, isCustom: true };
|
||||
});
|
||||
|
||||
// Custom models registered via /api/models/custom (provider "Add Model" button)
|
||||
const customAliasIds = new Set(customAliasModels.map((m) => m.id));
|
||||
const customRegisteredModels = customModels
|
||||
.filter((m) => m.providerAlias === alias && !hardcodedIds.has(m.id) && !customAliasIds.has(m.id))
|
||||
.map((m) => ({ id: m.id, name: m.name || m.id, value: `${alias}/${m.id}`, isCustom: true }));
|
||||
|
||||
const allModels = [
|
||||
...hardcodedModels.map((m) => ({ id: m.id, name: m.name, value: `${alias}/${m.id}` })),
|
||||
...customModels,
|
||||
...customAliasModels,
|
||||
...customRegisteredModels,
|
||||
];
|
||||
|
||||
if (allModels.length > 0) {
|
||||
@@ -179,7 +203,7 @@ export default function ModelSelectModal({
|
||||
});
|
||||
|
||||
return groups;
|
||||
}, [activeProviders, modelAliases, allProviders, providerNodes]);
|
||||
}, [activeProviders, modelAliases, allProviders, providerNodes, customModels]);
|
||||
|
||||
// Filter combos by search query
|
||||
const filteredCombos = useMemo(() => {
|
||||
@@ -304,7 +328,7 @@ export default function ModelSelectModal({
|
||||
const isPlaceholder = model.isPlaceholder;
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
key={model.value}
|
||||
onClick={() => handleSelect(model)}
|
||||
title={isPlaceholder ? "Select to pre-fill, then edit model ID in the input" : undefined}
|
||||
className={`
|
||||
|
||||
@@ -206,6 +206,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
|
||||
userAgent,
|
||||
apiKey,
|
||||
ccFilterNaming: !!chatSettings.ccFilterNaming,
|
||||
rtkEnabled: !!chatSettings.rtkEnabled,
|
||||
providerThinking,
|
||||
// Detect source format by endpoint + body
|
||||
sourceFormatOverride: request?.url ? detectFormatByEndpoint(new URL(request.url).pathname, body) : null,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getProviderConnections, validateApiKey, updateProviderConnection, getSettings } from "@/lib/localDb";
|
||||
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
||||
import { formatRetryAfter, checkFallbackError, isModelLockActive, buildModelLockUpdate, getEarliestModelLockUntil } from "open-sse/services/accountFallback.js";
|
||||
import { MAX_RATE_LIMIT_COOLDOWN_MS } from "open-sse/config/errorConfig.js";
|
||||
import { resolveProviderId, FREE_PROVIDERS } from "@/shared/constants/providers.js";
|
||||
import * as log from "../utils/logger.js";
|
||||
|
||||
@@ -179,7 +180,7 @@ export async function markAccountUnavailable(connectionId, status, errorText, pr
|
||||
let shouldFallback, cooldownMs, newBackoffLevel;
|
||||
if (resetsAtMs && resetsAtMs > Date.now()) {
|
||||
shouldFallback = true;
|
||||
cooldownMs = resetsAtMs - Date.now();
|
||||
cooldownMs = Math.min(resetsAtMs - Date.now(), MAX_RATE_LIMIT_COOLDOWN_MS);
|
||||
newBackoffLevel = 0;
|
||||
} else {
|
||||
({ shouldFallback, cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel));
|
||||
|
||||
Reference in New Issue
Block a user