mirror of
https://github.com/FBukovina/burn-baby-burn.git
synced 2026-10-27 15:14:32 +00:00
add codex
This commit is contained in:
parent
b50df75f56
commit
060c30d12c
4 changed files with 242 additions and 72 deletions
24
README.md
24
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,9 +30,9 @@ 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
|
||||
|
||||
|
|
@ -29,6 +40,7 @@ Drops `burn` into `~/.local/bin` — no sudo, no Homebrew tap. Needs
|
|||
burn 10000 # the minimum
|
||||
burn 50000 --model haiku # cheap and fast
|
||||
burn 100000 --model sonnet # walk away
|
||||
burn 50000 --backend codex # 🤖 burn OpenAI tokens
|
||||
```
|
||||
|
||||

|
||||
|
|
@ -49,11 +61,11 @@ burn 100000 --model sonnet # walk away
|
|||
|
||||
> "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**
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
233
bin/burn
233
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 <<EOF
|
||||
burn — spin up Claude Code in a loop to burn approximately N tokens.
|
||||
burn — spin up Claude Code or Codex in a loop to burn approximately N tokens.
|
||||
|
||||
Usage:
|
||||
burn TOKENS [--model MODEL]
|
||||
burn TOKENS [--model MODEL] [--backend claude|codex]
|
||||
|
||||
Arguments:
|
||||
TOKENS Target tokens to burn (minimum ${MIN_TOKENS}).
|
||||
TOKENS Target tokens to burn. Minimum per call:
|
||||
claude: $((CLAUDE_SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS))
|
||||
codex: $((CODEX_SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS))
|
||||
|
||||
Options:
|
||||
--model NAME Pass through to 'claude --model' (e.g. haiku, sonnet, opus).
|
||||
--model NAME Model to pass through (e.g. haiku, sonnet, gpt-5).
|
||||
--backend NAME Which CLI to drive: 'claude' or 'codex'.
|
||||
Default: claude if installed, else codex.
|
||||
-h, --help Show this help and exit.
|
||||
EOF
|
||||
}
|
||||
|
||||
model=""
|
||||
tokens=""
|
||||
backend=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
|
|
@ -45,6 +59,18 @@ while [[ $# -gt 0 ]]; do
|
|||
model="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--backend)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "error: --backend requires a value" >&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
|
||||
# `--tools` is variadic, so terminate flag parsing before the prompt.
|
||||
claude_cmd+=(--)
|
||||
fi
|
||||
fi
|
||||
|
||||
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=$!
|
||||
# </dev/null prevents codex from waiting on stdin when run in background.
|
||||
"${cli_cmd[@]}" "$prompt" >"$output_file" 2>/dev/null </dev/null &
|
||||
cli_pid=$!
|
||||
|
||||
local frame=0
|
||||
while kill -0 "$claude_pid" 2>/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 "<tokens> <cost>" (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")
|
||||
|
||||
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"
|
||||
|
|
|
|||
30
bin/codex_rates.sh
Normal file
30
bin/codex_rates.sh
Normal file
|
|
@ -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
|
||||
}
|
||||
11
install.sh
11
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}:"*)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue