[Backend] Define the LLM provider contract with declared context window and schema output #351
Closed
opened 2026-08-25 20:39:14 +00:00 by claude-bot
·
3 comments
No Branch/Tag specified
main
feat/1328-fill-it-in
feat/1328-fill-frontend
feat/1328-fill-backend
feat/9-foundry-push
fix/9-foundry-live
feat/9-foundry-push-frontend
renovate/celery-5.x
feat/5-foundry-connect
feat/9-foundry-push-backend
feat/9-foundry-push-module
feat/5-foundry-frontend
feat/5-foundry-backend
feat/5-foundry-module
release/v4.8.0-rc1
docs/d341-convert-draft
feat/958-update-to-version
feat/958-convert-sheet
feat/958-pathbuilder-import-frontend
fix/1360-heading-order
feat/958-pathbuilder-import-backend
feat/11-weekly-digest
feat/1341-auto-update
docs/d332-superseded
chore/1345-names-cleanup
feat/1342-mentioned-pages
feat/1328-recipes
fix/1346-pgdg-key
perf/1247-shared-recording-stream
chore/132-gitleaks-allowlist
release/v4.7.1
spike/foundry-module
release/v4.7.1-rc1
docs/decisions-d332-d337
fix/bot-certifi
chore/release-4.7.0
chore/release-4.7.0-rc4
chore/deps-4.7.0
chore/release-4.7.0-rc3
fix/1280-quote-must-say-it
fix/1257-names-row-squeeze
chore/release-4.7.0-rc2
fix/1245-pg-upgrade-count
chore/release-4.7.0-rc1
feat/1257-names-in-prep
feat/1257-be
feat/1257-fe
feat/1257-rating-fixes
design/1328-fill-it-in
feat/1257-name-splits
fix/1326-redis-503
feat/1280-fact-suggestions
feat/1280-be
feat/1280-fe
design/1280-fact-suggestions
feat/1279-infobox
feat/1279-be
feat/1279-fe
feat/1321-shelf-copies
design/1279-infobox
feat/1243-table-find
feat/1257-who-rated
feat/1243-prep-drawer
feat/1243-sheet-pickers
design/1243-browse-in-place
feat/1315-hotwords-default
feat/1242-system-filters
feat/1257-more-name-styles
feat/506-leg3-hotwords
design/1242-system-filters
fix/1302-packs-polish
feat/1241-rules-library
design/1241-rules-library
feat/1245-postgres-18
integ/4.6.2
fix/1263-map-ui-2
fix/1263-map-server
fix/1263-map-ui-1
fix/1261-editor-link-menu
fix/1229-axe-4.14-name-mismatch
renovate/axllent-mailpit-1.x
renovate/eslint-monorepo
renovate/authlib-1.x
chore/release-4.6.0
chore/release-4.6.0-rc2
fix/460-rc2-screens
fix/460-rc2-server
fix/release-prerelease-exit
chore/release-4.6.0-rc1
feat/125-campaign-templates
feat/126-calendar
feat/1181-pictures-and-files
feat/1190-event-people
integ/4.6.0
feat/1191-line-of-the-night
feat/1200-starter-packs
chore/bot-multidict-and-playwright-group
fix/1202-this-week-answered-count
fix/122-124-leftovers
fix/1178-get-routes-that-write
fix/1203-journeys-check-names
design/125-campaign-templates
feat/126-calendar-engine
design/126-in-world-calendar
fix/1189-cited-stamp-in-range
fix/1189-v4-looser-check
fix/1189-richer-beats
fix/1113-export-bind-lists
fix/1154-release-notes-by-file
chore/release-4.5.0
chore/release-4.5.0-rc12
fix/1135-wiki-markup-as-text
fix/demo-usage-cost-in-window
chore/release-4.5.0-rc11
fix/1121-sheet-consistency
fix/1121-sheet-5e-terms-and-card-hp
copy/1122-busiest-pages
fix/1121-relevant-choices
fix/1121-wiki-create-npc-save-sheet-links
fix/1029-menu-stays-on-screen
docs/prototype-transcript-bin-wording
fix/1023-editor-save-row-back-scroll
fix/1020-search-exact-first
fix/1020-table-prep
fix/1020-shell-wordmark-footer-bell
fix/1020-wiki-article-party
fix/872-admin-kept-until-phone-tables
feat/l13-milestone-level-waiting
fix/1000-1001-bell-older-setup-context
fix/999-pronouns-in-prompts
fix/l19-other-class-feats
fix/1010-transcript-window-ordinary-confirm
docs/d254-d269-decisions
fix/702-member-search
fix/680-transcript-bin
fix/993-unidentified-prompts
fix/841-redis-timeouts
fix/990-binned-not-exported
fix/830-export-prep-shelf
fix/deps-audit-2026-10-01
fix/877-title-reads
fix/798-audio-received-at
build/lane11-17-j7
build/lane11-16-levelup
build/lane11-14-seams
build/lane11-14-seed
build/lane11-15-start
build/lane11-13-old-block-out
fix/980-review-r3
build/lane11-12-block
build/lane11-8-packs
fix/demo-seed-canonical-names
build/lane11-11-hot-half
docs/l11-decisions
build/lane11-7-convert
build/lane11-10-editing
fix/npm-audit-brace-expansion
build/lane11-9-sheet-page
fix/bot-pyjwt
build/lane11-6-content
fix/lane11-rank-names
build/lane11-2b-language
build/lane11-4-js-evaluator
build/lane11-3-server
build/lane11-5a-pf2e
build/lane11-5b-5e
feat/design-admin
build/lane11-2-evaluator
build/lane11-1-sheet-pieces
build/lane11-0-drawing
fix/943-quest-objectives-fk
fix/prepsheet-shelf-flake
fix/945-leftovers
build/demo-gm-copy
fix/901-email-landings
build/lane10-8-journeys
build/demo-seed-lanes-8-10
fix/887-gm-own-confirmation
build/lane9-5-j1-admin
fix/918-audience-click
fix/useasync-overtaken
ci/939-critical-path
test/937-backend-suite-speed
chore/claude-orchestration
fix/924-xp-gm-only
fix/session-stale-poll
docs/decisions-d239-d242
fix/930-pypdf
build/lane8-journeys
build/925-quest-editor
fix/894-private-page-pictures
build/lane8-p4-drafts
build/lane8-p7-map
build/lane8-p6-handouts-rules
fix/wiki-fixture-names
build/lane10-d227-test-usage
fix/rsvp-horizon-test-tuesday
fix/media-volume-owner
build/lane8-p3-editor
fix/912-unidentified-rename
build/lane8-p5-party-quests
build/lane8-handout-takeback
build/lane10-pickers
build/lane8-p2-article
build/lane8-6-map
build/lane10-7b-email-pages
build/lane8-5-handouts
build/lane10-7a-test-the-bot-pages
build/lane8-p1-pages
build/lane8-4-quests
build/lane8-2b-feeds
build/lane8-3-parties
build/lane8-2a-links
build/lane10-5-commands
build/lane9-6-seed
build/lane10-4b-reactions
build/lane9-4d-pages
build/lane9-4c-pages
build/lane10-4a-posts
build/lane10-3-email
fix/891-web-fonts
build/lane9-4b-pages
build/lane10-6-test-the-bot
build/lane10-2-notify-out
build/lane8-1-wiki-text
build/lane9-4a-frame
design/lane10-drawings
fix/j6-budget
fix/j5-phone-flake
fix/869-test-leaks
fix/843-record-start-followups
build/lane7-all
build/lane6-all
build/lane9-b2-server
chore/changelog-fragments
docs/decisions-d218
build/lane9-a-owner-overview
fix/rule-card-level-label
fix/834-retention-semantics
fix/o3c-record-without-announcement
docs/decisions-d179
docs/decisions-d131-d170
fix/851-commit-before-response
build/lane6-j3-seed
build/lane9-b1-d67-identity
fix/841-blocking-loop
build/lane9-c-backups-audit
fix/vote-race-and-start-commit
fix/recording-announcement
fix/proposal-read-leak
fix/last-admin-guard
build/lane6-p4c-planning
build/lane6-p4b-margin
build/lane6-p4a-sheet
build/lane5-j4-seed
build/lane6-base
build/lane7-pages-talk
build/lane5-pages
build/lane7-pages-safety
build/lane7-p3-safety
build/lane7-p2-notify
build/lane6-p3-planning
build/lane6-p2-history
build/lane6-p1b-items
build/lane7-p1-threads
build/lane5-p6-faces
build/lane6-p1a-sheet
build/lane5-p5-find
build/lane5-p4a-board
build/lane5-p4b-scenes
build/lane5-p3-live
build/lane5-base
build/lane5-p2-cards
docs/lane5-decisions
build/review-r2-wide
fix/review-r1
build/lane5-p1-scenes
build/demo-seed
build/lane4-journeys-j5
build/lane4-page-review-b
build/lane4-page-review-a
build/lane4-page-read
build/lane4-journeys
build/lane4-page-sess2
build/lane4-page-sess1
build/lane4-notes
build/lane4-discord-on-approval
build/lane4-scheduling
build/lane4-versions
build/760-session-number
build/lane4-decisions
build/797-erase-no-regenerate
build/test-race-audit
build/lane3-completions
build/794-dark-only
build/796-titles-scroll
build/791-retention-anchor
build/a11y-sweep
build/shell-followups
build/retention-preview
build/753-backup-readback
docs/d91-d93
build/lane3-j1
build/ci-harness-paths-selftest
fix/useauth-test-race
build/ci-harness-paths
build/777-ai-settings-read
build/lane3-settings-c
build/lane3-settings-b
build/lane3-j6
build/lane3-sessionform
build/lane3-settings-a
build/lane3-members
build/lane3-thisweek
build/lane3-story
chore/87-align-python-3.12
v4.8.0-rc1
v4.7.1
v4.7.1-rc1
v4.7.0
v4.7.0-rc4
v4.7.0-rc3
v4.7.0-rc2
v4.7.0-rc1
v4.6.2
v4.6.2-rc4
v4.6.2-rc3
v4.6.2-rc2
v4.6.2-rc1
v4.6.1
v4.6.1-rc1
v4.6.0
v4.6.0-rc2
v4.6.0-rc1
v4.5.3
v4.5.3-rc1
v4.5.2
v4.5.2-rc1
v4.5.1
v4.5.1-rc2
v4.5.1-rc1
v4.5.0
v4.5.0-rc12
v4.5.0-rc11
v4.5.0-rc10
v4.5.0-rc9
v4.5.0-rc8
v4.5.0-rc7
v4.5.0-rc6
v4.5.0-rc5
v4.5.0-rc4
v4.5.0-rc3
v4.5.0-rc2
v4.5.0-rc1
v4.4.1
v4.4.0
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.2
v4.1.1
v4.1.0
v4.0.1
v4.0.0
v3.11.4
v3.11.3
v3.11.2
v3.11.1
v3.11.0
v3.10.0
v3.9.1
v3.9.0
v3.8.0
v3.7.0
v3.6.1
v3.6.0
v3.5.0
v3.4.1
v3.4.0
v3.3.0
v3.2.0
v3.1.0
v3.0.0
v2.1.0
v2.0.0
v1.1.0
v1.0.0
v0.12.0
v0.11.0
v0.10.0
v0.9.1
v0.3.3
v0.9.0
v0.8.0
v0.3.2
v0.3.1
v0.3.0
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
backend
Issue related to the backend
bot
Issue related to the bot
bug
Something is not working
duplicate
This issue or pull request already exists
enhancement
New feature
frontend
Issue related to the frontend
help wanted
Need some help
invalid
Something is wrong
question
More information is needed
recording
Issue related to the recording system
security
Security hardening / vulnerability work
wontfix
This won't be fixed
No labels
backend
bot
bug
duplicate
enhancement
frontend
help wanted
invalid
question
recording
security
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Quest-Board#351
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the August 2026 session lifecycle review (#319).
Why
Two of this project's most damaging pipeline bugs come from the absence of a declared context window: prompts are sent without knowing the budget, and truncation is never detected. The abstraction has to make the window a first-class, queryable property rather than a thing each call site assumes.
Proposed contract
json_schemaconstraintSchema-constrained decoding is the important capability. On llama.cpp it maps to GBNF grammar via
json_schema(see #281); on Anthropic and OpenAI to structured outputs.response_format: {"type":"json_object"}alone is advisory and demonstrably not enforced on the current deployment, which is why the tolerant parser exists. Providers that cannot enforce a schema must declare that, so the pipeline can fall back to the tolerant parser and mark the result as degraded rather than silently trusting it.Acceptance criteria
Picking this up alongside #350 on
feat/350-351-provider-contracts.Grounding: most of this already exists, unowned
The v4.0.0 context-budget work built much of what this issue asks for, and two comments in
llm_service.pyexplicitly name #351 as the issue that finishes them:LLMConfig.context_tokens(settings_service.py:517),resolve_window,DEFAULT_CONTEXT_TOKENS = 32_768prompt_budget_tokens/fits_in_context,PROMPT_BUDGET_FRACTION = 0.7_apply_json_schemahandles four provider spellings, including llama.cpp's both-forms workaround (#281)_warn_if_prompt_truncatedreads them, but only warns; nothing returns them to a callerSo the substance of this issue is narrower and sharper than its body suggests: the machinery is there, but nothing declares anything. The gap is a provider object to hang it on.
The actual defect: provider identity is guessed from the URL
generate_structured_text(llm_service.py:406) dispatches on URL substrings:This is the root of the "no commitment to any single provider" problem. A self-hoster behind a reverse proxy on their own domain is classified as llama.cpp regardless of what is actually behind it; an OpenAI-compatible gateway on a custom host is too. The four
_qa_*functions inaudio_service.pyrepeat the same sniffing independently — which is a large part of why #426 exists as a separate bug.Replacing the sniff with an explicit, configured provider selection is the load-bearing change here. Everything else in this issue hangs off having a provider object at all.
Two decisions already settled, recorded so they are not reopened
_reject_if_truncated(llm_service.py:240-280). The capability object does not get to re-decide this.preflight_promptstays a warning, not a raise — a departure from #336 recorded in its docstring, becauseestimate_tokensis deliberately pessimistic and a raise would refuse prompts that would have worked. This issue is the one that can change that: a provider declaring a real tokenizer and whether it front-truncates is what turns both soft checks hard. I am carrying the declaration but not flipping either check to a raise in this issue — that wants the conformance suite (#359) proving the tokenizer first.Landed in PR #483 (merged, CI green on all 7 jobs). Leaving this open — three of five criteria are fully met, two are partial and both partials are deliberate.
Criteria
LlmCapabilities+LlmProviderinapp/providers/llm.py. The provider declares and spells;llm_servicekeeps the transports, soapp.providersimports nothing fromapp.servicesand a provider builds in a test with no DB, settings or event loop.resolve_llm_provider. An explicitcontext_tokensstill wins; only an undeclared one now takes the provider's own window, which for Anthropic and OpenAI is the model's documented figure rather than the universal conservative 32k. Ollama and llama.cpp declare that same 32k, so the local paths are unchanged.SchemaEnforcementis a three-state property and all four spellings are pinned by tests to the proven_apply_json_schema(#281). But nothing yet consumes the declaration to mark a result degraded.SummarisationRun.schema_degradedexists and is set when the tolerant parser repairs, which catches the symptom rather than the declared capability. Closing this properly means a provider declaringADVISORY/NONEmarking the run degraded up front — that belongs with #358, which is what renders it.prompt_cachingandbatch_modeare declared, andprompt_cachingis already load-bearing (see below). There is no hint API to pass yet, so "no-op cleanly" is currently true only vacuously. Batch mode wants #357's cost work to have a reason to exist.The defect this issue exposed
Mapping the four providers' usage fields to write
read_usageshowed that_warn_if_prompt_truncatedhad exactly one caller — the Ollama path. llama.cpp, Anthropic and OpenAI all parsedusagefor completion tokens and never looked at the prompt side, so a front-truncated prompt on three of four providers produced no signal at all. That is the input-side half of the failure #331/#336 exist to prevent, live on the majority of deployments._check_usagenow runs both checks in one place, so a new transport cannot forget one.prompt_cachingis what makes the check honest rather than noisy: Anthropic and OpenAI report only newly evaluated tokens, so "truncated" and "cache hit" are indistinguishable and the check is skipped; Ollama and llama.cpp neither cache nor reject an over-long prompt, so there a low count has one meaning. This is exactly the declaration_warn_if_prompt_truncated's docstring had been asking for since #331, and the docstring is updated to say so.Provider identity is no longer guessed
URL-substring dispatch is replaced by
app/providers/registry.py, with a validated select in Admin → Bot Settings. The old heuristic survives as a named, logged fallback so nothing changes on upgrade, and the log line names the field to set. Blank preserves; anautosentinel clears — without it a selection could be set but never undone, since blank already means preserve everywhere on that endpoint.Recorded so it is not reopened
preflight_promptis still a warning, not a raise. This issue makes the hard check possible — a declaredfront_truncates, and eventually a declared tokenizer — but flipping it should wait for #359 proving the tokenizer against a real endpoint.estimate_tokensis deliberately pessimistic, so raising today would refuse prompts that would have worked.The threading is a known weak point, not an oversight.
llm_providertravels alongsidecontext_tokensthrough 19 signatures, 25 pass-throughs and 14 origin sites, which is the patternDEFAULT_CONTEXT_TOKENS' comment warns about. It holds only because omitting it falls back to the sniff. Filed as #484 (v4.6.0): stop destructuringLLMConfiginto loose parameters, which kills both threading problems permanently. It gets cheaper the sooner it is done.Closing with v4.2.0. The two criteria left partial on 1 September were each finished by the issue they were deferred to:
_stamp_provider_limitsinaudio_service.pyreads the resolved provider'sSchemaEnforcementbefore the run and setsschema_degradedfrom the declaration, not from the tolerant parser tripping.capability_servicealso turns a non-ENFORCEDdeclaration into theGAP_SCHEMA_NOT_ENFORCEDgap (severity high) on the Admin capability panel and on the run record, so the summary is marked as produced under a limited configuration up front.prompt_cachingis load-bearing and no-ops where absent: on a caching provider_check_usageskips the truncation check outright; on the others it runs.batch_modeis declared, reported by/health-driven discovery and shown on the capability panel. There is deliberately no batch hint API: no call site batches (the pipeline is one session at a time and the usage work in #357 gave no reason to change that), so an API with nothing to no-op would be speculative surface. When a batching consumer appears, add the hint then.Still recorded so it is not reopened:
preflight_promptremains a warning, not a raise. The conformance suite (#359) now proves the endpoint contract live, but not the tokenizer; flipping to a raise still wants that proof first.