Same words, three times the bill
The Tokens You Cannot See
The second call's usage block, from the median run of three (the first call's said output_tokens: 77):
"usage": {
"input_tokens": 80,
"input_tokens_details": { "cache_write_tokens": 0, "cached_tokens": 0 },
"output_tokens": 231,
"output_tokens_details": { "reasoning_tokens": 152 },
"total_tokens": 311
}231 output tokens for the same 61 words. 152 of them are reasoning tokens: output tokens the model writes before the answer, which you pay for and do not see. OpenAI's reasoning guide says it directly:
"While reasoning tokens are not visible via the API, they still occupy space in the model's context window and are billed as output tokens."
output_tokens 231 billed
reasoning_tokens 152 billed, never shown
visible text 79 (231 - 152), the same 61 wordsYou might expect output tokens to be the words you see. That is wrong on every model that can reason before it answers. output_tokens is the total, output_tokens_details.reasoning_tokens is the hidden part inside it, and the visible text is what is left. Anthropic calls the same thing "thinking"; the billing is the same.
This is why a reasoning item came first in the second call's output: the model writes its reasoning, then its answer. So the hidden part also delays the first visible word, 5.0 seconds on the second call against 1.3 on the first.
The setting Nora changed is called effort, the request field reasoning.effort. Her first call ran at the default, medium, as the response echoed; the second sent high. Here is the same call at each effort, three runs each through the SDK, streamed so the first visible word could be timed. Medians, measured November 4, 2026:
| Model and effort | Reasoning tokens | Visible output | First word | Whole call | Cost |
|---|---|---|---|---|---|
mid-size, low | 0 | 77 | 1.42 s | 2.18 s | $0.00093 |
mid-size, medium (default) | 0 | 77 | 1.31 s | 1.91 s | $0.00093 |
mid-size, high | 152 | 79 | 5.02 s | 5.67 s | $0.00247 |
small, none | 0 | 77 | 1.06 s | 1.77 s | $0.0000465 |
small, low | 0 | 77 | 0.89 s | 1.42 s | $0.0000465 |
Every row has 80 input tokens and returned the same 61-word paragraph; the costs come from the price card in the next section. There is no none row for the mid-size model: it does not accept that value. OpenAI's latest-model guide says the mid-size and flagship models "do not support the none reasoning effort", and a request that sends it gets a 400.
Now a prediction from the table's other side. Nora lowers the effort from medium to low. Is the call faster, cheaper, or both?
On this call, neither. At medium the model already wrote no reasoning, so there was nothing to save. Effort is an upper limit, not an amount: the model reasons as much as the request seems to need, up to the setting. A one-line request to fix 63 words needs none at medium.
On real work it is different. Inkstep's full check, with its instructions, examples and output format, reasons a median of 175 tokens at medium and none at low, and takes a median of 11.8 seconds against 7.3. There, lower effort is faster and cheaper. The price can be worse answers, and only a test on real learner texts can tell you; the course runs that test when it prices the feature.
Check today. On 2026-11-03, the mid-size model did not accept the effort
none, and it rejected anytemperaturewith a 400; the small model accepted every effort fromnonetomax, and took atemperatureonly atnone. Each new model has its own list, and the defaults move with it. Check the current values before you rely on them: https://developers.openai.com/api/docs/guides/latest-model
Recap. output_tokens includes reasoning tokens, which you pay for and never see. Effort sets the most reasoning the model may do before the answer, so it can trade time and money for more work. Which effort Inkstep runs at is decided later, with evidence.
What the Call Cost
Every number you need to price a call is in its own response. The other half is the price card:
per million tokens, OpenAI standard tier, checked 2026-11-03
input cached input cache write (30 min) output
mid-size gpt-6.1-sol $2.00 $0.10 $2.50 $10.00
small gpt-6-luna $0.10 $0.01 $0.125 $0.50Output costs five times input on both models, and the small model is twenty times cheaper than the mid-size one. The cached columns belong to the prompt cache, which comes when the course works on the bill.
First, the input. Nora's input string is 74 tokens by the local count, and the Responses API adds 6 tokens of framing to a single string input, on every input the lab tried:
input tokens
"Correct this paragraph:\n\n" + paragraph 74 tokens (counted locally, o200k_base)
framing added by the API 6 tokens (measured in the lab)
total 80 tokens (input_tokens on both models)Now the two bills, one step at a time:
NORA'S FIRST CALL, mid-size model, default effort (medium)
input 80 tokens x $2.00 / 1,000,000 = $0.00016
output 77 tokens x $10.00 / 1,000,000 = $0.00077 (0 of them reasoning)
total $0.00093 about a tenth of a cent
THE SAME CALL AT EFFORT HIGH
input 80 tokens x $2.00 / 1,000,000 = $0.00016
output 231 tokens x $10.00 / 1,000,000 = $0.00231 (152 of them reasoning)
total $0.00247 about a quarter of a cent
of which reasoning 152 x $10.00 / 1,000,000 = $0.00152
reasoning share $0.00152 / $0.00247 = 62 percentAt high, the part Nora wrote is the smallest line, and the largest part of the bill is text nobody will ever read. On one call that does not matter. If this call ran 20,000 times a day, high would add 20,000 x $0.00154 = $30.80 a day for exactly the same words. The course comes back to the invisible part of the bill when it prices a whole month.
The One Value to Log
The response body is not all that comes back. The HTTP headers carry more. Here are some from Nora's call:
HTTP/2 200
content-type: application/json
openai-processing-ms: 1767
openai-version: 2020-10-01
x-ratelimit-limit-requests: 10000
...
x-request-id: req_fb2bffe91eab4707a425f24e7168e3d4The x-ratelimit-* headers belong to failures. The one to keep now is x-request-id, the provider's name for this request. If a check goes wrong and the provider's support team needs to look, this id is how they find it: "OpenAI recommends logging request IDs in production deployments." Inkstep logs it for every check.
Recap. Every call's cost is in its own response: input tokens times the input price, plus output tokens, reasoning included, times the output price. And every response carries a request id worth keeping.
The Same Call in TypeScript
Inkstep's server is TypeScript, and so is the rest of this course: the feature ends in a browser, so one language covers server and client; Zod, a TypeScript library, gives a schema, a validator and a type from one object; and the wire format changes more slowly than any wrapper library. The course uses the official openai package (version 7.39.0) on Node 24, which runs .ts files directly. Wrapper libraries that hide several providers behind one API are fine once you know what they wrap, but they change often (the Vercel AI SDK went from version 3 to version 7 in about two years), so this course teaches the layer underneath.
First, the settings:
// src/config.ts: every model id, setting and price lives here and nowhere else.
export const MODEL = "gpt-6.1-sol"; // the mid-size model
// dollars per million tokens, OpenAI standard tier, checked 2026-11-03
export const PRICES = {
"gpt-6.1-sol": { input: 2.0, cached: 0.1, cacheWrite: 2.5, output: 10.0 },
"gpt-6-luna": { input: 0.1, cached: 0.01, cacheWrite: 0.125, output: 0.5 },
};MODEL is the only place a model id appears in Inkstep's code, and PRICES is the price card with its date, so a price change is a one-line change. Second, the paragraph, in a file of its own with the facts about the learner that later steps need:
// checks/specimen.ts
export const specimen = {
task: "Describe your last weekend",
level: "beginner",
explanationLanguage: "Portuguese",
text:
"Last weekend I go to the beach with my friends. We was very happy because " +
"the weather was sunny. I eated a grilled fish and we played volleyball until " +
"the night. In the evening we returned to home and I watched a film in my " +
"sofa. I am very tired, but it was a amazing weekend and I want to repeat it soon.",
};Today the call uses only text. The other three fields wait for a prompt that works.
Third, the call:
// src/first-call.ts: one call, and everything that comes back.
import OpenAI from "openai";
import { MODEL } from "./config.ts";
import { specimen } from "../checks/specimen.ts";
const client = new OpenAI(); // reads OPENAI_API_KEY from the environment
const response = await client.responses.create({
model: MODEL,
input: `Correct this paragraph:\n\n${specimen.text}`,
max_output_tokens: 1500,
store: false,
});
console.log(response.status, response.incomplete_details);
console.log(response.output_text);
console.log(response.usage);
console.log(response._request_id); // the x-request-id header (errors carry it as err.requestID)new OpenAI() reads the key from the OPENAI_API_KEY environment variable, so the key never appears in code and never leaves the server. client.responses.create sends the same POST /v1/responses with the same four fields. The first console.log prints why it stopped, before anything else, on purpose. Then come the joined text, the usage, and _request_id, the id from the one value to log. Run node src/first-call.ts and you read the same fields as on the wire, because it is the same response.
The SDK also does two things you did not ask for: it waits up to 10 minutes for a response (600,000 ms) and sends a failed request again up to 2 times (maxRetries). For a learner waiting on a button, both are wrong, and failures replaces them.
One habit to start now. This first script writes its cap, 1,500, in the call, and relies on a default effort that Nora never wrote down. Once the prompt moves into code, config.ts also holds MAX_OUTPUT_TOKENS = 1500 and EFFORT = "medium", and every request sends both. Then a change in the provider's default can never change Inkstep's feature without a line of Nora's code changing too.
What changed. Many tutorials from 2023 to 2025 use the older Chat Completions API (
POST /v1/chat/completions), read the text fromchoices[0].message.content, and cap output withmax_tokens. OpenAI now says Responses "is recommended for all new projects", and its newer models keep some features for Responses only. Copy old code into a Responses call and you get400 Unknown parameter: 'max_tokens'.(the field ismax_output_tokens). Old code that sendstemperaturefails too: the mid-size model answers400 Unsupported parameter: 'temperature' is not supported with this model., and the small model accepts it only at effortnone.
Recap. The SDK sends the same request and returns the same response. It reads the key from the environment and adds a 10-minute timeout and 2 retries of its own. The model id and the prices live in config.ts, and the cap and the effort join them next.
The Same Idea on Claude and Gemini
Every idea in this lesson, and in what one call sends and returns, exists on the other two large providers, under different names. The concept comes first, the field second. From the docs, checked 2026-11-03:
| Concept | OpenAI Responses | Anthropic Messages | Gemini (Interactions / generateContent) |
|---|---|---|---|
| Endpoint | POST /v1/responses | POST /v1/messages (+ anthropic-version: 2026-10-21) | POST /v1beta/interactions / :generateContent |
| Output cap | max_output_tokens (optional) | max_tokens (required) | generation_config.max_output_tokens / maxOutputTokens |
| Reasoning setting | reasoning.effort | output_config.effort | thinking_level / thinkingConfig.thinkingLevel |
| Why it stopped | status, incomplete_details.reason | stop_reason | status / finishReason |
| Reasoning tokens | usage.output_tokens_details.reasoning_tokens | usage.output_tokens_details.thinking_tokens | total_thought_tokens / thoughtsTokenCount |
| Stored by default? | yes (store, 30 days) | no (stateless) | yes (store) |
| Request id | x-request-id header | request-id header | (none documented) |
One difference matters on day one. On Anthropic's API the output cap is required. On OpenAI's it is optional, which makes it easy to forget, and a forgotten cap is an unbounded worst case.
The Takeaway
What Nora paid for and could not see, on the second call, was 152 reasoning tokens: 62 percent of a bill for the same 61 words.
output_tokensincludes reasoning tokens, billed and never shown. Effort sets the most the model may write before its answer.- Price every call from its own
usage, and log the request id. - The SDK adds a 10-minute timeout and 2 retries; model ids, settings and prices live in config.
The one sentence to keep: output tokens include reasoning you never see, so price every call from its own usage, not from its text.
An open question to carry forward: the model returned a corrected paragraph, with no list of mistakes and no word of Portuguese. How do you make it give the same useful kind of answer to every learner, in their own language? That is the next step, a prompt that works.
References
Provider documentation (checked 2026-11-03):
- Responses API reference: create a response (OpenAI): the request fields,
store("response data will be stored for at least 30 days"),max_output_tokens,reasoning.effort, and the output item types - API reference overview (OpenAI):
Authorization: Bearer, thex-request-idheader, "OpenAI recommends logging request IDs in production deployments." - Reasoning models (OpenAI): "While reasoning tokens are not visible via the API, they still occupy space in the model's context window and are billed as output tokens."
- Using the latest model (OpenAI): the mid-size model does "not support the
nonereasoning effort"; removetemperaturewhen effort is notnone - Migrate to the Responses API (OpenAI): "Responses is recommended for all new projects."
- Pricing (OpenAI): the price card above
- Messages API (Anthropic) and API overview (Anthropic):
max_tokensrequired,output_config.effort,stop_reason,anthropic-version: 2026-10-21,request-id - Interactions API (Google) and generateContent (Google):
generation_config,thinking_level,store,finishReason,thoughtsTokenCount
SDK:
openaifor Node.js, README:_request_id"is added from thex-request-idresponse header so that you can quickly log failing requests and report them back to OpenAI"openaifor Node.js,src/client.tsat c3f42fa: the default timeout of 600,000 ms and the default of 2 retries
Lab runs (Apple M1 Max, Node 24.14.1 with openai 7.39.0, Python with openai 3.45.0, 2026-11-03 and 2026-11-04):
- The same call through the SDK, streamed, 3 runs at each effort (
gpt-6.1-solatlow,medium,high;gpt-6-lunaatnone,low): the table above; onlyhighreasoned (172, 126 and 152 tokens) and returned a reasoning item - Inkstep's full check on 30 learner texts, 3 runs each,
gpt-6.1-sol: reasoning 175 tokens atmediumand 0 atlow(medians), 11.8 s and 7.3 s per check - The 6 framing tokens on a single string input, measured against
usage.input_tokensand the token counting endpoint max_tokenson Responses:400 Unknown parameter: 'max_tokens'.;temperatureon the mid-size model:400 Unsupported parameter: 'temperature' is not supported with this model.- The specimen's token count:
js-tiktoken1.0.21 witho200k_base, run locally (no API call)
Pinned packages: openai 7.39.0, zod 4.6.7, Node 24.14.1.

