show the local model's context window before the first token count - #8882
Conversation
The chat header rendered the context bar only when `contextUsage` was set, so a resident GGUF showed nothing until a token count landed. The recount that produces one is best-effort: it stands down for images, audio, Deep Research, a pending RAG turn and a busy backend, and a chat that has never been sent has no usage at all. In every one of those cases the context length was invisible exactly while the user was picking a model to fit a prompt into (unslothai#8867). The bar now renders as soon as the resident model's window is known, showing `— / 32.8k` until a count arrives. An uncounted chat is kept distinct from a counted zero: `percent` stays null, so no fill, no percentage and no near-limit warning are drawn. Reporting an unmeasured prompt as 0% of the window would be the same misreport in a new place. `hasKnownContextWindow` gates on the resident checkpoint rather than the picked one, so a load in flight, an evicted model and an API selection all keep their windows off the header. `deriveContextUsageBar` moves the prop-to-text derivation out of the component so the node suite can drive it directly.
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
Before/after UI evidence for this change, from two isolated Studio installs: BEFORE at the merge base Chat header with The same pair at full window, which is the control: same model, same variant, empty thread, and the Deep research pill armed in the composer on both halves. How the uncounted state was reached, and why it is reproducible rather than staged: Readings taken from the same servers that were photographed:
The zero count on both sides is the part that makes the pair mean anything: the AFTER bar is drawn from the resident model's window alone, not from a count that happened to land on one side. Two things in the images that are not this PR: the empty-chat greeting is randomised per mount, so the halves read differently, and free GPU memory moved 9433 MiB to 7139 MiB between the two runs because another session on this box took the card. Neither touches the claim, the model was loaded with Downloadable copySame two images as a CI artifact, for anyone who wants the originals rather than the rendered inline copies: Artifact name:
The bundle is byte-identical to the images above: No GIF here. Nothing in this pair is a motion effect, and the run recorded static screenshots rather than a frame series, so there is no cadence to assemble honestly. |
|
I can reproduce an issue with Reproduction:
Actual result: Expected result: Since the conversation has not started, the usage numerator should be unknown ( |
|
Confirmed and fixed. Final shape is 267986f; the earlier commit put the check on the client and Codex was right that it could not see the effective tool policy, so the whole decision moved into the backend. CauseThe recount counted whatever branch it found, including an empty one, and llama.cpp renders an empty message list as the bare generation marker. Against That 1 was published as usage, so the header read Change
It has to be decided there rather than in the browser:
VerificationSame model you used, Fresh chat, nothing sent, same server on both halves:
The two states that must not regress, both on the fixed build:
Backend |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1e528c89a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 267986fc8d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
for more information, see https://pre-commit.ci
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Studio's chat header prices a conversation against the model's context window, and the recount behind it had a llama.cpp branch only. For an MLX-served model the endpoint answered "No GGUF model loaded", so the bar showed a dash for the whole conversation until the first reply reported its own usage -- the case unslothai#8882 made visible by rendering the bar as soon as the window is known. The endpoint now asks MLX after finding no GGUF backend, below the refusals that are about the request rather than the backend and above llama.cpp's own render concerns. A count is only worth showing if it renders what the completion for the same request would render, so this answers the questions that completion answers rather than the ones the request makes easy. Which of its two paths claims the request: the tool loop, or the relay that a client catalog or replayed tool history goes to. What each then renders: the loop's action nudge and its strip of stale call markup, the relay's rebuild that keeps structured tool_calls through templating, the launcher's tools-on default where a request stated no intent of its own and its withdrawal where the request did, the catalog a named template's tool_use branch advertises, and a zero tool budget suppressing the loop entirely. Where a helper carries one of those decisions it is called rather than restated. Three shapes are refused rather than priced short: a pending turn whose retrieval would reach the document store, since only running the search would say what it adds; an image, which /apply-template swaps for a short marker; and an empty prompt, whose bare generation marker reads as a started conversation. A vision model is counted rather than skipped. It serves text turns through the processor render its generation uses, so counting shares that render, and asks mlx_vlm which special markers tokenization should add -- a per-model answer, and on the releases from before mlx_vlm exported it, the rule those releases inline. Both store questions on this path -- the enabled MCP servers and whether retrieval can run -- open SQLite, and the second can load an extension and create the schema, so they run on the worker thread and only where their answer decides something.




Fixes #8867.
Problem
The chat header rendered the context bar only when
contextUsagewas set, so a resident GGUF showed nothing until a token count landed. The recount that produces that count is best-effort and stands down for images, audio, Deep Research, a pending RAG turn and a busy backend, and a chat nobody has sent has no usage at all. In all of those the context length was invisible exactly while the user was choosing a model to fit a prompt into.Change
The bar now renders as soon as the resident model's window is known, showing
— / 32.8kuntil a count arrives.An uncounted chat stays distinct from a counted zero:
percentremains null, so no fill, no percentage and no near-limit warning are drawn. Reporting an unmeasured prompt as 0% of the window would be the same misreport in a different place.lib/context-window-known.ts—hasKnownContextWindowgates on the resident checkpoint rather than the picked one, so a load in flight, an evicted model and an API selection all keep their windows off the header.lib/context-usage-bar-state.ts—deriveContextUsageBarmoves the prop-to-text derivation out of the component so the node suite can drive it directly.chat-page.tsx— the header render gate.Screenshots
Chat header with a GGUF resident and no token count available.
Before — no bar at all:
After — the window is named, with no usage claimed:
Once a count lands, unchanged from before:
Verification
Manually verified in a browser against the real frontend, with a real
llama-serverserving a real GGUF (stories260K.gguf,n_ctx2048) and token counts computed live through the same/apply-template→/tokenizepair the backend uses. Every state below was checked by hand./chat/count_tokens19 / 2.0k, fill— / 2.0k, no fill19 / 2.0k, fillstudio/frontend/tests/context-window-visible.test.tscovers the predicate and every text state. The suite was mutation-checked: falling back to0instead of null, dropping the external-model guard, and dropping the percent clamp each fail it.Frontend suite 2716 passing,
tsc -bandtsc -p tsconfig.test.jsonclean,vite buildclean.Empty-chat count
Reported by @oobabooga on this PR (comment): a fresh chat with
unsloth/Phi-4-mini-instruct-GGUFQ4_K_Mresident read1 / 131,072at 0.0%, not-.The recount counted an empty message list. llama.cpp renders that as the bare generation marker (
<|assistant|>for this model, one token), so/chat/count_tokensreturned 1 and it was published as usage./chat/count_tokensnow refuses with 503 when the resolved prompt is empty, checked after the system prompt, the canvas instruction, the tool selection and the action nudge have been folded in. The backend is the only side that can decide it:unsloth run --enable-toolsoutranks a request's ownenable_tools: false, so a pill-less request can still carry schemas the browser cannot see. An empty chat carrying a system prompt, project instructions, the canvas instruction or tool schemas is still counted.Verified in a browser against the reported model with
n_ctxpinned to 32768:- / 32.8kwith no fill on a fresh chat,15 / 32.8kwith a system prompt set,13 / 32.8kafter one turn. Full readings in the reply.