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
This commit is contained in:
parent
8650533697
commit
140c8a75ee
@ -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<HTMLSelectElement>) => void;
|
||||
onReasoningChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
onNumCtxChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onNumCtxCommit: (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => 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 (
|
||||
<div className="border-b border-border-subtle shrink-0">
|
||||
@ -261,6 +271,27 @@ export default function SessionPanel({
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
{providers.find(p => p._id === selectedProviderId)?.apiType !== 'openai' && (
|
||||
<div>
|
||||
<div className="text-xs text-text-muted">Context Window</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<input
|
||||
type="range"
|
||||
min={16384}
|
||||
max={getSelectedModel()?.contextWindow || 131072}
|
||||
step={8192}
|
||||
value={sessionNumCtx}
|
||||
onChange={onNumCtxChange}
|
||||
onMouseUp={onNumCtxCommit}
|
||||
onTouchEnd={onNumCtxCommit}
|
||||
className="flex-1 h-2 bg-bg-tertiary rounded appearance-none cursor-pointer accent-brand"
|
||||
/>
|
||||
<span className="text-xs text-text-secondary w-16 text-right tabular-nums">
|
||||
{sessionNumCtx.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -68,6 +68,8 @@ export default function ChatSessionView() {
|
||||
const [selectedProviderId, setSelectedProviderId] = useState<string>('');
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||||
const [sessionReasoningEffort, setSessionReasoningEffort] = useState<string>('off');
|
||||
const [sessionNumCtx, setSessionNumCtx] = useState<number>(131072);
|
||||
const [isUpdatingNumCtx, setIsUpdatingNumCtx] = useState(false);
|
||||
const [isEditingProvider, setIsEditingProvider] = useState(false);
|
||||
const [editProviderId, setEditProviderId] = useState('');
|
||||
const [editModelId, setEditModelId] = useState('');
|
||||
@ -215,6 +217,15 @@ export default function ChatSessionView() {
|
||||
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');
|
||||
appContext?.setStatusMessage('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<HTMLSelectElement>) => {
|
||||
if (!session) return;
|
||||
|
||||
@ -821,6 +835,28 @@ export default function ChatSessionView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNumCtxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSessionNumCtx(parseInt(e.target.value, 10));
|
||||
};
|
||||
|
||||
const handleNumCtxCommit = async (e: React.MouseEvent<HTMLInputElement> | React.TouchEvent<HTMLInputElement>) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
<ProjectPanel
|
||||
|
||||
@ -179,6 +179,7 @@ class ChatSessionController extends DtpController {
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: "off" | "low" | "medium" | "high";
|
||||
numCtx: number;
|
||||
}> = {};
|
||||
|
||||
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,
|
||||
|
||||
@ -30,6 +30,7 @@ export const ChatSessionSchema = new Schema<IChatSession>({
|
||||
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 },
|
||||
|
||||
@ -69,6 +69,7 @@ export const ChatTurnSchema = new Schema<IChatTurn>({
|
||||
enum: ["off", "low", "medium", "high"],
|
||||
default: "off",
|
||||
},
|
||||
numCtx: { type: Number },
|
||||
mode: {
|
||||
type: String,
|
||||
enum: ChatSessionMode,
|
||||
|
||||
@ -202,6 +202,7 @@ class ChatSessionService extends DtpService {
|
||||
selectedModel: string;
|
||||
mode: ChatSessionMode;
|
||||
reasoningEffort: ReasoningEffort;
|
||||
numCtx: number;
|
||||
}>,
|
||||
): Promise<IChatSession> {
|
||||
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: {
|
||||
|
||||
@ -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<IDroneModelConfig, "provider"> {
|
||||
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,
|
||||
);
|
||||
|
||||
@ -35,6 +35,7 @@ export interface IChatSession {
|
||||
provider: IAiProvider | GadgetId;
|
||||
selectedModel: string;
|
||||
reasoningEffort?: ReasoningEffort;
|
||||
numCtx?: number;
|
||||
stats: {
|
||||
turnCount: number;
|
||||
toolCallCount: number;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user