diff --git a/server/routes/commands.js b/server/routes/commands.js index b13a8f3..5446734 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -209,6 +209,86 @@ Custom commands can be created in: }; }, + '/cost': async (args, context) => { + const tokenUsage = context?.tokenUsage || {}; + const provider = context?.provider || 'claude'; + const model = + context?.model || + (provider === 'cursor' + ? CURSOR_MODELS.DEFAULT + : provider === 'codex' + ? CODEX_MODELS.DEFAULT + : CLAUDE_MODELS.DEFAULT); + + const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0; + const total = + Number( + tokenUsage.total ?? + tokenUsage.contextWindow ?? + parseInt(process.env.CONTEXT_WINDOW || '160000', 10), + ) || 160000; + const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0; + + const inputTokensRaw = + Number( + tokenUsage.inputTokens ?? + tokenUsage.input ?? + tokenUsage.cumulativeInputTokens ?? + tokenUsage.promptTokens ?? + 0, + ) || 0; + const outputTokens = + Number( + tokenUsage.outputTokens ?? + tokenUsage.output ?? + tokenUsage.cumulativeOutputTokens ?? + tokenUsage.completionTokens ?? + 0, + ) || 0; + const cacheTokens = + Number( + tokenUsage.cacheReadTokens ?? + tokenUsage.cacheCreationTokens ?? + tokenUsage.cacheTokens ?? + tokenUsage.cachedTokens ?? + 0, + ) || 0; + + // If we only have total used tokens, treat them as input for display/estimation. + const inputTokens = + inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used; + + // Rough default rates by provider (USD / 1M tokens). + const pricingByProvider = { + claude: { input: 3, output: 15 }, + cursor: { input: 3, output: 15 }, + codex: { input: 1.5, output: 6 }, + }; + const rates = pricingByProvider[provider] || pricingByProvider.claude; + + const inputCost = (inputTokens / 1_000_000) * rates.input; + const outputCost = (outputTokens / 1_000_000) * rates.output; + const totalCost = inputCost + outputCost; + + return { + type: 'builtin', + action: 'cost', + data: { + tokenUsage: { + used, + total, + percentage, + }, + cost: { + input: inputCost.toFixed(4), + output: outputCost.toFixed(4), + total: totalCost.toFixed(4), + }, + model, + }, + }; + }, + '/status': async (args, context) => { // Read version from package.json const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json'); diff --git a/src/hooks/chat/useChatComposerState.ts b/src/hooks/chat/useChatComposerState.ts index 4c75933..a04ab62 100644 --- a/src/hooks/chat/useChatComposerState.ts +++ b/src/hooks/chat/useChatComposerState.ts @@ -280,7 +280,7 @@ export function useChatComposerState({ projectName: selectedProject.name, sessionId: currentSessionId, provider, - model: provider === 'cursor' ? cursorModel : claudeModel, + model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : claudeModel, tokenUsage: tokenBudget, }; @@ -298,7 +298,14 @@ export function useChatComposerState({ }); if (!response.ok) { - throw new Error('Failed to execute command'); + let errorMessage = `Failed to execute command (${response.status})`; + try { + const errorData = await response.json(); + errorMessage = errorData?.message || errorData?.error || errorMessage; + } catch { + // Ignore JSON parse failures and use fallback message. + } + throw new Error(errorMessage); } const result = (await response.json()) as CommandExecutionResult; @@ -324,6 +331,7 @@ export function useChatComposerState({ }, [ claudeModel, + codexModel, currentSessionId, cursorModel, handleBuiltInCommand,