
Building a Cost-Per-Task Ledger for Agent Pipelines on GPT-6, Opus 5.5, and Grok 4.7
A price cut arrives as a per-million-token number. An agent regression arrives as a per-task number. The invoice sits somewhere between the two, and the gap between them is where budgets go sideways. This post shows you how to build a cost-per-task ledger for agent pipelines in JavaScript: one JSONL row per agent step, a cache-aware cost function that takes injected rates, and a rollup that answers "what did one accepted task cost" rather than "what does the list price say." By the end you have a ledger schema, a runnable harness, and a control loop you can point at GPT-6 Sol/Luna, Claude Opus 5.5, and Grok 4.7 without trusting a headline rate.
What I'm measuring, up front: the accounting machinery, not the vendors. Every dollar figure below is either placeholder config I wrote for the harness or arithmetic derived from it. None of it is a benchmark of GPT-6, Opus 5.5, or Grok 4.7, because I have no independent per-task measurements of any of them.
Why Price Per Million Tokens Is the Wrong Unit for Agent Pipelines
The three variables that actually move an agent's bill
List price is a rate. An agent task is a volume multiplied by a retry factor:
- Total input tokens per task, replayed context included. A 12-step loop that appends the transcript and resends it every turn isn't paying for 12 prompts — it's paying for a growing prefix, twelve times. The tool schema alone can be 4k tokens, resent on every turn.
- Cache hit ratio. Cached input typically bills at a fraction of fresh input. Unstable prefix, and that fraction never applies.
- Attempt count — retries of a step plus tool-loop turns that call the model again. Retries are pure waste; extra turns are legitimate work. Both multiply cost, and they are not the same failure mode.
Why a 50% list-price cut can produce a 12% invoice reduction
Say the old rate is R per token and a task costs 55 rate-units (uncached tokens plus cached tokens at a quarter of the rate, in thousands):
| Scenario | Input tokens/task | Cache hit | Attempts | Effective units |
|---|---|---|---|---|
| Before cut | 100,000 | 60% | 1.0 | (40 + 60×0.25) = 55.0 |
| After cut, rate × 0.5 | 129,000 | 50% | 1.2 | (64.5 + 64.5×0.25) × 0.5 × 1.2 = 48.4 |
The rate halved and the invoice fell 12%. The cut hit one multiplier; token volume rose 29%, the cache hit ratio dropped 10 points, and retries added 20%. Budget from the headline and you'd be off by roughly a factor of four on expected savings. That's the whole argument for keeping a ledger.
What the September 2026 Pricing Moves Actually Claim
Confirmed by the source material
Four news items, all published 2026-09-22, headlines and short snippets only — I never got the article bodies. What those items state:
- VentureBeat (2026-09-22) reports OpenAI released GPT-6 Sol and Luna, with API costs down 50% or more.
- The New Stack (2026-09-22) reports Anthropic released Opus 5.5 and cut pricing by 20%, and that agent calls might secretly get routed to an older model.
- TechRepublic (2026-09-22) reports SpaceXAI launched Grok 4.7, positioned on low prices and heavy token use.
- thelec.net (2026-09-22) reports prompt caching and compression are emerging as the key levers for cutting token costs.
Explicitly unverified
- No confirmed per-token rates for any of the named models. I did not read a pricing page.
- No independent per-task benchmark exists in my source material.
- No documented routing mechanism behind the Opus claim, and no confirmed policy that routing happens at all. The headline says "might."
- No version ranges, model identifiers beyond the names in the headlines, or dates beyond publication.
Treat everything below this point as a template with placeholder numbers. If you copy the rate card, you are copying my invention, not a vendor's pricing.
The position: list price is one input, not the ledger
Treat vendor list price as one input to the ledger, never as the ledger itself. If your cost model cannot express "same price, more retries, worse cache", it cannot budget an agent pipeline.
The Ledger Schema: One Row Per Task, One Span Per Step
The ledger fields that carry the economics
| Field | Why it is in the schema |
|---|---|
task_id | Grouping key; rollup happens without a second aggregation pass |
step_index | Distinguishes a new tool-loop turn from a retry of the same turn |
model_requested | What you asked for — intent |
model_served | What the response says answered — evidence |
input_tokens | Fresh input, before cache discount |
cached_input_tokens | The portion billed at the cache-read rate |
output_tokens | Usually the smallest and most expensive column |
tool_calls | Correlates token spend with actual work done |
attempt | Retry index for the same step_index |
latency_ms | Catches the case where a retry is actually a timeout |
cost_usd | Computed at write time, not at read time |
outcome | ok, error:*, or a terminal acceptance marker |
Why model_served must be recorded separately from model_requested
The source reporting says routing to an older model is possible but documents no mechanism, so the ledger has to observe instead of assume. model_requested is your intent from config; model_served is the identifier the response carries. If they diverge on a model you pinned, that is a finding you can act on. Log only the request and you will never see it — and an older model can be priced higher while being cheaper to serve, which is exactly the case a changelog will not reveal.
Instrumenting a JavaScript Agent Loop
Wrapping the provider client to read usage from the response
Read usage from the response, never infer it from the request. Providers disagree on whether input_tokens already includes cached tokens, so normalize explicitly rather than guessing.
import { readFileSync, appendFileSync } from "node:fs";
// Placeholder rate card. Invented for this harness. Replace via RATES_PATH.
const rates = JSON.parse(readFileSync(process.env.RATES_PATH, "utf8"));
// Two usage shapes exist in the wild. Record raw fields; normalize here.
export function splitInput(row, cachedIncludedInInput) {
return cachedIncludedInInput
? { fresh: Math.max(row.input_tokens - row.cached_input_tokens, 0),
cached: row.cached_input_tokens }
: { fresh: row.input_tokens, cached: row.cached_input_tokens };
}
export function stepCost(row) {
const rate = rates.models[row.model_served];
if (!rate) throw new Error("no rate for served model: " + row.model_served);
const { fresh, cached } = splitInput(row, rates.accounting.cached_in_input);
return (fresh * rate.input + cached * rate.cached_input + row.output_tokens * rate.output) / 1e6;
}Persist one appended row per step
task_id is the grouping key, so per-step rows roll up to a per-task total with no second pass and no state held in memory.
export function instrument(callModel, { taskId, ledgerPath }) {
let stepIndex = 0;
return async function step(request, { attempt = 1 } = {}) {
const started = Date.now();
const base = { task_id: taskId, step_index: stepIndex++, attempt,
model_requested: request.model, latency_ms: 0 };
let row;
try {
const res = await callModel(request);
row = { ...base,
model_served: res.model,
input_tokens: res.usage.input_tokens,
cached_input_tokens: res.usage.cached_input_tokens ?? 0,
output_tokens: res.usage.output_tokens,
tool_calls: (res.tool_calls ?? []).length,
outcome: "ok" };
} catch (err) {
row = { ...base, model_served: null, input_tokens: 0,
cached_input_tokens: 0, output_tokens: 0, tool_calls: 0,
outcome: "error:" + err.name };
}
row.latency_ms = Date.now() - started;
row.cost_usd = row.model_served ? stepCost(row) : 0;
appendFileSync(ledgerPath, JSON.stringify(row) + "\n");
if (row.model_served === null) throw new Error(row.outcome);
return row;
};
}A runnable harness that prints the cost rollup
The harness runs a fixed task set — a 12-step agent run over a 4k-token tool schema — and prints the rollup. Acceptance is written as a separate record kind, so retries and failed tasks price in correctly.
RATES_PATH=./rates.placeholder.json node harness.js --tasks 20 --ledger ./ledger.jsonl
Output from my run, using the placeholder rate card:
task_id steps attempts in_tok cached out_tok served cost_usd accepted
t-0001 12 1.00 126400 63200 7400 gpt-6-luna 0.0542 yes
t-0002 12 2.00 251800 102400 15100 gpt-6-luna 0.1158 no
t-0003 12 1.00 129400 64700 7200 gpt-6-luna 0.0543 yes
...
20 tasks | 18 accepted | billed $1.6317 | $0.0906 per accepted task
Illustrative figures, produced by the harness against invented rates. They are not measurements of any named vendor.
How Caching and Compression Change the Denominator
Prompt caching — what it rewards, and what silently defeats it
Caching rewards stability: a long system prompt that never changes, a tool schema serialized once, a fixed few-shot block, context appended only at the end. It is defeated by anything that perturbs the prefix — a timestamp in the system prompt, a per-request UUID, reordered context blocks, or JSON.stringify over an object whose key order varies between processes. That last one is the most common reason a cache hit ratio sits near zero while the code looks correct.
Compression: fewer tokens, more retries
The source material credits compression as a cost lever, and mechanically it does reduce input tokens. The honest trade-off is that compression can drop the detail a tool call needed. That failure does not appear as a cheaper task. It appears as a second attempt. Which means:
Reading the ledger for the cache/compression interaction
Take a compression change that cuts input tokens 20%, with attempts moving from 1.15 to 1.60 because tool calls now retry:
cost index = token multiplier × attempt multiplier = 0.80 × (1.60 / 1.15) = 1.113
Net effect: +11% cost per accepted task, with a lower input-token count. Any dashboard tracking tokens will report a win. The ledger reports a loss, because it stores attempt and divides by accepted tasks. That is the specific class of mistake this schema exists to catch.
Comparing GPT-6, Opus 5.5, and Grok 4.7 on Task Economics
Worked example: fixed task set, placeholder rates
Fix the task definition so the only variables are provider behaviour: 12 steps, a 4k-token tool schema, the same 20-task set, the same harness. Placeholder rates in USD per 1M tokens, invented for this table: Sol 2.00 / 0.50 / 8.00, Luna 0.50 / 0.125 / 2.00, Opus 5.5 10.00 / 2.50 / 40.00, Grok 4.7 1.00 / 0.25 / 4.00 (input / cached input / output).
| Model | Avg attempts | Input tok/task | Cached | Output tok/task | Cost/task | Cost per accepted |
|---|---|---|---|---|---|---|
| gpt-6-luna | 1.35 | 126,000 | 63,000 | 7,400 | $0.054 | $0.073 |
| gpt-6-sol | 1.15 | 129,400 | 64,700 | 7,200 | $0.219 | $0.252 |
| grok-4.7 | 1.20 | 310,600 | 155,300 | 17,280 | $0.263 | $0.316 |
| opus-5-5 | 1.15 | 129,400 | 64,700 | 7,200 | $1.097 | $1.261 |
That ordering is a property of my placeholder rate card, not a claim about the vendors. Different rates reshuffle it; real per-task token counts reshuffle it again.
Where Grok 4.7's stated position cuts both ways
A low price only wins if tokens per task stay comparable. In the table above, Grok 4.7 carries a 2× lower input rate than Sol and still lands slightly more expensive per task, because it consumes 2.4× the tokens. That's the entire crossover argument: if "heavy token use" is real, the ledger shows it immediately, and tokens-per-task has to sit in the same row as the rate. A price table without a token column is not a comparison.
Quality-adjusted cost: divide by accepted tasks, not attempts
Divide total billed cost by accepted tasks, not attempted ones. Attempts are already inside the cost numerator; acceptance is what makes the denominator honest. This also prices in a silent model downgrade for free: if model_served shifts to a model with worse task completion, acceptance drops and cost per accepted task rises, whether or not the rate went down.
Turning the Ledger Into a Control Loop
Two concrete checks for the control loop
- Routing divergence. Alert when
model_served != model_requestedfor any model you pinned. If the source reporting about routing is accurate, this fires before your quality metrics degrade. If it never fires, you have evidence the concern does not apply to your account — also useful. - Cost per accepted task, week over week. Alert on a rise past your threshold, even when the headline rate fell. Compute it from the rollup, not from a dashboard that averages tokens.
Keep a regression baseline
Re-run the fixed task set after any provider pricing or routing change and diff the ledger. Diffing the cost_usd, attempt, and model_served columns against a stored baseline is cheap and catches changes a changelog will not mention. Store the baseline ledger next to the harness.
Conclusion — Own the Unit, Not the Rate
The position, restated
Headline cuts are a vendor-side number. Cost per accepted task is the only figure that survives an agent loop, and computing it needs three things a price page cannot give you: served-model visibility, cache accounting, and retry accounting. Without model_served you cannot see routing. Without cached_input_tokens you cannot see whether your prefix is cacheable. Without attempt you cannot see whether compression paid for itself.
Name the limitation
The source material behind this post is four news snippets dated 2026-09-22, and I read only their headlines and short summaries. Re-verify every vendor figure against live pricing and usage endpoints before you budget against it. The harness, the schema, and the cost function are the parts of this post I would stand behind; the numbers in the tables are placeholders.
Further Reading
News reporting (all published 2026-09-22)
These are news items, not primary vendor documentation. Links go to aggregator entries for the original articles.
- VentureBeat — OpenAI releases GPT-6 Sol and Luna models, slashing API costs 50% or more
- The New Stack — Anthropic releases Opus 5.5 and cuts pricing by 20%; agent calls might be routed to an older model
- TechRepublic — SpaceXAI launches Grok 4.7: low prices, heavy token use
- thelec.net — Prompt caching, compression emerge as key to cutting token costs
Primary sources to verify against
To confirm current rates and served-model behaviour, go to the vendors' own pricing and model documentation pages — OpenAI's API pricing page, Anthropic's pricing page, and the SpaceXAI/xAI developer documentation. I am deliberately not linking those pages here: their paths change often, and I will not ship a guessed URL for a rate you might budget against. Navigate from the vendor console, not from a blog post.


