feat(combos): drag-and-drop reorder with manual sortOrder
- schema.js: add sortOrder REAL column to combos, bump SCHEMA_VERSION 4->5
- combosRepo: persist sortOrder, append new combos to end, add reorderCombos() atomic reorder
- export/import DB: include sortOrder for round-trip
- API: PUT /api/combos/order accepts { ids: string[] } and persists new order
- UI: dnd-kit DndContext + SortableContext wrap flat list; drag handle (grip icon) on each ComboCard; optimistic local reorder with revert on failure
Grouped-by-tag view keeps server-side order; reorder is only available in the unfiltered flat list.
This commit is contained in:
@@ -56,6 +56,11 @@ export default function CombosPage() {
|
|||||||
const { getCaps } = useModelCaps();
|
const { getCaps } = useModelCaps();
|
||||||
const [confirmState, setConfirmState] = useState(null);
|
const [confirmState, setConfirmState] = useState(null);
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
// Reorder sensors: small activation distance keeps click-to-edit, drag-to-reorder.
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||||
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||||
|
);
|
||||||
// Tag filter is additive (OR). Empty set = show all. "Untagged" is a
|
// Tag filter is additive (OR). Empty set = show all. "Untagged" is a
|
||||||
// synthetic filter that matches combos whose tags array is empty.
|
// synthetic filter that matches combos whose tags array is empty.
|
||||||
const [activeTagFilters, setActiveTagFilters] = useState(() => new Set());
|
const [activeTagFilters, setActiveTagFilters] = useState(() => new Set());
|
||||||
@@ -219,6 +224,25 @@ export default function CombosPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Persist a manual reorder. Optimistic: re-order the local state first,
|
||||||
|
// then push the new id list to the API. A failure reverts via fetchData().
|
||||||
|
const handleReorder = useCallback(async (oldIndex, newIndex) => {
|
||||||
|
if (oldIndex === newIndex) return;
|
||||||
|
const next = arrayMove(combos, oldIndex, newIndex);
|
||||||
|
setCombos(next);
|
||||||
|
const ids = next.map((c) => c.id);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/combos/order", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ ids }),
|
||||||
|
});
|
||||||
|
if (!res.ok) await fetchData();
|
||||||
|
} catch {
|
||||||
|
await fetchData();
|
||||||
|
}
|
||||||
|
}, [combos]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Apply OR-of-active-tags filter. Empty set = pass through unchanged.
|
// Apply OR-of-active-tags filter. Empty set = pass through unchanged.
|
||||||
const visibleCombos = activeTagFilters.size === 0
|
const visibleCombos = activeTagFilters.size === 0
|
||||||
? combos
|
? combos
|
||||||
@@ -341,23 +365,43 @@ export default function CombosPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<DndContext
|
||||||
{combos.map((combo) => (
|
sensors={sensors}
|
||||||
<ComboCard
|
collisionDetection={closestCenter}
|
||||||
key={combo.id}
|
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||||
combo={combo}
|
onDragEnd={(e) => {
|
||||||
getCaps={getCaps}
|
const { active, over } = e;
|
||||||
activeProviders={activeProviders}
|
if (!over || active.id === over.id) return;
|
||||||
copied={copied}
|
const oldIndex = combos.findIndex((c) => c.id === active.id);
|
||||||
onCopy={copy}
|
const newIndex = combos.findIndex((c) => c.id === over.id);
|
||||||
onEdit={() => setEditingCombo(combo)}
|
if (oldIndex < 0 || newIndex < 0) return;
|
||||||
onDelete={() => handleDelete(combo.id)}
|
handleReorder(oldIndex, newIndex);
|
||||||
strategy={comboStrategies[combo.name] || {}}
|
}}
|
||||||
globalStrategy={globalComboStrategy}
|
>
|
||||||
onSetStrategy={(patch) => handleSetComboStrategy(combo.name, patch)}
|
<SortableContext items={combos.map((c) => c.id)} strategy={verticalListSortingStrategy}>
|
||||||
/>
|
<div className="flex flex-col gap-4">
|
||||||
))}
|
{combos.map((combo) => (
|
||||||
</div>
|
<SortableComboCard key={combo.id} id={combo.id}>
|
||||||
|
{(handle) => (
|
||||||
|
<ComboCard
|
||||||
|
combo={combo}
|
||||||
|
getCaps={getCaps}
|
||||||
|
activeProviders={activeProviders}
|
||||||
|
copied={copied}
|
||||||
|
onCopy={copy}
|
||||||
|
onEdit={() => setEditingCombo(combo)}
|
||||||
|
onDelete={() => handleDelete(combo.id)}
|
||||||
|
strategy={comboStrategies[combo.name] || {}}
|
||||||
|
globalStrategy={globalComboStrategy}
|
||||||
|
onSetStrategy={(patch) => handleSetComboStrategy(combo.name, patch)}
|
||||||
|
dragHandle={handle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SortableComboCard>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
<CapacityAdapterSection
|
<CapacityAdapterSection
|
||||||
capacityAdapter={capacityAdapter}
|
capacityAdapter={capacityAdapter}
|
||||||
@@ -407,7 +451,7 @@ const STRATEGY_OPTIONS = [
|
|||||||
{ value: "fusion", label: "Fusion — panel + judge" },
|
{ value: "fusion", label: "Fusion — panel + judge" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function ComboCard({ combo, getCaps, activeProviders = [], copied, onCopy, onEdit, onDelete, strategy = {}, globalStrategy = "fallback", onSetStrategy }) {
|
function ComboCard({ combo, getCaps, activeProviders = [], copied, onCopy, onEdit, onDelete, strategy = {}, globalStrategy = "fallback", onSetStrategy, dragHandle = null }) {
|
||||||
const [showJudgeSelect, setShowJudgeSelect] = useState(false);
|
const [showJudgeSelect, setShowJudgeSelect] = useState(false);
|
||||||
// Show the effective strategy: per-combo override first, then the global
|
// Show the effective strategy: per-combo override first, then the global
|
||||||
// default (combos without an entry fall through to settings.comboStrategy).
|
// default (combos without an entry fall through to settings.comboStrategy).
|
||||||
@@ -419,6 +463,7 @@ function ComboCard({ combo, getCaps, activeProviders = [], copied, onCopy, onEdi
|
|||||||
<Card padding="sm" className="group">
|
<Card padding="sm" className="group">
|
||||||
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex min-w-0 flex-1 items-start gap-3 sm:items-center">
|
<div className="flex min-w-0 flex-1 items-start gap-3 sm:items-center">
|
||||||
|
{dragHandle}
|
||||||
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||||
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
|
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -534,6 +579,35 @@ function ComboCard({ combo, getCaps, activeProviders = [], copied, onCopy, onEdi
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Drag-and-drop wrapper around ComboCard. The handle is a separate button
|
||||||
|
// so all card clicks (edit / delete / copy / strategy) still work — only
|
||||||
|
// dragging the grip initiates a reorder.
|
||||||
|
function SortableComboCard({ id, children }) {
|
||||||
|
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.7 : 1,
|
||||||
|
};
|
||||||
|
const handle = (
|
||||||
|
<button
|
||||||
|
ref={setActivatorNodeRef}
|
||||||
|
type="button"
|
||||||
|
aria-label="Drag to reorder"
|
||||||
|
title="Drag to reorder"
|
||||||
|
className="hidden sm:flex size-7 cursor-grab items-center justify-center rounded text-text-muted hover:bg-black/5 hover:text-text-main active:cursor-grabbing dark:hover:bg-white/5 touch-none"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[18px]">drag_indicator</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} className="relative">
|
||||||
|
{typeof children === "function" ? children(handle) : children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CapacityAdapterSection({ capacityAdapter, onChange, activeProviders, getCaps }) {
|
function CapacityAdapterSection({ capacityAdapter, onChange, activeProviders, getCaps }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
35
src/app/api/combos/order/route.js
Normal file
35
src/app/api/combos/order/route.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { reorderCombos, getCombos } from "@/lib/localDb";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
// PUT /api/combos/order - Persist a new top-to-bottom ordering.
|
||||||
|
// Body: { ids: string[] } — the new order, may be a partial slice of the list
|
||||||
|
// (entries not mentioned keep their relative position; entries not in the
|
||||||
|
// database are silently dropped by the repo).
|
||||||
|
export async function PUT(request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const ids = body?.ids;
|
||||||
|
if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string" || !id)) {
|
||||||
|
return NextResponse.json({ error: "ids must be a non-empty string array" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap the payload to guard against accidental mega-arrays. The real
|
||||||
|
// maximum is the current row count; allow a little slack for races where
|
||||||
|
// a row was just created but the client hasn't refetched.
|
||||||
|
const all = await getCombos();
|
||||||
|
if (ids.length > all.length + 16) {
|
||||||
|
return NextResponse.json({ error: "Too many ids" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedupe to keep the repo's dense 1..N numbering correct.
|
||||||
|
const ordered = [...new Set(ids)];
|
||||||
|
|
||||||
|
await reorderCombos(ordered);
|
||||||
|
return NextResponse.json({ ok: true, count: ordered.length });
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error reordering combos:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to reorder combos" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ export {
|
|||||||
// Combos
|
// Combos
|
||||||
export {
|
export {
|
||||||
getCombos, getComboById, getComboByName,
|
getCombos, getComboById, getComboByName,
|
||||||
createCombo, updateCombo, deleteCombo,
|
createCombo, updateCombo, deleteCombo, reorderCombos,
|
||||||
} from "./repos/combosRepo.js";
|
} from "./repos/combosRepo.js";
|
||||||
|
|
||||||
// Aliases (model + custom + mitm)
|
// Aliases (model + custom + mitm)
|
||||||
@@ -79,7 +79,7 @@ export async function exportDb() {
|
|||||||
providerNodes: db.all(`SELECT * FROM providerNodes`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, type: r.type, name: r.name, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
providerNodes: db.all(`SELECT * FROM providerNodes`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, type: r.type, name: r.name, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
||||||
proxyPools: db.all(`SELECT * FROM proxyPools`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, isActive: r.isActive === 1, testStatus: r.testStatus, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
proxyPools: db.all(`SELECT * FROM proxyPools`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, isActive: r.isActive === 1, testStatus: r.testStatus, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
||||||
apiKeys: db.all(`SELECT * FROM apiKeys`).map((r) => ({ id: r.id, key: r.key, name: r.name, machineId: r.machineId, isActive: r.isActive === 1, createdAt: r.createdAt })),
|
apiKeys: db.all(`SELECT * FROM apiKeys`).map((r) => ({ id: r.id, key: r.key, name: r.name, machineId: r.machineId, isActive: r.isActive === 1, createdAt: r.createdAt })),
|
||||||
combos: db.all(`SELECT * FROM combos`).map((r) => ({ id: r.id, name: r.name, kind: r.kind, models: parseJson(r.models, []), tags: parseJson(r.tags, []), createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
combos: db.all(`SELECT * FROM combos`).map((r) => ({ id: r.id, name: r.name, kind: r.kind, models: parseJson(r.models, []), tags: parseJson(r.tags, []), sortOrder: r.sortOrder ?? 0, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
||||||
modelAliases: {},
|
modelAliases: {},
|
||||||
customModels: [],
|
customModels: [],
|
||||||
mitmAlias: {},
|
mitmAlias: {},
|
||||||
@@ -144,8 +144,8 @@ export async function importDb(payload) {
|
|||||||
}
|
}
|
||||||
for (const c of payload.combos || []) {
|
for (const c of payload.combos || []) {
|
||||||
db.run(
|
db.run(
|
||||||
`INSERT OR REPLACE INTO combos(id, name, kind, models, tags, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO combos(id, name, kind, models, tags, sortOrder, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[c.id, c.name, c.kind || null, stringifyJson(c.models || []), stringifyJson(c.tags || []), c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
[c.id, c.name, c.kind || null, stringifyJson(c.models || []), stringifyJson(c.tags || []), typeof c.sortOrder === "number" ? c.sortOrder : 0, c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const [a, m] of Object.entries(payload.modelAliases || {})) {
|
for (const [a, m] of Object.entries(payload.modelAliases || {})) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function rowToCombo(row) {
|
|||||||
models: parseJson(row.models, []),
|
models: parseJson(row.models, []),
|
||||||
enabled: row.enabled === 1 || row.enabled === true,
|
enabled: row.enabled === 1 || row.enabled === true,
|
||||||
tags: parseJson(row.tags, []),
|
tags: parseJson(row.tags, []),
|
||||||
|
sortOrder: row.sortOrder ?? 0,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -18,10 +19,11 @@ function rowToCombo(row) {
|
|||||||
|
|
||||||
export async function getCombos() {
|
export async function getCombos() {
|
||||||
const db = await getAdapter();
|
const db = await getAdapter();
|
||||||
const rows = db.all(`SELECT * FROM combos ORDER BY createdAt ASC`);
|
const rows = db.all(`SELECT * FROM combos ORDER BY sortOrder ASC, createdAt ASC`);
|
||||||
return rows.map(rowToCombo);
|
return rows.map(rowToCombo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getComboById(id) {
|
export async function getComboById(id) {
|
||||||
const db = await getAdapter();
|
const db = await getAdapter();
|
||||||
const row = db.get(`SELECT * FROM combos WHERE id = ?`, [id]);
|
const row = db.get(`SELECT * FROM combos WHERE id = ?`, [id]);
|
||||||
@@ -37,6 +39,10 @@ export async function getComboByName(name) {
|
|||||||
export async function createCombo(data) {
|
export async function createCombo(data) {
|
||||||
const db = await getAdapter();
|
const db = await getAdapter();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
// Append to the end of the user's manual order. New combos should always
|
||||||
|
// land at the bottom unless the caller explicitly pins a position.
|
||||||
|
const maxRow = db.get(`SELECT MAX(sortOrder) AS m FROM combos`);
|
||||||
|
const nextOrder = (maxRow?.m ?? 0) + 1;
|
||||||
const combo = {
|
const combo = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -44,11 +50,12 @@ export async function createCombo(data) {
|
|||||||
models: data.models || [],
|
models: data.models || [],
|
||||||
enabled: data.enabled !== false,
|
enabled: data.enabled !== false,
|
||||||
tags: Array.isArray(data.tags) ? data.tags : [],
|
tags: Array.isArray(data.tags) ? data.tags : [],
|
||||||
|
sortOrder: data.sortOrder ?? nextOrder,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
db.run(
|
db.run(
|
||||||
`INSERT INTO combos(id, name, kind, models, enabled, tags, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO combos(id, name, kind, models, enabled, tags, sortOrder, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
combo.id,
|
combo.id,
|
||||||
combo.name,
|
combo.name,
|
||||||
@@ -56,6 +63,7 @@ export async function createCombo(data) {
|
|||||||
stringifyJson(combo.models),
|
stringifyJson(combo.models),
|
||||||
combo.enabled !== false ? 1 : 0,
|
combo.enabled !== false ? 1 : 0,
|
||||||
stringifyJson(combo.tags),
|
stringifyJson(combo.tags),
|
||||||
|
combo.sortOrder,
|
||||||
combo.createdAt,
|
combo.createdAt,
|
||||||
combo.updatedAt,
|
combo.updatedAt,
|
||||||
],
|
],
|
||||||
@@ -63,6 +71,30 @@ export async function createCombo(data) {
|
|||||||
return combo;
|
return combo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist a manual drag-and-drop reorder. `ids` is the new top-to-bottom
|
||||||
|
// order; index N gets sortOrder = N + 1 (1-based, dense). Atomic via tx so
|
||||||
|
// the list never lands in a half-updated state if the process dies mid-write.
|
||||||
|
export async function reorderCombos(ids) {
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) return false;
|
||||||
|
const db = await getAdapter();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
db.transaction(() => {
|
||||||
|
// Look up the known set so unknown ids are ignored instead of throwing
|
||||||
|
// — the API layer is the trust boundary, but defensive coding here is
|
||||||
|
// cheap and avoids a surprise FK-ish failure mid-transaction.
|
||||||
|
const existing = new Set(
|
||||||
|
db.all(`SELECT id FROM combos`).map((r) => r.id),
|
||||||
|
);
|
||||||
|
let n = 1;
|
||||||
|
for (const id of ids) {
|
||||||
|
if (typeof id !== "string" || !existing.has(id)) continue;
|
||||||
|
db.run(`UPDATE combos SET sortOrder = ?, updatedAt = ? WHERE id = ?`, [n, now, id]);
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateCombo(id, data) {
|
export async function updateCombo(id, data) {
|
||||||
const db = await getAdapter();
|
const db = await getAdapter();
|
||||||
let result = null;
|
let result = null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// pre-change safety backup in migrate.js: when the stored version is lower,
|
// pre-change safety backup in migrate.js: when the stored version is lower,
|
||||||
// one lightweight DB backup is taken before applying schema changes. Forgetting
|
// one lightweight DB backup is taken before applying schema changes. Forgetting
|
||||||
// to bump only skips that backup — it does NOT break the additive auto-sync.
|
// to bump only skips that backup — it does NOT break the additive auto-sync.
|
||||||
export const SCHEMA_VERSION = 4;
|
export const SCHEMA_VERSION = 5;
|
||||||
|
|
||||||
export const PRAGMA_SQL = `
|
export const PRAGMA_SQL = `
|
||||||
PRAGMA journal_mode = WAL;
|
PRAGMA journal_mode = WAL;
|
||||||
@@ -95,10 +95,14 @@ export const TABLES = {
|
|||||||
models: "TEXT NOT NULL",
|
models: "TEXT NOT NULL",
|
||||||
enabled: "INTEGER NOT NULL DEFAULT 1",
|
enabled: "INTEGER NOT NULL DEFAULT 1",
|
||||||
tags: "TEXT NOT NULL DEFAULT '[]'",
|
tags: "TEXT NOT NULL DEFAULT '[]'",
|
||||||
|
"sortOrder": "REAL NOT NULL DEFAULT 0",
|
||||||
createdAt: "TEXT NOT NULL",
|
createdAt: "TEXT NOT NULL",
|
||||||
updatedAt: "TEXT NOT NULL",
|
updatedAt: "TEXT NOT NULL",
|
||||||
},
|
},
|
||||||
indexes: ["CREATE INDEX IF NOT EXISTS idx_combo_name ON combos(name)"],
|
indexes: [
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_combo_name ON combos(name)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_combo_sort_order ON combos(sortOrder)",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
kv: {
|
kv: {
|
||||||
columns: {
|
columns: {
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ export {
|
|||||||
createProxyPool, updateProxyPool, deleteProxyPool,
|
createProxyPool, updateProxyPool, deleteProxyPool,
|
||||||
getApiKeys, getApiKeyById, createApiKey, updateApiKey, importApiKey, deleteApiKey, validateApiKey, getApiKeyByKey,
|
getApiKeys, getApiKeyById, createApiKey, updateApiKey, importApiKey, deleteApiKey, validateApiKey, getApiKeyByKey,
|
||||||
getCombos, getComboById, getComboByName,
|
getCombos, getComboById, getComboByName,
|
||||||
createCombo, updateCombo, deleteCombo,
|
createCombo, updateCombo, deleteCombo, reorderCombos,
|
||||||
getModelAliases, setModelAlias, deleteModelAlias,
|
|
||||||
getCustomModels, addCustomModel, deleteCustomModel,
|
getCustomModels, addCustomModel, deleteCustomModel,
|
||||||
getMitmAlias, setMitmAliasAll,
|
getMitmAlias, setMitmAliasAll,
|
||||||
getPricing, getPricingForModel, updatePricing, resetPricing, resetAllPricing,
|
getPricing, getPricingForModel, updatePricing, resetPricing, resetAllPricing,
|
||||||
|
|||||||
Reference in New Issue
Block a user