Skip to content

Commit ea109b4

Browse files
committed
feat: expose token usage as step outputs
Accumulate input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, and turns during SDK execution and surface them as step outputs. Closes #59, addresses #136. Also adds a docs/usage.md section showing how to log, budget-gate, and forward token counts to a cost dashboard. Made-with: Cursor
1 parent 38ec876 commit ea109b4

File tree

4 files changed

+122
-0
lines changed

4 files changed

+122
-0
lines changed

action.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ outputs:
167167
session_id:
168168
description: "The Claude Code session ID that can be used with --resume to continue this conversation"
169169
value: ${{ steps.run.outputs.session_id }}
170+
input_tokens:
171+
description: "Number of input tokens used in this run"
172+
value: ${{ steps.run.outputs.input_tokens }}
173+
output_tokens:
174+
description: "Number of output tokens generated in this run"
175+
value: ${{ steps.run.outputs.output_tokens }}
176+
cache_read_tokens:
177+
description: "Number of tokens read from the prompt cache"
178+
value: ${{ steps.run.outputs.cache_read_tokens }}
179+
cache_write_tokens:
180+
description: "Number of tokens written to the prompt cache"
181+
value: ${{ steps.run.outputs.cache_write_tokens }}
182+
turns:
183+
description: "Number of agent turns in this run"
184+
value: ${{ steps.run.outputs.turns }}
170185

171186
runs:
172187
using: "composite"

base-action/src/run-claude-sdk.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export type ClaudeRunResult = {
1414
sessionId?: string;
1515
conclusion: "success" | "failure";
1616
structuredOutput?: string;
17+
inputTokens?: number;
18+
outputTokens?: number;
19+
cacheReadTokens?: number;
20+
cacheWriteTokens?: number;
21+
numTurns?: number;
1722
};
1823

1924
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
@@ -156,6 +161,10 @@ export async function runClaudeWithSdk(
156161

157162
const messages: SDKMessage[] = [];
158163
let resultMessage: SDKResultMessage | undefined;
164+
let totalInputTokens = 0;
165+
let totalOutputTokens = 0;
166+
let totalCacheReadTokens = 0;
167+
let totalCacheWriteTokens = 0;
159168

160169
try {
161170
for await (const message of query({ prompt, options: sdkOptions })) {
@@ -169,6 +178,14 @@ export async function runClaudeWithSdk(
169178
if (message.type === "result") {
170179
resultMessage = message as SDKResultMessage;
171180
}
181+
182+
if (message.type === "assistant") {
183+
const usage = (message as any).message?.usage || {};
184+
totalInputTokens += usage.input_tokens || 0;
185+
totalOutputTokens += usage.output_tokens || 0;
186+
totalCacheReadTokens += usage.cache_read_input_tokens || 0;
187+
totalCacheWriteTokens += usage.cache_creation_input_tokens || 0;
188+
}
172189
}
173190
} catch (error) {
174191
console.error("SDK execution error:", error);
@@ -205,6 +222,12 @@ export async function runClaudeWithSdk(
205222
const isSuccess = resultMessage.subtype === "success";
206223
result.conclusion = isSuccess ? "success" : "failure";
207224

225+
result.inputTokens = totalInputTokens;
226+
result.outputTokens = totalOutputTokens;
227+
result.cacheReadTokens = totalCacheReadTokens;
228+
result.cacheWriteTokens = totalCacheWriteTokens;
229+
result.numTurns = resultMessage.num_turns;
230+
208231
// Handle structured output
209232
if (hasJsonSchema) {
210233
if (

docs/usage.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,85 @@ See `examples/test-failure-analysis.yml` for a working example that:
257257
For complete details on JSON Schema syntax and Agent SDK structured outputs:
258258
https://docs.claude.com/en/docs/agent-sdk/structured-outputs
259259

260+
## Tracking Token Usage and Cost
261+
262+
`claude-code-action` exposes token usage as step outputs after each run:
263+
264+
| Output | Description |
265+
|--------|-------------|
266+
| `input_tokens` | Tokens in the prompt |
267+
| `output_tokens` | Tokens generated by Claude |
268+
| `cache_read_tokens` | Tokens read from the prompt cache |
269+
| `cache_write_tokens` | Tokens written to the prompt cache |
270+
| `turns` | Number of agent turns in the run |
271+
272+
### Logging token usage
273+
274+
```yaml
275+
- uses: anthropics/claude-code-action@v1
276+
id: claude
277+
with:
278+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
279+
prompt: "Review this PR for security issues"
280+
281+
- name: Log token usage
282+
run: |
283+
echo "Input tokens: ${{ steps.claude.outputs.input_tokens }}"
284+
echo "Output tokens: ${{ steps.claude.outputs.output_tokens }}"
285+
echo "Cache read tokens: ${{ steps.claude.outputs.cache_read_tokens }}"
286+
echo "Cache write tokens: ${{ steps.claude.outputs.cache_write_tokens }}"
287+
echo "Turns: ${{ steps.claude.outputs.turns }}"
288+
```
289+
290+
### Budget enforcement
291+
292+
```yaml
293+
- uses: anthropics/claude-code-action@v1
294+
id: claude
295+
with:
296+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
297+
prompt: "..."
298+
299+
- name: Check cost threshold
300+
env:
301+
INPUT_TOKENS: ${{ steps.claude.outputs.input_tokens }}
302+
OUTPUT_TOKENS: ${{ steps.claude.outputs.output_tokens }}
303+
run: |
304+
# claude-sonnet-4-5: $3/1M input, $15/1M output (adjust for your model)
305+
COST=$(echo "scale=4; ($INPUT_TOKENS * 3 + $OUTPUT_TOKENS * 15) / 1000000" | bc)
306+
echo "Estimated cost: \$$COST"
307+
if (( $(echo "$COST > 0.50" | bc -l) )); then
308+
echo "::error::Run exceeded $0.50 cost threshold"
309+
exit 1
310+
fi
311+
```
312+
313+
### Sending to a cost dashboard
314+
315+
For teams tracking spend across many agent workflow runs, the token outputs can be
316+
forwarded to a cost tracking tool. [AgentMeter](https://agentmeter.app) is a
317+
GitHub-native cost dashboard built for this — it receives token counts from your
318+
workflow and shows per-run cost, per-repo spend trends, and budget alerts.
319+
320+
```yaml
321+
- uses: anthropics/claude-code-action@v1
322+
id: claude
323+
with:
324+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
325+
prompt: "..."
326+
327+
- uses: AgentMeter/agentmeter-action@v1
328+
with:
329+
api_key: ${{ secrets.AGENTMETER_API_KEY }}
330+
model: claude-sonnet-4-5
331+
input_tokens: ${{ steps.claude.outputs.input_tokens }}
332+
output_tokens: ${{ steps.claude.outputs.output_tokens }}
333+
cache_read_tokens: ${{ steps.claude.outputs.cache_read_tokens }}
334+
cache_write_tokens: ${{ steps.claude.outputs.cache_write_tokens }}
335+
turns: ${{ steps.claude.outputs.turns }}
336+
status: ${{ job.status }}
337+
```
338+
260339
## Ways to Tag @claude
261340

262341
These examples show how to interact with Claude using comments in PRs and issues. By default, Claude will be triggered anytime you mention `@claude`, but you can customize the exact trigger phrase using the `trigger_phrase` input in the workflow.

src/entrypoints/run.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,11 @@ async function run() {
294294
core.setOutput("structured_output", claudeResult.structuredOutput);
295295
}
296296
core.setOutput("conclusion", claudeResult.conclusion);
297+
core.setOutput("input_tokens", claudeResult.inputTokens ?? "");
298+
core.setOutput("output_tokens", claudeResult.outputTokens ?? "");
299+
core.setOutput("cache_read_tokens", claudeResult.cacheReadTokens ?? "");
300+
core.setOutput("cache_write_tokens", claudeResult.cacheWriteTokens ?? "");
301+
core.setOutput("turns", claudeResult.numTurns ?? "");
297302
} catch (error) {
298303
const errorMessage = error instanceof Error ? error.message : String(error);
299304
// Only mark as prepare failure if we haven't completed the prepare phase

0 commit comments

Comments
 (0)