From 140c8a75ee68e2fde2b130478f157f2ce0865ebf Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Fri, 15 May 2026 16:02:22 -0400 Subject: [PATCH] add numCtx (Context Window) setting to SESSION panel - IChatSession/IChatTurn interfaces: add optional numCtx field - Mongoose schemas: add numCtx field to ChatSession and ChatTurn - API controller: validate numCtx (>=16384, multiple of 8192) - ChatSessionService: persist numCtx on session, pass through to turn - SessionPanel: add range slider for Context Window (hidden for OpenAI) - Drone buildDroneModelConfig: prefer turn.numCtx over model defaults --- .../frontend/src/components/SessionPanel.tsx | 33 +++++++++++- gadget-code/frontend/src/lib/api.ts | 2 + .../frontend/src/pages/ChatSessionView.tsx | 54 +++++++++++++++++-- .../src/controllers/api/v1/chat-session.ts | 7 +++ gadget-code/src/models/chat-session.ts | 1 + gadget-code/src/models/chat-turn.ts | 1 + gadget-code/src/services/chat-session.ts | 5 ++ gadget-drone/src/services/agent.ts | 7 +-- packages/api/src/interfaces/chat-session.ts | 1 + packages/api/src/interfaces/chat-turn.ts | 1 + 10 files changed, 105 insertions(+), 7 deletions(-) diff --git a/gadget-code/frontend/src/components/SessionPanel.tsx b/gadget-code/frontend/src/components/SessionPanel.tsx index 9a78eb3..da6f59e 100644 --- a/gadget-code/frontend/src/components/SessionPanel.tsx +++ b/gadget-code/frontend/src/components/SessionPanel.tsx @@ -1,4 +1,4 @@ -import { ChatSessionMode, type AiProvider, type ChatSession } from '../lib/api'; +import { ChatSessionMode, type AiProvider, type ChatSession, type AiModel } from '../lib/api'; import { WorkspaceMode } from '../lib/types'; import WorkspaceModeIndicator from './WorkspaceModeIndicator'; @@ -10,6 +10,7 @@ interface SessionPanelProps { selectedProviderId: string; selectedModelId: string; sessionReasoningEffort: string; + sessionNumCtx: number; isEditingProvider: boolean; editProviderId: string; editModelId: string; @@ -19,9 +20,12 @@ interface SessionPanelProps { isUpdatingName: boolean; isUpdatingMode: boolean; isUpdatingReasoning: boolean; + isUpdatingNumCtx: boolean; onWorkspaceModeChange: (mode: WorkspaceMode) => void; onModeChange: (e: React.ChangeEvent) => void; onReasoningChange: (e: React.ChangeEvent) => void; + onNumCtxChange: (e: React.ChangeEvent) => void; + onNumCtxCommit: (e: React.MouseEvent | React.TouchEvent) => void; onStartEditProvider: () => void; onSaveProvider: () => void; onCancelEditProvider: () => void; @@ -36,6 +40,7 @@ interface SessionPanelProps { getSortedModels: (providerId: string) => AiProvider['models']; truncateModelName: (name: string, maxLength?: number) => string; getSelectedModelCapabilities: () => { hasThinking: boolean } | null; + getSelectedModel: () => AiModel | null; } export default function SessionPanel({ @@ -46,6 +51,7 @@ export default function SessionPanel({ selectedProviderId, selectedModelId, sessionReasoningEffort, + sessionNumCtx, isEditingProvider, editProviderId, editModelId, @@ -55,9 +61,12 @@ export default function SessionPanel({ isUpdatingName, isUpdatingMode, isUpdatingReasoning, + isUpdatingNumCtx, onWorkspaceModeChange, onModeChange, onReasoningChange, + onNumCtxChange, + onNumCtxCommit, onStartEditProvider, onSaveProvider, onCancelEditProvider, @@ -72,6 +81,7 @@ export default function SessionPanel({ getSortedModels, truncateModelName, getSelectedModelCapabilities, + getSelectedModel, }: SessionPanelProps) { return (
@@ -261,6 +271,27 @@ export default function SessionPanel({
+ {providers.find(p => p._id === selectedProviderId)?.apiType !== 'openai' && ( +
+
Context Window
+
+ + + {sessionNumCtx.toLocaleString()} + +
+
+ )} ); diff --git a/gadget-code/frontend/src/lib/api.ts b/gadget-code/frontend/src/lib/api.ts index dbe0bb5..99a4a72 100644 --- a/gadget-code/frontend/src/lib/api.ts +++ b/gadget-code/frontend/src/lib/api.ts @@ -358,6 +358,7 @@ export interface ChatSession { provider: string | AiProvider; selectedModel: string; reasoningEffort?: string; + numCtx?: number; stats: ChatSessionStats; pins: Array<{ _id?: string; content: string }>; } @@ -421,6 +422,7 @@ export interface ChatTurn { session: string | ChatSession; provider: string | AiProvider; llm: string; + numCtx?: number; mode: ChatSessionMode; status: "processing" | "finished" | "aborted" | "error"; prompts: ChatTurnPrompts; diff --git a/gadget-code/frontend/src/pages/ChatSessionView.tsx b/gadget-code/frontend/src/pages/ChatSessionView.tsx index bfe6b7c..a03d7a8 100644 --- a/gadget-code/frontend/src/pages/ChatSessionView.tsx +++ b/gadget-code/frontend/src/pages/ChatSessionView.tsx @@ -68,6 +68,8 @@ export default function ChatSessionView() { const [selectedProviderId, setSelectedProviderId] = useState(''); const [selectedModelId, setSelectedModelId] = useState(''); const [sessionReasoningEffort, setSessionReasoningEffort] = useState('off'); + const [sessionNumCtx, setSessionNumCtx] = useState(131072); + const [isUpdatingNumCtx, setIsUpdatingNumCtx] = useState(false); const [isEditingProvider, setIsEditingProvider] = useState(false); const [editProviderId, setEditProviderId] = useState(''); const [editModelId, setEditModelId] = useState(''); @@ -214,6 +216,15 @@ export default function ChatSessionView() { setSelectedProviderId(providerId || ''); setSelectedModelId(sessionData.selectedModel || ''); setSessionReasoningEffort(sessionData.reasoningEffort || 'off'); + + // Initialize numCtx: persisted value wins, otherwise model's contextWindow + if (sessionData.numCtx) { + setSessionNumCtx(sessionData.numCtx); + } else { + const provider = allProviders.find(p => p._id === providerId); + const model = provider?.models.find(m => m.id === sessionData.selectedModel); + setSessionNumCtx(model?.contextWindow || 131072); + } // Set connection state to connecting - will update on socket connect setConnectionState('connecting'); @@ -793,13 +804,16 @@ export default function ChatSessionView() { } }; - const getSelectedModelCapabilities = useCallback(() => { + const getSelectedModel = useCallback(() => { const provider = providers.find(p => p._id === selectedProviderId); if (!provider) return null; - const model = provider.models.find(m => m.id === selectedModelId); - return model?.capabilities || null; + return provider.models.find(m => m.id === selectedModelId) || null; }, [providers, selectedProviderId, selectedModelId]); + const getSelectedModelCapabilities = useCallback(() => { + return getSelectedModel()?.capabilities || null; + }, [getSelectedModel]); + const handleReasoningChange = async (e: React.ChangeEvent) => { if (!session) return; @@ -821,6 +835,28 @@ export default function ChatSessionView() { } }; + const handleNumCtxChange = (e: React.ChangeEvent) => { + setSessionNumCtx(parseInt(e.target.value, 10)); + }; + + const handleNumCtxCommit = async (e: React.MouseEvent | React.TouchEvent) => { + if (!session || isUpdatingNumCtx) return; + + const newValue = parseInt(e.currentTarget.value, 10); + setIsUpdatingNumCtx(true); + try { + const updatedSession = await chatSessionApi.update(session._id, { + numCtx: newValue + }); + setSession(updatedSession); + showToast(`Context window set to ${newValue.toLocaleString()}`); + } catch (err) { + showToast(`Failed to change context window: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setIsUpdatingNumCtx(false); + } + }; + const getProviderName = (id: string) => { return providers.find(p => p._id === id)?.name ?? 'None'; }; @@ -859,6 +895,13 @@ export default function ChatSessionView() { setSession(updatedSession); setSelectedProviderId(editProviderId); setSelectedModelId(editModelId); + + // Reset numCtx to new model's max context window + const newProvider = providers.find(p => p._id === editProviderId); + const newModel = newProvider?.models.find(m => m.id === editModelId); + const newMaxCtx = newModel?.contextWindow || 131072; + setSessionNumCtx(newMaxCtx); + setIsEditingProvider(false); showToast('Provider and model updated'); } catch (err) { @@ -1173,6 +1216,7 @@ export default function ChatSessionView() { selectedProviderId={selectedProviderId} selectedModelId={selectedModelId} sessionReasoningEffort={sessionReasoningEffort} + sessionNumCtx={sessionNumCtx} isEditingProvider={isEditingProvider} editProviderId={editProviderId} editModelId={editModelId} @@ -1182,9 +1226,12 @@ export default function ChatSessionView() { isUpdatingName={isUpdatingName} isUpdatingMode={isUpdatingMode} isUpdatingReasoning={isUpdatingReasoning} + isUpdatingNumCtx={isUpdatingNumCtx} onWorkspaceModeChange={handleWorkspaceModeChange} onModeChange={handleModeChange} onReasoningChange={handleReasoningChange} + onNumCtxChange={handleNumCtxChange} + onNumCtxCommit={handleNumCtxCommit} onStartEditProvider={handleStartEditProvider} onSaveProvider={handleSaveProvider} onCancelEditProvider={handleCancelEditProvider} @@ -1199,6 +1246,7 @@ export default function ChatSessionView() { getSortedModels={getSortedModels} truncateModelName={truncateModelName} getSelectedModelCapabilities={getSelectedModelCapabilities} + getSelectedModel={getSelectedModel} /> = {}; if (updates.name !== undefined) { @@ -204,6 +205,12 @@ class ChatSessionController extends DtpController { } allowedUpdates.reasoningEffort = updates.reasoningEffort; } + if (updates.numCtx !== undefined) { + if (typeof updates.numCtx !== "number" || !Number.isInteger(updates.numCtx) || updates.numCtx < 16384 || updates.numCtx % 8192 !== 0) { + throw new Error(`Invalid numCtx: ${updates.numCtx}. Must be an integer >= 16384 and a multiple of 8192.`); + } + allowedUpdates.numCtx = updates.numCtx; + } const session = await ChatSessionService.update( res.locals.chatSession._id, diff --git a/gadget-code/src/models/chat-session.ts b/gadget-code/src/models/chat-session.ts index 81a126a..e55ec58 100644 --- a/gadget-code/src/models/chat-session.ts +++ b/gadget-code/src/models/chat-session.ts @@ -30,6 +30,7 @@ export const ChatSessionSchema = new Schema({ enum: ["off", "low", "medium", "high"], default: "off", }, + numCtx: { type: Number }, stats: { turnCount: { type: Number, default: 0, required: true }, toolCallCount: { type: Number, default: 0, required: true }, diff --git a/gadget-code/src/models/chat-turn.ts b/gadget-code/src/models/chat-turn.ts index 453bb0e..83dff94 100644 --- a/gadget-code/src/models/chat-turn.ts +++ b/gadget-code/src/models/chat-turn.ts @@ -69,6 +69,7 @@ export const ChatTurnSchema = new Schema({ enum: ["off", "low", "medium", "high"], default: "off", }, + numCtx: { type: Number }, mode: { type: String, enum: ChatSessionMode, diff --git a/gadget-code/src/services/chat-session.ts b/gadget-code/src/services/chat-session.ts index 01513da..3c287ec 100644 --- a/gadget-code/src/services/chat-session.ts +++ b/gadget-code/src/services/chat-session.ts @@ -202,6 +202,7 @@ class ChatSessionService extends DtpService { selectedModel: string; mode: ChatSessionMode; reasoningEffort: ReasoningEffort; + numCtx: number; }>, ): Promise { const session = await ChatSession.findById(chatSessionId); @@ -230,6 +231,9 @@ class ChatSessionService extends DtpService { if (updates.reasoningEffort !== undefined) { session.reasoningEffort = updates.reasoningEffort; } + if (updates.numCtx !== undefined) { + session.numCtx = updates.numCtx; + } await session.save(); @@ -276,6 +280,7 @@ class ChatSessionService extends DtpService { provider: session.provider, llm: session.selectedModel, reasoningEffort: (session as IChatSession).reasoningEffort || "off", + numCtx: (session as IChatSession).numCtx, mode: session.mode, status: ChatTurnStatus.Processing, prompts: { diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index 46dabfd..16845ee 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -218,7 +218,7 @@ class AgentService extends GadgetService { try { response = await AiService.chat( turn.provider, - this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning), + this.buildDroneModelConfig(turn.provider, turn.llm, currentReasoning, turn.numCtx), chatOptions, this.makeStreamHandler(socket), ); @@ -474,6 +474,7 @@ class AgentService extends GadgetService { provider: IAiProvider | GadgetId, modelId: string, reasoning: boolean | "low" | "medium" | "high", + numCtxOverride?: number, ): Omit { const settings = this.getModelSettings(provider, modelId); return { @@ -484,7 +485,7 @@ class AgentService extends GadgetService { topP: settings?.topP ?? 0.9, topK: settings?.topK ?? 40, numPredict: settings?.numPredict ?? -1, // -1 = unlimited (Ollama) - numCtx: settings?.numCtx ?? 131072, // 128k fallback + numCtx: numCtxOverride ?? settings?.numCtx ?? 131072, // session override > model settings > 128k fallback maxCompletionTokens: settings?.maxCompletionTokens ?? 16384, // 16k fallback }, }; @@ -626,7 +627,7 @@ class AgentService extends GadgetService { const response = await AiService.chat( provider, - this.buildDroneModelConfig(provider, turn.llm, false), + this.buildDroneModelConfig(provider, turn.llm, false, turn.numCtx), chatOptions, streamHandler, ); diff --git a/packages/api/src/interfaces/chat-session.ts b/packages/api/src/interfaces/chat-session.ts index 4bf2ef5..da6bbd7 100644 --- a/packages/api/src/interfaces/chat-session.ts +++ b/packages/api/src/interfaces/chat-session.ts @@ -35,6 +35,7 @@ export interface IChatSession { provider: IAiProvider | GadgetId; selectedModel: string; reasoningEffort?: ReasoningEffort; + numCtx?: number; stats: { turnCount: number; toolCallCount: number; diff --git a/packages/api/src/interfaces/chat-turn.ts b/packages/api/src/interfaces/chat-turn.ts index cd973b9..cab75e8 100644 --- a/packages/api/src/interfaces/chat-turn.ts +++ b/packages/api/src/interfaces/chat-turn.ts @@ -86,6 +86,7 @@ export interface IChatTurn { provider: IAiProvider | GadgetId; llm: string; // id/name of the model used to process the prompt reasoningEffort?: ReasoningEffort; + numCtx?: number; mode: ChatSessionMode; status: ChatTurnStatus; prompts: IChatTurnPrompts;