diff --git a/server/claude-sdk.js b/server/claude-sdk.js index 918a7bd6d..bbb79ee04 100644 --- a/server/claude-sdk.js +++ b/server/claude-sdk.js @@ -279,7 +279,7 @@ function transformMessage(sdkMessage) { * @param {Object} resultMessage - SDK result message * @returns {Object|null} Token budget object or null */ -function extractTokenBudget(resultMessage) { +function extractTokenBudget(resultMessage, selectedModel) { if (resultMessage.type !== 'result' || !resultMessage.modelUsage) { return null; } @@ -302,9 +302,17 @@ function extractTokenBudget(resultMessage) { // Total used = input + output + cache tokens const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; - // Use configured context window budget from environment (default 160000) - // This is the user's budget limit, not the model's context window - const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000; + // Determine context window: env override > model usage data > selected model > default + let contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 0; + if (!contextWindow) { + if (modelData.contextWindow && modelData.contextWindow > 0) { + contextWindow = modelData.contextWindow; + } else if (selectedModel && selectedModel.toLowerCase().includes('1m')) { + contextWindow = 1000000; + } else { + contextWindow = 200000; + } + } // Token calc logged via token-budget WS event @@ -664,7 +672,7 @@ async function queryClaudeSDK(command, options = {}, ws) { if (models.length > 0) { // Model info available in result message } - const tokenBudgetData = extractTokenBudget(message); + const tokenBudgetData = extractTokenBudget(message, sdkOptions.model); if (tokenBudgetData) { ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' })); } diff --git a/shared/modelConstants.js b/shared/modelConstants.js index 514a17725..bf76b8969 100644 --- a/shared/modelConstants.js +++ b/shared/modelConstants.js @@ -18,6 +18,7 @@ export const CLAUDE_MODELS = { { value: "haiku", label: "Haiku" }, { value: "opusplan", label: "Opus Plan" }, { value: "sonnet[1m]", label: "Sonnet [1M]" }, + { value: "opus[1m]", label: "Opus [1M]" }, ], DEFAULT: "sonnet", diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index e952ee1a6..1865c78e0 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -548,13 +548,21 @@ export function useChatSessionState({ const sessionProvider = selectedSession.__provider || 'claude'; if (sessionProvider !== 'claude') return; + // Fetch initial token usage but don't overwrite a higher value from WebSocket + let cancelled = false; const fetchInitialTokenUsage = async () => { try { const url = `/api/projects/${selectedProject.name}/sessions/${selectedSession.id}/token-usage`; const response = await authenticatedFetch(url); - if (response.ok) { - setTokenBudget(await response.json()); - } else { + if (response.ok && !cancelled) { + const data = await response.json(); + setTokenBudget((prev: Record | null) => { + if (prev && typeof prev.total === 'number' && prev.total > (data.total || 0)) { + return prev; + } + return data; + }); + } else if (!cancelled) { setTokenBudget(null); } } catch (error) { @@ -562,6 +570,7 @@ export function useChatSessionState({ } }; fetchInitialTokenUsage(); + return () => { cancelled = true; }; }, [selectedProject, selectedSession?.id, selectedSession?.__provider]); const visibleMessages = useMemo(() => { diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 19483f649..39a5c74d1 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -352,6 +352,7 @@ function ChatInterface({ thinkingMode={thinkingMode} setThinkingMode={setThinkingMode} tokenBudget={tokenBudget} + selectedModel={claudeModel} slashCommandsCount={slashCommandsCount} onToggleCommandMenu={handleToggleCommandMenu} hasInput={Boolean(input.trim())} diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index e6da236d9..14f2e9f8e 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -49,6 +49,7 @@ interface ChatComposerProps { thinkingMode: string; setThinkingMode: Dispatch>; tokenBudget: { used?: number; total?: number } | null; + selectedModel?: string; slashCommandsCount: number; onToggleCommandMenu: () => void; hasInput: boolean; @@ -105,6 +106,7 @@ export default function ChatComposer({ thinkingMode, setThinkingMode, tokenBudget, + selectedModel, slashCommandsCount, onToggleCommandMenu, hasInput, @@ -192,6 +194,7 @@ export default function ChatComposer({ thinkingMode={thinkingMode} setThinkingMode={setThinkingMode} tokenBudget={tokenBudget} + selectedModel={selectedModel} slashCommandsCount={slashCommandsCount} onToggleCommandMenu={onToggleCommandMenu} hasInput={hasInput} diff --git a/src/components/chat/view/subcomponents/ChatInputControls.tsx b/src/components/chat/view/subcomponents/ChatInputControls.tsx index 1c05c02ae..bb8f3a8c8 100644 --- a/src/components/chat/view/subcomponents/ChatInputControls.tsx +++ b/src/components/chat/view/subcomponents/ChatInputControls.tsx @@ -11,6 +11,7 @@ interface ChatInputControlsProps { thinkingMode: string; setThinkingMode: React.Dispatch>; tokenBudget: { used?: number; total?: number } | null; + selectedModel?: string; slashCommandsCount: number; onToggleCommandMenu: () => void; hasInput: boolean; @@ -27,6 +28,7 @@ export default function ChatInputControls({ thinkingMode, setThinkingMode, tokenBudget, + selectedModel, slashCommandsCount, onToggleCommandMenu, hasInput, @@ -78,7 +80,7 @@ export default function ChatInputControls({ {}} className="" /> )} - +