feat: expose token usage as step outputs#1189
feat: expose token usage as step outputs#1189adamhenson wants to merge 4 commits intoanthropics:mainfrom
Conversation
|
|
||
| const messages: SDKMessage[] = []; | ||
| let resultMessage: SDKResultMessage | undefined; | ||
| let totalInputTokens = 0; |
There was a problem hiding this comment.
what's your opinion about having just one const object and updating its props?
I feel it seems more structured but your approach is already fine
There was a problem hiding this comment.
I'm suggesting something like
const budget = {
tokens: {
input: 0,
output: 0,
},
cache: {
read: 0,
write: 0,
}
} satisfies TYPE
There was a problem hiding this comment.
Appreciate the suggestion! The flat variables feel more conventional here -- the rest of the codebase uses the same pattern (e.g. resultMessage, commentId, claudeBranch are all individual let declarations rather than grouped objects). A nested object would also require introducing a new type just for internal accumulation, without adding much clarity given these four values flow into separate fields on ClaudeRunResult right after.
|
|
||
| if (message.type === "assistant") { | ||
| const usage = (message as SDKAssistantMessage).message.usage; | ||
| totalInputTokens += usage.input_tokens || 0; |
There was a problem hiding this comment.
I would prefer ?? instead of ||. It feels safer.
There was a problem hiding this comment.
Good catch. Fixed in the latest commit -- changed all four to ??. You're right that || would incorrectly treat 0 as falsy, and the cache fields are typed as number | null in BetaUsage, so ?? is the correct operator here.
| } | ||
|
|
||
| if (message.type === "assistant") { | ||
| const usage = (message as SDKAssistantMessage).message.usage; |
There was a problem hiding this comment.
this casting is used above this line, maybe moving out and using the same casting for both branches seems cleaner.
There was a problem hiding this comment.
The two casts are for different types in different branches of the same discriminated union -- SDKResultMessage when message.type === "result" and SDKAssistantMessage when message.type === "assistant". Moving them outside would mean casting message to two different types before the branches even run, which doesn't model the intent. Keeping each cast inside its own type-narrowed branch is the standard TypeScript pattern here.
| } | ||
| core.setOutput("conclusion", claudeResult.conclusion); | ||
| if (claudeResult.inputTokens !== undefined) { | ||
| core.setOutput("input_tokens", String(claudeResult.inputTokens)); |
There was a problem hiding this comment.
do we really need to transform intro string?
There was a problem hiding this comment.
Yes -- core.setOutput signature is (name: string, value: string) and the fields on ClaudeRunResult are number | undefined, so TypeScript requires the conversion. Passing the number directly would be a type error.
Accumulate input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, and turns during SDK execution and surface them as step outputs. Closes anthropics#59, addresses anthropics#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
Made-with: Cursor
Made-with: Cursor
f30e31c to
50d4c4a
Compare
Closes #59, closes #136.
The action tracks token usage internally but never surfaces it as step outputs, so workflows can't gate on cost, log spend, or forward counts to external tools. This PR wires it up.
Changes
base-action/src/run-claude-sdk.ts— ExtendedClaudeRunResultwith 5 new optional fields. Added accumulators in the SDK message loop summingmessage.usagefrom each assistant message, and set them on the result after the loop alongsidenum_turnsfromresultMessage.src/entrypoints/run.ts— 5 newcore.setOutput()calls after conclusion, using?? ""as fallback (same pattern as the rest of the codebase).action.yml— 5 new entries in theoutputs:section.docs/usage.md— New## Tracking Token Usage and Costsection inserted between Structured Outputs and Ways to Tag @claude, with sub-sections covering logging, budget enforcement, and forwarding to a cost dashboard.Usage after this PR
Budget enforcement is now possible via a simple workflow step checking the outputs — partially addressing #136.