From 25c2ad7360900178a8c071424412ad6f8a5c1e4d Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 27 Feb 2026 10:29:11 +0700 Subject: [PATCH] feat: implement model lock functionality for connection management --- open-sse/services/accountFallback.js | 58 ++++++ src/app/(dashboard)/dashboard/combos/page.js | 169 ++++++++++-------- .../dashboard/providers/[id]/page.js | 25 ++- .../dashboard/providers/[id]/page.new.js | 24 ++- .../(dashboard)/dashboard/providers/page.js | 3 +- src/sse/services/auth.js | 145 ++++----------- 6 files changed, 219 insertions(+), 205 deletions(-) diff --git a/open-sse/services/accountFallback.js b/open-sse/services/accountFallback.js index 276cb82b..0e1b2d1b 100644 --- a/open-sse/services/accountFallback.js +++ b/open-sse/services/accountFallback.js @@ -138,6 +138,64 @@ export function formatRetryAfter(rateLimitedUntil) { return `reset after ${parts.join(" ")}`; } +/** Prefix for model lock flat fields on connection record */ +export const MODEL_LOCK_PREFIX = "modelLock_"; + +/** Special key used when no model is known (account-level lock) */ +export const MODEL_LOCK_ALL = `${MODEL_LOCK_PREFIX}__all`; + +/** Build the flat field key for a model lock */ +export function getModelLockKey(model) { + return model ? `${MODEL_LOCK_PREFIX}${model}` : MODEL_LOCK_ALL; +} + +/** + * Check if a model lock on a connection is still active. + * Reads flat field `modelLock_${model}` (or `modelLock___all` when model=null). + */ +export function isModelLockActive(connection, model) { + const key = getModelLockKey(model); + const expiry = connection[key] || connection[MODEL_LOCK_ALL]; + if (!expiry) return false; + return new Date(expiry).getTime() > Date.now(); +} + +/** + * Get earliest active model lock expiry across all modelLock_* fields. + * Used for UI cooldown display. + */ +export function getEarliestModelLockUntil(connection) { + if (!connection) return null; + let earliest = null; + const now = Date.now(); + for (const [key, val] of Object.entries(connection)) { + if (!key.startsWith(MODEL_LOCK_PREFIX) || !val) continue; + const t = new Date(val).getTime(); + if (t <= now) continue; + if (!earliest || t < earliest) earliest = t; + } + return earliest ? new Date(earliest).toISOString() : null; +} + +/** + * Build update object to set a model lock on a connection. + */ +export function buildModelLockUpdate(model, cooldownMs) { + const key = getModelLockKey(model); + return { [key]: new Date(Date.now() + cooldownMs).toISOString() }; +} + +/** + * Build update object to clear all model locks on a connection. + */ +export function buildClearModelLocksUpdate(connection) { + const cleared = {}; + for (const key of Object.keys(connection)) { + if (key.startsWith(MODEL_LOCK_PREFIX)) cleared[key] = null; + } + return cleared; +} + /** * Filter available accounts (not in cooldown) */ diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index 1e436597..31e7dfed 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -31,10 +31,7 @@ export default function CombosPage() { if (combosRes.ok) setCombos(combosData.combos || []); if (providersRes.ok) { - const active = (providersData.connections || []).filter( - c => c.testStatus === "active" || c.testStatus === "success" - ); - setActiveProviders(active); + setActiveProviders(providersData.connections || []); } } catch (error) { console.log("Error fetching data:", error); @@ -228,6 +225,80 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) { ); } +// Inline editable model item +function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown, onRemove }) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(model); + + const commit = () => { + const trimmed = draft.trim(); + if (trimmed && trimmed !== model) onEdit(trimmed); + else setDraft(model); // revert if empty or unchanged + setEditing(false); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") commit(); + if (e.key === "Escape") { setDraft(model); setEditing(false); } + }; + + return ( +
+ {/* Index badge */} + {index + 1} + + {/* Inline editable model value */} + {editing ? ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={handleKeyDown} + className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-white dark:bg-black/20 border border-primary/40 rounded outline-none text-text-main" + /> + ) : ( +
setEditing(true)} + title="Click to edit" + > + {model} +
+ )} + + {/* Priority arrows */} +
+ + +
+ + {/* Remove */} + +
+ ); +} + function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { // Initialize state with combo values - key prop on parent handles reset on remount const [name, setName] = useState(combo?.name || ""); @@ -236,25 +307,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const [saving, setSaving] = useState(false); const [nameError, setNameError] = useState(""); const [modelAliases, setModelAliases] = useState({}); - const [providerNodes, setProviderNodes] = useState([]); const fetchModalData = async () => { try { - const [aliasesRes, nodesRes] = await Promise.all([ - fetch("/api/models/alias"), - fetch("/api/provider-nodes"), - ]); - - if (!aliasesRes.ok || !nodesRes.ok) { - throw new Error(`Failed to fetch data: aliases=${aliasesRes.status}, nodes=${nodesRes.status}`); - } - - const [aliasesData, nodesData] = await Promise.all([ - aliasesRes.json(), - nodesRes.json(), - ]); + const aliasesRes = await fetch("/api/models/alias"); + if (!aliasesRes.ok) return; + const aliasesData = await aliasesRes.json(); setModelAliases(aliasesData.aliases || {}); - setProviderNodes(nodesData.nodes || []); } catch (error) { console.error("Error fetching modal data:", error); } @@ -294,21 +353,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { setModels(models.filter((_, i) => i !== index)); }; - // Format model display name with readable provider name - const formatModelDisplay = useCallback((modelValue) => { - const parts = modelValue.split('/'); - if (parts.length !== 2) return modelValue; - - const [providerId, modelId] = parts; - const matchedNode = providerNodes.find(node => node.id === providerId); - - if (matchedNode) { - return `${matchedNode.name}/${modelId}`; - } - - return modelValue; - }, [providerNodes]); - const handleMoveUp = (index) => { if (index === 0) return; const newModels = [...models]; @@ -366,52 +410,26 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { ) : (
{models.map((model, index) => ( -
- {/* Index badge */} - {index + 1} - - {/* Model display - show readable name only */} -
- {formatModelDisplay(model)} -
- - {/* Priority arrows - horizontal, always visible */} -
- - -
- - {/* Remove - always visible */} - -
+ index={index} + model={model} + isFirst={index === 0} + isLast={index === models.length - 1} + onEdit={(newVal) => { + const updated = [...models]; + updated[index] = newVal; + setModels(updated); + }} + onMoveUp={() => handleMoveUp(index)} + onMoveDown={() => handleMoveDown(index)} + onRemove={() => handleRemoveModel(index)} + /> ))}
)} - {/* Add Model button - moved to bottom */} + {/* Add Model button */}