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:
Catalin Stanciu
2026-01-06 16:46:07 +02:00
committed by decolua
parent 9c3d6f4ad8
commit 9ebd7d3062
3 changed files with 91 additions and 5 deletions

View File

@@ -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>

View File

@@ -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 });
}
}

View File

@@ -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,