Skip to content

Commit 66252c9

Browse files
committed
Add calculations for claude code costs
1 parent a5b3ee2 commit 66252c9

File tree

6 files changed

+117
-42
lines changed

6 files changed

+117
-42
lines changed

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"cy:ci": "cypress run"
2424
},
2525
"devDependencies": {
26-
"@anthropic-ai/sdk": "^0.27.3",
26+
"@anthropic-ai/sdk": "^0.59.0",
2727
"@gitbutler/shared": "workspace:*",
2828
"@gitbutler/svelte-comment-injector": "workspace:*",
2929
"@gitbutler/ui": "workspace:*",

apps/desktop/src/components/codegen/CodegenPage.svelte

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import CodegenSidebar from '$components/codegen/CodegenSidebar.svelte';
77
import CodegenSidebarEntry from '$components/codegen/CodegenSidebarEntry.svelte';
88
import { CLAUDE_CODE_SERVICE } from '$lib/codegen/claude';
9-
import { formatMessages } from '$lib/codegen/messages';
9+
import { formatMessages, usageStats } from '$lib/codegen/messages';
1010
import { STACK_SERVICE } from '$lib/stacks/stackService.svelte';
1111
import { combineResults } from '$lib/state/helpers';
1212
import { inject } from '@gitbutler/shared/context';
@@ -135,18 +135,26 @@
135135
{#snippet sidebarContentEntry(projectId: string, stackId: string, head: string)}
136136
{@const branch = stackService.branchByName(projectId, stackId, head)}
137137
{@const commits = stackService.commits(projectId, stackId, head)}
138-
<ReduxResult result={combineResults(branch.current, commits.current)} {projectId} {stackId}>
139-
{#snippet children([branch, commits], { projectId: _projectId, stackId })}
140-
{stackId}
138+
{@const events = claudeCodeService.messages({
139+
projectId,
140+
stackId: selectedBranch?.stackId || ''
141+
})}
142+
<ReduxResult
143+
result={combineResults(branch.current, commits.current, events.current)}
144+
{projectId}
145+
{stackId}
146+
>
147+
{#snippet children([branch, commits, events], { projectId: _projectId, stackId })}
148+
{@const usage = usageStats(events)}
141149
<CodegenSidebarEntry
142150
onclick={() => {
143151
selectedBranch = { stackId, head: branch.name };
144152
}}
145153
selected={selectedBranch?.stackId === stackId && selectedBranch?.head === branch.name}
146154
branchName={branch.name}
147155
status="vibes"
148-
tokensUsed={69}
149-
cost={4.2}
156+
tokensUsed={usage.tokens}
157+
cost={usage.cost}
150158
commitCount={commits.length}
151159
commits={commitsList}
152160
/>

apps/desktop/src/components/codegen/CodegenSidebarEntry.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<div class="flex gap-8 items-center">
4040
<p class="text-12">{tokensUsed}</p>
4141
<div class="iddy-biddy-line"></div>
42-
<p class="text-12">{cost.toFixed(2)}</p>
42+
<p class="text-12">${cost.toFixed(2)}</p>
4343
</div>
4444
</div>
4545
{/snippet}

apps/desktop/src/lib/codegen/messages.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,78 @@ export function formatMessages(events: ClaudeMessage[]): Message[] {
9494

9595
return out;
9696
}
97+
98+
/** Anthropic prices, per 1M tokens */
99+
const pricing = [
100+
{
101+
name: 'claude-opus',
102+
input: 15,
103+
output: 75,
104+
writeCache: 18.75,
105+
readCache: 1.5
106+
},
107+
{
108+
name: 'claude-sonnet',
109+
input: 3,
110+
output: 6,
111+
writeCache: 3.75,
112+
readCache: 0.3
113+
},
114+
{
115+
name: 'claude-haiku',
116+
input: 0.8,
117+
output: 4,
118+
writeCache: 1,
119+
readCache: 0.08
120+
}
121+
] as const;
122+
123+
/** Cost of anthropic making web request calls per 1K calls */
124+
const webRequestCost = 10;
125+
126+
/**
127+
* Calculates the usage stats from the message log.
128+
*
129+
* This makes use of the "assistant" messages rather than the "result" ones
130+
* because the "assistant" ones come in more frequently.
131+
*
132+
* For some reason the final quantity of tokens ends up slightly greater than if
133+
* you were using the result, however, the calculated cost ends up being the
134+
* same as the cost provided in the result based messages.
135+
*
136+
* I can only assume that there is a mistake in the token counting code on CC's
137+
* side.
138+
*/
139+
export function usageStats(events: ClaudeMessage[]): { tokens: number; cost: number } {
140+
let tokens = 0;
141+
let cost = 0;
142+
for (const event of events) {
143+
if (event.content.type !== 'claudeOutput') continue;
144+
const message = event.content.subject;
145+
if (message.type !== 'assistant') continue;
146+
147+
const usage = message.message.usage;
148+
tokens += usage.input_tokens;
149+
tokens += usage.output_tokens;
150+
151+
const modelPricing = findModelPricing(message.message.model);
152+
if (!modelPricing) continue;
153+
154+
cost += (usage.input_tokens * modelPricing.input) / 1_000_000;
155+
cost += (usage.output_tokens * modelPricing.output) / 1_000_000;
156+
cost += ((usage.cache_creation_input_tokens || 0) * modelPricing.writeCache) / 1_000_000;
157+
cost += ((usage.cache_read_input_tokens || 0) * modelPricing.readCache) / 1_000_000;
158+
cost += ((usage.server_tool_use?.web_search_requests || 0) * webRequestCost) / 1_000;
159+
}
160+
161+
return { tokens, cost };
162+
}
163+
164+
function findModelPricing(name: string) {
165+
for (const p of pricing) {
166+
// We do a starts with so we don't have to deal with all the versioning
167+
if (name.startsWith(p.name)) {
168+
return p;
169+
}
170+
}
171+
}

apps/desktop/src/lib/codegen/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Message, MessageParam } from '@anthropic-ai/sdk/resources/index.mjs';
1+
import type { Message, MessageParam, Usage } from '@anthropic-ai/sdk/resources/index.mjs';
22

33
/**
44
* Represents different types of events that can occur during Claude code interactions
@@ -43,6 +43,7 @@ export type ClaudeCodeMessage =
4343
num_turns: number;
4444
session_id: string;
4545
total_cost_usd: number;
46+
usage: Usage;
4647
}
4748

4849
/** Emitted as the first message at the start of a conversation */

pnpm-lock.yaml

Lines changed: 24 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)