mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
* fix(models): keep OpenRouter :batch variants out of chat routing
ModelSync imported OpenRouter's Batch-API-only variants into the chat
catalogue. A chat completion against one is rejected upstream with
404 This model is only available through the Batch API.
Use the /api/beta/batches endpoint instead.
which #13596 measured 91 times in 41 hours, third by volume, plus the
`model not found - locking mode` failover churn behind it.
OpenRouter's /models carries no endpoint metadata that separates a batch
variant from a chat one, so `classifyExplicitEndpoints` cannot decide it
and the rule belongs in `modelEndpointPolicy`, beside the OpenAI
image/video policy and for the same reason: the file exists so discovery,
import and catalog projection agree on one answer.
Matched as the exact `:batch` suffix, not "has a variant suffix" --
`:free`, `:nitro`, `:floor`, `:online`, `:extended` and `:thinking` are
routing hints on the same chat model, and excluding them would silently
shrink the routable catalogue. Applied unconditionally for this provider:
there is no "batch" endpoint name an upstream could declare next to a chat
one, and the already-stored rows carry the synthetic `["chat"]` default
that re-imported them in the first place.
Closes #13596
* docs(changelog): add fragment for the OpenRouter batch-variant fix
137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
const { filterChatSelectableModels, getModelEndpointDecision, isChatSelectableModel } =
|
|
await import("../../open-sse/services/modelEndpointPolicy.ts");
|
|
|
|
test("OpenAI image models are not chat-selectable without upstream endpoint metadata", () => {
|
|
for (const modelId of [
|
|
"gpt-image-2",
|
|
"gpt-image-1.5",
|
|
"gpt-image-1-mini",
|
|
"dall-e-3",
|
|
"chatgpt-image-latest",
|
|
]) {
|
|
assert.deepEqual(getModelEndpointDecision("openai", modelId), {
|
|
kind: "image",
|
|
chatSelectable: false,
|
|
reason: "provider-policy",
|
|
});
|
|
}
|
|
});
|
|
|
|
test("OpenAI video models are not chat-selectable without upstream endpoint metadata", () => {
|
|
assert.deepEqual(getModelEndpointDecision("openai", "sora-2-pro"), {
|
|
kind: "video",
|
|
chatSelectable: false,
|
|
reason: "provider-policy",
|
|
});
|
|
});
|
|
|
|
test("OpenRouter :batch variants are not chat-selectable", () => {
|
|
for (const modelId of [
|
|
"google/gemini-3.6-flash:batch",
|
|
"anthropic/claude-sonnet-4.5:batch",
|
|
"minimax/minimax-m3:batch",
|
|
"inkling:batch",
|
|
]) {
|
|
assert.deepEqual(getModelEndpointDecision("openrouter", modelId), {
|
|
kind: "non-chat",
|
|
chatSelectable: false,
|
|
reason: "provider-policy",
|
|
});
|
|
}
|
|
});
|
|
|
|
test("OpenRouter's other variant suffixes stay chat-selectable", () => {
|
|
// Excluding these would shrink the routable catalogue -- they are routing
|
|
// hints on the same chat model, not a different endpoint.
|
|
for (const modelId of [
|
|
"google/gemini-3.6-flash:free",
|
|
"anthropic/claude-sonnet-4.5:thinking",
|
|
"meta-llama/llama-4-70b:nitro",
|
|
"perplexity/sonar:online",
|
|
"google/gemini-3.6-flash",
|
|
]) {
|
|
assert.equal(isChatSelectableModel("openrouter", { id: modelId }), true, modelId);
|
|
}
|
|
});
|
|
|
|
test("a synthetic chat default does not re-admit an OpenRouter batch variant", () => {
|
|
// The rows already stored for these carry `["chat"]` as the synthetic import
|
|
// default, which is exactly what re-imported them.
|
|
assert.equal(
|
|
isChatSelectableModel("openrouter", {
|
|
id: "google/gemini-3.6-flash:batch",
|
|
supportedEndpoints: ["chat"],
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("the batch policy is scoped to OpenRouter", () => {
|
|
assert.equal(isChatSelectableModel("custom-provider", { id: "some-model:batch" }), true);
|
|
assert.equal(isChatSelectableModel(null, { id: "some-model:batch" }), true);
|
|
});
|
|
|
|
test("filterChatSelectableModels drops the batch variant and keeps its base model", () => {
|
|
const models = [
|
|
{ id: "google/gemini-3.6-flash" },
|
|
{ id: "google/gemini-3.6-flash:batch" },
|
|
{ id: "google/gemini-3.6-flash:free" },
|
|
];
|
|
|
|
assert.deepEqual(
|
|
filterChatSelectableModels("openrouter", models).map((model) => model.id),
|
|
["google/gemini-3.6-flash", "google/gemini-3.6-flash:free"]
|
|
);
|
|
});
|
|
|
|
test("provider policy is scoped and does not classify another provider by model name", () => {
|
|
assert.equal(
|
|
isChatSelectableModel("custom-provider", { id: "gpt-image-shaped-chat-model" }),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("explicit chat capability wins for a multi-endpoint model", () => {
|
|
assert.equal(
|
|
isChatSelectableModel("openai", {
|
|
id: "gpt-image-shaped-multimodal-model",
|
|
supportedEndpoints: ["/v1/images/generations", "/v1/responses"],
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("a synthetic chat default does not override a known OpenAI specialty model", () => {
|
|
assert.equal(
|
|
isChatSelectableModel("openai", {
|
|
id: "sora-2",
|
|
supportedEndpoints: ["chat"],
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("explicit non-chat endpoints are excluded even when the model ID is unknown", () => {
|
|
assert.equal(
|
|
isChatSelectableModel("openai-compatible", {
|
|
id: "vendor-specialty-model",
|
|
supportedEndpoints: ["videos/generations"],
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("chat import filtering keeps ordinary OpenAI models only", () => {
|
|
assert.deepEqual(
|
|
filterChatSelectableModels("openai", [
|
|
{ id: "gpt-5.6" },
|
|
{ id: "gpt-image-2" },
|
|
{ id: "sora-2" },
|
|
]).map((model) => model.id),
|
|
["gpt-5.6"]
|
|
);
|
|
});
|