Skip to content

feat: expose token usage as step outputs#1189

Open
adamhenson wants to merge 4 commits intoanthropics:mainfrom
adamhenson:docs/token-cost-tracking
Open

feat: expose token usage as step outputs#1189
adamhenson wants to merge 4 commits intoanthropics:mainfrom
adamhenson:docs/token-cost-tracking

Conversation

@adamhenson
Copy link
Copy Markdown

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 — Extended ClaudeRunResult with 5 new optional fields. Added accumulators in the SDK message loop summing message.usage from each assistant message, and set them on the result after the loop alongside num_turns from resultMessage.

src/entrypoints/run.ts — 5 new core.setOutput() calls after conclusion, using ?? "" as fallback (same pattern as the rest of the codebase).

action.yml — 5 new entries in the outputs: section.

docs/usage.md — New ## Tracking Token Usage and Cost section 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

- uses: anthropics/claude-code-action@v1
  id: claude
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    prompt: "..."

- name: Log token usage
  run: |
    echo "Input tokens:  ${{ steps.claude.outputs.input_tokens }}"
    echo "Output tokens: ${{ steps.claude.outputs.output_tokens }}"
    echo "Turns:         ${{ steps.claude.outputs.turns }}"

Budget enforcement is now possible via a simple workflow step checking the outputs — partially addressing #136.

This was referenced Apr 6, 2026
Copy link
Copy Markdown

@andrevandal andrevandal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thank you!


const messages: SDKMessage[] = [];
let resultMessage: SDKResultMessage | undefined;
let totalInputTokens = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm suggesting something like

const budget = {
    tokens: {
      input: 0,
      output: 0,
    },
    cache: {
      read: 0,
      write: 0,
  }
} satisfies TYPE

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread base-action/src/run-claude-sdk.ts Outdated

if (message.type === "assistant") {
const usage = (message as SDKAssistantMessage).message.usage;
totalInputTokens += usage.input_tokens || 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer ?? instead of ||. It feels safer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this casting is used above this line, maybe moving out and using the same casting for both branches seems cleaner.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/entrypoints/run.ts
}
core.setOutput("conclusion", claudeResult.conclusion);
if (claudeResult.inputTokens !== undefined) {
core.setOutput("input_tokens", String(claudeResult.inputTokens));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to transform intro string?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@adamhenson adamhenson force-pushed the docs/token-cost-tracking branch from f30e31c to 50d4c4a Compare April 19, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mechanism for setting a cap on cost Cost report

2 participants