Repository navigation
fix(local): cap a chat question at its share of the window - #2053
Conversation
budget.py priced the question at 1,024 tokens and said MessageText enforced it, but MessageText had no maximum, so a pasted page walked past its share and llama.cpp cut the reply off wherever the window ran out, with no error. MessageText now refuses more than QUESTION_CHARS, 4,096 characters: QUESTION_TOKENS at the four-characters-a-token estimate history.py already prices turns with, which moves to budget.py as CHARS_PER_TOKEN so there is one ratio. The refusal is a 422 at the wire, before any model is resolved or retrieval runs. One global cap rather than one per window, because it runs before the model is known and the budget prices the question at the same 1,024 for every window. The composer stops at the same cap through maxLength and, once there, says so in a status line, so nobody finds out on send. Fixes MODSetter#1943
|
@ybai08 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What
MessageTextnow carriesmax_length=QUESTION_CHARS, 4,096 characters. A longer message is a422at the wire, before any model is resolved or retrieval runs.QUESTION_CHARS = QUESTION_TOKENS * CHARS_PER_TOKENlives inbudget.py, so the comment there is now true.CHARS_PER_TOKEN = 4is the ratiohistory.pyalready used inline in_tokens(); it moves tobudget.pyand_tokens()reads it, so there is one ratio, not two.maxLengthon the input, and once the text reaches it a status line says "Messages are limited to 4,096 characters." (role="status", in the footer row of a running thread, under the composer on an empty one). The constant is mirrored infeatures/chat/question-limit.tswith a comment pointing atbudget.py. The string is translated in all nine catalogs;en.jsonregenerated.chat.mdand the budget table note infit.mdnow describe the enforced cap, and both Known gaps lines are deleted.The bound, and why it is global. 1,024 tokens at four characters a token is 4,096 characters. One cap for every window rather than a window-aware one, for two reasons: the refusal has to run before any model is resolved (that is what makes it a 422 at the wire rather than a failure mid-turn), and the budget already prices the question at the same 1,024 for every window, so a window-aware cap would make the question the one part whose price and limit disagree. A remote model with a 128k window gets the same 4,096, which is the tension the issue names; the one-line comment on
QUESTION_CHARSrecords the choice. Dense scripts run richer than four characters a token, so the cap is loose for them, never tight.Why the notice only at the cap. A counter would be noise on every message; the cap is the one moment where typing, or the tail of a paste, stops landing, so that is where the explanation goes.
Why
The budget treated the question as one of four real caps, and it was the one that was not. A pasted page pushed a full-history turn past the window, and llama.cpp cut the reply off mid-sentence with no error.
Fixes #1943
How to test
Tests first, all three failed before the change:
test_budget.py::test_the_question_cannot_exceed_its_share:QUESTION_CHARS // CHARS_PER_TOKEN == QUESTION_TOKENS, a message of exactly the cap passes (whitespace stripped first), one more character is refused.test_chat.py::test_a_question_past_its_share_is_refused_at_the_wire: with no model selected, an oversized message is a422, not the409a valid message gets, which shows the schema refused it before resolution.chat-composer.test.tsx: pasting 4,097 characters leaves 4,096 in the textbox and shows the status line.Locally: 3,062 backend unit tests pass, the chat integration file passes (the encoder-backed cases skip without the fetched model, as on
dev), 304 frontend tests pass (run withTZ=UTC, see #2050), lint andtsc -bclean, 805 keys × 10 languages verified, 102 docs checked.High-level PR Summary
This PR enforces a 4,096-character limit on chat questions (corresponding to 1,024 tokens at 4 chars/token) to prevent oversized messages from exceeding the model's context window. The backend now validates message length at the wire with a 422 error, the frontend composer enforces the same limit with
maxLength, and a status message appears when users hit the cap. The change consolidates theCHARS_PER_TOKENconstant inbudget.pyand updates documentation to reflect that this is now an enforced limit rather than just a budget estimate.⏱️ Estimated Review Time: 15-30 minutes
💡 Review Order Suggestion
surfsense_local/backend/modules/chat/budget.pysurfsense_local/backend/modules/chat/schemas.pysurfsense_local/backend/modules/chat/history.pysurfsense_local/backend/tests/unit/chat/test_budget.pysurfsense_local/backend/tests/integration/chat/test_chat.pysurfsense_local/frontend/src/features/chat/question-limit.tssurfsense_local/frontend/src/features/chat/chat-composer.tsxsurfsense_local/frontend/src/features/chat/chat-composer.test.tsxdocs/architecture/chat.mddocs/architecture/local-models/fit.mdsurfsense_local/frontend/translations/en.jsonsurfsense_local/frontend/translations/de.jsonsurfsense_local/frontend/translations/es.jsonsurfsense_local/frontend/translations/fr.jsonsurfsense_local/frontend/translations/hi.jsonsurfsense_local/frontend/translations/ja.jsonsurfsense_local/frontend/translations/ko.jsonsurfsense_local/frontend/translations/pt-BR.jsonsurfsense_local/frontend/translations/ru.jsonsurfsense_local/frontend/translations/zh-CN.json