fix(providers): read the vLLM context window from max_model_len (#12897)

* fix(providers): read the vLLM context window from max_model_len

normalizeDiscoveredModels resolved the window from inputTokenLimit,
context_length, contextLength and top_provider.context_length. vLLM
reports it as max_model_len and nothing else, so a synced vLLM model
carried no inputTokenLimit and the resolver fell back to the 128K
default - half the window on a 250K deployment.

The native vllm provider and the OpenAI/Anthropic-compatible custom
providers all pass raw records through this function, so one chain entry
covers the three connection shapes.

Closes #12858

* docs(changelog): fragment for #12897

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Nguyen Thanh Dat
2026-09-18 04:37:34 +07:00
committed by GitHub
parent 9688032451
commit ac0a63117e
3 changed files with 75 additions and 0 deletions

View File

@@ -0,0 +1 @@
- **fix(providers):** vLLM connections now advertise the real context window: model discovery reads `max_model_len` instead of falling back to the 128K default ([#12897](https://github.com/diegosouzapw/OmniRoute/pull/12897), closes [#12858](https://github.com/diegosouzapw/OmniRoute/issues/12858))

View File

@@ -426,10 +426,16 @@ export function normalizeDiscoveredModels(
// Keep the total context window distinct from an explicit maximum-input limit. Existing
// providers historically stored context_length as inputTokenLimit, so retain that compatibility
// outside Vertex while persisting the separate contextWindow field for new consumers.
// vLLM — and every server that copies its /v1/models shape — reports the
// window as `max_model_len`, the value the engine was actually started with.
// Without it a vLLM model syncs with no window at all and the resolver hands
// out the 128K default, understating a 250K deployment by half. #12858
const contextWindow = firstPositiveNumber(
record.context_length,
record.contextLength,
record.contextWindow,
record.max_model_len,
record.maxModelLen,
topProvider.context_length
);
const isVertexProvider = providerId === "vertex" || providerId === "vertex-partner";

View File

@@ -0,0 +1,68 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { normalizeDiscoveredModels } from "@/lib/providerModels/modelDiscovery";
// Regression guard for #12858.
//
// vLLM's /v1/models reports the context window as `max_model_len` — the value
// the engine was started with — and nothing else. The native `vllm` provider is
// `passthroughModels: true`, and the OpenAI- and Anthropic-compatible custom
// providers pass the same raw records through, so all three connection shapes
// land here. Before the fix no window field matched, the model was persisted
// with no `inputTokenLimit`, and `/v1/models` advertised the 128K provider
// default for a deployment serving 250K.
//
// Same class of bug as #3202 (OpenRouter's `context_length`): the provider
// declares the window under a name the normalizer does not read.
test("#12858 maps vLLM max_model_len into inputTokenLimit", () => {
const [model] = normalizeDiscoveredModels([
{ id: "qwen3.8", owned_by: "vllm", root: "/models/Qwen3.8-27B-FP8", max_model_len: 250000 },
]);
assert.equal(model.id, "qwen3.8");
assert.equal(model.inputTokenLimit, 250000);
});
test("#12858 accepts the camelCase spelling too", () => {
// `contextLength` is already accepted next to `context_length`; keep the two
// spellings of this field symmetric with that pair.
const [model] = normalizeDiscoveredModels([{ id: "vendor/camel", maxModelLen: 131072 }]);
assert.equal(model.inputTokenLimit, 131072);
});
test("#12858 preserves an explicit inputTokenLimit over max_model_len", () => {
const [model] = normalizeDiscoveredModels([
{ id: "vendor/explicit-wins", inputTokenLimit: 200000, max_model_len: 999999 },
]);
assert.equal(model.inputTokenLimit, 200000);
});
test("#12858 keeps context_length ahead of max_model_len when a record has both", () => {
// OpenRouter-style catalogs are the older contract (#3202); a record carrying
// both fields must not change meaning because this one was added.
const [model] = normalizeDiscoveredModels([
{ id: "vendor/both", context_length: 262144, max_model_len: 131072 },
]);
assert.equal(model.inputTokenLimit, 262144);
});
test("#12858 ignores a non-positive or non-numeric max_model_len", () => {
const [zero] = normalizeDiscoveredModels([{ id: "vendor/zero", max_model_len: 0 }]);
const [negative] = normalizeDiscoveredModels([{ id: "vendor/negative", max_model_len: -1 }]);
const [text] = normalizeDiscoveredModels([{ id: "vendor/text", max_model_len: "250000" }]);
assert.equal(zero.inputTokenLimit, undefined);
assert.equal(negative.inputTokenLimit, undefined);
assert.equal(text.inputTokenLimit, undefined);
});
test("#12858 leaves inputTokenLimit unset when no window field is present", () => {
const [model] = normalizeDiscoveredModels([{ id: "vendor/no-window" }]);
assert.equal(model.inputTokenLimit, undefined);
});