How Token Counting Estimates AI Usage and Costs in gh-aw

gh-aw estimates AI usage and costs by applying a 4-characters-per-token heuristic for rapid guardrail checks and parsing JSON usage metadata for precise per-engine token and cost extraction.

The github/gh-aw CLI tool monitors AI-assisted workflow runs by converting raw log output into measurable token counts and monetary costs. Understanding how token counting estimates AI usage and costs helps developers audit spending and prevent runaway token consumption through automated guardrails.

Two-Stage Token Estimation Pipeline

The implementation uses a dual-layer approach: a fast character-based estimate for safety checks, followed by precise JSON parsing for accurate billing metrics.

Rough Character-Based Estimation (Guardrail)

For rapid size validation before deep parsing, the estimateTokens helper in pkg/cli/mcp_logs_guardrail.go applies the industry-standard OpenAI approximation:

const CharsPerToken = 4 // ≈4 characters ≈ 1 token

func estimateTokens(text string) int {
    return len(text) / CharsPerToken
}

This logic appears at lines 44-48. When the estimated count exceeds DefaultMaxMCPLogsOutputTokens (12,000), the guardrail triggers and returns a truncated JSON payload instead of processing the full log.

Precise JSON Parsing Per Engine

For accurate accounting, engine-specific parsers extract exact token counts from structured JSON embedded in logs. Each provider reports usage differently, requiring dedicated parsers in the pkg/workflow/ directory.

Copilot logs are processed by pkg/workflow/copilot_logs.go, which unmarshals JSON entries and sums InputTokens and OutputTokens from the Usage field:

totalTokenUsage := 0
for _, entry := range jsonEntries {
    totalTokenUsage += entry.Usage.InputTokens + entry.Usage.OutputTokens
}
metrics.TokenUsage = totalTokenUsage

Claude logs use pkg/workflow/claude_logs.go, which handles dynamic JSON structures by checking for the usage key and extracting input_tokens and output_tokens:

if usage, ok := jsonData["usage"]; ok {
    if m, ok := usage.(map[string]any); ok {
        in := toInt(m["input_tokens"])
        out := toInt(m["output_tokens"])
        metrics.TokenUsage += in + out
    }
}

Cost Extraction from Provider Logs

After determining token counts, gh-aw extracts monetary costs using the generic ExtractJSONCost function defined in pkg/workflow/metrics.go at lines 71-88. This helper searches for common cost field names across different AI providers:

func ExtractJSONCost(data map[string]any) float64 {
    // Prefer explicit total_cost_usd
    if v, ok := data["total_cost_usd"]; ok {
        if c := ConvertToFloat(v); c > 0 {
            return c
        }
    }
    // Fallback to common aliases
    costFields := []string{"cost", "price", "amount", "total_cost", "estimated_cost"}
    for _, f := range costFields {
        if v, ok := data[f]; ok {
            if c := ConvertToFloat(v); c > 0 {
                return c
            }
        }
    }
    return 0
}

Engine-specific parsers invoke this helper after unmarshaling JSON blocks, adding the result to the running EstimatedCost total.

Aggregation and Guardrail Enforcement

All per-engine metrics converge in the MetricsData struct defined in pkg/workflow/metrics.go (lines 29-41):

type MetricsData struct {
    TokenUsage    int
    EstimatedCost float64
    Turns         int
    // …other fields like ToolCalls, Errors
}

The guardrail logic in pkg/cli/mcp_logs_guardrail.go uses the rough token estimate to protect against oversized responses. When estimateTokens returns a value exceeding the configurable max_tokens limit (defaulting to 12,000), the system returns a guardrail JSON payload containing the token count and schema information rather than processing the full log content.

Practical Implementation Examples

Checking Log Size with the Guardrail

Use the CheckMCPLogSize wrapper to validate log size before processing:

import "github.com/github/gh-aw/pkg/cli"

func main() {
    rawLog := fetchMCPLog() // string from MCP server
    trimmed, guarded := cli.CheckMCPLogSize(rawLog, 0) // 0 uses default limit (12,000)
    if guarded {
        fmt.Println("Guardrail triggered:")
        fmt.Println(trimmed) // JSON with token count & schema
        return
    }
    fmt.Println("Log size OK, proceed with normal parsing")
}

Extracting Metrics from Copilot Logs

Parse Copilot-specific logs to extract precise token and cost metrics:

import "github.com/github/gh-aw/pkg/workflow"

func main() {
    // logContent is raw text from Copilot MCP tool
    metrics := workflow.ParseCopilotLog(logContent, true) // true enables verbose parsing
    fmt.Printf("Tokens used: %d\n", metrics.TokenUsage)
    fmt.Printf("Estimated cost: $%.4f\n", metrics.EstimatedCost)
}

CLI Workflow with JSON Output

The complete pipeline from command invocation to metric extraction:


# Fetch logs and output structured metrics

gh aw logs --workflow my-workflow --run 123456 --json

This command executes:

  1. Guardrail validation using estimateTokens against the 12,000 token default
  2. Engine detection and specific parsing (Copilot, Claude, etc.)
  3. Cost extraction via ExtractJSONCost
  4. JSON serialization of the MetricsData struct containing token_usage and estimated_cost

Key Implementation Files

Purpose File Path
Rough token estimate & guardrail logic pkg/cli/mcp_logs_guardrail.go
Generic cost extraction helper pkg/workflow/metrics.go
Copilot log parsing pkg/workflow/copilot_logs.go
Claude log parsing pkg/workflow/claude_logs.go
Metrics aggregation struct pkg/workflow/metrics.go
CLI guardrail integration pkg/cli/logs_report.go
Copilot token extraction tests pkg/workflow/copilot_token_parsing_test.go
Claude cost handling tests pkg/workflow/claude_logs_test.go

Summary

  • gh-aw employs a two-stage approach to estimate AI usage: a fast character-based heuristic (4 characters per token) for guardrail checks, and precise JSON parsing for accurate billing metrics.
  • The guardrail in pkg/cli/mcp_logs_guardrail.go prevents processing logs exceeding 12,000 tokens by default, protecting against runaway costs.
  • Engine-specific parsers in pkg/workflow/copilot_logs.go and pkg/workflow/claude_logs.go extract exact input_tokens and output_tokens from structured JSON logs.
  • The cost extraction helper ExtractJSONCost in pkg/workflow/metrics.go normalizes provider-specific cost fields into a unified USD value.
  • All metrics aggregate into the MetricsData struct, exposing TokenUsage and EstimatedCost to CLI commands and audit tools.

Frequently Asked Questions

How accurate is the 4-characters-per-token estimate?

The 4-characters-per-token heuristic in estimateTokens follows the OpenAI rule of thumb for English text. While sufficient for guardrail protection against oversized logs, it typically overestimates actual token counts for code or structured JSON. For billing accuracy, gh-aw relies on provider-reported JSON usage fields rather than this rough estimate.

What happens when the token limit is exceeded?

When CheckMCPLogSize detects that the estimated token count exceeds the configurable max_tokens limit (defaulting to 12,000), it immediately returns a guardrail JSON payload containing the token count and schema metadata. The caller receives this truncated response instead of the full log content, preventing excessive API charges and memory consumption.

Which AI providers does gh-aw support for cost tracking?

Currently, gh-aw implements dedicated parsers for GitHub Copilot and Claude, located in pkg/workflow/copilot_logs.go and pkg/workflow/claude_logs.go respectively. The generic ExtractJSONCost helper supports any provider embedding standard cost fields like total_cost_usd, cost, or price in JSON log fragments, making the system extensible to additional engines.

Can I adjust the token limit for the guardrail?

Yes, the CheckMCPLogSize function accepts a maxTokens parameter. Passing 0 uses the default limit of 12,000 tokens defined as DefaultMaxMCPLogsOutputTokens in pkg/cli/mcp_logs_guardrail.go. You can specify any positive integer to customize the threshold based on your infrastructure constraints or cost sensitivity.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →