mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 20:57:32 +00:00
fix(commands): restore /cost slash command and improve command execution errors
The /cost command was listed as built-in but had no handler, causing execution to
fall through to custom command logic and return 400 ("command path is required").
- Add a built-in /cost handler in server/routes/commands.js
- Return the expected payload shape for the chat UI (`action: "cost"`, token usage,
estimated cost, model)
- Normalize token usage inputs and compute usage percentage
- Add provider-based default pricing for cost estimation
- Fix model selection in command execution context so codex uses `codexModel`
instead of `claudeModel`
- Improve frontend command error handling by parsing backend error responses and
showing meaningful error messages instead of a generic failure
This commit is contained in:
@@ -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) => {
|
'/status': async (args, context) => {
|
||||||
// Read version from package.json
|
// Read version from package.json
|
||||||
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ export function useChatComposerState({
|
|||||||
projectName: selectedProject.name,
|
projectName: selectedProject.name,
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
provider,
|
provider,
|
||||||
model: provider === 'cursor' ? cursorModel : claudeModel,
|
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : claudeModel,
|
||||||
tokenUsage: tokenBudget,
|
tokenUsage: tokenBudget,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,7 +298,14 @@ export function useChatComposerState({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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;
|
const result = (await response.json()) as CommandExecutionResult;
|
||||||
@@ -324,6 +331,7 @@ export function useChatComposerState({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
claudeModel,
|
claudeModel,
|
||||||
|
codexModel,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
cursorModel,
|
cursorModel,
|
||||||
handleBuiltInCommand,
|
handleBuiltInCommand,
|
||||||
|
|||||||
Reference in New Issue
Block a user