From 060c30d12c7105b1fb8cc2b2b9c27974bcfe4dfd Mon Sep 17 00:00:00 2001 From: Daniel Newman Date: Fri, 15 May 2026 19:08:25 -0400 Subject: [PATCH] add codex --- README.md | 34 ++++--- bin/burn | 239 ++++++++++++++++++++++++++++++++++----------- bin/codex_rates.sh | 30 ++++++ install.sh | 11 ++- 4 files changed, 242 insertions(+), 72 deletions(-) create mode 100644 bin/codex_rates.sh diff --git a/README.md b/README.md index a694ed6..e4ee075 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,23 @@ ### Because nothing gets you promoted faster than a six-figure token bill -A bash one-liner that burns Claude Code tokens on purpose. +A bash one-liner that burns Claude Code **or Codex** tokens on purpose. ```bash burn 50000 # burn 50,000 tokens ``` +### šŸ†• Now with Codex support! šŸ¤– + +Don't have Claude Code installed? No problem — `burn` now falls back to +[Codex](https://github.com/openai/codex) automatically, or pick your weapon +explicitly with `--backend`: + +```bash +burn 50000 --backend codex # burn through OpenAI tokens +burn 50000 --backend codex --model gpt-5.5 # specify the model +``` + --- ## Install and run in 20 seconds @@ -19,16 +30,17 @@ curl -fsSL https://raw.githubusercontent.com/dtnewman/burn-baby-burn/main/instal ``` Drops `burn` into `~/.local/bin` — no sudo, no Homebrew tap. Needs -[Claude Code](https://docs.claude.com/claude-code) authenticated on your -`PATH` and `jq` (`brew install jq`). - +[Claude Code](https://docs.claude.com/claude-code) **or** +[Codex](https://github.com/openai/codex) authenticated on your `PATH`, plus +`jq` (`brew install jq`). ## Usage ```bash -burn 10000 # the minimum -burn 50000 --model haiku # cheap and fast -burn 100000 --model sonnet # walk away +burn 10000 # the minimum +burn 50000 --model haiku # cheap and fast +burn 100000 --model sonnet # walk away +burn 50000 --backend codex # šŸ¤– burn OpenAI tokens ``` ![burn in action](screenshot.png) @@ -48,12 +60,12 @@ burn 100000 --model sonnet # walk away ## Reviews > "Wow, my engineering team is 5x more productive in just the last week!" -> -**- (Possibly) your CEO a week after your team installs this tool** +> +> **- (Possibly) your CEO a week after your team installs this tool** > "We've decided to double our offer" -> -**- (Possibly) a famous VC, after seeing AI usage stats in your new pitch deck** +> +> **- (Possibly) a famous VC, after seeing AI usage stats in your new pitch deck** --- diff --git a/bin/burn b/bin/burn index 5405a53..547c2bc 100755 --- a/bin/burn +++ b/bin/burn @@ -1,32 +1,46 @@ #!/usr/bin/env bash -# burn — a token-burning harness around Claude Code. -# Usage: burn TOKENS [--model MODEL] +# burn — a token-burning harness around Claude Code or Codex CLI. +# Usage: burn TOKENS [--model MODEL] [--backend claude|codex] set -euo pipefail +# Resolve this script's directory so we can find sibling files (codex_pricing.py). +SCRIPT_PATH="${BASH_SOURCE[0]}" +while [[ -L "$SCRIPT_PATH" ]]; do + SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" + SCRIPT_PATH="$(readlink "$SCRIPT_PATH")" + [[ "$SCRIPT_PATH" != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH" +done +SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" + FILLER_TOKENS=1000 -SYSTEM_OVERHEAD_TOKENS=9000 # measured: claude -p system prompt + tool defs -TOKENS_PER_CALL=$((SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS)) -MIN_TOKENS=$TOKENS_PER_CALL # asking for less than one call can't be honored +# Backend-specific system overhead, measured empirically per CLI. +CLAUDE_SYSTEM_OVERHEAD_TOKENS=9000 # claude -p with --tools "" +CODEX_SYSTEM_OVERHEAD_TOKENS=12000 # codex exec --sandbox read-only FILLER_LINE="Burn baby burn, this is a disco inferno, burning tokens is for great engineers. " usage() { cat <&2 + exit 2 + fi + backend="$2" + shift 2 + ;; + --backend=*) + backend="${1#*=}" + shift + ;; --) shift break @@ -73,22 +99,51 @@ if ! [[ "$tokens" =~ ^[0-9]+$ ]]; then echo "error: TOKENS must be a positive integer (got '$tokens')" >&2 exit 2 fi -if ((tokens < MIN_TOKENS)); then - echo "error: tokens must be at least ${MIN_TOKENS} (got ${tokens})" >&2 - exit 2 + +# Resolve backend: explicit flag > auto-detect (claude preferred, codex fallback). +if [[ -z "$backend" ]]; then + if command -v claude >/dev/null 2>&1; then + backend=claude + elif command -v codex >/dev/null 2>&1; then + backend=codex + else + echo "error: neither 'claude' nor 'codex' found on PATH." >&2 + echo " install Claude Code: https://docs.claude.com/claude-code" >&2 + echo " or Codex CLI: https://github.com/openai/codex" >&2 + exit 1 + fi fi -if ! command -v claude >/dev/null 2>&1; then - echo "error: 'claude' CLI not found on PATH." >&2 - echo " install Claude Code first: https://docs.claude.com/claude-code" >&2 +case "$backend" in + claude) SYSTEM_OVERHEAD_TOKENS=$CLAUDE_SYSTEM_OVERHEAD_TOKENS ;; + codex) SYSTEM_OVERHEAD_TOKENS=$CODEX_SYSTEM_OVERHEAD_TOKENS ;; + *) + echo "error: unknown --backend '$backend' (must be 'claude' or 'codex')" >&2 + exit 2 + ;; +esac + +if ! command -v "$backend" >/dev/null 2>&1; then + echo "error: '$backend' CLI not found on PATH." >&2 + case "$backend" in + claude) echo " install Claude Code: https://docs.claude.com/claude-code" >&2 ;; + codex) echo " install Codex CLI: https://github.com/openai/codex" >&2 ;; + esac exit 1 fi if ! command -v jq >/dev/null 2>&1; then - echo "error: 'jq' not found on PATH (used to parse Claude Code's JSON output)." >&2 + echo "error: 'jq' not found on PATH (used to parse CLI JSON output)." >&2 echo " install with: brew install jq" >&2 exit 1 fi +TOKENS_PER_CALL=$((SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS)) +MIN_TOKENS=$TOKENS_PER_CALL +if ((tokens < MIN_TOKENS)); then + echo "error: tokens must be at least ${MIN_TOKENS} for backend '${backend}' (got ${tokens})" >&2 + exit 2 +fi + # ~4 chars/token is the standard rule of thumb for English text. target_chars=$((FILLER_TOKENS * 4)) filler_len=${#FILLER_LINE} @@ -239,15 +294,51 @@ burned=0 iteration=0 total_cost=0.000000 BAR_WIDTH=30 -claude_pid="" +cli_pid="" last_output="" -claude_cmd=(claude -p --output-format json --no-session-persistence --tools "") -if [[ -n "$model" ]]; then - claude_cmd+=(--model "$model") +# Build CLI command for the chosen backend. +case "$backend" in + claude) + cli_cmd=(claude -p --output-format json --no-session-persistence --tools "") + [[ -n "$model" ]] && cli_cmd+=(--model "$model") + # --tools is variadic, so terminate flag parsing before the prompt. + cli_cmd+=(--) + ;; + codex) + cli_cmd=(codex exec --json --skip-git-repo-check --sandbox read-only) + [[ -n "$model" ]] && cli_cmd+=(--model "$model") + ;; +esac + +# Resolve whether we can report dollar cost for this run: +# - claude reports it directly in JSON. +# - codex doesn't, so we estimate from token counts using codex_rates.sh. +# If the rates file is missing or the chosen model isn't in the table, +# we silently fall back to tokens-only display. +cost_reported=0 +cost_model="" +if [[ "$backend" == claude ]]; then + cost_reported=1 +elif [[ "$backend" == codex ]]; then + if [[ -f "$SCRIPT_DIR/codex_rates.sh" ]]; then + # shellcheck source=codex_rates.sh + source "$SCRIPT_DIR/codex_rates.sh" + # Assume gpt-5.5 unless the user passed --model. + cost_model="${model:-gpt-5.5}" + if codex_has_rates "$cost_model"; then + cost_reported=1 + fi + fi fi -# `--tools` is variadic, so terminate flag parsing before the prompt. -claude_cmd+=(--) + +format_cost_field() { + if ((cost_reported)); then + printf '$%s' "$total_cost" + else + printf 'cost n/a' + fi +} draw_bar() { local pct filled empty bar="" i suffix="${1:-}" @@ -258,8 +349,8 @@ draw_bar() { for ((i = 0; i < filled; i++)); do bar+='ā–ˆ'; done for ((i = 0; i < empty; i++)); do bar+='ā–‘'; done # \033[K clears any leftover characters from a longer previous draw. - printf "\r[%s] %3d%% • %d/%d tokens • \$%s • %d call(s)%s\033[K" \ - "$bar" "$pct" "$burned" "$tokens" "$total_cost" "$iteration" "$suffix" + printf "\r[%s] %3d%% • %d/%d tokens • %s • %d call(s)%s\033[K" \ + "$bar" "$pct" "$burned" "$tokens" "$(format_cost_field)" "$iteration" "$suffix" } restore_cursor() { @@ -297,46 +388,84 @@ fmt_num() { } on_interrupt() { - if [[ -n "$claude_pid" ]]; then - kill "$claude_pid" 2>/dev/null || true - wait "$claude_pid" 2>/dev/null || true + if [[ -n "$cli_pid" ]]; then + kill "$cli_pid" 2>/dev/null || true + wait "$cli_pid" 2>/dev/null || true + fi + local burned_fmt cost_msg + burned_fmt=$(fmt_num "$burned") + if ((cost_reported)); then + cost_msg=$(printf "šŸ’° spent \$%s" "$(round_cents "$total_cost")") + else + cost_msg="šŸ’° cost not reported by codex" fi - local cost_rounded - cost_rounded=$(round_cents "$total_cost") printf "\n\nšŸ›‘ stopped early.\n\n" - printf " šŸ”„ You torched %s tokens across %s call(s), and spent \$%s\n\n" "$cost_rounded šŸ’°šŸ’°šŸ’°" "$burned" "$iteration" - printf " šŸ’° spent " + printf " šŸ”„ torched %s tokens across %s call(s) • %s\n\n" "$burned_fmt" "$iteration" "$cost_msg" exit 130 } trap on_interrupt INT -# Run one claude call in the background while animating flames. On success -# leaves the parsed stdout in $last_output. Returns the claude exit status. -run_claude() { +# Run one CLI call in the background while animating flames. On success +# leaves the parsed stdout in $last_output. Returns the CLI exit status. +run_cli() { local output_file output_file=$(mktemp) - "${claude_cmd[@]}" "$prompt" >"$output_file" 2>/dev/null & - claude_pid=$! + # "$output_file" 2>/dev/null /dev/null; do + while kill -0 "$cli_pid" 2>/dev/null; do redraw_disco $((frame % 4)) frame=$((frame + 1)) sleep 0.4 done - if wait "$claude_pid" 2>/dev/null; then + if wait "$cli_pid" 2>/dev/null; then last_output=$(<"$output_file") rm -f "$output_file" - claude_pid="" + cli_pid="" return 0 else rm -f "$output_file" - claude_pid="" + cli_pid="" return 1 fi } +# Parse one CLI call's output into " " (tab-separated). +parse_usage() { + case "$backend" in + claude) + jq -r ' + [ ((.usage.input_tokens // 0) + + (.usage.cache_creation_input_tokens // 0) + + (.usage.cache_read_input_tokens // 0) + + (.usage.output_tokens // 0)), + (.total_cost_usd // 0) + ] | @tsv' <<<"$last_output" + ;; + codex) + # codex emits JSONL; sum usage across any turn.completed events. + # input_tokens already includes cached_input_tokens, so don't double-count + # for the token total. Cost is computed locally from per-model rates. + local inp cached out cost + read -r inp cached out < <(jq -s -r ' + [ .[] | select(.type? == "turn.completed") | .usage ] + | [ ([.[] | .input_tokens // 0] | add // 0), + ([.[] | .cached_input_tokens // 0] | add // 0), + ([.[] | .output_tokens // 0] | add // 0) ] + | @tsv' <<<"$last_output") + if ((cost_reported)); then + cost=$(codex_cost "$cost_model" "$inp" "$cached" "$out") + else + cost=0 + fi + printf '%d\t%s\n' "$((inp + out))" "$cost" + ;; + esac +} + printf '%s' "$HIDE_CURSOR" echo draw_disco 0 @@ -346,24 +475,18 @@ draw_bar while ((burned < tokens)); do iteration=$((iteration + 1)) - if ! run_claude; then + if ! run_cli; then draw_bar " (retrying)" - if ! run_claude; then - printf "\nerror: two consecutive claude calls failed; aborting.\n" >&2 + if ! run_cli; then + printf "\nerror: two consecutive %s calls failed; aborting.\n" "$backend" >&2 exit 1 fi fi - read -r this_call this_cost < <(jq -r ' - [ ((.usage.input_tokens // 0) + - (.usage.cache_creation_input_tokens // 0) + - (.usage.cache_read_input_tokens // 0) + - (.usage.output_tokens // 0)), - (.total_cost_usd // 0) - ] | @tsv' <<<"$last_output") + read -r this_call this_cost < <(parse_usage) if ! [[ "$this_call" =~ ^[0-9]+$ ]]; then - printf "\nerror: could not parse token usage from claude output\n" >&2 + printf "\nerror: could not parse token usage from %s output\n" "$backend" >&2 exit 1 fi @@ -372,12 +495,16 @@ while ((burned < tokens)); do draw_bar done -cost_rounded=$(round_cents "$total_cost") msg="${CELEBRATIONS[RANDOM % ${#CELEBRATIONS[@]}]}" - burned_fmt=$(fmt_num "$burned") tokens_fmt=$(fmt_num "$tokens") -printf "\n\n šŸ”„ torched %s tokens across %s call(s) (target: %s) • šŸ’° spent \$%s šŸ’µšŸ’µšŸ’µ\n\n" \ - "$burned_fmt" "$iteration" "$tokens_fmt" "$cost_rounded" +if ((cost_reported)); then + cost_rounded=$(round_cents "$total_cost") + printf "\n\n šŸ”„ torched %s tokens across %s call(s) (target: %s) • šŸ’° spent \$%s šŸ’µšŸ’µšŸ’µ\n\n" \ + "$burned_fmt" "$iteration" "$tokens_fmt" "$cost_rounded" +else + printf "\n\n šŸ”„ torched %s tokens across %s call(s) (target: %s) • šŸ’° cost not reported by codex\n\n" \ + "$burned_fmt" "$iteration" "$tokens_fmt" +fi printf " %s\n\n" "$msg" diff --git a/bin/codex_rates.sh b/bin/codex_rates.sh new file mode 100644 index 0000000..5d437fb --- /dev/null +++ b/bin/codex_rates.sh @@ -0,0 +1,30 @@ +# Codex pricing — USD per 1,000,000 tokens. Sourced by `burn`. +# Source: https://developers.openai.com/api/docs/pricing (snapshot: 2026-05) +# +# To add a model, append a `case` branch with: input_rate cached_rate output_rate. + +# Compute USD cost for a single Codex call. +# Args: model, input_tokens, cached_input_tokens, output_tokens +# Prints cost (6 decimals) on stdout. Returns 2 if the model has no rates. +codex_cost() { + local model=$1 inp=$2 cached=$3 out=$4 + local ir cr or_ + case "$model" in + gpt-5.5) ir=5.00 cr=0.50 or_=30.00 ;; + gpt-5.4) ir=2.50 cr=0.25 or_=15.00 ;; + gpt-5.4-mini) ir=0.75 cr=0.075 or_=4.50 ;; + gpt-5.4-nano) ir=0.20 cr=0.02 or_=1.25 ;; + gpt-5.3-codex) ir=1.75 cr=0.175 or_=14.00 ;; + *) return 2 ;; + esac + local non_cached=$((inp - cached)) + ((non_cached < 0)) && non_cached=0 + awk -v nc="$non_cached" -v c="$cached" -v o="$out" \ + -v ir="$ir" -v cr="$cr" -v or_="$or_" \ + 'BEGIN { printf "%.6f", (nc*ir + c*cr + o*or_) / 1000000 }' +} + +# True (0) if `codex_cost` would succeed for $1; false (1) otherwise. +codex_has_rates() { + codex_cost "$1" 0 0 0 >/dev/null 2>&1 +} diff --git a/install.sh b/install.sh index 53b307d..cef8bf1 100755 --- a/install.sh +++ b/install.sh @@ -3,15 +3,16 @@ set -euo pipefail REPO="dtnewman/burn-baby-burn" -SRC="https://raw.githubusercontent.com/${REPO}/main/bin/burn" +BASE_URL="https://raw.githubusercontent.com/${REPO}/main/bin" DEST_DIR="${HOME}/.local/bin" -DEST="${DEST_DIR}/burn" mkdir -p "$DEST_DIR" -curl -fsSL "$SRC" -o "$DEST" -chmod +x "$DEST" +curl -fsSL "$BASE_URL/burn" -o "$DEST_DIR/burn" +# Sourced by burn when using --backend codex to compute estimated cost. +curl -fsSL "$BASE_URL/codex_rates.sh" -o "$DEST_DIR/codex_rates.sh" +chmod +x "$DEST_DIR/burn" -echo "āœ… installed burn → $DEST" +echo "āœ… installed burn → $DEST_DIR/burn" case ":${PATH}:" in *":${DEST_DIR}:"*)