SculptAI Engineering
Kimi K3 spends your token budget thinking before it answers
On OpenRouter, Kimi K3's reasoning tokens come out of max_tokens before any answer is written. At max_tokens 1200 we got a 200 OK with content: null and a full bill. Measured cost, latency and reasoning-effort numbers, with the command to reproduce them.
SculptAI · · 7 min read
On OpenRouter, Kimi K3’s reasoning tokens are drawn from max_tokens before a single character of the answer is written. Set max_tokens to the size of the reply you want and you can get an HTTP 200 with content: null, finish_reason: “length”, and a full invoice. We lost every answer at max_tokens: 1200. The same request at 4000 came back clean after 667 tokens of thinking.
We hit this building SculptAI, where a language model reads one sentence a player has written and reports how a simulated settlement hears it. The task is small — a classification and three lines of dialogue — so 1200 tokens looked generous. It was not generous; it was irrelevant, because the model never got to the answer.
Why does Kimi K3 return an empty response with a 200 OK?
Because thinking and answering share one budget, and thinking goes first. Here is the same request at two budgets, everything else identical:
| max_tokens | reasoning | finish_reason | content |
|---|---|---|---|
| 1200 | 1200 | length | null |
| 4000 | 667 | stop | valid JSON |
Nothing about the first row looks like an error. The status is 200, there is no error object, and usage reports a completed call. The only signal is that choices[0].message.content is null while finish_reason is length — and you are charged for all 1200 tokens of thinking that produced nothing.
The practical consequence: size the budget for the thinking, not for the reply. A short answer from a reasoning model is not a small request. We now default to 4000 for a one-sentence task and treat the pairing of null content with finish_reason: length as a distinct, named failure, because the fix for it is nothing like the fix for a malformed reply.
How much does one Kimi K3 call cost?
More than the output length suggests, because you are paying for the thinking at completion rates. Below is one fixed task — read a sentence for a settlement, return a classification and three short lines of dialogue — run at each reasoning effort. Measured 7 August 2026 through OpenRouter, routed to Together, at that day’s list price of $3.00 in and $15.00 out per million tokens.
| effort | thinking | completion | cost | latency | read it right? |
|---|---|---|---|---|---|
| low | 61 | 308 | $0.0060 | 12.9s | no |
| medium | 547 | 731 | $0.0117 | 25.5s | yes |
| high | 1196 | 1426 | $0.0228 | 44.4s | yes |
| unset | 1809 | 2167 | $0.0339 | 69.9s | yes |
Token counts are the durable figure here; prices move. By 8 August the cheapest of K3’s twelve OpenRouter endpoints was $2.50 in and $14.00 out per million. Reasoning was 83% of completion tokens at default settings, which is the number that actually decides your bill.
Which reasoning effort should you use?
Medium, as a floor. The temptation is obvious: low is five times cheaper and five times faster than the default. Then you read what it wrote.
Our settlement is described by six numbers, and this one had food: 3out of 10 — a starving valley. At low effort K3 opened with “food stores decent but enemies pressing.” At medium it read “a hungry, trapped place.” At high, “grain low, no road out, raiders near.”
Low effort did not produce a worse sentence. It produced a confident sentence about a different world. For a task whose entire purpose is that conditions change the reading, that is not a cheap option, it is a broken one. If your task tolerates being occasionally wrong about its input, low is excellent value. Ours does not.
The other finding in that table is the one worth acting on today: leaving effort unset is the most expensive setting available. It cost 49% more than high, took 57% longer, and read the world no better. Omitting the parameter is not a neutral default. Send it explicitly.
Does strict json_schema matter?
Yes, and more than we expected. We asked for an object with a shape field whose only legal values were five fixed words.
In strict json_schema mode, K3 returned "shape": "prohibition". In plain json_object mode, the same prompt returned "shape" set to “A gate with the bar dropped. The word never is the bar itself” — a sentence of genuinely lovely prose, in an enum field.
This matters for provider routing. OpenRouter spreads requests across endpoints, and response_format support varies between them. If your client silently downgrades to json_objectwhen strict mode is rejected, you will get replies that parse as JSON and fail every semantic check you have. Ours downgrades, and it logs the downgrade, because “the validator refused it” sends you hunting through your prompt when the cause was the request.
How do you use a non-deterministic model in a deterministic engine?
This is why we were measuring at all. SculptAI’s simulation is seeded and replayable: the same seed and the same decisions must produce byte-identical history forever, or shared replays and daily challenges stop working. A model is the opposite of that.
The resolution is that the model never runs inside the simulation tick.It is consulted before the tick, its answer is validated against a fixed schema, and the validated result is frozen into the save alongside the decision that used it. A replay reads what was decided at the time rather than asking again. Three fields are taken out of the model’s hands entirely and supplied by code: the player’s original words, the world’s condition, and the label recording that a model produced the reading at all.
That last one is not a technical constraint. If the call fails — no key, a timeout, a reply the validator refuses — the game falls back to an offline heuristic and says so on screen. A player should never be shown a canned response while being told they are watching a model think.
Reproducing these numbers
Every figure above came from a probe committed in the repository, not from a one-off script. Model behaviour and pricing both move; a number without a command to regenerate it is a claim rather than a fact.
npm run probe:interpret -- "Share the harvest with anyone who asks."The probe prints the model’s reading next to an offline heuristic’s, for two opposed worlds, with tokens, cost and latency on every call. Conditions when we ran it: moonshotai/kimi-k3 through OpenRouter, routed to Together, 7 August 2026.
What we would tell you in one line
Send reasoning.effort explicitly, set max_tokens for the thinking rather than the answer, insist on strict json_schema, and treat a null content with finish_reason: length as its own failure with its own message. Those four changes took our per-call cost from $0.034 to $0.012 and our latency from 70 seconds to 26, with no loss of quality we could detect.
