feat(llm): Added max context length configuration and statistics

This commit is contained in:
2026-07-11 18:29:34 +05:30
parent 99c11cbbfe
commit 77be8d59a5
11 changed files with 218 additions and 92 deletions

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -37,6 +37,7 @@ export default function ConfigPage() {
const [editModel, setEditModel] = useState("gemini-2.5-flash");
const [editIsActive, setEditIsActive] = useState(false);
const [editType, setEditType] = useState<"generative" | "embedding">("generative");
const [editMaxContext, setEditMaxContext] = useState<number>(32768);
useEffect(() => {
if (selectedInstanceId === null) {
@@ -46,6 +47,7 @@ export default function ConfigPage() {
setEditModel("gemini-2.5-flash");
setEditIsActive(false);
setEditType("generative");
setEditMaxContext(32768);
} else if (selectedInstanceId === "new") {
setEditName("");
const defaultProvider = "google-genai";
@@ -55,6 +57,7 @@ export default function ConfigPage() {
const pMeta = availableProviders.find((p) => p.id === defaultProvider);
setEditModel(pMeta?.defaultModel || "gemini-2.5-flash");
setEditIsActive(false);
setEditMaxContext(32768);
} else {
const inst = instances.find((i) => i.id === selectedInstanceId);
if (inst) {
@@ -65,6 +68,7 @@ export default function ConfigPage() {
const pMeta = availableProviders.find((p) => p.id === inst.providerName);
setEditModel(inst.modelName || (inst.type === "embedding" ? pMeta?.defaultEmbeddingModel : pMeta?.defaultModel) || "gemini-2.5-flash");
setEditIsActive(inst.isActive);
setEditMaxContext(inst.maxContext !== undefined && inst.maxContext !== null ? inst.maxContext : 32768);
}
}
}, [selectedInstanceId, instances, availableProviders]);
@@ -132,13 +136,14 @@ export default function ConfigPage() {
setLoading(false);
return;
}
const created = await createProviderInstance(editName, editProvider, editKey, editModel || undefined, editType);
const created = await createProviderInstance(editName, editProvider, editKey, editModel || undefined, editType, editType === "generative" ? editMaxContext : 0);
if (editIsActive) {
await setActiveProviderInstance(created.id);
}
targetInstanceId = created.id;
setSelectedInstanceId(created.id);
} else {
if (!selectedInstanceId) return;
const inst = instances.find((i) => i.id === selectedInstanceId);
if (inst && inst.type === "embedding") {
const isMapped = mappings["embeddings"] === selectedInstanceId;
@@ -158,7 +163,7 @@ export default function ConfigPage() {
}
}
await updateProviderInstance(selectedInstanceId, editName, editProvider, editKey || undefined, editModel || undefined, editType);
await updateProviderInstance(selectedInstanceId, editName, editProvider, editKey || undefined, editModel || undefined, editType, editType === "generative" ? editMaxContext : 0);
if (editIsActive) {
await setActiveProviderInstance(selectedInstanceId);
}
@@ -167,7 +172,7 @@ export default function ConfigPage() {
await loadInstances();
await loadMappings();
if (shouldRegenerate && targetInstanceId !== "new") {
if (shouldRegenerate && targetInstanceId && targetInstanceId !== "new") {
await regenerateEmbeddings(targetInstanceId);
}
} catch (err) {
@@ -381,6 +386,23 @@ export default function ConfigPage() {
/>
</div>
{editType === "generative" && (
<div className="flex flex-col gap-1.5">
<label htmlFor="formMaxContext" className="text-xs font-medium text-gray-700">
Max Context Length (Tokens, 0 for infinite)
</label>
<input
id="formMaxContext"
type="number"
value={editMaxContext}
onChange={(e) => setEditMaxContext(parseInt(e.target.value) || 0)}
min={0}
placeholder="e.g. 32768"
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm outline-none transition-[border-color,box-shadow] focus:border-blue-500 focus:ring-3 focus:ring-blue-500/15"
/>
</div>
)}
<div className="mt-1 flex flex-row items-center gap-2">
<input
id="formActive"

View File

@@ -243,8 +243,9 @@ export async function createProviderInstance(
apiKey: string,
modelName?: string,
type: "generative" | "embedding" = "generative",
maxContext?: number,
): Promise<ModelProviderInstance> {
return ProviderManager.create(name, providerName, apiKey, modelName, type);
return ProviderManager.create(name, providerName, apiKey, modelName, type, maxContext);
}
export async function deleteProviderInstance(id: string): Promise<void> {
@@ -262,8 +263,9 @@ export async function updateProviderInstance(
apiKey?: string,
modelName?: string,
type: "generative" | "embedding" = "generative",
maxContext?: number,
): Promise<void> {
ProviderManager.update(id, name, providerName, apiKey, modelName, type);
ProviderManager.update(id, name, providerName, apiKey, modelName, type, maxContext);
}
export async function getProviderMappings(): Promise<Record<string, string>> {

View File

@@ -13,7 +13,7 @@ import {
listProviderInstances,
} from "@/app/play/actions";
import type { SimSnapshot } from "@/lib/simulation-types";
import type { LLMProviderInstance } from "@omnia/llm";
import type { ModelProviderInstance } from "@omnia/llm";
function IntentTag({
intent,
@@ -90,9 +90,9 @@ function PromptModal({
const memTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
return [
{ label: "System Prompt", pct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "World Info", pct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Recent Memories", pct: memPct, tokens: memTokens, type: "memories", content: memStr || "(No memories yet.)" },
{ label: "System Prompt", pct: sysPct, relativePct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "World Info", pct: worldPct, relativePct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Recent Memories", pct: memPct, relativePct: memPct, tokens: memTokens, type: "memories", content: memStr || "(No memories yet.)" },
];
};
@@ -124,15 +124,35 @@ function PromptModal({
const proseTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
return [
{ label: "System Prompt", pct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "Decoder Context", pct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Narrative Prose", pct: prosePct, tokens: proseTokens, type: "memories", content: proseStr },
{ label: "System Prompt", pct: sysPct, relativePct: sysPct, tokens: sysTokens, type: "system", content: systemPrompt },
{ label: "Decoder Context", pct: worldPct, relativePct: worldPct, tokens: worldTokens, type: "world", content: worldStr },
{ label: "Narrative Prose", pct: prosePct, relativePct: prosePct, tokens: proseTokens, type: "memories", content: proseStr },
];
};
const actorBreakdown = (entry.rawPrompt && entry.usage) ? parseActorPrompt(entry.rawPrompt.systemPrompt, entry.rawPrompt.userContext, entry.usage.inputTokens) : null;
const decoderBreakdown = (entry.decoderPrompt && entry.decoderUsage) ? parseDecoderPrompt(entry.decoderPrompt.systemPrompt, entry.decoderPrompt.userContext, entry.decoderUsage.inputTokens) : null;
const actorMaxContext = entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768;
const actorUsedTokens = entry.usage?.inputTokens || 0;
const actorUsagePctOfContext = actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0;
const isActorAbsolute = actorMaxContext > 0 && actorUsagePctOfContext >= 20;
const scaledActorBreakdown = actorBreakdown ? actorBreakdown.map((item) => ({
...item,
pct: isActorAbsolute ? item.relativePct * (actorUsedTokens / actorMaxContext) : item.relativePct
})) : null;
const decoderMaxContext = entry.decoderUsage?.maxContext !== undefined ? entry.decoderUsage.maxContext : 32768;
const decoderUsedTokens = entry.decoderUsage?.inputTokens || 0;
const decoderUsagePctOfContext = decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0;
const isDecoderAbsolute = decoderMaxContext > 0 && decoderUsagePctOfContext >= 20;
const scaledDecoderBreakdown = decoderBreakdown ? decoderBreakdown.map((item) => ({
...item,
pct: isDecoderAbsolute ? item.relativePct * (decoderUsedTokens / decoderMaxContext) : item.relativePct
})) : null;
useEffect(() => {
if (!entry.rawPrompt && entry.decoderPrompt) {
setActiveTab("decoder");
@@ -180,37 +200,57 @@ function PromptModal({
</div>
)}
{actorBreakdown && (
{scaledActorBreakdown && (
<div className="prompt-breakdown-container">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: "0.75rem", color: "#6b7280", marginBottom: "0.25rem" }}>
<span style={{ fontWeight: 600 }}>Input Prompt Breakdown</span>
<span>Total Input Tokens: <strong>{entry.usage?.inputTokens}</strong></span>
<span>
Total Input Tokens: <strong>{actorUsedTokens}</strong>
{actorMaxContext > 0 ? (
<span> / {actorMaxContext} ({actorUsagePctOfContext.toFixed(1)}% used)</span>
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="prompt-breakdown-bar">
{actorBreakdown.map((item, idx) => (
{scaledActorBreakdown.map((item, idx) => {
const displayPct = actorMaxContext > 0 ? (item.tokens / actorMaxContext) * 100 : item.relativePct;
return (
<div
key={idx}
className={`bar-section ${item.type}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isActorAbsolute && (
<div
key={idx}
className={`bar-section ${item.type}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${item.pct.toFixed(1)}%)`}
className="bar-section-empty"
style={{ width: `${100 - actorUsagePctOfContext}%`, backgroundColor: "#ffffff", height: "100%" }}
title={`Available: ${actorMaxContext - actorUsedTokens} tokens (${(100 - actorUsagePctOfContext).toFixed(1)}% remaining)`}
/>
))}
)}
</div>
<div className="breakdown-accordion">
{actorBreakdown.map((item, idx) => (
<details key={idx} className="breakdown-accordion-item" open={idx === 0}>
<summary className="accordion-header">
<span className={`legend-color ${item.type}`} />
<span className="header-text">
{item.label}: <strong>{item.tokens}</strong> tokens ({item.pct.toFixed(0)}%)
</span>
<span className="accordion-chevron"></span>
</summary>
<div className="accordion-content">
<pre>{item.content}</pre>
</div>
</details>
))}
{scaledActorBreakdown.map((item, idx) => {
const displayPct = actorMaxContext > 0 ? (item.tokens / actorMaxContext) * 100 : item.relativePct;
return (
<details key={idx} className="breakdown-accordion-item" open={idx === 0}>
<summary className="accordion-header">
<span className={`legend-color ${item.type}`} />
<span className="header-text">
{item.label}: <strong>{item.tokens}</strong> tokens ({displayPct.toFixed(0)}%)
</span>
<span className="accordion-chevron"></span>
</summary>
<div className="accordion-content">
<pre>{item.content}</pre>
</div>
</details>
);
})}
</div>
</div>
)}
@@ -240,37 +280,57 @@ function PromptModal({
</div>
)}
{decoderBreakdown && (
{scaledDecoderBreakdown && (
<div className="prompt-breakdown-container">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: "0.75rem", color: "#6b7280", marginBottom: "0.25rem" }}>
<span style={{ fontWeight: 600 }}>Input Prompt Breakdown</span>
<span>Total Input Tokens: <strong>{entry.decoderUsage?.inputTokens}</strong></span>
<span>
Total Input Tokens: <strong>{decoderUsedTokens}</strong>
{decoderMaxContext > 0 ? (
<span> / {decoderMaxContext} ({decoderUsagePctOfContext.toFixed(1)}% used)</span>
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="prompt-breakdown-bar">
{decoderBreakdown.map((item, idx) => (
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct = decoderMaxContext > 0 ? (item.tokens / decoderMaxContext) * 100 : item.relativePct;
return (
<div
key={idx}
className={`bar-section ${item.type}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isDecoderAbsolute && (
<div
key={idx}
className={`bar-section ${item.type}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${item.pct.toFixed(1)}%)`}
className="bar-section-empty"
style={{ width: `${100 - decoderUsagePctOfContext}%`, backgroundColor: "#ffffff", height: "100%" }}
title={`Available: ${decoderMaxContext - decoderUsedTokens} tokens (${(100 - decoderUsagePctOfContext).toFixed(1)}% remaining)`}
/>
))}
)}
</div>
<div className="breakdown-accordion">
{decoderBreakdown.map((item, idx) => (
<details key={idx} className="breakdown-accordion-item" open={idx === 0}>
<summary className="accordion-header">
<span className={`legend-color ${item.type}`} />
<span className="header-text">
{item.label}: <strong>{item.tokens}</strong> tokens ({item.pct.toFixed(0)}%)
</span>
<span className="accordion-chevron"></span>
</summary>
<div className="accordion-content">
<pre>{item.content}</pre>
</div>
</details>
))}
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct = decoderMaxContext > 0 ? (item.tokens / decoderMaxContext) * 100 : item.relativePct;
return (
<details key={idx} className="breakdown-accordion-item" open={idx === 0}>
<summary className="accordion-header">
<span className={`legend-color ${item.type}`} />
<span className="header-text">
{item.label}: <strong>{item.tokens}</strong> tokens ({displayPct.toFixed(0)}%)
</span>
<span className="accordion-chevron"></span>
</summary>
<div className="accordion-content">
<pre>{item.content}</pre>
</div>
</details>
);
})}
</div>
</div>
)}
@@ -488,7 +548,7 @@ export function PlayView() {
const [availableEntities, setAvailableEntities] = useState<{ id: string; name: string }[]>([]);
const [selectedEntity, setSelectedEntity] = useState("");
const [providerInstances, setProviderInstances] = useState<LLMProviderInstance[]>([]);
const [providerInstances, setProviderInstances] = useState<ModelProviderInstance[]>([]);
// Load scenarios and provider instances on mount
useEffect(() => {

View File

@@ -26,6 +26,7 @@ export interface LogEntry {
totalTokens: number;
modelName?: string;
providerInstanceName?: string;
maxContext?: number;
};
decoderPrompt?: {
systemPrompt: string;
@@ -37,6 +38,7 @@ export interface LogEntry {
totalTokens: number;
modelName?: string;
providerInstanceName?: string;
maxContext?: number;
};
}

View File

@@ -235,11 +235,12 @@ class SimulationManager {
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
const instanceName = inst ? inst.name : undefined;
const maxContext = inst ? inst.maxContext : undefined;
if (providerName === "google-genai") {
return new GeminiProvider(key, modelName, instanceName);
return new GeminiProvider(key, modelName, instanceName, maxContext);
} else if (providerName === "openrouter") {
return new OpenRouterProvider(key, modelName, instanceName);
return new OpenRouterProvider(key, modelName, instanceName, maxContext);
} else {
return new MockLLMProvider([]);
}
@@ -702,9 +703,9 @@ class SimulationManager {
}
if (inst.providerName === "google-genai") {
return new GeminiProvider(inst.apiKey, inst.modelName, inst.name);
return new GeminiProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
} else if (inst.providerName === "openrouter") {
return new OpenRouterProvider(inst.apiKey, inst.modelName, inst.name);
return new OpenRouterProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
} else {
return new MockLLMProvider([]);
}