mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
feat: add round-robin routing strategy
Implements a round-robin (least recently used) account selection strategy alongside the existing fill-first priority system. Adds a toggle in the Profile dashboard to switch between strategies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Badge, Toggle } from "@/shared/components";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { theme, setTheme, isDark } = useTheme();
|
||||
const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setSettings(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch settings:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateFallbackStrategy = async (strategy) => {
|
||||
try {
|
||||
const res = await fetch("/api/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fallbackStrategy: strategy }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSettings(prev => ({ ...prev, fallbackStrategy: strategy }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to update settings:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@@ -28,6 +59,31 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Routing Preferences */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Routing Strategy</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Round Robin</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
Cycle through accounts to distribute load
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.fallbackStrategy === "round-robin"}
|
||||
onChange={() => updateFallbackStrategy(settings.fallbackStrategy === "round-robin" ? "fill-first" : "round-robin")}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted italic pt-2 border-t border-border/50">
|
||||
{settings.fallbackStrategy === "round-robin"
|
||||
? "Currently distributing requests across all available accounts."
|
||||
: "Currently using accounts in priority order (Fill First)."}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Theme Preferences */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Appearance</h3>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -10,3 +10,14 @@ export async function GET() {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const settings = await updateSettings(body);
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
console.log("Error updating settings:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { getProviderConnections, validateApiKey, updateProviderConnection } from "@/lib/localDb";
|
||||
import { getProviderConnections, validateApiKey, updateProviderConnection, getSettings } from "@/lib/localDb";
|
||||
import { isAccountUnavailable, getUnavailableUntil } from "open-sse/services/accountFallback.js";
|
||||
import * as log from "../utils/logger.js";
|
||||
|
||||
/**
|
||||
* Get provider credentials from localDb
|
||||
* Filters out unavailable accounts and returns the highest priority available account
|
||||
* Filters out unavailable accounts and returns the selected account based on strategy
|
||||
* @param {string} provider - Provider name
|
||||
* @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account)
|
||||
*/
|
||||
export async function getProviderCredentials(provider, excludeConnectionId = null) {
|
||||
const connections = await getProviderConnections({ provider, isActive: true });
|
||||
|
||||
|
||||
if (connections.length === 0) {
|
||||
log.warn("AUTH", `No credentials for ${provider}`);
|
||||
return null;
|
||||
@@ -28,7 +28,26 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
return null;
|
||||
}
|
||||
|
||||
const connection = availableConnections[0];
|
||||
const settings = await getSettings();
|
||||
const strategy = settings.fallbackStrategy || "fill-first";
|
||||
|
||||
let connection;
|
||||
if (strategy === "round-robin") {
|
||||
// Sort by lastUsed (nulls first) to pick the least recently used
|
||||
const sorted = [...availableConnections].sort((a, b) => {
|
||||
if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999);
|
||||
if (!a.lastUsedAt) return -1;
|
||||
if (!b.lastUsedAt) return 1;
|
||||
return new Date(a.lastUsedAt) - new Date(b.lastUsedAt);
|
||||
});
|
||||
connection = sorted[0];
|
||||
|
||||
// Update lastUsedAt asynchronously
|
||||
updateProviderConnection(connection.id, { lastUsedAt: new Date().toISOString() }).catch(() => {});
|
||||
} else {
|
||||
// Default: fill-first (already sorted by priority in getProviderConnections)
|
||||
connection = availableConnections[0];
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: connection.apiKey,
|
||||
|
||||
Reference in New Issue
Block a user