Skip to content

Custom openai-compatible providers get limits from the pattern table, not the synced catalog — hyphenated ids (e.g. kenari/glm-5-3-flash) resolve to 200K/128K instead of 1M/131072 #5421

Open

Description

@wildnfth

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, master source checked out for reading open-sse/providers/*
  • Provider: custom node, type: openai-compatible, name Kenari.id
  • Upstream: kenari (present in models.dev with doc: 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) iterates Object.entries(catalog) — every provider models.dev publishes, so a custom provider id like kenari gets models["kenari:glm-5-3-flash"].
  • providers (limits) iterates entries from collectEntries(), which walks open-sse/providers/registry/index.js. A custom openai-compatible node 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 Kenari

That 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:

  1. Include custom providers in the limits half of the sync. Let collectEntries() also emit the models a custom openai-compatible node advertises, so providers[providerId][model] gets written the same way modalities already do. This fixes the whole class, including ids nobody has reported.
  2. Normalize the lookup key before the exact/pattern steps — e.g. also try a -→. variant of baseModel in getCapabilitiesForModel, or make matchPattern case-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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions