From 5d07bf32fe46ded62a8700e8af8a9c16f9f95f64 Mon Sep 17 00:00:00 2001 From: b3nw Date: Sun, 30 Aug 2026 09:09:19 -0500 Subject: [PATCH] feat(catalog): add feature flag to disable thinking level variants in catalog (#11971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature flag para desabilitar variantes de nível de thinking no catálogo, com testes de gate e de settings. Validado no worktree combinado. Obrigado! --- .../disable-thinking-level-variants.md | 1 + src/app/api/v1/models/catalogResponse.ts | 19 ++++++++--- src/lib/db/featureFlags.ts | 14 +++++--- .../constants/featureFlagDefinitions.ts | 24 ++++++++++++++ src/shared/utils/featureFlags.ts | 33 +++++++++++++++++++ ...sable-thinking-level-variants-gate.test.ts | 25 ++++++++++++++ tests/unit/feature-flags-settings.test.ts | 18 ++++++++-- 7 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 changelog.d/features/disable-thinking-level-variants.md create mode 100644 tests/unit/disable-thinking-level-variants-gate.test.ts diff --git a/changelog.d/features/disable-thinking-level-variants.md b/changelog.d/features/disable-thinking-level-variants.md new file mode 100644 index 0000000000..c5d2930e3f --- /dev/null +++ b/changelog.d/features/disable-thinking-level-variants.md @@ -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)) diff --git a/src/app/api/v1/models/catalogResponse.ts b/src/app/api/v1/models/catalogResponse.ts index 4005bd4e40..b1d5a032d7 100644 --- a/src/app/api/v1/models/catalogResponse.ts +++ b/src/app/api/v1/models/catalogResponse.ts @@ -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"; @@ -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() } ); } @@ -151,7 +160,9 @@ export async function applyCatalogPostFilters( // #7694: advertise `/-` 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(); diff --git a/src/lib/db/featureFlags.ts b/src/lib/db/featureFlags.ts index ea28690498..338a4188dc 100644 --- a/src/lib/db/featureFlags.ts +++ b/src/lib/db/featureFlags.ts @@ -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", ]); /** @@ -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(); @@ -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) { diff --git a/src/shared/constants/featureFlagDefinitions.ts b/src/shared/constants/featureFlagDefinitions.ts index 1ad6616056..36e96694b5 100644 --- a/src/shared/constants/featureFlagDefinitions.ts +++ b/src/shared/constants/featureFlagDefinitions.ts @@ -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// 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", diff --git a/src/shared/utils/featureFlags.ts b/src/shared/utils/featureFlags.ts index 54874fcbff..84f44f9565 100644 --- a/src/shared/utils/featureFlags.ts +++ b/src/shared/utils/featureFlags.ts @@ -112,6 +112,39 @@ export function getModelsCatalogPrefixMode(): ModelsCatalogPrefixMode { return "dual"; } +/** + * No-thinking gateway alias master switch (`no-think//`). + * + * 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"); } diff --git a/tests/unit/disable-thinking-level-variants-gate.test.ts b/tests/unit/disable-thinking-level-variants-gate.test.ts new file mode 100644 index 0000000000..fc80e31e44 --- /dev/null +++ b/tests/unit/disable-thinking-level-variants-gate.test.ts @@ -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", + ] + ); + }); +}); diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index 708ebed493..d954561b55 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -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// 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 @@ -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",