diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js
index 591c8a72fc..92386ba8a5 100644
--- a/src/app/(dashboard)/dashboard/combos/page.js
+++ b/src/app/(dashboard)/dashboard/combos/page.js
@@ -278,7 +278,16 @@ function ComboCard({ combo, getCaps, activeProviders = [], copied, onCopy, onEdi
layers
-
{combo.name}
+
+ {combo.name}
+ {combo.context_length && (
+
+ {combo.context_length >= 1000000
+ ? `${(combo.context_length / 1000000).toFixed(1)}M ctx`
+ : `${(combo.context_length / 1000).toFixed(0)}k ctx`}
+
+ )}
+
{combo.models.length === 0 ? (
No models
@@ -480,6 +489,7 @@ function ModelItem({ id, index, model, isFirst, isLast, onEdit, onMoveUp, onMove
function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindFilter = null }) {
// Initialize state with combo values - key prop on parent handles reset on remount
const [name, setName] = useState(combo?.name || "");
+ const [contextLength, setContextLength] = useState(combo?.context_length || "");
const [models, setModels] = useState(combo?.models || []);
const [showModelSelect, setShowModelSelect] = useState(false);
const [saving, setSaving] = useState(false);
@@ -573,7 +583,11 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
const handleSave = async () => {
if (!validateName(name)) return;
setSaving(true);
- await onSave({ name: name.trim(), models });
+ await onSave({
+ name: name.trim(),
+ models,
+ context_length: contextLength ? Number(contextLength) : null
+ });
setSaving(false);
};
@@ -601,6 +615,53 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
+ {/* Context Length */}
+
+
setContextLength(e.target.value)}
+ placeholder="e.g. 1000000 (leave blank for auto)"
+ />
+
+ Presets:
+ {[
+ { label: "128k", value: 128000 },
+ { label: "256k", value: 256000 },
+ { label: "512k", value: 512000 },
+ { label: "1M", value: 1000000 },
+ { label: "2M", value: 2000000 },
+ ].map((p) => (
+
+ ))}
+ {contextLength && (
+
+ )}
+
+
+ warning
+ Note: Advertised context window tokens via /v1/models. Real capacity depends on the underlying models.
+
+
+
{/* Models */}
diff --git a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js
index aa6857e5b0..c040a0e8f8 100644
--- a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js
@@ -90,7 +90,7 @@ function ComboProvidersCard({ providers, onAdd, onMove, onRemove }) {
+
);
@@ -122,6 +122,7 @@ export default function ComboDetailPage() {
const [combo, setCombo] = useState(null);
const [loading, setLoading] = useState(true);
const [name, setName] = useState("");
+ const [contextLength, setContextLength] = useState("");
const [nameError, setNameError] = useState("");
const [providers, setProviders] = useState([]);
const [roundRobin, setRoundRobin] = useState(false);
@@ -154,6 +155,7 @@ export default function ComboDetailPage() {
const c = await comboRes.json();
setCombo(c);
setName(c.name);
+ setContextLength(c.context_length || "");
setProviders(c.models || []);
const s = settingsRes.ok ? await settingsRes.json() : {};
setRoundRobin(s.comboStrategies?.[c.name]?.fallbackStrategy === "round-robin");
@@ -189,6 +191,12 @@ export default function ComboDetailPage() {
const ok = await saveCombo({ name });
if (ok) await fetchAll();
};
+ const handleSaveContextLength = async () => {
+ const val = contextLength ? Number(contextLength) : null;
+ if (val === combo.context_length) return;
+ const ok = await saveCombo({ context_length: val });
+ if (ok) await fetchAll();
+ };
const handleAddModel = async (model) => {
const value = model?.value || model;
if (!value || providers.includes(value)) return;
@@ -316,6 +324,53 @@ export default function ComboDetailPage() {
Only letters, numbers, -, _ and .
+
Round Robin
@@ -411,4 +466,4 @@ export default function ComboDetailPage() {
)}
);
-}
+}
diff --git a/src/app/api/combos/[id]/route.js b/src/app/api/combos/[id]/route.js
index af6dd70057..beaac01122 100644
--- a/src/app/api/combos/[id]/route.js
+++ b/src/app/api/combos/[id]/route.js
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { getComboById, updateCombo, deleteCombo, getComboByName } from "@/lib/localDb";
import { resetComboRotation } from "open-sse/services/combo.js";
+import { invalidateAllowedModelsCache } from "@/sse/services/allowedModels.js";
// Validate combo name: only a-z, A-Z, 0-9, -, _
const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
@@ -49,9 +50,10 @@ export async function PUT(request, { params }) {
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
}
- // Invalidate rotation state (models/strategy/name may have changed)
+ // Invalidate rotation state and models list cache (models/strategy/name/context_length may have changed)
if (prev?.name) resetComboRotation(prev.name);
if (combo.name && combo.name !== prev?.name) resetComboRotation(combo.name);
+ invalidateAllowedModelsCache();
return NextResponse.json(combo);
} catch (error) {
@@ -74,6 +76,7 @@ export async function DELETE(request, { params }) {
}
if (prev?.name) resetComboRotation(prev.name);
+ invalidateAllowedModelsCache();
return NextResponse.json({ success: true });
} catch (error) {
diff --git a/src/app/api/combos/route.js b/src/app/api/combos/route.js
index 38966ed140..e7f885fba7 100644
--- a/src/app/api/combos/route.js
+++ b/src/app/api/combos/route.js
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { getCombos, createCombo, getComboByName } from "@/lib/localDb";
+import { invalidateAllowedModelsCache } from "@/sse/services/allowedModels.js";
export const dynamic = "force-dynamic";
@@ -21,7 +22,7 @@ export async function GET() {
export async function POST(request) {
try {
const body = await request.json();
- const { name, models, kind } = body;
+ const { name, models, kind, context_length } = body;
if (!name) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
@@ -38,7 +39,8 @@ export async function POST(request) {
return NextResponse.json({ error: "Combo name already exists" }, { status: 400 });
}
- const combo = await createCombo({ name, models: models || [], kind: kind || null });
+ const combo = await createCombo({ name, models: models || [], kind: kind || null, context_length: context_length ? Number(context_length) : null });
+ invalidateAllowedModelsCache();
return NextResponse.json(combo, { status: 201 });
} catch (error) {
diff --git a/src/lib/db/repos/combosRepo.js b/src/lib/db/repos/combosRepo.js
index 3e85be9b53..209a5f608b 100644
--- a/src/lib/db/repos/combosRepo.js
+++ b/src/lib/db/repos/combosRepo.js
@@ -8,6 +8,7 @@ function rowToCombo(row) {
id: row.id,
name: row.name,
kind: row.kind,
+ context_length: row.context_length ?? null,
models: parseJson(row.models, []),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
@@ -39,13 +40,14 @@ export async function createCombo(data) {
id: randomUUID(),
name: data.name,
kind: data.kind || null,
+ context_length: data.context_length || null,
models: data.models || [],
createdAt: now,
updatedAt: now,
};
db.run(
- `INSERT INTO combos(id, name, kind, models, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
- [combo.id, combo.name, combo.kind, stringifyJson(combo.models), combo.createdAt, combo.updatedAt]
+ `INSERT INTO combos(id, name, kind, context_length, models, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?)`,
+ [combo.id, combo.name, combo.kind, combo.context_length, stringifyJson(combo.models), combo.createdAt, combo.updatedAt]
);
return combo;
}
@@ -58,8 +60,8 @@ export async function updateCombo(id, data) {
if (!row) return;
const merged = { ...rowToCombo(row), ...data, updatedAt: new Date().toISOString() };
db.run(
- `UPDATE combos SET name = ?, kind = ?, models = ?, updatedAt = ? WHERE id = ?`,
- [merged.name, merged.kind, stringifyJson(merged.models || []), merged.updatedAt, id]
+ `UPDATE combos SET name = ?, kind = ?, context_length = ?, models = ?, updatedAt = ? WHERE id = ?`,
+ [merged.name, merged.kind, merged.context_length || null, stringifyJson(merged.models || []), merged.updatedAt, id]
);
result = merged;
});
diff --git a/src/lib/db/schema.js b/src/lib/db/schema.js
index 095187814b..48cbc02984 100644
--- a/src/lib/db/schema.js
+++ b/src/lib/db/schema.js
@@ -91,6 +91,7 @@ export const TABLES = {
id: "TEXT PRIMARY KEY",
name: "TEXT UNIQUE NOT NULL",
kind: "TEXT",
+ context_length: "INTEGER",
models: "TEXT NOT NULL",
createdAt: "TEXT NOT NULL",
updatedAt: "TEXT NOT NULL",
diff --git a/src/sse/services/allowedModels.js b/src/sse/services/allowedModels.js
index 2fade87a31..367ca5ffe5 100644
--- a/src/sse/services/allowedModels.js
+++ b/src/sse/services/allowedModels.js
@@ -302,7 +302,19 @@ async function buildAllModelEntries(kindFilter, combos, customModels, modelAlias
if (combo.kind === "webSearch" || combo.kind === "webFetch") {
entry.kind = combo.kind;
}
+ if (combo.context_length) {
+ entry.context_length = Number(combo.context_length);
+ }
entries.push(entry);
+
+ const bareEntry = { id: combo.name, object: "model", owned_by: "combo" };
+ if (combo.kind === "webSearch" || combo.kind === "webFetch") {
+ bareEntry.kind = combo.kind;
+ }
+ if (combo.context_length) {
+ bareEntry.context_length = Number(combo.context_length);
+ }
+ entries.push(bareEntry);
}
if (!dbAvailable) {
@@ -590,6 +602,7 @@ export async function buildModelsList(kindFilter, options = {}) {
};
if (entry.kind) model.kind = entry.kind;
if (entry.capabilities) model.capabilities = entry.capabilities;
+ if (entry.context_length) model.context_length = entry.context_length;
dedupedModels.push(model);
}