Repository navigation
fix(models): read a discovered model's real capacity from the probe instead of hardcoding 131072 - #7817
Merged
Conversation
`merge_discovered_models` is the only writer for gateway- and locally-discovered catalog entries, and it hardcoded `context_window: 131_072` / `max_output_tokens: 16_384` because nothing upstream could supply either — `parse_openai_models` threw away every capacity field the listing carried and returned an empty `model_info`. Fix it at the probe layer. `DiscoveredModelInfo` gains optional `context_window` / `max_output_tokens`, and the OpenAI-compatible parser now reads them through the shared key-priority parsers in `model_metadata` (vLLM `max_model_len`, LM Studio and llama.cpp `context_length`, LiteLLM `max_input_tokens` / `max_output_tokens`, OpenRouter `context_length` and `top_provider.max_completion_tokens`). When the endpoint reports nothing the entry no longer acquires a number. Both limits stay at the catalog's documented `0` (unknown) encoding and `ModelCatalogEntry` records provenance in a new `limits_known` flag, so the existing `> 0` guards in `resolve_context_window` and the agent loop's `ContextBudget` fall through to the conservative `UNKNOWN_MODEL_CONTEXT_WINDOW` and log the warning that names the `agent.toml` field an operator can set. Refs #7780
houko
enabled auto-merge (squash)
August 23, 2026 23:57
DaBlitzStein
added a commit
to DaBlitzStein/librefang
that referenced
this pull request
Aug 24, 2026
…7819, librefang#7817, librefang#7820) Brings in four upstream commits and reconciles them with the fork's own work in the same files. Every conflict keeps both sides' intent; nothing from either side was dropped. Conflicts resolved: - crates/librefang-api/dashboard/src/components/ui/SliderInput.tsx — both sides fixed the same dimming bug from opposite directions. Upstream's bounds normalization (`lowerBound` / `upperBound` / `clamp` / `boundedValue` / `emitValue`) now feeds the fork's index-addressed `steps` mode, so the stepped context-window slider inherits inverted-bound and non-finite-value handling it never had. The fork's position-accurate tick legend replaces upstream's `flex justify-between`, which spaces labels evenly regardless of value; upstream's `${tick}-${index}` key is adopted so a repeated tick value still renders both labels. The fork's toggle geometry (w-9 track, p-0.5 inset, w-4 knob) is kept over upstream's w-8/h-[18px] because it contains the knob by construction. - crates/librefang-api/dashboard/src/components/ui/SliderInput.test.tsx — added by both. Union of the two suites: the fork's toggle-geometry, tick-position and fixed-steps cases, plus upstream's inverted-bounds, clamping, non-finite-input and inherited-state cases. 25 tests pass. - crates/librefang-api/src/routes/agent_templates.rs — upstream added `promotion_preview` to `get_agent_template`; the fork had renamed that handler to `get_agent_type` behind the canonical `/api/agent-types` routes with `/templates` kept as a deprecated alias. `promotion_preview` is wired into the canonical handler, so both path spellings serve it. The response keeps the fork's full serialized manifest rather than upstream's hand-picked subset — a superset of what upstream's tests read. - crates/librefang-kernel/src/kernel/mod.rs — both re-exports kept (`PendingSkillMcpDeclarations`, `SemanticMemoryAccess`, `SkillReloadOutcome`). - crates/librefang-kernel/src/kernel/tools_and_skills.rs — the fork's unconditional `workflow_create` injection and upstream's semantic-memory capability gate are independent blocks; both run, injection first. - openapi.json — regenerated from source rather than hand-merged. - xtask/baselines/openapi.sha256 — regenerated from the regenerated spec. Integration fixes the merge itself required: - crates/librefang-types/src/manifest_privacy.rs — upstream's `reduce_model` exhaustively destructures `ModelConfig`, which the fork has extended with `mode` and `router_override`. Both survive promotion: they select how a type picks a model, which is what the type does, and neither carries a host path, credential binding or private endpoint. Stripping them would republish a flexible-routing type as a fixed one — a behaviour change, not a redaction. - crates/librefang-api/src/custom_gateway_catalog.rs — upstream's new `ModelCatalogEntry::limits_known` reached the fork's gateway discovery path through `..Default::default()`, which is `true`. That path legitimately produces `context_window: 0` when a gateway publishes no capacity, and `limits_known: true` there records the gateway's silence as a discovered ceiling. The flag is now derived alongside the numbers, the way `pricing_known` already was: true only when `/model/info` reported a value or a prior entry carried a real one forward. Two tests pin both directions. Verification: - cargo check -p librefang-types -p librefang-memory -p librefang-kernel -p librefang-api --lib — clean. - cargo check -p librefang-cli --bins — clean. - cargo test -p librefang-types --lib manifest_privacy — 34 passed. - cargo test -p librefang-kernel --lib tools_and_skills — 13 passed. - cargo test -p librefang-kernel --lib semantic_memory — 6 passed. - cargo test -p librefang-api --test profiles_templates_routes_integration — 15 passed, 2 failed. Both failures are pre-existing on this branch and unrelated to the merge: `templates_list_carries_provider_and_model_from_the_manifest` and `templates_list_skips_a_malformed_manifest_instead_of_failing` seed `workspaces/agents/`, which this branch's `list_agent_types` deliberately stopped listing. Fixed in the following commit. - dashboard: vitest SliderInput 25 passed, tsc --noEmit clean. Locale keys: upstream touched no `.ftl` file in these four commits; key counts are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7780.
What was wrong
merge_discovered_models(crates/librefang-runtime/src/model_catalog.rs) is the only writer for gateway- and locally-discovered catalog entries, and it hardcoded two literals:The issue reads that as "no upstream can supply a real value", and that half is true —
DiscoveredModelInfohad no field for either one. But the reason it had no field is one layer further down:parse_openai_models(crates/librefang-runtime/src/provider_health.rs) returnedmodel_info: vec![]unconditionally, so every capacity field an OpenAI-compatible listing did carry was parsed away and discarded. Fixing the merge site alone would have papered over a probe that was throwing the answer on the floor.Why a wrong context window is not cosmetic
The fabricated value is indistinguishable from a measured one at every consumer, and it lands directly in the turn's token budget:
manifest_helpers::resolve_context_window(crates/librefang-kernel/src/kernel/manifest_helpers.rs:117) takes the catalog entry'scontext_windowwith.filter(|w| *w > 0).131_072 > 0, so the guess is accepted as arm 2 of the precedence chain.agent_loop::mod.rs:878turns that intoContextBudget::new(ctx_window). Compaction fires at 80% of the window.So an 8K model behind a LiteLLM gateway got a 131K budget: compaction never fires, the daemon packs a prompt to ~104K tokens, and the provider rejects the request after the input tokens have been billed. That is precisely the failure the fallback comment three lines above already warns about ("a 200K assumption silently bills the user for prompts the provider then rejects with HTTP 400 (#3349)") — the discovered-entry literal reintroduced it by a different route.
The opposite direction is equally silent: a 1M-context model reached through a gateway is clamped to 131K, and
ContextCompressorsummarises away prompt content nobody asked to drop.Neither failure is visible from either end, which is what makes this an
area/securitydefect rather than a display bug: the operator reading131072in the Models page has every reason to believe LibreFang learned it from the gateway.What changed
Probe layer — read what the endpoint reports.
DiscoveredModelInfogainscontext_window: Option<u64>/max_output_tokens: Option<u64>, andparse_openai_modelspopulates them through the key-priority parsers inmodel_metadata, which already encoded the per-server key zoo for the single-model endpoint and are nowpub(crate)and shared: vLLMmax_model_len, LM Studio / llama.cppcontext_length, some proxies'context_window, LiteLLMmax_input_tokens/max_output_tokens, OpenRoutercontext_lengthandtop_provider.max_completion_tokens. A second parser,parse_openai_model_max_output, handles the output ceiling with a key list deliberately disjoint from the context-window list —max_tokensis already claimed there as a last-ditch reading of the full window, and counting it as both would silently assert an output ceiling equal to the whole context.parse_ollama_tagsreportsNonefor both:/api/tagsgenuinely carries no token capacity (it lives in/api/show'smodel_info, which costs one extra request per model). Guessing from the family name would be the same defect wearing a different hat.Merge site — never invent.
An entry now gets numbers only when the endpoint supplied them. When it did not, both fields stay at the catalog's documented
0("unknown") encoding, and every existing> 0guard falls through to the conservativeUNKNOWN_MODEL_CONTEXT_WINDOW(8192) whose warning already namesagent.toml: model.context_windowas the operator's fix. A conservative window plus a loud warning is recoverable; an invented one is not observable from either side.Capacity also follows the same never-downgrade rule the capability flags already follow: a later probe fills in a limit that is still unknown, and a probe that stops reporting one (an older proxy in front of an upgraded gateway) cannot erase what an earlier probe learned.
limits_knownonModelCatalogEntry— adopting #7786's shape deliberately.0alone is ambiguous, and the repo's own doc comment says so: it means "unknown / not applicable". An image or audio entry legitimately has no token context. A discovered model with no reported capacity does have one and we do not know it. Those two behave differently for any consumer that wants to render a badge or clamp a request, and they were indistinguishable once written into the entry.limits_knownrecords provenance:truewhen a registry entry, an operator override, or the endpoint supplied the numbers;falsewhen nothing did. It mirrors the existingpricing_knownfield exactly — same#[serde(default = "default_true")], same "older entries predate this and carry real numbers" reasoning — so it is a convention the catalog already has rather than a new one.It is threaded through
model_metadata::synthesize_entryas well, because that pipeline's L5 substring table and its two provider-shaped defaults are guesses this crate invented, and leaving them marked as sourced would contradict the field's own contract.MetadataSourcealready drew that line; the flag puts it on the entry so a caller holding only the entry can see it too.The three model routes emit
limits_knownnext topricing_known, which is what makes the fix assertable end to end.Drive-by, forced by the new fields. The bare
DiscoveredModelInfo { name, None, None, … }literal was triplicated acrosslibrefang-api(twice) andlibrefang-kernel(once) purely becauseparse_openai_modelsdiscarded its info. All three now call oneDiscoveredModelInfo::bare(name)constructor, so a future field cannot be quietly given a fabricated value at one of several sites. Net deletion.Collision with #7786 — and which side should rebase
Stated explicitly because it is a hard same-hunk conflict, not a merge hiccup. PR #7786 (
feat/modelparams-tristate, @DaBlitzStein) insertslimits_known: falseplus a matching field into the sameModelCatalogEntryhunk incrates/librefang-types/src/model_catalog.rs, and also touchescrates/librefang-runtime/src/model_catalog.rs,crates/librefang-runtime/src/model_metadata.rsandcrates/librefang-testing/src/mock_kernel.rs— every one of them a file this PR changes.I did not touch that branch. The honest engineering answer is that the tri-state is the right model, so this PR adopts that shape rather than fighting it:
limits_known: boolwithdefault = "default_true", matchingpricing_known. The two designs now agree on the field name and its semantics, which turns a design conflict into a textual one.#7786 should rebase onto this, for one reason and one reason only: #7786 is already
CONFLICTINGagainstmainindependently of this PR, so it has to be rebased regardless, and after that rebaselimits_knownalready exists and its insertion of the field simply drops out of the diff. Rebasing this PR onto #7786 instead would block a one-crate correctness fix behind a 44-file feature branch that is not landing imminently. If the maintainer prefers the other order, this branch is small and I will rebase it.Interaction with #7816 (fix/7775-openai-compatible-discovery)
Flagging rather than acting, since it is not my branch. PR #7816 is
MERGEABLEand adds a fourth copy of the bareDiscoveredModelInfoliteral incrates/librefang-api/src/routes/providers.rs(in the newmerge_probe_into_catalog). That copy will not compile against the two new fields, and it conflicts textually with the two copies this PR replaced withDiscoveredModelInfo::bare. Whichever lands second needs the one-line change to::bare(name).Worth noting for the reviewer: #7816's changelog says "being in the catalog is also what stops the runtime assuming an 8k context window for a model that handles far more". After this PR, a gateway model whose endpoint reports no capacity does get the conservative 8K — because that is the honest answer, and the issue asks for exactly that ("the compaction/budget math can treat it conservatively"). The two PRs are complementary rather than opposed: #7816 makes the model reachable, this one makes sure the number attached to it came from somewhere. The operator-facing remedy for the still-unknown case is
agent.toml: model.context_window, which the fallback warning already names, and #7774 is the editing surface for it.Verification
Commands and their actual output are in the collapsed section below. Summary:
cargo check -p librefang-api -p librefang-cli -p librefang-kernel-metering -p librefang-testing --lib— clean.cargo test -p librefang-runtime --lib -- model_catalog::tests::test_merge_ provider_health::tests:: model_metadata::tests::— 74 passed, 0 failed.cargo test -p librefang-api --test providers_routes_test -- list_models— 4 passed, 0 failed, including the new integration test.New tests:
provider_health::tests::test_parse_openai_models_reads_reported_capacity— one listing body covering the vLLM, LiteLLM and OpenRouter key shapes plus a silent entry; asserts each capacity reachesDiscoveredModelInfoand the silent one staysNone.provider_health::tests::test_parse_openai_models_rejects_zero_capacity— a server reporting0is not recorded as having answered.model_catalog::tests::test_merge_records_capacity_reported_by_the_gateway— the reported-capacity requirement: gateway says 8192, entry carries 8192,limits_knownis true.model_catalog::tests::test_merge_does_not_invent_capacity_when_the_gateway_reports_none— the unknown-capacity requirement: asserts!= 131_072explicitly as well as== 0, so the literal cannot come back by any route, andlimits_knownis false.model_catalog::tests::test_merge_accepts_a_partially_reported_capacity— a gateway reporting only the window.model_catalog::tests::test_merge_upgrades_unknown_capacity_but_never_erases_a_known_one— three probes: silent, reporting, silent again; the measured window survives.providers_routes_test::list_models_serves_probed_capacity_and_never_the_hardcoded_literal— the same two cases end to end throughGET /api/modelsagainst the real router, asserting the JSON body.Not run locally, and why:
cargo clippyand any wider build. This machine's sharedCARGO_TARGET_DIRis used concurrently by ~28 other worktrees; free disk fell from 45 GiB to 2.4 GiB during this session and I stopped building at that point rather than take the machine down. Clippy is left to CI. One clippy hazard was fixed by reading rather than running: the capacity back-fill in the in-place upgrade branch is written as two flatif field == 0branches instead of a nestedif let, since the enclosing block would otherwise contain only the innerifand tripcollapsible_if.A note on local test runs in this repo, since it cost me four wasted invocations: with a shared
CARGO_TARGET_DIRacross worktrees,cargo testreportedFresh librefang-runtimeand then ran a test binary containing another worktree's tests (#7774's, by name), silently omitting mine while reportingok.touching the edited sources before the run forces a real rebuild. Every result quoted above was taken after such a touch, and I verified by name that each new test actually appears in the run output.Verification output
Deliberately out of scope
ModelsPagechange; none are needed.formatCtxinModelsPage.tsx:41starts withif (!tokens) return "—", so an unknown capacity already renders as an em dash rather than as0or as a fabricated131K. No new user-facing string exists, so no locale key does either. Thelimits_knownflag is on the wire for whoever wants to render a badge — fix(models): agent inference settings win over the per-model override #7786's UI work, most likely.Option<u64>typing forapi.ts. Left for the PR that actually consumes the flag in the UI, rather than adding an unread field to the TS model type here.ModelOverridesis a separate map on the catalog and is untouched bymerge_discovered_models, so an operator-set window already survives a re-probe.parse_openrouter_model_entries(model_catalog.rs:2105) does the same.unwrap_or(131_072)for OpenRouter, andmodel_metadata's L5 substring table is a family-name guess. Both are now marked honestly vialimits_known(L5 and the provider defaults arefalse), but their numbers are unchanged: zeroing them would change behaviour in paths this issue does not cover, and OpenRouter's is a curated live catalog rather than an operator's private gateway. Worth a separate issue if the maintainer wants the clamp bug(models): a discovered model gets a hardcoded 131072-token context window that nothing can source or correct #7780's comment thread contemplates, since a clamp is only safe against alimits_known: trueceiling./api/show. Reading the realllama.context_lengthper discovered Ollama model means one extra HTTP request per model on every probe. That is a discovery-cost decision, not a correctness one, and I did not want to make it inside a fix for the "do not invent numbers" bug.