From 28a0daa3175c41a464ae35d63d6aabdf17fc9601 Mon Sep 17 00:00:00 2001 From: mahdiwafy Date: Wed, 22 Jul 2026 12:07:11 +0700 Subject: [PATCH 1/5] feat(combos): add custom context_length configuration for model combos --- src/app/(dashboard)/dashboard/combos/page.js | 32 ++++++++++++++++++-- src/app/api/combos/route.js | 4 +-- src/lib/db/repos/combosRepo.js | 10 +++--- src/lib/db/schema.js | 1 + src/sse/services/allowedModels.js | 13 ++++++++ 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index 591c8a72fc..aea5d6f5bd 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,20 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF

+ {/* Context Length */} +
+ setContextLength(e.target.value)} + placeholder="e.g. 1000000 (leave blank for auto)" + /> +

+ Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens) +

+
+ {/* Models */}
diff --git a/src/app/api/combos/route.js b/src/app/api/combos/route.js index 38966ed140..71fcf9018b 100644 --- a/src/app/api/combos/route.js +++ b/src/app/api/combos/route.js @@ -21,7 +21,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 +38,7 @@ 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 }); 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); } From e0c7f1ee185b9b0b0dcd9ce9a7cd0bab440f8598 Mon Sep 17 00:00:00 2001 From: mahdiwafy Date: Wed, 22 Jul 2026 12:23:48 +0700 Subject: [PATCH 2/5] feat(combos): add advertised context_length input to media provider combo page --- .../dashboard/media-providers/combo/[id]/page.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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..2d52363cc5 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,10 @@ export default function ComboDetailPage() { { setName(e.target.value); validateName(e.target.value); }} onBlur={handleSaveName} error={nameError} />

Only letters, numbers, -, _ and .

+
+ setContextLength(e.target.value)} onBlur={handleSaveContextLength} placeholder="e.g. 1000000 (leave blank for auto)" /> +

Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens)

+

Round Robin

@@ -411,4 +423,4 @@ export default function ComboDetailPage() { )}
); -} +} From 1d3e3b950f75864f61ee21eff1aa7fd009921647 Mon Sep 17 00:00:00 2001 From: mahdiwafy Date: Wed, 22 Jul 2026 12:42:36 +0700 Subject: [PATCH 3/5] feat(combos): add preset buttons (256k, 512k, 1M, 2M) for context length --- src/app/(dashboard)/dashboard/combos/page.js | 33 ++++++++++++++- .../media-providers/combo/[id]/page.js | 41 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index aea5d6f5bd..2f1b76ec60 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -624,7 +624,38 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF onChange={(e) => setContextLength(e.target.value)} placeholder="e.g. 1000000 (leave blank for auto)" /> -

+

+ Presets: + {[ + { label: "256k", value: 256000 }, + { label: "512k", value: 512000 }, + { label: "1M", value: 1000000 }, + { label: "2M", value: 2000000 }, + ].map((p) => ( + + ))} + {contextLength && ( + + )} +
+

Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens)

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 2d52363cc5..e8df472d8e 100644 --- a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js @@ -326,7 +326,46 @@ export default function ComboDetailPage() {
setContextLength(e.target.value)} onBlur={handleSaveContextLength} placeholder="e.g. 1000000 (leave blank for auto)" /> -

Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens)

+
+ Presets: + {[ + { label: "256k", value: 256000 }, + { label: "512k", value: 512000 }, + { label: "1M", value: 1000000 }, + { label: "2M", value: 2000000 }, + ].map((p) => ( + + ))} + {contextLength && ( + + )} +
+

Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens)

From 1ecbf15066ec6211dfae197991fed14272f15290 Mon Sep 17 00:00:00 2001 From: mahdiwafy Date: Wed, 22 Jul 2026 12:47:53 +0700 Subject: [PATCH 4/5] feat(combos): add 128k, 256k, 512k, 1M, 2M preset buttons and model capacity warning note --- src/app/(dashboard)/dashboard/combos/page.js | 6 ++++-- .../dashboard/media-providers/combo/[id]/page.js | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.js b/src/app/(dashboard)/dashboard/combos/page.js index 2f1b76ec60..92386ba8a5 100644 --- a/src/app/(dashboard)/dashboard/combos/page.js +++ b/src/app/(dashboard)/dashboard/combos/page.js @@ -627,6 +627,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
Presets: {[ + { label: "128k", value: 128000 }, { label: "256k", value: 256000 }, { label: "512k", value: 512000 }, { label: "1M", value: 1000000 }, @@ -655,8 +656,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF )}
-

- Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens) +

+ warning + Note: Advertised context window tokens via /v1/models. Real capacity depends on the underlying 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 e8df472d8e..c040a0e8f8 100644 --- a/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js +++ b/src/app/(dashboard)/dashboard/media-providers/combo/[id]/page.js @@ -329,6 +329,7 @@ export default function ComboDetailPage() {
Presets: {[ + { label: "128k", value: 128000 }, { label: "256k", value: 256000 }, { label: "512k", value: 512000 }, { label: "1M", value: 1000000 }, @@ -365,7 +366,10 @@ export default function ComboDetailPage() { )}
-

Advertised max context window tokens via /v1/models (e.g. 1000000 for 1M tokens)

+

+ warning + Note: Advertised context window tokens via /v1/models. Real capacity depends on the underlying models. +

From 630e3c42a21d8e2c69885f17cda99d681a9c9570 Mon Sep 17 00:00:00 2001 From: mahdiwafy Date: Wed, 22 Jul 2026 12:58:51 +0700 Subject: [PATCH 5/5] fix(combos): invalidate models list cache on combo create, update, and delete --- src/app/api/combos/[id]/route.js | 5 ++++- src/app/api/combos/route.js | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 71fcf9018b..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"; @@ -39,6 +40,7 @@ export async function POST(request) { } 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) {