Loading video…
Sign in to react
The callSolidCode at first-call

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):

JSON
"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."

Text
output_tokens        231   billed
  reasoning_tokens   152   billed, never shown
  visible text        79   (231 - 152), the same 61 words

You 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 effortReasoning tokensVisible outputFirst wordWhole callCost
mid-size, low0771.42 s2.18 s$0.00093
mid-size, medium (default)0771.31 s1.91 s$0.00093
mid-size, high152795.02 s5.67 s$0.00247
small, none0771.06 s1.77 s$0.0000465
small, low0770.89 s1.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 any temperature with a 400; the small model accepted every effort from none to max, and took a temperature only at none. 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:

Text
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.50

Output 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:

Text
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:

Text
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 percent

At 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:

Text
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_fb2bffe91eab4707a425f24e7168e3d4

The 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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 from choices[0].message.content, and cap output with max_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 get 400 Unknown parameter: 'max_tokens'. (the field is max_output_tokens). Old code that sends temperature fails too: the mid-size model answers 400 Unsupported parameter: 'temperature' is not supported with this model., and the small model accepts it only at effort none.

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:

ConceptOpenAI ResponsesAnthropic MessagesGemini (Interactions / generateContent)
EndpointPOST /v1/responsesPOST /v1/messages (+ anthropic-version: 2026-10-21)POST /v1beta/interactions / :generateContent
Output capmax_output_tokens (optional)max_tokens (required)generation_config.max_output_tokens / maxOutputTokens
Reasoning settingreasoning.effortoutput_config.effortthinking_level / thinkingConfig.thinkingLevel
Why it stoppedstatus, incomplete_details.reasonstop_reasonstatus / finishReason
Reasoning tokensusage.output_tokens_details.reasoning_tokensusage.output_tokens_details.thinking_tokenstotal_thought_tokens / thoughtsTokenCount
Stored by default?yes (store, 30 days)no (stateless)yes (store)
Request idx-request-id headerrequest-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.

  1. output_tokens includes reasoning tokens, billed and never shown. Effort sets the most the model may write before its answer.
  2. Price every call from its own usage, and log the request id.
  3. 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):

SDK:

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-sol at low, medium, high; gpt-6-luna at none, low): the table above; only high reasoned (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 at medium and 0 at low (medians), 11.8 s and 7.3 s per check
  • The 6 framing tokens on a single string input, measured against usage.input_tokens and the token counting endpoint
  • max_tokens on Responses: 400 Unknown parameter: 'max_tokens'.; temperature on the mid-size model: 400 Unsupported parameter: 'temperature' is not supported with this model.
  • The specimen's token count: js-tiktoken 1.0.21 with o200k_base, run locally (no API call)

Pinned packages: openai 7.39.0, zod 4.6.7, Node 24.14.1.