Repository navigation
Description
Summary
A model served by a custom openai-compatible provider that is listed on models.dev still gets its limits from the hand-written pattern table, not from the synced catalog. When that provider uses hyphenated model ids, the specific patterns miss and a broad family pattern wins:
GET /v1/models → kenari/glm-5-3-flash
context_length: 200000 max_completion_tokens: 128000
models.dev → kenari → glm-5-3-flash
limit.context: 1000000 limit.output: 131072
The same weights under the dotted id resolve correctly, so this is purely an id-normalization / catalog-coverage gap:
| provider | model | contextWindow | maxOutput |
|---|---|---|---|
kenari |
glm-5.3-flash |
1000000 | 131072 |
kenari |
glm-5-3-flash |
200000 | 128000 |
Because the endpoint is also what clients read, OpenCode/Cline/etc. compact at 200K and cap max_tokens at 128K for a 1M-context model.
Environment
- 9router
0.5.112, Windows,mastersource checked out for readingopen-sse/providers/* - Provider: custom node,
type: openai-compatible, nameKenari.id - Upstream:
kenari(present in models.dev withdoc: https://kenari.id/docs) - Model:
glm-5-3-flash - Direct model, no combo involved — the combo symptom in GLM-5.3 Flash in Combo shows only a 200k context window #4544 is downstream of this
Repro
Against open-sse/providers/capabilities.js on master (no server needed):
import { getCapabilitiesForModel } from "./open-sse/providers/capabilities.js";
for (const id of ["glm-5.3-flash", "glm-5-3-flash", "glm-5.3", "glm-5-3", "glm-5.2", "glm-5-2"]) {
const c = getCapabilitiesForModel("kenari", id);
console.log(id.padEnd(16), c.contextWindow, c.maxOutput);
}glm-5.3-flash 1000000 131072
glm-5-3-flash 200000 128000 <-- hyphen falls through
glm-5.3 1000000 128000
glm-5-3 200000 128000 <-- hyphen falls through
glm-5.2 1000000 131072
glm-5-2 200000 128000 <-- hyphen falls through
Matching /v1/models on a live server, and deepseek-v4-1-flash resolving correctly on the same provider, confirms the hyphen is the only variable.
Root cause
Two independent gaps, both of which have to be closed for this provider to resolve.
1. The catalog sync writes modalities for every models.dev provider but limits only for registry providers
src/lib/modelCatalog/sync.js builds the two halves from different loops:
models(modalities) iteratesObject.entries(catalog)— every provider models.dev publishes, so a custom provider id likekenarigetsmodels["kenari:glm-5-3-flash"].providers(limits) iteratesentriesfromcollectEntries(), which walksopen-sse/providers/registry/index.js. A customopenai-compatiblenode is not in that registry, so it never contributes an entry.
Observed in ~/.9router/model-catalog.json on a machine with one custom provider:
providers WITH modalities: ~250 providers (incl. kenari)
providers WITH limits : 25 providers (cerebras, github, glm-cn, ..., agnes)
kenari in providers[] : absent
So getCatalogLimits("kenari", "glm-5-3-flash") returns null and the limits fall back to the tables no matter what models.dev says. PROVIDER_ALIASES can't help here either — kenari isn't in it, and the limits loop only runs over registry entries anyway.
2. MODEL_CAPABILITIES only carries the dotted id, and the patterns are dot-based
// open-sse/providers/capabilities.js:150
"glm-5.3-flash": { ..., contextWindow: 1000000, maxOutput: 131072 },
// open-sse/providers/capabilities.js:592, 599
{ pattern: "*glm-5.3*", caps: { ..., contextWindow: 1000000, maxOutput: 128000 } },
{ pattern: "*glm-5*", caps: { ..., contextWindow: 200000, maxOutput: 128000 } },matchPattern escapes * and treats everything else literally, so *glm-5.3* never matches glm-5-3-flash. Step 2 misses, the first matching pattern is *glm-5*, and the result is 200000/128000.
deepseek-v4-1-flash is correct on the same provider only because #4293 added the hyphenated alias by hand:
"deepseek-v4.1-flash": { ..., contextWindow: 1000000, maxOutput: 384000 },
"deepseek-v4-1-flash": { ..., contextWindow: 1000000, maxOutput: 384000 }, // alias for KenariThat is the current workaround pattern, and it only holds for models someone has hit yet.
Scope
Not specific to GLM. Comparing all 93 kenari/* models the server advertises against models.dev's kenari provider (40 exact-id matches after stripping punctuation, 26 with wrong limits):
claude-sonnet-4-6 1000000/128000 1000000/64000 out
gemma-4-31b-it 128000/64000 262144/32768 ctx out
glm-5-2 200000/128000 1000000/131072 ctx out
glm-5-3 200000/128000 1000000/131072 ctx out
glm-5-3-flash 200000/128000 1000000/131072 ctx out
gpt-5-6-luna 400000/128000 1050000/128000 ctx
gpt-5-6-sol 400000/128000 1050000/128000 ctx
grok-4-5 256000/64000 500000/500000 ctx out
grok-4-6 256000/64000 500000/500000 ctx out
minimax-m3 1000000/131072 1048576/512000 ctx out
qwen3-8-flash 262144/64000 1000000/131072 ctx out
step-3-7-flash 128000/64000 256000/256000 ctx out
... 14 more
Several of these overstate (gemma-4-31b-it claims 128K context vs 262144 real; claude-sonnet-4-6 claims 128K output vs 64000 real), which is the dangerous direction: a client sizes its request against a window the backend does not have. Some understate badly (grok-4-5 256K vs 500K; minimax-m3 131K vs 512K output), which wastes most of the window.
qwen3-8-flash is an example of the dotted/hyphen split showing up as the opposite error: its dotted id has no kenari entry in models.dev, so it falls to *qwen* (262144) while models.dev's alibaba:qwen3.8-flash publishes 1000000.
Suggested fix
Either of these closes it, both are small:
- Include custom providers in the limits half of the sync. Let
collectEntries()also emit the models a customopenai-compatiblenode advertises, soproviders[providerId][model]gets written the same way modalities already do. This fixes the whole class, including ids nobody has reported. - Normalize the lookup key before the exact/pattern steps — e.g. also try a
-→.variant ofbaseModelingetCapabilitiesForModel, or makematchPatterncase-insensitive and separator-insensitive. Cheaper, but it will still only apply to limits the tables happen to know.
Doing both is ideal: (2) fixes models nobody catalogued, (1) stops the tables from being the only source for custom providers.
A regression test in the shape of tests/unit/kenari-deepseek-vision-zed-modal.test.js would cover it:
it("resolves catalog limits for a hyphenated id on a custom provider", () => {
const caps = getCapabilitiesForModel("kenari", "glm-5-3-flash");
expect(caps.contextWindow).toBe(1000000);
expect(caps.maxOutput).toBe(131072);
});Workaround
Client-side only: override limit in the client config (OpenCode providers.<id>.models.<model>.limit) and set max_tokens explicitly. The 9router endpoint accepts it — max_tokens: 200000 and 131072 both return 200 on this connection — the metadata is simply understated.
Related
- GLM-5.3 Flash in Combo shows only a 200k context window #4544 — the same wrong number seen through a Combo (combo takes
min(contextWindow)/max(maxOutput)across seats, so the bad per-model value propagates) - deepseek-v4-1-flash capabilities wrong: vision=false, should be true #4293 — the hand-added
deepseek-v4-1-flashhyphen alias, i.e. the workaround this issue generalizes - getCapabilitiesForModel(): exact-match branches never call refine(), so they can't benefit from the models.dev catalog sync #4203 — exact-match branches skip
refine(), which would have let the catalog correctMODEL_CAPABILITIESif the limits half had akenarientry
Activity