fix: keep LM Studio (llama.cpp) on-model instead of falling back on grammar errors - #67349
fix: keep LM Studio (llama.cpp) on-model instead of falling back on grammar errors#67349rsegrest wants to merge 3 commits into
Conversation
|
Thanks for the focused regression fix. Current main recognizes the existing llama.cpp phrases at Automated hermes-sweeper review. |
2b3fcd3 to
1b6c774
Compare
|
Thanks for the review. Adding test coverage for the second commit in b50b0c945 — it changed two behaviors that 1. The streaming path. The predicate was relaxed from 2. The broadened strip. Beyond All three regression tests fail against the pre-fix code and pass after. |
|
On the The reactive strip does mutate |
b50b0c9 to
978753e
Compare
|
Hit this exact failure independently on our fork (LM Studio, Bisection we ran, model-independent:
Confirms this is engine-level (llama.cpp GBNF), not model-specific — consistent with what you found with the This PR looks like the right fix (covers our exact phrasing plus the streaming |
|
Two follow-ups on my comment above. Correction. I reported our bisection pointing at ClickUp's date- Doesn't change the fix — it covers both keyword families — but the record should be accurate. Rebase available. This is showing I've rebased the three commits onto Also verified the fix end-to-end on a profile with no fallback model configured, so the successful tool call can only come from the strip-and-retry path firing rather than a silent failover. |
|
Retracting my correction above — it was wrong, and the original report was right. I re-tested
So it is specifically the Net effect for this PR: Separately, I ran your three commits end-to-end on a deployed container against live LM Studio, using a throwaway stdio MCP server carrying both hostile keyword families and no fallback model configured, so a successful tool call can only come from the strip-and-retry path: Note One testing gotcha worth recording for anyone else verifying this: my first end-to-end run "passed" without the recovery ever firing, because tool-search deferral was active and the model reached the tool via |
llama.cpp's json-schema-to-grammar failures surface with different wording depending on the build. LM Studio's engine reports this as "Failed to initialize samplers: failed to parse grammar", which didn't match any of the existing patterns, so the request exhausted its retries and fell over to a fallback provider instead of triggering the existing pattern/format-stripping recovery.
…/count bounds LM Studio (llama.cpp) rejects tool requests whose schemas contain large bounded-repetition keywords — e.g. the apple-notes MCP's batch-delete-notes / batch-move-notes tools carry "maxLength": 2000 on array-item strings, which llama.cpp expands into a GBNF grammar too large to parse (HTTP 400 "failed to parse grammar" / "unable to generate parser"). Every agent turn that loaded those tools 400'd three times and fell through to the fallback model. The existing reactive recovery didn't fire, for two independent reasons: 1. Streaming path: LM Studio's error surfaces as a bare APIError with status_code=None (not BadRequestError/400), so the classifier's `status_code == 400` guard never matched — the grammar error wasn't even recognized. Relax the guard to `status_code in (400, None)`; the phrases are llama.cpp-specific and safe to match without a status code. 2. Non-stream path: the error was classified, but the strip only removed `pattern`/`format`, and these schemas have neither. Broaden the reactive strip to also drop maxLength/minLength/maxItems/minItems. Verified end-to-end against a live LM Studio server with the real apple-notes tool set: attempt 1 → 400, recovery strips 93 keywords, attempt 2 succeeds — no fallback — for both streaming and non-streaming requests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n strip
The fix commit changed two behaviors without adding coverage:
* the classifier predicate was relaxed from ``status_code == 400`` to
``status_code in (400, None)``, and
* the reactive strip was broadened beyond pattern/format to the bounded
repetition keywords maxLength/minLength/maxItems/minItems.
Both are now pinned:
* test_llama_cpp_grammar_error_without_status_code — LM Studio's identical
error arrives as a bare APIError with status_code=None on the *streaming*
path (BadRequestError/400 only on non-streaming). Gating on 400 missed every
streamed request, so the recovery branch never ran and the turn fell through
to the fallback model.
* test_unrelated_error_without_status_code_is_not_grammar — guards the relaxed
gate against swallowing unrelated status-code-less errors.
* test_strip_removes_max_length_on_array_items — the real-world trigger
("maxLength": 2000 on array items, observed via apple-notes'
batch-delete-notes), asserting the structure the model still needs survives.
* test_strip_removes_min_bounds — the min* siblings.
* test_strip_preserves_property_named_max_length — a property *named*
maxLength is data, not a schema keyword; mirrors the existing `pattern` case.
All three regression tests fail against the pre-fix code and pass after.
tests/agent/test_error_classifier.py + tests/tools/test_schema_sanitizer.py:
250 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
978753e to
976db08
Compare
Problem
With a local LM Studio / llama.cpp provider as the primary model, every agent
turn that loaded certain tool schemas failed at request time with
and, after 3 retries, silently fell through to the cloud fallback model. The
network and server were fine — LM Studio was rejecting the grammar llama.cpp
builds from the tool schemas.
Root cause
An MCP tool schema (in our case apple-notes'
batch-delete-notes/batch-move-notes) carried"maxLength": 2000on array-item strings.llama.cpp expands that into a GBNF rule repeating a char class up to 2000×
inside array repetition; the generated grammar is too large/malformed and the
whole request 400s. (
maxItemsalone and top-levelmaxLengthare fine — it'slarge
maxLengthnested in arrayitems.)Hermes already had a reactive recovery for llama.cpp grammar rejections, but it
never helped here, for two independent reasons:
APIErrorwithstatus_code=None(notBadRequestError/400), so the classifier'sstatus_code == 400guard never matched and the error wasn't recognized asa grammar failure at all.
pattern/format, and these schemas have neither, so 0 keywords werestripped and it fell through to fallback.
Changes
agent/error_classifier.py— recognize the grammar error whenstatus_codeis400orNone(fixes the streaming path). The phrasesare llama.cpp-specific and safe to match without a status code.
tools/schema_sanitizer.py— broaden the reactive strip to also removemaxLength/minLength/maxItems/minItems, not justpattern/format.agent/conversation_loop.py— recovery log wording.Cloud providers still receive the full schema hints; the strip runs only after
a llama.cpp backend actually rejects the request.
Verification
End-to-end against a live LM Studio server with the real apple-notes tool set,
both transports:
BadRequestError400 → classifiedAPIErrorsc=None → now classified🤖 Generated with Claude Code