feat(sse): provider-family auto combos auto/glm, auto/minimax, auto/zai, auto/mimo, auto/gemma, auto/llama, auto/gemini (#6453) (#6509)

Provider-family auto combos (#6453). Integrated into release/v3.8.46; vitest autoCombo suite 11/11 green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-07 01:00:12 -03:00
committed by GitHub
parent 526048da5e
commit 0d3d31c21f
6 changed files with 339 additions and 9 deletions

View File

@@ -8,6 +8,7 @@
### ✨ New Features
- **feat(sse):** **provider-family auto combos**`auto/glm`, `auto/minimax`, `auto/mimo`, `auto/zai`, `auto/gemma`, `auto/llama`, `auto/gemini` ([#6453](https://github.com/diegosouzapw/OmniRoute/issues/6453)) — new routable ids that materialize an on-demand virtual combo spanning whatever installed backends currently expose that model family, degrading gracefully as backends rotate. A new pure `open-sse/services/autoCombo/modelFamily.ts` (`detectModelFamily`) classifies by model-id prefix for six families; `zai` is instead resolved by provider id (z.ai's hosted API serves the same `glm-*` model ids as every other GLM backend, so `auto/zai` means "route to my z.ai backend specifically" vs `auto/glm`'s "any connected GLM backend"). Reuses the existing `createVirtualAutoCombo` on-demand materialization path (no DB writes) and the `/v1/models` catalog advertising loop. Regression guard: `tests/unit/autoCombo/provider-family-combos.test.ts` (11).
- **feat(proxy):** native **proxy-pool round-robin / egress IP rotation** ([#6365](https://github.com/diegosouzapw/OmniRoute/issues/6365)) — a scope (global / provider / account) can now hold **multiple** proxies as a pool with a rotation strategy, so outbound requests cycle their egress IP instead of pinning one proxy per scope. Migration `117_proxy_pool_rotation.sql` lifts the `UNIQUE(scope, scope_id)` constraint (rebuild via the canonical rename/copy/drop; existing single assignments become 1-element pools) and adds a `proxy_scope_rotation` companion table holding the per-scope strategy + a persisted monotonic round-robin cursor. Strategies: `round-robin` (default, monotonic cursor — never `Math.random`), `random`, and `sticky-per-N-min`. Resolution (`resolveProxyForScopeFromRegistry` / `resolveProxyForConnectionFromRegistry`) now fetches the alive, position-ordered candidate set (unchanged `PROXY_ALIVE_PREDICATE`) and applies the strategy; an empty / all-dead pool still returns `null` — the #6246 fail-closed guard is **untouched** (never falls through to direct egress). Backend + DB only; dashboard pool-builder UI is a follow-up. Regression guard: `tests/unit/proxy-pool-rotation-6365.test.ts` (8, incl. fail-closed + backward-compat).
- **feat(providers):** end-to-end **tool/function calling on the native Gemini `/v1beta` endpoint** ([#6222](https://github.com/diegosouzapw/OmniRoute/issues/6222)) — both directions of the Gemini↔OpenAI conversion now preserve tool data (previously silently dropped). Request side: `convertGeminiToInternal` (extracted to its own testable module) maps `tools[].functionDeclarations` → OpenAI `tools`, prior `functionCall` parts → assistant `tool_calls`, and `functionResponse` parts → `tool`-role messages. Response side: `convertOpenAIResponseToGemini` emits `parts[].functionCall {name,args}` from `message.tool_calls`, and the streaming `openAIChunkToGeminiChunk` accumulates fragmented `tool_calls` deltas by index into complete `functionCall` parts. The non-Gemini client paths (Claude, OpenAI-Responses) already preserved tool calls — this closes the gap specific to the native Gemini surface. Regression guard: `tests/unit/v1beta-gemini-tool-calling-6222.test.ts` (6, incl. a streaming SSE round-trip).
- **feat(providers):** copilot-m365-web **enterprise / work tier** support ([#6334](https://github.com/diegosouzapw/OmniRoute/issues/6334)) — mirrors the EDU-tier pattern (#6210): `M365ConnectionParams` gains an `agent` field, a new opt-in `M365_ENTERPRISE_OVERRIDES` preset (`agent=work`, `scenario=officeweb`, `licenseType=Premium`) applies via `providerSpecificData.tier="enterprise"` (alias `"work"`), and `agent` is also overridable directly via `providerSpecificData.agent`. `buildWsUrl` was hardcoding `agent="web"` (the one enterprise-distinguishing param with no override path), so a Premium work account handshook then returned an empty stream. The individual and EDU paths are untouched. Kilo's dup flag vs #6210 (EDU tier) was a false positive — different tier. Regression guard: `tests/unit/copilot-m365-enterprise-6334.test.ts` (7). End-to-end confirmation on a real Premium work account is a live-VPS validation follow-up (Hard Rule #18). (thanks @Forcerecon)

View File

@@ -1,6 +1,9 @@
import type { AutoVariant } from "./autoPrefix";
import { VALID_VARIANTS } from "./autoPrefix";
import { parseAutoSuffix } from "./suffixComposition";
import { isValidModelFamily, AUTO_FAMILY_IDS } from "./modelFamily";
export { AUTO_FAMILY_IDS };
/**
* Built-in `auto/*` catalog → AutoVariant resolution.
@@ -78,7 +81,11 @@ export function resolveAutoVariant(modelStr: string, suffix: string): ResolvedAu
* decide whether an `auto/` model is a valid built-in before materializing it.
*/
export function isRecognizedBuiltinAuto(modelStr: string, suffix: string): boolean {
return resolveAutoVariant(modelStr, suffix).recognized || parseAutoSuffix(suffix).valid;
return (
resolveAutoVariant(modelStr, suffix).recognized ||
parseAutoSuffix(suffix).valid ||
isValidModelFamily(suffix)
);
}
export async function createBuiltinAutoCombo(modelStr: string, suffix: string) {
@@ -105,5 +112,15 @@ export async function createBuiltinAutoCombo(modelStr: string, suffix: string) {
return virtualCombo;
}
// #6453: `auto/<family>` (e.g. auto/glm, auto/minimax, auto/zai, auto/mimo,
// auto/gemma, auto/llama, auto/gemini) — spans whatever installed backends
// currently expose that model family, degrading gracefully as backends rotate.
if (isValidModelFamily(suffix)) {
const virtualCombo = await createVirtualAutoCombo(undefined, { family: suffix });
virtualCombo.name = modelStr;
virtualCombo.id = modelStr;
return virtualCombo;
}
throw new Error(`Unknown built-in auto combo: ${modelStr}`);
}

View File

@@ -0,0 +1,95 @@
/**
* #6453 — Provider-family auto combos.
*
* `auto/<family>` (e.g. `auto/glm`, `auto/minimax`) is a NEW axis alongside the
* existing task-mode (`auto/best-coding`) and category:tier (`auto/coding:fast`)
* combos: instead of grouping by routing intent, it groups by underlying MODEL
* FAMILY and materializes an on-demand virtual combo spanning whatever installed
* backends currently expose that family, degrading gracefully as backends
* rotate — same on-demand mechanism (`createVirtualAutoCombo`), a different
* candidate filter.
*
* Kept as a pure, dependency-free module so `detectModelFamily` is unit-testable
* in isolation without touching the DB/registry-backed virtual factory.
*/
export type ModelFamily = "glm" | "minimax" | "mimo" | "zai" | "gemma" | "llama" | "gemini";
export const MODEL_FAMILIES: readonly ModelFamily[] = [
"glm",
"minimax",
"mimo",
"zai",
"gemma",
"llama",
"gemini",
];
const MODEL_FAMILY_SET: ReadonlySet<string> = new Set(MODEL_FAMILIES);
/** Model-id prefix → family. Matched against the bare model id (provider prefix, if
* any, stripped) so both `glm-5.2` and `zai/glm-5.2` resolve the same way. */
const FAMILY_ID_PATTERNS: ReadonlyArray<{ family: ModelFamily; pattern: RegExp }> = [
{ family: "glm", pattern: /^glm-/i },
{ family: "minimax", pattern: /^minimax-/i },
{ family: "mimo", pattern: /^mimo-/i },
{ family: "gemma", pattern: /^gemma-/i },
{ family: "llama", pattern: /^llama-/i },
{ family: "gemini", pattern: /^gemini-/i },
];
/**
* `zai` is deliberately NOT a model-id prefix rule: Zhipu's z.ai hosted API serves
* the same `glm-*` model ids as every other GLM backend (`glm` provider, custom
* OpenAI/Anthropic-compatible connections, etc — see
* `open-sse/config/providers/registry/zai/index.ts`). Aliasing `auto/zai` to a
* model-name prefix would make it identical to `auto/glm`, so instead it is
* resolved by PROVIDER id: "route to my z.ai backend specifically", distinct from
* `auto/glm` ("route to any connected provider currently serving a GLM model,
* z.ai included"). Documented here rather than deferred because the distinction
* is meaningful and the rule is a one-line lookup.
*/
export const FAMILY_PROVIDER_OVERRIDE: Readonly<Partial<Record<ModelFamily, string>>> = {
zai: "zai",
};
export function isValidModelFamily(value: string | null | undefined): value is ModelFamily {
return typeof value === "string" && MODEL_FAMILY_SET.has(value);
}
/**
* Detect the model family from a bare or provider-prefixed model id.
* Returns `null` when the id doesn't match any known family prefix — including
* `zai`, which is never detected from a model id (see `FAMILY_PROVIDER_OVERRIDE`).
*/
export function detectModelFamily(modelId: string | null | undefined): ModelFamily | null {
if (typeof modelId !== "string" || modelId.trim().length === 0) return null;
const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
for (const { family, pattern } of FAMILY_ID_PATTERNS) {
if (pattern.test(bare)) return family;
}
return null;
}
interface FamilyPoolCandidate {
provider: string;
model: string;
}
/**
* Build the candidate filter for `auto/<family>`. Provider-override families
* (currently only `zai`) filter by connection provider id; every other family
* filters by `detectModelFamily(model) === family`.
*/
export function buildFamilyCandidateFilter(
family: ModelFamily
): (candidate: FamilyPoolCandidate) => boolean {
const providerOverride = FAMILY_PROVIDER_OVERRIDE[family];
if (providerOverride) {
return (candidate) => candidate.provider === providerOverride;
}
return (candidate) => detectModelFamily(candidate.model) === family;
}
/** Advertised `auto/<family>` catalog ids (#6453), e.g. `auto/glm`, `auto/minimax`. */
export const AUTO_FAMILY_IDS: readonly string[] = MODEL_FAMILIES.map((family) => `auto/${family}`);

View File

@@ -17,12 +17,16 @@ import {
type AutoCategory,
type AutoTier,
} from "./suffixComposition";
import { buildFamilyCandidateFilter, type ModelFamily } from "./modelFamily";
import { getHiddenModelsByProvider } from "@/models";
/** #4235 Phase B: optional category/tier overlay for `auto/<category>:<tier>` combos. */
/** #4235 Phase B: optional category/tier overlay for `auto/<category>:<tier>` combos.
* #6453: optional `family` overlay for `auto/<family>` combos (e.g. `auto/glm`) —
* mutually exclusive with category/tier, applied instead of them when present. */
export interface AutoComboSpec {
category?: AutoCategory;
tier?: AutoTier;
family?: ModelFamily;
}
/** Minimal connection shape needed for virtual auto-combo factory */
@@ -306,26 +310,40 @@ export async function createVirtualAutoCombo(
// connected. Operators who want the old "never break routing, lose the bias"
// behavior can opt back in via the env var below.
let effectivePool = candidatePool;
const candidateFilter = spec ? buildAutoCandidateFilter(spec.category, spec.tier) : null;
// #6453: `auto/<family>` narrows by model family instead of category/tier. The
// two overlays are mutually exclusive on the spec (family takes precedence when
// both are somehow present, which callers never do in practice).
const candidateFilter = spec?.family
? buildFamilyCandidateFilter(spec.family)
: spec
? buildAutoCandidateFilter(spec.category, spec.tier)
: null;
if (candidateFilter) {
const narrowed = candidatePool.filter((c) =>
candidateFilter({ provider: c.provider, model: c.model })
);
const label = spec?.family
? `auto/${spec.family}`
: `auto/${spec?.category ?? ""}${spec?.tier ? `:${spec.tier}` : ""}`;
if (narrowed.length > 0) {
effectivePool = narrowed;
} else if (
process.env.OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL === "true" ||
process.env.OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL === "1"
!spec?.family &&
(process.env.OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL === "true" ||
process.env.OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL === "1")
) {
// Opt-in legacy behavior: warn loudly, then keep the full pool.
// Opt-in legacy behavior (category/tier only): warn loudly, then keep the full pool.
log.warn(
"AUTO",
`auto/${spec?.category ?? ""}${spec?.tier ? `:${spec.tier}` : ""} matched no connected models; falling back to the full pool (OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true)`
`${label} matched no connected models; falling back to the full pool (OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true)`
);
} else {
// Family combos always degrade to an empty pool when unavailable — a family
// is a hard identity constraint, not a soft optimization bias, so there is
// no sensible "fall back to the full pool" behavior for it.
log.warn(
"AUTO",
`auto/${spec?.category ?? ""}${spec?.tier ? `:${spec.tier}` : ""} matched no connected models; returning an empty pool. Set OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true to restore the legacy "use full pool" behavior.`
`${label} matched no connected models; returning an empty pool.${spec?.family ? "" : ' Set OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true to restore the legacy "use full pool" behavior.'}`
);
effectivePool = [];
}

View File

@@ -24,6 +24,7 @@ import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo";
import {
AUTO_TEMPLATE_VARIANTS,
AUTO_SUFFIX_VARIANTS,
AUTO_FAMILY_IDS,
createBuiltinAutoCombo,
} from "@omniroute/open-sse/services/autoCombo/builtinCatalog";
import { getAllSyncedAvailableModels, type SyncedAvailableModel } from "@/lib/db/models";
@@ -585,7 +586,12 @@ async function buildUnifiedModelsResponseCore(
// combo cannot be materialized (e.g. no eligible connections yet) the minimal
// #4164 entry is emitted instead, so the id is never dropped.
// #4235 Phase B: also advertise the curated `auto/<category>[:<tier>]` combos.
for (const autoId of [...Object.keys(AUTO_TEMPLATE_VARIANTS), ...AUTO_SUFFIX_VARIANTS]) {
// #6453: also advertise the `auto/<family>` combos (auto/glm, auto/minimax, ...).
for (const autoId of [
...Object.keys(AUTO_TEMPLATE_VARIANTS),
...AUTO_SUFFIX_VARIANTS,
...AUTO_FAMILY_IDS,
]) {
if (blockedProviders.has("auto") || listedIds.has(autoId)) continue; // #5192
listedIds.add(autoId);
const baseAutoEntry = {

View File

@@ -0,0 +1,193 @@
/**
* #6453 — Provider-family auto combos (`auto/glm`, `auto/minimax`, `auto/zai`,
* `auto/mimo`, `auto/gemma`, `auto/llama`, `auto/gemini`).
*
* See: `open-sse/services/autoCombo/modelFamily.ts` (pure family detection) and
* `open-sse/services/autoCombo/builtinCatalog.ts` (recognition + materialization).
*
* NOTE: tests/unit/autoCombo/ is a vitest-only scope (see vitest.mcp.config.ts);
* the node:test runner does not walk this dir.
*/
import { describe, it, beforeEach, afterAll, vi } from "vitest";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
detectModelFamily,
isValidModelFamily,
AUTO_FAMILY_IDS,
} from "../../../open-sse/services/autoCombo/modelFamily";
// First-touch DB migrations run once per worker and can exceed vitest's 5s
// default in a cold thread; the DB-backed materialization tests below need it.
vi.setConfig({ testTimeout: 20_000 });
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-family-combo-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const providersDb = await import("../../../src/lib/db/providers.ts");
const builtinCatalog = await import("../../../open-sse/services/autoCombo/builtinCatalog.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
beforeEach(async () => {
await resetStorage();
});
afterAll(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
});
describe("detectModelFamily (pure)", () => {
it("recognizes glm model ids", () => {
assert.equal(detectModelFamily("glm-5.2"), "glm");
assert.equal(detectModelFamily("zai/glm-5.2"), "glm");
});
it("recognizes minimax, mimo, gemma, llama, gemini model ids", () => {
assert.equal(detectModelFamily("minimax-m3"), "minimax");
assert.equal(detectModelFamily("mimo-v2.5"), "mimo");
assert.equal(detectModelFamily("gemma-3-27b"), "gemma");
assert.equal(detectModelFamily("llama-3.3-70b"), "llama");
assert.equal(detectModelFamily("gemini-3-pro"), "gemini");
});
it("returns null for unrelated model ids", () => {
assert.equal(detectModelFamily("gpt-4o"), null);
assert.equal(detectModelFamily(""), null);
assert.equal(detectModelFamily(null), null);
});
it("never detects zai from a model id (provider-override family)", () => {
// zai's own models are named glm-*, not zai-*; auto/zai is resolved by
// provider id, not by a model-name prefix (see modelFamily.ts comment).
assert.equal(detectModelFamily("zai-glm-5.2"), null);
});
it("isValidModelFamily accepts exactly the 7 advertised families", () => {
for (const family of ["glm", "minimax", "mimo", "zai", "gemma", "llama", "gemini"]) {
assert.equal(isValidModelFamily(family), true);
}
assert.equal(isValidModelFamily("gpt"), false);
assert.equal(isValidModelFamily(undefined), false);
});
it("advertises exactly one auto/<family> catalog id per family", () => {
assert.deepEqual(
[...AUTO_FAMILY_IDS].sort(),
["auto/gemini", "auto/gemma", "auto/glm", "auto/llama", "auto/mimo", "auto/minimax", "auto/zai"]
);
});
});
describe("auto/<family> materialization (#6453)", () => {
it("resolves auto/glm to a virtual combo spanning every connected GLM backend", async () => {
await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: "GLM direct",
apiKey: "sk-test-glm",
defaultModel: "glm-5.2",
});
await providersDb.createProviderConnection({
provider: "zai",
authType: "apikey",
name: "z.ai",
apiKey: "sk-test-zai",
defaultModel: "glm-5.2",
});
await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "OpenAI",
apiKey: "sk-test-openai",
defaultModel: "gpt-4o-mini",
});
const combo = await builtinCatalog.createBuiltinAutoCombo("auto/glm", "glm");
assert.equal(combo.id, "auto/glm");
assert.equal(combo.strategy, "auto");
const providerIds = combo.models.map((m) => m.providerId).sort();
assert.deepEqual(providerIds, ["glm", "zai"]);
assert.ok(combo.models.every((m) => m.model.endsWith("glm-5.2")));
});
it("resolves auto/zai to ONLY the zai-provider connection (provider-override family)", async () => {
await providersDb.createProviderConnection({
provider: "glm",
authType: "apikey",
name: "GLM direct",
apiKey: "sk-test-glm",
defaultModel: "glm-5.2",
});
await providersDb.createProviderConnection({
provider: "zai",
authType: "apikey",
name: "z.ai",
apiKey: "sk-test-zai",
defaultModel: "glm-5.2",
});
const combo = await builtinCatalog.createBuiltinAutoCombo("auto/zai", "zai");
assert.equal(combo.id, "auto/zai");
const providerIds = combo.models.map((m) => m.providerId);
assert.deepEqual(providerIds, ["zai"]);
});
it("degrades gracefully to the family subset, excluding connected-but-unrelated providers", async () => {
// Free/noAuth backends may still expose a family model (e.g. opencode serves
// minimax under its own catalog) — the family combo is expected to include
// those, but MUST exclude a connected provider whose model is a different
// family entirely. This is the "subset available" degrade path (#6453).
await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "OpenAI",
apiKey: "sk-test-openai",
defaultModel: "gpt-4o-mini",
});
const combo = await builtinCatalog.createBuiltinAutoCombo("auto/minimax", "minimax");
assert.equal(combo.id, "auto/minimax");
assert.ok(
combo.models.every((m) => m.providerId !== "openai"),
"auto/minimax must not include the connected openai/gpt-4o-mini candidate"
);
assert.ok(
combo.models.every((m) => detectModelFamily(m.model) === "minimax"),
"every candidate in auto/minimax must actually be a minimax model"
);
});
it("rejects auto/<unknownfamily> with the same clean error as any unknown combo", async () => {
await assert.rejects(
() => builtinCatalog.createBuiltinAutoCombo("auto/unknownfam", "unknownfam"),
/Unknown built-in auto combo/
);
});
it("isRecognizedBuiltinAuto recognizes every auto/<family> id", () => {
for (const family of ["glm", "minimax", "mimo", "zai", "gemma", "llama", "gemini"]) {
assert.equal(builtinCatalog.isRecognizedBuiltinAuto(`auto/${family}`, family), true);
}
assert.equal(builtinCatalog.isRecognizedBuiltinAuto("auto/unknownfam", "unknownfam"), false);
});
});