Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/features/disable-thinking-level-variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(catalog):** add `OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS` feature flag to optionally filter out thinking level variants from model catalog ([#PR_NUMBER](https://github.com/diegosouzapw/OmniRoute/pull/PR_NUMBER))
19 changes: 15 additions & 4 deletions src/app/api/v1/models/catalogResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import {
type CatalogEnrichmentSnapshot,
} from "@/lib/modelMetadataRegistry";
import { createModelCapabilityResolutionSnapshot } from "@/lib/modelCapabilityResolutionSnapshot";
import { isModelCatalogNamesEnabled } from "@/shared/utils/featureFlags";
import {
isModelCatalogNamesEnabled,
isNoThinkingAliasEnabled,
isDisableThinkingLevelVariantsEnabled,
} from "@/shared/utils/featureFlags";
import { extractApiKey } from "@/sse/services/auth";
import { maybeOmitCatalogModelName } from "./catalogHelpers";
import { isCodexModelCatalogClient } from "./catalogRequest";
Expand Down Expand Up @@ -85,11 +89,16 @@ export async function applyCatalogPostFilters(
// Advertise no-thinking gateway variants (Fase 8.1). Derived from the already
// key-filtered list, so a variant only appears when its real model is permitted.
// #9418: skip when hideNoThinkVariants is on — the ids are still routable when
// sent explicitly, just not advertised in the catalog.
// sent explicitly, just not advertised in the catalog. The NO_THINKING_ALIAS_ENABLED
// feature flag is the stronger switch: it also stops the ids from routing (see
// src/sse/handlers/chat.ts), so nothing is advertised when it is off. Resolved once
// here and injected, keeping the open-sse helper I/O-free (one flag read per catalog
// build, not one per model).
if (!ctx.hideNoThinkVariants) {
finalModels = appendNoThinkingVariants(
finalModels,
ctx.prefixMode === "canonical" ? ctx.aliasToProviderId : undefined
ctx.prefixMode === "canonical" ? ctx.aliasToProviderId : undefined,
{ featureEnabled: isNoThinkingAliasEnabled() }
);
}

Expand Down Expand Up @@ -151,7 +160,9 @@ export async function applyCatalogPostFilters(
// #7694: advertise `<provider>/<model>-<tier>` variants for synced models that
// captured `reasoning.supported_efforts` at sync time (capabilities.effort_tiers).
// Derived from the already key-filtered list; skips codex/kimi (own suffix mechanism).
finalModels = appendSyncedEffortVariants(finalModels);
if (!isDisableThinkingLevelVariantsEnabled()) {
finalModels = appendSyncedEffortVariants(finalModels);
}

await yieldTurn();

Expand Down
14 changes: 10 additions & 4 deletions src/lib/db/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const CATALOG_RELEVANT_FEATURE_FLAGS = new Set([
"MODEL_CATALOG_INCLUDE_NAMES",
"MODELS_CATALOG_PREFIX_MODE",
"EXPOSE_CC_DISCOVERY_ALIASES",
"NO_THINKING_ALIAS_ENABLED",
"OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS",
]);

/**
Expand Down Expand Up @@ -60,14 +62,14 @@ export function setFeatureFlagOverride(key: string, value: string): void {
!definition.enumValues.includes(value)
) {
throw new Error(
`Invalid value "${value}" for enum flag ${key}. Allowed: ${definition.enumValues.join(", ")}`,
`Invalid value "${value}" for enum flag ${key}. Allowed: ${definition.enumValues.join(", ")}`
);
}
const db = getDbInstance();
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
NAMESPACE,
key,
value,
value
);
if (CATALOG_RELEVANT_FEATURE_FLAGS.has(key)) {
finishModelCatalogWriteWithoutBackup();
Expand All @@ -91,10 +93,14 @@ export function removeFeatureFlagOverride(key: string): void {
*/
export function clearAllFeatureFlagOverrides(): void {
const db = getDbInstance();
// Placeholders are derived from the set size — a hardcoded `IN (?, ?, ?)` breaks
// (parameter-count mismatch) the moment a flag is added to the set above.
const catalogFlags = Array.from(CATALOG_RELEVANT_FEATURE_FLAGS);
const placeholders = catalogFlags.map(() => "?").join(", ");
const hadRelevantOverride = Boolean(
db
.prepare("SELECT 1 FROM key_value WHERE namespace = ? AND key IN (?, ?, ?) LIMIT 1")
.get(NAMESPACE, ...Array.from(CATALOG_RELEVANT_FEATURE_FLAGS)),
.prepare(`SELECT 1 FROM key_value WHERE namespace = ? AND key IN (${placeholders}) LIMIT 1`)
.get(NAMESPACE, ...catalogFlags)
);
db.prepare("DELETE FROM key_value WHERE namespace = ?").run(NAMESPACE);
if (hadRelevantOverride) {
Expand Down
24 changes: 24 additions & 0 deletions src/shared/constants/featureFlagDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,30 @@ export const FEATURE_FLAG_DEFINITIONS: FeatureFlagDefinition[] = [
requiresRestart: false,
warningLevel: "info",
},
{
key: "NO_THINKING_ALIAS_ENABLED",
label: "No-Thinking Model Aliases",
description:
"Master switch for the no-think/<provider>/<model> gateway aliases. On (default): /v1/models advertises a no-thinking variant for every eligible thinking-capable Claude model, and a no-think/ id sent on a request resolves back to the real model with reasoning suppressed. Off: no variants are advertised and a no-think/ id is treated like any other unknown model id. The per-model ModelSpec.noThinkingAlias opt-in/opt-out still applies while this is on.",
descriptionI18nKey: "featureFlagNoThinkingAliasEnabledDescription",
category: "runtime",
defaultValue: "true",
type: "boolean",
requiresRestart: false,
warningLevel: "info",
},
{
key: "OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS",
label: "Disable Thinking Level Variants",
description:
"Disable the generation of thinking level variants (e.g. -low, -medium, -high) in the /v1/models catalog.",
descriptionI18nKey: "featureFlagOmnirouteDisableThinkingLevelVariantsDescription",
category: "runtime",
defaultValue: "false",
type: "boolean",
requiresRestart: false,
warningLevel: "info",
},
{
key: "OMNIROUTE_CHAT_VIRTUAL_LANES",
label: "Adaptive Virtual Admission Lanes",
Expand Down
33 changes: 33 additions & 0 deletions src/shared/utils/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,39 @@ export function getModelsCatalogPrefixMode(): ModelsCatalogPrefixMode {
return "dual";
}

/**
* No-thinking gateway alias master switch (`no-think/<provider>/<model>`).
*
* Fail-safe on: an unreadable flag store must not silently strip catalog
* variants a client already has configured, nor stop suppressing reasoning for
* a `no-think/…` id that was selected precisely to disable thinking. Matches the
* definition default (`"true"`), so the only way the feature turns off is an
* explicit operator override.
*/
export function isNoThinkingAliasEnabled(): boolean {
try {
return isFeatureFlagEnabled("NO_THINKING_ALIAS_ENABLED");
} catch (error) {
console.error(
"[featureFlags] Failed to resolve NO_THINKING_ALIAS_ENABLED, defaulting to enabled:",
error instanceof Error ? error.message : error
);
return true;
}
}

export function isDisableThinkingLevelVariantsEnabled(): boolean {
try {
return isFeatureFlagEnabled("OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS");
} catch (error) {
console.error(
"[featureFlags] Failed to resolve OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS, defaulting to disabled:",
error instanceof Error ? error.message : error
);
return false;
}
}

export function isArenaEloSyncEnabled(): boolean {
return isFeatureFlagEnabled("ARENA_ELO_SYNC_ENABLED");
}
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/disable-thinking-level-variants-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { appendSyncedEffortVariants } from "../../open-sse/utils/syncedEffortVariants";

describe("OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS helper behavior", () => {
it("appendSyncedEffortVariants generates variants for eligible models", () => {
const input = [
{
id: "my-provider/my-model",
capabilities: { effort_tiers: ["low", "medium", "high"] },
},
];
const result = appendSyncedEffortVariants(input);
assert.equal(result.length, 4);
assert.deepEqual(
result.map((m) => m.id),
[
"my-provider/my-model",
"my-provider/my-model-low",
"my-provider/my-model-medium",
"my-provider/my-model-high",
]
);
});
});
18 changes: 16 additions & 2 deletions tests/unit/feature-flags-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ const {
isArenaEloSyncEnabled,
isControlPlaneProxyDirectFallbackEnabled,
areContextWindowChecksDisabled,
isDisableThinkingLevelVariantsEnabled,
} = await import("../../src/shared/utils/featureFlags.ts");

// #10889 added OMNIROUTE_OIDC_DISABLE_PASSWORD_LOGIN, bumping the count to 51.
// The codex-app-server work then added OMNIROUTE_CODEX_APP_SERVER_ENABLED
// (feature flag gating the opt-in Codex app-server WebSocket transport),
// bumping it from 51 to 52.
const EXPECTED_FEATURE_FLAG_COUNT = 52;
// bumping it from 51 to 52. NO_THINKING_ALIAS_ENABLED (master switch for the
// no-think/<provider>/<model> gateway aliases) then bumped it from 52 to 53.
// OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS bumped it from 53 to 54.
const EXPECTED_FEATURE_FLAG_COUNT = 54;

// ──────────────────────────────────────────────────────
// Test group 1 — Flag definitions registry
Expand Down Expand Up @@ -198,6 +201,17 @@ describe("featureFlagDefinitions", () => {
assert.strictEqual(def.requiresRestart, false);
});

it("defines the no-thinking alias master switch as a runtime boolean enabled by default", () => {
// Default ON: turning the shipped no-think/ alias feature into a flag must not
// silently drop catalog variants operators already point their clients at.
const def = FEATURE_FLAG_DEFINITIONS.find((d) => d.key === "NO_THINKING_ALIAS_ENABLED");
assert.ok(def, "NO_THINKING_ALIAS_ENABLED should exist");
assert.strictEqual(def.category, "runtime");
assert.strictEqual(def.type, "boolean");
assert.strictEqual(def.defaultValue, "true");
assert.strictEqual(def.requiresRestart, false);
});

it("defines CLI profile auto-sync flags as CLI booleans disabled by default", () => {
for (const key of [
"OMNIROUTE_AUTO_SYNC_CODEX_PROFILES",
Expand Down