mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
fix(models): normalize GLM-5.2 provider context (#6091)
Hosted GLM-5.2 provider aliases now respect their declared context caps instead of inheriting the native 1M; native/bare + verified OpenCode/ZenMux routes stay at 1M. Regression guards added. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -22,6 +22,8 @@
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **models (GLM-5.2 context normalization):** stop treating every hosted GLM-5.2 provider alias as the native 1M-context model. Native/bare GLM-5.2 and verified OpenCode / ZenMux routes keep their 1,000,000-token context, while hosted-provider aliases now respect the caps declared in their provider metadata instead of inheriting the native max. Regression guards: `tests/unit/model-capabilities-registry.test.ts`, `tests/unit/models-catalog-route.test.ts`. ([#6091](https://github.com/diegosouzapw/OmniRoute/pull/6091) — thanks @Thinkscape)
|
||||
|
||||
- **providers (Gemini Web):** refresh the Gemini Web cookie handling and model catalog so live Gemini Web sessions keep authenticating and routing to current models. Regression guard: `tests/unit/gemini-web.test.ts`. ([#6095](https://github.com/diegosouzapw/OmniRoute/pull/6095) — thanks @backryun)
|
||||
|
||||
- **providers (Perplexity Web):** refresh the Perplexity Web model catalog to the current set (GPT-5.4/5.5, Claude Sonnet 5.0 / Opus 4.8, GLM-5.2, Kimi K2.6, Nemotron 3 Ultra) and update the internal mode / `model_preference` mappings and thinking variants so requests resolve to live upstream models. Regression guard: `tests/unit/perplexity-web.test.ts`. ([#6106](https://github.com/diegosouzapw/OmniRoute/pull/6106) — thanks @backryun)
|
||||
|
||||
@@ -3,7 +3,13 @@ import {
|
||||
PROVIDER_MODELS,
|
||||
} from "@omniroute/open-sse/config/providerModels.ts";
|
||||
import { parseModel, resolveCanonicalProviderModel } from "@omniroute/open-sse/services/model.ts";
|
||||
import { MODEL_SPECS, getModelSpec, type ModelSpec } from "@/shared/constants/modelSpecs";
|
||||
import {
|
||||
MODEL_SPECS,
|
||||
getAuthoritativeContextWindow,
|
||||
getAuthoritativeProviderContextWindow,
|
||||
getModelSpec,
|
||||
type ModelSpec,
|
||||
} from "@/shared/constants/modelSpecs";
|
||||
import { getSyncedCapability } from "@/lib/modelsDevSync";
|
||||
import { getModelContextOverride } from "@/lib/db/modelContextOverrides";
|
||||
import { isVisionModelId } from "@/shared/constants/visionModels";
|
||||
@@ -178,6 +184,22 @@ function getStaticSpec(modelId: string | null, rawModel: string | null): ModelSp
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getAuthoritativeStaticContextWindow(
|
||||
provider: string | null,
|
||||
modelId: string | null,
|
||||
rawModel: string | null
|
||||
): number | null {
|
||||
for (const candidate of [modelId, rawModel]) {
|
||||
const providerContextWindow = getAuthoritativeProviderContextWindow(provider, candidate);
|
||||
if (typeof providerContextWindow === "number") return providerContextWindow;
|
||||
}
|
||||
for (const candidate of [modelId, rawModel]) {
|
||||
const contextWindow = getAuthoritativeContextWindow(candidate);
|
||||
if (typeof contextWindow === "number") return contextWindow;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getStaticSpecCanonicalModelId(modelId: string | null, rawModel: string | null) {
|
||||
const candidates = [modelId, rawModel].filter(
|
||||
(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0
|
||||
@@ -351,7 +373,13 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
|
||||
: null) ??
|
||||
(typeof spec?.supportsThinking === "boolean" ? spec.supportsThinking : null));
|
||||
|
||||
const authoritativeContextWindow = getAuthoritativeStaticContextWindow(
|
||||
resolved.provider,
|
||||
resolved.model,
|
||||
resolved.rawModel
|
||||
);
|
||||
const contextWindow =
|
||||
authoritativeContextWindow ??
|
||||
synced?.limit_context ??
|
||||
(typeof registryModel?.contextLength === "number" ? registryModel.contextLength : null) ??
|
||||
spec?.contextWindow ??
|
||||
@@ -378,7 +406,7 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo
|
||||
structuredOutput: synced?.structured_output ?? null,
|
||||
temperature: synced?.temperature ?? null,
|
||||
contextWindow,
|
||||
maxInputTokens: synced?.limit_input ?? contextWindow,
|
||||
maxInputTokens: authoritativeContextWindow ?? synced?.limit_input ?? contextWindow,
|
||||
maxOutputTokens:
|
||||
synced?.limit_output ??
|
||||
(typeof registryModel?.maxOutputTokens === "number" ? registryModel.maxOutputTokens : null) ??
|
||||
|
||||
@@ -4,6 +4,8 @@ import { getModelInfo } from "@/sse/services/model";
|
||||
import { getModelAliases } from "@/lib/db/models";
|
||||
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
|
||||
import {
|
||||
getAuthoritativeContextWindow,
|
||||
getAuthoritativeProviderContextWindow,
|
||||
getModelSpec,
|
||||
resolveModelAlias as resolveStaticModelAlias,
|
||||
} from "@/shared/constants/modelSpecs";
|
||||
@@ -273,6 +275,11 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
|
||||
|
||||
const nextEntry: JsonRecord = { ...entry };
|
||||
const existingName = asNonEmptyString(entry.name);
|
||||
const authoritativeContextWindow =
|
||||
getAuthoritativeProviderContextWindow(metadata.provider, metadata.model) ??
|
||||
getAuthoritativeProviderContextWindow(provider, model) ??
|
||||
getAuthoritativeContextWindow(metadata.model) ??
|
||||
getAuthoritativeContextWindow(model);
|
||||
const capabilityFields = {
|
||||
...(typeof metadata.capabilities.vision === "boolean"
|
||||
? { vision: metadata.capabilities.vision }
|
||||
@@ -309,7 +316,7 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
|
||||
}
|
||||
|
||||
if (
|
||||
typeof nextEntry.context_length !== "number" &&
|
||||
(typeof nextEntry.context_length !== "number" || authoritativeContextWindow !== null) &&
|
||||
typeof metadata.limits.contextWindow === "number"
|
||||
) {
|
||||
nextEntry.context_length = metadata.limits.contextWindow;
|
||||
@@ -319,7 +326,10 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
|
||||
nextEntry.max_output_tokens = metadata.limits.maxOutputTokens;
|
||||
}
|
||||
|
||||
if (typeof metadata.limits.maxInputTokens === "number") {
|
||||
if (
|
||||
typeof metadata.limits.maxInputTokens === "number" &&
|
||||
(typeof nextEntry.max_input_tokens !== "number" || authoritativeContextWindow !== null)
|
||||
) {
|
||||
nextEntry.max_input_tokens = metadata.limits.maxInputTokens;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,23 @@ const BEDROCK_CLAUDE_ALIASES = (...modelIds: string[]) => [
|
||||
),
|
||||
];
|
||||
|
||||
// Provider discovery/sync sources can under-report GLM-5.2 IDs as 128K.
|
||||
// Keep native/bare Z.AI GLM-5.2 context authoritative, but do not blindly apply
|
||||
// it to every provider-wrapped alias: hosted providers can and do cap lower.
|
||||
const AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS = new Set(["glm-5.2", "glm-5.2-high", "glm-5.2-max"]);
|
||||
const AUTHORITATIVE_PROVIDER_CONTEXT_WINDOWS = new Map<string, number>([
|
||||
["cloudflare-ai/@cf/zai-org/glm-5.2", 262144],
|
||||
// Hugging Face Router has 1M-capable backends, but bare routing can select
|
||||
// lower-context providers (notably Together at 262K), so advertise a safe floor
|
||||
// unless the caller can pin a 1M-capable backend.
|
||||
["huggingface/zai-org/glm-5.2", 262144],
|
||||
["opencode/glm-5.2", 1000000],
|
||||
["opencode-zen/glm-5.2", 1000000],
|
||||
["opencode-go/glm-5.2", 1000000],
|
||||
["zenmux/z-ai/glm-5.2", 1000000],
|
||||
["zenmux/z-ai/glm-5.2-free", 1000000],
|
||||
]);
|
||||
|
||||
export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
"gpt-5.5": {
|
||||
maxOutputTokens: 128000,
|
||||
@@ -476,29 +493,53 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
__default__: {},
|
||||
};
|
||||
|
||||
export function getModelSpec(modelId: string): ModelSpec | undefined {
|
||||
if (MODEL_SPECS[modelId]) return MODEL_SPECS[modelId];
|
||||
export function getCanonicalModelSpecId(modelId: string): string | null {
|
||||
if (MODEL_SPECS[modelId]) return modelId;
|
||||
|
||||
// Case-insensitive lookups: upstream model ids are often capitalized
|
||||
// (e.g. "MiniMax-M2.7") while specs/aliases use lowercase ids (#3141).
|
||||
const lower = modelId.toLowerCase();
|
||||
|
||||
// Exact match (case-insensitive)
|
||||
for (const [canonical, spec] of Object.entries(MODEL_SPECS)) {
|
||||
if (canonical.toLowerCase() === lower) return spec;
|
||||
for (const canonical of Object.keys(MODEL_SPECS)) {
|
||||
if (canonical.toLowerCase() === lower) return canonical;
|
||||
}
|
||||
|
||||
// Buscas por alias (case-insensitive)
|
||||
for (const [, spec] of Object.entries(MODEL_SPECS)) {
|
||||
if (spec.aliases?.some((alias) => alias.toLowerCase() === lower)) return spec;
|
||||
for (const [canonical, spec] of Object.entries(MODEL_SPECS)) {
|
||||
if (spec.aliases?.some((alias) => alias.toLowerCase() === lower)) return canonical;
|
||||
}
|
||||
|
||||
// Prefix matching (case-insensitive)
|
||||
for (const [key, spec] of Object.entries(MODEL_SPECS)) {
|
||||
if (key !== "__default__" && lower.startsWith(key.toLowerCase())) return spec;
|
||||
for (const key of Object.keys(MODEL_SPECS)) {
|
||||
if (key !== "__default__" && lower.startsWith(key.toLowerCase())) return key;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getModelSpec(modelId: string): ModelSpec | undefined {
|
||||
const canonical = getCanonicalModelSpecId(modelId);
|
||||
return canonical ? MODEL_SPECS[canonical] : undefined;
|
||||
}
|
||||
|
||||
export function getAuthoritativeContextWindow(modelId: string | null | undefined): number | null {
|
||||
if (typeof modelId !== "string" || modelId.length === 0) return null;
|
||||
const normalized = modelId.toLowerCase();
|
||||
for (const canonical of AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS) {
|
||||
if (canonical.toLowerCase() === normalized)
|
||||
return MODEL_SPECS[canonical]?.contextWindow ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAuthoritativeProviderContextWindow(
|
||||
provider: string | null | undefined,
|
||||
modelId: string | null | undefined
|
||||
): number | null {
|
||||
if (typeof provider !== "string" || typeof modelId !== "string") return null;
|
||||
const key = `${provider}/${modelId}`.toLowerCase();
|
||||
return AUTHORITATIVE_PROVIDER_CONTEXT_WINDOWS.get(key) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -207,3 +207,53 @@ test("Kimi K2.7 Code resolves full capabilities instead of the degraded import d
|
||||
assert.notEqual(ollama.contextWindow, 128000);
|
||||
assert.notEqual(ollama.maxOutputTokens, 8192);
|
||||
});
|
||||
|
||||
test("GLM-5.2 context limits respect provider-hosted caps", () => {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
huggingface: {
|
||||
"zai-org/GLM-5.2": buildCapability({
|
||||
limit_context: 128000,
|
||||
limit_input: 128000,
|
||||
limit_output: 128000,
|
||||
}),
|
||||
},
|
||||
"cloudflare-ai": {
|
||||
"@cf/zai-org/glm-5.2": buildCapability({
|
||||
limit_context: 128000,
|
||||
limit_input: 128000,
|
||||
limit_output: 128000,
|
||||
}),
|
||||
},
|
||||
zenmux: {
|
||||
"z-ai/glm-5.2": buildCapability({
|
||||
limit_context: 128000,
|
||||
limit_input: 128000,
|
||||
limit_output: 128000,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
for (const modelId of ["glm-5.2", "opencode-go/glm-5.2", "opencode/glm-5.2", "oc/glm-5.2"]) {
|
||||
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
|
||||
assert.equal(capabilities.contextWindow, 1000000, modelId);
|
||||
assert.equal(capabilities.maxInputTokens, 1000000, modelId);
|
||||
}
|
||||
|
||||
for (const modelId of ["zenmux/z-ai/glm-5.2", "zenmux/z-ai/glm-5.2-free"]) {
|
||||
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
|
||||
assert.equal(capabilities.contextWindow, 1000000, modelId);
|
||||
assert.equal(capabilities.maxInputTokens, 1000000, modelId);
|
||||
}
|
||||
|
||||
for (const modelId of ["cloudflare-ai/@cf/zai-org/glm-5.2", "cf/@cf/zai-org/glm-5.2"]) {
|
||||
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
|
||||
assert.equal(capabilities.contextWindow, 262144, modelId);
|
||||
assert.equal(capabilities.maxInputTokens, 262144, modelId);
|
||||
}
|
||||
|
||||
for (const modelId of ["huggingface/zai-org/GLM-5.2", "hf/zai-org/GLM-5.2"]) {
|
||||
const capabilities = modelCapabilities.getResolvedModelCapabilities(modelId);
|
||||
assert.equal(capabilities.contextWindow, 262144, modelId);
|
||||
assert.equal(capabilities.maxInputTokens, 262144, modelId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -843,6 +843,99 @@ test("v1 models catalog includes synced non-Gemini provider models from discover
|
||||
assert.equal(syncedModel.context_length, 262144);
|
||||
});
|
||||
|
||||
test("v1 models catalog advertises GLM-5.2 provider aliases with hosted context limits", async () => {
|
||||
const hfConnection = await seedConnection("huggingface", {
|
||||
name: "huggingface-glm52",
|
||||
apiKey: "hf-key",
|
||||
});
|
||||
const cfConnection = await seedConnection("cloudflare-ai", {
|
||||
name: "cloudflare-glm52",
|
||||
apiKey: "cf-key",
|
||||
});
|
||||
const zenmuxConnection = await seedConnection("zenmux", {
|
||||
name: "zenmux-glm52",
|
||||
apiKey: "zen-key",
|
||||
});
|
||||
await seedConnection("opencode-go", {
|
||||
name: "opencode-go-glm52",
|
||||
apiKey: "go-key",
|
||||
});
|
||||
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection(
|
||||
"huggingface",
|
||||
(hfConnection as any).id,
|
||||
[
|
||||
{
|
||||
id: "zai-org/GLM-5.2",
|
||||
name: "GLM 5.2",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
inputTokenLimit: 128000,
|
||||
outputTokenLimit: 128000,
|
||||
},
|
||||
]
|
||||
);
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection(
|
||||
"cloudflare-ai",
|
||||
(cfConnection as any).id,
|
||||
[
|
||||
{
|
||||
id: "@cf/zai-org/glm-5.2",
|
||||
name: "GLM 5.2",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
inputTokenLimit: 128000,
|
||||
outputTokenLimit: 128000,
|
||||
},
|
||||
]
|
||||
);
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("zenmux", (zenmuxConnection as any).id, [
|
||||
{
|
||||
id: "z-ai/glm-5.2",
|
||||
name: "GLM 5.2",
|
||||
source: "imported",
|
||||
supportedEndpoints: ["chat"],
|
||||
inputTokenLimit: 128000,
|
||||
outputTokenLimit: 128000,
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
huggingface: {
|
||||
"zai-org/GLM-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
|
||||
},
|
||||
"cloudflare-ai": {
|
||||
"@cf/zai-org/glm-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
|
||||
},
|
||||
zenmux: {
|
||||
"z-ai/glm-5.2": capability({ limit_context: 128000, limit_input: 128000 }),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const byId = new Map(body.data.map((item) => [item.id, item]));
|
||||
|
||||
for (const [id, expectedContext] of [
|
||||
["huggingface/zai-org/GLM-5.2", 262144],
|
||||
["cloudflare-ai/@cf/zai-org/glm-5.2", 262144],
|
||||
["opencode-go/glm-5.2", 1000000],
|
||||
["zenmux/z-ai/glm-5.2", 1000000],
|
||||
] as const) {
|
||||
const model = byId.get(id) as any;
|
||||
assert.ok(model, `expected ${id} in catalog`);
|
||||
assert.equal(model.context_length, expectedContext, id);
|
||||
assert.equal(model.max_input_tokens, expectedContext, id);
|
||||
assert.notEqual(model.context_length, 128000, id);
|
||||
}
|
||||
} finally {
|
||||
modelsDevSync.saveModelsDevCapabilities({});
|
||||
}
|
||||
});
|
||||
|
||||
test("v1 models catalog includes media, moderation, rerank, video, and music models for active providers", async () => {
|
||||
await seedConnection("openai", { name: "openai-media" });
|
||||
await seedConnection("cohere", { name: "cohere-rerank" });
|
||||
|
||||
Reference in New Issue
Block a user