Skip to content
48 changes: 15 additions & 33 deletions claude/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ outputs:
description: >-
JSON object with token usage: input_tokens, output_tokens,
cache_creation_input_tokens, cache_read_input_tokens, turns,
model, cost_usd
model, cost_usd, partial. `partial` is true when the run emitted no
result event (typically a cancellation) and the counts were
reconstructed from the session log; `cost_usd` is null in that case,
since only the result event carries it.
value: ${{ steps.tokens.outputs.usage }}

runs:
Expand Down Expand Up @@ -527,14 +530,10 @@ runs:
echo "" >> "$GITHUB_STEP_SUMMARY"
fi

# Parse the stream-json the headless run wrote to stdout. It is NDJSON (one
# SDK message event per line) containing one or more `type: "result"`
# events. Sessions that use `run_in_background: true` Bash emit a second
# `result` on wakeup; `usage.*` and `num_turns` are per-event, while
# `total_cost_usd` is cumulative. Sum the per-event fields across all
# entries and take cost from the last. Output shape mirrors the interactive
# harness so downstream consumers (review-reviewers' evidence gist,
# token-report.sh, dashboards) don't branch on harness.
# Account the run's token usage from the stream-json the headless run wrote
# to stdout, falling back to the session JSONL when the run was cancelled
# before it emitted a `type: "result"` event. Both parsers live in
# compute-token-usage.sh so pytest can cover them.
- name: Token usage
if: always()
id: tokens
Expand All @@ -555,29 +554,9 @@ runs:
cp -a "${AGENT_HOME}/.claude/projects/." "$LOGS_DIR/" 2>/dev/null || true
cp -a "$RUNNER_TEMP/tend-claude-stderr.log" "$LOGS_DIR/" 2>/dev/null || true

if [ -n "${STREAM_JSON:-}" ] && [ -s "$STREAM_JSON" ]; then
USAGE=$(jq -s -c --arg model "$MODEL" '
(map(select(.type == "result"))) as $rs |
if ($rs | length) == 0 then
{input_tokens:0, output_tokens:0, cache_creation_input_tokens:0,
cache_read_input_tokens:0, turns:0, model:$model, cost_usd:0}
else
($rs | last) as $r |
{
input_tokens: ([$rs[].usage.input_tokens // 0] | add),
output_tokens: ([$rs[].usage.output_tokens // 0] | add),
cache_creation_input_tokens: ([$rs[].usage.cache_creation_input_tokens // 0] | add),
cache_read_input_tokens: ([$rs[].usage.cache_read_input_tokens // 0] | add),
turns: ([$rs[].num_turns // 0] | add),
model: $model,
cost_usd: (($r.total_cost_usd // 0) * 100 | round / 100)
}
end
' "$STREAM_JSON" 2>/dev/null || echo '')
fi
if [ -z "${USAGE:-}" ]; then
USAGE='{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"turns":0,"model":"'"$MODEL"'","cost_usd":0}'
fi
# Runs after the copy above, so the fallback can read the session JSONL.
USAGE=$(LOGS_DIR="$LOGS_DIR" \
bash "${{ github.action_path }}/../shared/steps/compute-token-usage.sh")

echo "$USAGE" > "$LOGS_DIR/token-usage.json"
echo "usage=$(jq -c . <<< "$USAGE")" >> "$GITHUB_OUTPUT"
Expand All @@ -593,7 +572,7 @@ runs:
fi

jq -r '
def usd: tostring | if test("\\.") then split(".") | "\(.[0]).\((.[1] + "00")[:2])" else . + ".00" end | "$" + .;
def usd: if . == null then "unknown" else tostring | if test("\\.") then split(".") | "\(.[0]).\((.[1] + "00")[:2])" else . + ".00" end | "$" + . end;
"## Token Usage",
"| Metric | Value |",
"|--------|-------|",
Expand All @@ -604,6 +583,9 @@ runs:
"| Cost | \(.cost_usd | usd) |",
"| Turns | \(.turns) |",
"",
if .partial then
"*Reconstructed from the session log: this run emitted no result event (most often a cancellation), so the token counts are its own but the cost is not recoverable.*"
else empty end,
"*Cost at API list prices — a large multiple of the effective rate on Claude Code subscriptions.*"
' <<< "$USAGE" >> "$GITHUB_STEP_SUMMARY"

Expand Down
Loading
Loading