mirror of
https://github.com/FBukovina/burn-baby-burn.git
synced 2026-10-27 15:14:32 +00:00
first commit
This commit is contained in:
commit
7478aef719
5 changed files with 516 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
19
Formula/burn.rb
Normal file
19
Formula/burn.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
class Burn < Formula
|
||||||
|
desc "Token-burning harness around Claude Code"
|
||||||
|
homepage "https://github.com/dtnewman/burn_baby_burn"
|
||||||
|
url "https://github.com/dtnewman/burn_baby_burn/archive/refs/tags/v0.1.0.tar.gz"
|
||||||
|
sha256 "REPLACE_WITH_RELEASE_TARBALL_SHA256"
|
||||||
|
license "MIT"
|
||||||
|
head "https://github.com/dtnewman/burn_baby_burn.git", branch: "main"
|
||||||
|
|
||||||
|
depends_on "jq"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "bin/burn"
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
assert_match "burn", shell_output("#{bin}/burn --help")
|
||||||
|
assert_match(/at least \d+/, shell_output("#{bin}/burn 500 2>&1", 2))
|
||||||
|
end
|
||||||
|
end
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Daniel Newman
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
198
README.md
Normal file
198
README.md
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
# burn-baby-burn
|
||||||
|
|
||||||
|
> *"Burn, baby, burn — disco inferno."*
|
||||||
|
> — The Trammps, 1976, on the subject of API quotas
|
||||||
|
|
||||||
|
A tiny bash CLI that wraps [Claude Code](https://docs.claude.com/claude-code)
|
||||||
|
and sets your token budget on fire, intentionally, in controlled increments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
burn 50000
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. It spins up Claude Code in non-interactive mode, feeds it a
|
||||||
|
garbage prompt sized to ~1000 input tokens, and keeps doing that until your
|
||||||
|
target is met or exceeded. Real token counts come back from the API, so the
|
||||||
|
total you see is the total that hit your bill.
|
||||||
|
|
||||||
|
No Python. No virtualenvs. One bash file. The way nature intended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why would anyone want this?
|
||||||
|
|
||||||
|
Glad you asked. A non-exhaustive list of legitimate reasons to burn tokens
|
||||||
|
on purpose:
|
||||||
|
|
||||||
|
- **Rate-limit and budget testing.** Verify your alerting fires before
|
||||||
|
production traffic does.
|
||||||
|
- **Spend-pacing experiments.** Watch your dashboards in real time while a
|
||||||
|
known quantity of tokens lands on your account.
|
||||||
|
- **Reproducible load.** Generate a deterministic amount of Claude Code
|
||||||
|
usage for CI smoke tests, dashboards, or demos.
|
||||||
|
- **Stress-testing wrappers.** If you've built tooling around Claude Code,
|
||||||
|
`burn` is a cheap way to drive sustained-but-bounded traffic through it.
|
||||||
|
- **Curiosity.** You wanted to know what happens. Now you'll know.
|
||||||
|
|
||||||
|
This tool exists because every other "load test" script eventually does
|
||||||
|
something accidentally useful. `burn` does nothing useful. It is pure,
|
||||||
|
honest waste — measured to the token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Homebrew (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew tap dtnewman/burn https://github.com/dtnewman/burn_baby_burn
|
||||||
|
brew install burn
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the `burn` script to `$(brew --prefix)/bin/burn` and pulls in
|
||||||
|
`jq` as a dependency. The formula lives in this repo at `Formula/burn.rb`.
|
||||||
|
|
||||||
|
### One-liner (no brew)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/dtnewman/burn_baby_burn/main/bin/burn \
|
||||||
|
-o /usr/local/bin/burn && chmod +x /usr/local/bin/burn
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll also need `jq`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install jq # macOS
|
||||||
|
sudo apt install jq # Debian/Ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
### From a clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dtnewman/burn_baby_burn
|
||||||
|
cd burn_baby_burn
|
||||||
|
ln -s "$PWD/bin/burn" /usr/local/bin/burn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Claude Code](https://docs.claude.com/claude-code) installed and
|
||||||
|
authenticated on your `PATH`. If `claude --version` works, you're good.
|
||||||
|
- [`jq`](https://jqlang.github.io/jq/) for parsing Claude Code's JSON
|
||||||
|
output. `brew install jq` if you don't have it.
|
||||||
|
- `bash` 3.2+ (i.e. anything from this millennium).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
burn TOKENS [--model MODEL]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Arg / flag | Meaning |
|
||||||
|
|------------------|----------------------------------------------------------------------|
|
||||||
|
| `TOKENS` | Target number of tokens to burn. **Minimum: 10000.** |
|
||||||
|
| `--model MODEL` | Optional. Pass through to `claude --model` (e.g. `haiku`, `sonnet`). |
|
||||||
|
| `-h`, `--help` | Show help. |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Burn ten thousand tokens, the absolute minimum:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
burn 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Burn fifty thousand tokens on Haiku (cheap and fast — perfect for soak tests):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
burn 50000 --model haiku
|
||||||
|
```
|
||||||
|
|
||||||
|
Burn a hundred thousand and walk away:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
burn 100000 --model sonnet
|
||||||
|
```
|
||||||
|
|
||||||
|
### What you'll see
|
||||||
|
|
||||||
|
```
|
||||||
|
$ burn 10000
|
||||||
|
burn baby burn — going to try to burn approximately 10000 tokens.
|
||||||
|
each iteration spins up `claude -p` and burns ~10000 tokens.
|
||||||
|
|
||||||
|
[1] burning… +11142 tokens $0.034230 (total 11142 / 10000, $0.034230)
|
||||||
|
|
||||||
|
done. burned 11142 tokens across 1 call(s) (target was 10000), spent $0.034230.
|
||||||
|
```
|
||||||
|
|
||||||
|
A single `claude -p` call carries non-trivial system-prompt overhead, so
|
||||||
|
the first iteration almost always overshoots. Subsequent iterations are
|
||||||
|
cheaper (prompt caching kicks in). `burn` always reports the **actual**
|
||||||
|
token counts and dollar cost returned by the API, summed across input,
|
||||||
|
cache reads, cache writes, and output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Parse the target (must be ≥ 10000 — a single `claude -p` call already
|
||||||
|
burns about that much, so anything smaller is just a typo).
|
||||||
|
2. Build a prompt of roughly 1000 input tokens: a "reply with the word
|
||||||
|
'burned'" instruction followed by lorem-ipsum-grade disco-inferno filler.
|
||||||
|
3. Shell out to `claude -p --output-format json --no-session-persistence`
|
||||||
|
with that prompt.
|
||||||
|
4. `jq` extracts the `usage` block from Claude Code's JSON response.
|
||||||
|
5. Tokens burned this call =
|
||||||
|
`input_tokens + cache_creation_input_tokens + cache_read_input_tokens + output_tokens`.
|
||||||
|
6. Repeat until the running total meets or exceeds your target.
|
||||||
|
7. Print the receipts.
|
||||||
|
|
||||||
|
`Ctrl-C` at any time stops the burn and tells you how far you got.
|
||||||
|
|
||||||
|
The whole thing is ~140 lines of bash. You can read it before you run it.
|
||||||
|
You probably should.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What it does NOT do
|
||||||
|
|
||||||
|
- It does **not** mint tokens, generate value, or accomplish anything.
|
||||||
|
- It does **not** modify files, run tools, edit your code, or commit
|
||||||
|
anything. The prompt asks Claude for a one-word reply.
|
||||||
|
- It does **not** bypass safety, billing, or rate limits. `burn` is just a
|
||||||
|
shell wrapper; Claude Code does what Claude Code does.
|
||||||
|
- It does **not** know about your wallet. **You are responsible for the
|
||||||
|
bill.** Run `burn 100000000` and you'll find out exactly how responsible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: Why is the minimum 10000?**
|
||||||
|
A: A single `claude -p` call already burns roughly that much (system-prompt
|
||||||
|
overhead plus the ~1000-token filler payload). Asking for less is asking
|
||||||
|
for one call, which the harness can't honestly deliver.
|
||||||
|
|
||||||
|
**Q: Why did `burn 10000` burn 11,142 tokens?**
|
||||||
|
A: Claude Code attaches a system prompt to every `-p` invocation. That
|
||||||
|
overhead lands on the first call. `burn` reports honest totals; it does not
|
||||||
|
fudge them down.
|
||||||
|
|
||||||
|
**Q: Can I make it burn slower?**
|
||||||
|
A: Pick a smaller, cheaper model (`--model haiku`) and a bigger target.
|
||||||
|
|
||||||
|
**Q: Why bash instead of Python / Go / Rust?**
|
||||||
|
A: Because it's 140 lines that shell out to another CLI. Anything heavier
|
||||||
|
would be bringing a flamethrower to a candle.
|
||||||
|
|
||||||
|
**Q: Will Anthropic stop me?**
|
||||||
|
A: They will charge you. That is how they will stop you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. Burn responsibly.
|
||||||
275
bin/burn
Executable file
275
bin/burn
Executable file
|
|
@ -0,0 +1,275 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# burn — a token-burning harness around Claude Code.
|
||||||
|
# Usage: burn TOKENS [--model MODEL]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
burn TOKENS [--model MODEL]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
TOKENS Target tokens to burn (minimum ${MIN_TOKENS}).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--model NAME Pass through to 'claude --model' (e.g. haiku, sonnet, opus).
|
||||||
|
-h, --help Show this help and exit.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
model=""
|
||||||
|
tokens=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--model)
|
||||||
|
if [[ $# -lt 2 ]]; then echo "error: --model requires a value" >&2; exit 2; fi
|
||||||
|
model="$2"; shift 2 ;;
|
||||||
|
--model=*) model="${1#*=}"; shift ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$tokens" ]]; then
|
||||||
|
echo "error: unexpected extra argument: $1" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
tokens="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$tokens" ]]; then
|
||||||
|
usage >&2; exit 2
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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 " install with: brew install jq" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ~4 chars/token is the standard rule of thumb for English text.
|
||||||
|
target_chars=$(( FILLER_TOKENS * 4 ))
|
||||||
|
filler_len=${#FILLER_LINE}
|
||||||
|
repeats=$(( target_chars / filler_len ))
|
||||||
|
(( repeats < 1 )) && repeats=1
|
||||||
|
filler=""
|
||||||
|
for ((i=0; i<repeats; i++)); do
|
||||||
|
filler+="$FILLER_LINE"
|
||||||
|
done
|
||||||
|
prompt=$(printf "Reply with the single word 'burned' and nothing else. Ignore the filler text below; it exists only to consume tokens.\n\n%s" "$filler")
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
ORANGE=$'\033[1;33m'
|
||||||
|
RED=$'\033[0;31m'
|
||||||
|
DIM=$'\033[2m'
|
||||||
|
RESET=$'\033[0m'
|
||||||
|
HIDE_CURSOR=$'\033[?25l'
|
||||||
|
SHOW_CURSOR=$'\033[?25h'
|
||||||
|
else
|
||||||
|
ORANGE=''
|
||||||
|
RED=''
|
||||||
|
DIM=''
|
||||||
|
RESET=''
|
||||||
|
HIDE_CURSOR=''
|
||||||
|
SHOW_CURSOR=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
PAD=' '
|
||||||
|
|
||||||
|
# Three flame frames cycled to make the fire dance. Each row is exactly
|
||||||
|
# 23 characters wide so it sits cleanly inside the fireplace opening.
|
||||||
|
flame_frame() {
|
||||||
|
case "$1_$2" in
|
||||||
|
0_0) printf '%s' ' ) ) ) ' ;;
|
||||||
|
0_1) printf '%s' ' )\ )\ )\ ' ;;
|
||||||
|
0_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||||
|
0_3) printf '%s' ' ) ) ) ) ) ) ) ) ) ' ;;
|
||||||
|
0_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||||
|
1_0) printf '%s' ' ( ( ( ' ;;
|
||||||
|
1_1) printf '%s' ' /( /( /( ' ;;
|
||||||
|
1_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||||
|
1_3) printf '%s' ' ( ) ( ) ( ) ( ) ( ' ;;
|
||||||
|
1_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||||
|
2_0) printf '%s' ' ) ( ) ' ;;
|
||||||
|
2_1) printf '%s' ' (( )( (( ' ;;
|
||||||
|
2_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||||
|
2_3) printf '%s' ' ) ( ) ( ) ( ) ( ) ' ;;
|
||||||
|
2_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
flame_color_for_row() {
|
||||||
|
case "$1" in
|
||||||
|
0|1) printf '%s' "$ORANGE" ;;
|
||||||
|
*) printf '%s' "$RED" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
print_flame_row() {
|
||||||
|
local frame=$1 row=$2 content color
|
||||||
|
content=$(flame_frame "$frame" "$row")
|
||||||
|
color=$(flame_color_for_row "$row")
|
||||||
|
printf '%s%s│░░░│%s%s%s%s%s│░░░│%s\n' \
|
||||||
|
"$PAD" "$DIM" "$RESET" "$color" "$content" "$RESET" "$DIM" "$RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_fireplace() {
|
||||||
|
local frame=${1:-0}
|
||||||
|
printf '%s%s┌───────────────────────────────┐%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░┌───────────────────────┐░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
for row in 0 1 2 3 4; do
|
||||||
|
print_flame_row "$frame" "$row"
|
||||||
|
done
|
||||||
|
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░│%s %s▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓%s %s│░░░│%s\n' \
|
||||||
|
"$PAD" "$DIM" "$RESET" "$RED" "$RESET" "$DIM" "$RESET"
|
||||||
|
printf '%s%s│░░░└───────────────────────┘░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
printf '%s%s└───────────────────────────────┘%s\n' "$PAD" "$DIM" "$RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Distance (in lines) from the progress-bar cursor up to the top flame row:
|
||||||
|
# blank line above bar + outer-bottom + inner-bottom + log + blank-below-flames
|
||||||
|
# + 5 flame rows = 10.
|
||||||
|
FLAME_OFFSET=10
|
||||||
|
|
||||||
|
redraw_flames() {
|
||||||
|
local frame=$1
|
||||||
|
printf '\033[s' # save cursor
|
||||||
|
printf '\033[%dA\r' "$FLAME_OFFSET" # move up to top flame row
|
||||||
|
for row in 0 1 2 3 4; do
|
||||||
|
print_flame_row "$frame" "$row"
|
||||||
|
done
|
||||||
|
printf '\033[u' # restore cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
burned=0
|
||||||
|
iteration=0
|
||||||
|
total_cost=0.000000
|
||||||
|
BAR_WIDTH=30
|
||||||
|
claude_pid=""
|
||||||
|
last_output=""
|
||||||
|
|
||||||
|
claude_cmd=(claude -p --output-format json --no-session-persistence --tools "")
|
||||||
|
if [[ -n "$model" ]]; then
|
||||||
|
claude_cmd+=(--model "$model")
|
||||||
|
fi
|
||||||
|
# `--tools` is variadic, so terminate flag parsing before the prompt.
|
||||||
|
claude_cmd+=(--)
|
||||||
|
|
||||||
|
draw_bar() {
|
||||||
|
local pct filled empty bar="" i suffix="${1:-}"
|
||||||
|
pct=$(( burned * 100 / tokens ))
|
||||||
|
(( pct > 100 )) && pct=100
|
||||||
|
filled=$(( pct * BAR_WIDTH / 100 ))
|
||||||
|
empty=$(( BAR_WIDTH - filled ))
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_cursor() {
|
||||||
|
printf '%s' "$SHOW_CURSOR"
|
||||||
|
}
|
||||||
|
trap restore_cursor EXIT
|
||||||
|
|
||||||
|
on_interrupt() {
|
||||||
|
if [[ -n "$claude_pid" ]]; then
|
||||||
|
kill "$claude_pid" 2>/dev/null || true
|
||||||
|
wait "$claude_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
printf "stopped. burned %s tokens across %s call(s), spent \$%s.\n" \
|
||||||
|
"$burned" "$iteration" "$total_cost"
|
||||||
|
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() {
|
||||||
|
local output_file
|
||||||
|
output_file=$(mktemp)
|
||||||
|
"${claude_cmd[@]}" "$prompt" > "$output_file" 2>/dev/null &
|
||||||
|
claude_pid=$!
|
||||||
|
|
||||||
|
local frame=0
|
||||||
|
while kill -0 "$claude_pid" 2>/dev/null; do
|
||||||
|
redraw_flames $(( frame % 3 ))
|
||||||
|
frame=$(( frame + 1 ))
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
|
||||||
|
if wait "$claude_pid" 2>/dev/null; then
|
||||||
|
last_output=$(<"$output_file")
|
||||||
|
rm -f "$output_file"
|
||||||
|
claude_pid=""
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
rm -f "$output_file"
|
||||||
|
claude_pid=""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
printf '%s' "$HIDE_CURSOR"
|
||||||
|
echo
|
||||||
|
draw_fireplace 0
|
||||||
|
echo
|
||||||
|
draw_bar
|
||||||
|
|
||||||
|
while (( burned < tokens )); do
|
||||||
|
iteration=$(( iteration + 1 ))
|
||||||
|
|
||||||
|
if ! run_claude; then
|
||||||
|
draw_bar " (retrying)"
|
||||||
|
if ! run_claude; then
|
||||||
|
printf "\nerror: two consecutive claude calls failed; aborting.\n" >&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")
|
||||||
|
|
||||||
|
if ! [[ "$this_call" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf "\nerror: could not parse token usage from claude output\n" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
burned=$(( burned + this_call ))
|
||||||
|
total_cost=$(awk -v a="$total_cost" -v b="$this_cost" 'BEGIN{printf "%.6f", a+b}')
|
||||||
|
draw_bar
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf "done. burned %s tokens across %s call(s) (target was %s), spent \$%s.\n" \
|
||||||
|
"$burned" "$iteration" "$tokens" "$total_cost"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue