From 9fc3b2921733a7d4ceb0cb22e539eb2650147088 Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:31:12 +0800 Subject: [PATCH] fix(catalog): scope combo reasoning efforts by connection (#10723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — locally validated (72/72 focused tests, typecheck:core clean, all static gates green) after resolving base-drift conflicts (catalog.ts cooperative-yield insertion point, modelMetadataRegistry.ts snapshot-param signature). CI's red checks (Unit Tests fast-path shards, Fast Quality Gates, Docs Gates) are confirmed PRE-EXISTING base-red on the pure release tip — reproduced tests/unit/db-driver-bundling-externals.test.ts, tests/unit/model-catalog-runtime-invalidation.test.ts and others failing identically against origin/release/v3.8.50 with zero PR content, unrelated to this change. Thanks for the design and for absorbing #10724's value here — great work on both review rounds! --- ...mbo-connection-scoped-reasoning-efforts.md | 1 + open-sse/config/grokBuild.ts | 1 + open-sse/executors/grok-cli.ts | 6 +- .../ModelCapabilityOverridesTab.tsx | 40 +- .../api/model-capability-overrides/route.ts | 42 +- .../models/discovery/providerModelsConfig.ts | 40 +- src/app/api/v1/models/catalog.ts | 142 ++++++- src/app/api/v1/models/catalogHelpers.ts | 60 +++ src/i18n/messages/en.json | 1 + src/lib/db/modelCapabilityOverrides.ts | 103 ++++- src/lib/db/models.ts | 16 +- src/lib/modelCapabilities.ts | 48 ++- src/lib/modelCapabilityResolutionSnapshot.ts | 18 + src/lib/modelMetadataRegistry.ts | 16 +- .../reasoning/reasoningEffortsOverride.ts | 55 +++ tests/unit/catalog-helpers-extraction.test.ts | 68 ++++ tests/unit/grok-cli-responses-compat.test.ts | 1 + tests/unit/model-capability-overrides.test.ts | 54 +++ ...model-catalog-runtime-invalidation.test.ts | 16 + .../models-catalog-combo-metadata.test.ts | 366 +++++++++++++++++- .../provider-models-discovery-split.test.ts | 29 ++ .../reasoning-efforts-override-parser.test.ts | 41 ++ ...del-capability-overrides-tab-9557.test.tsx | 76 ++++ 23 files changed, 1159 insertions(+), 81 deletions(-) create mode 100644 changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md create mode 100644 src/shared/reasoning/reasoningEffortsOverride.ts create mode 100644 tests/unit/reasoning-efforts-override-parser.test.ts diff --git a/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md new file mode 100644 index 0000000000..3a53f1323a --- /dev/null +++ b/changelog.d/fixes/combo-connection-scoped-reasoning-efforts.md @@ -0,0 +1 @@ +- **fix(catalog):** derive combo reasoning-effort tiers from the exact runtime-selectable connection scope, intersecting dynamic, pinned, allowlisted, and compatible provider-node evidence while failing closed on unknown capabilities. diff --git a/open-sse/config/grokBuild.ts b/open-sse/config/grokBuild.ts index c6e7dca7bf..ab648afd18 100644 --- a/open-sse/config/grokBuild.ts +++ b/open-sse/config/grokBuild.ts @@ -11,6 +11,7 @@ export const GROK_BUILD_TOKEN_URL = `${GROK_BUILD_OAUTH_ISSUER}/oauth2/token`; export const GROK_BUILD_DEFAULT_CLIENT_VERSION = "0.2.106"; export const GROK_BUILD_DEFAULT_CONTEXT_WINDOW = 256_000; export const GROK_BUILD_DEFAULT_REASONING_EFFORT = "high"; +export const GROK_BUILD_SUPPORTED_REASONING_EFFORTS = Object.freeze(["low", "medium", "high"]); export const GROK_BUILD_CLIENT_IDENTIFIER = "grok-shell"; export const GROK_BUILD_TOKEN_AUTH = "xai-grok-cli"; export const GROK_BUILD_REASONING_INCLUDE = "reasoning.encrypted_content"; diff --git a/open-sse/executors/grok-cli.ts b/open-sse/executors/grok-cli.ts index 275e8ebfdd..fc37b23ce2 100644 --- a/open-sse/executors/grok-cli.ts +++ b/open-sse/executors/grok-cli.ts @@ -12,13 +12,14 @@ import { GROK_BUILD_DEFAULT_REASONING_EFFORT, GROK_BUILD_REASONING_INCLUDE, GROK_BUILD_RESPONSES_URL, + GROK_BUILD_SUPPORTED_REASONING_EFFORTS, GROK_BUILD_TOKEN_URL, } from "../config/grokBuild.ts"; import { resolvePublicCred } from "../utils/publicCreds.ts"; import { BaseExecutor, type ExecutorLog, type ProviderCredentials } from "./base.ts"; const GROK_BUILD_MAX_TOOLS = 200; -const GROK_BUILD_SUPPORTED_REASONING_EFFORTS = new Set(["low", "medium", "high"]); +const GROK_BUILD_REASONING_EFFORT_SET = new Set(GROK_BUILD_SUPPORTED_REASONING_EFFORTS); const GROK_BUILD_REFRESH_MAX_ATTEMPTS = 3; const GROK_BUILD_REFRESH_MIN_DELAY_MS = 200; const GROK_BUILD_TERMINAL_REFRESH_ERRORS = new Set(["invalid_grant", "invalid_client"]); @@ -33,7 +34,6 @@ const GROK_BUILD_UNSUPPORTED_PARAMS = [ "reasoning_effort", ]; - /** * Grok Build's cli-chat-proxy is stricter about Responses `function_call_output.output` * than OpenAI's Responses API. Agent tool results can contain truncated / incomplete @@ -128,7 +128,7 @@ function normalizeGrokBuildReasoning( ): Record | null { const reasoning = asRequestRecord(value); const hasExplicitEffort = Object.prototype.hasOwnProperty.call(reasoning, "effort"); - if (!GROK_BUILD_SUPPORTED_REASONING_EFFORTS.has(String(reasoning.effort))) { + if (!GROK_BUILD_REASONING_EFFORT_SET.has(String(reasoning.effort))) { delete reasoning.effort; } if (model === "grok-composer-2.5-fast") { diff --git a/src/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab.tsx index 4989c397eb..e0b0ee66ab 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab.tsx @@ -9,7 +9,9 @@ import { type PricingCatalogProvider, } from "@/lib/modelCapabilityOverrideTargets"; -type ModelOverrideKey = "context_length" | "max_input_tokens" | "max_output_tokens"; +type ModelOverrideKey = + "context_length" | "max_input_tokens" | "max_output_tokens" | "reasoning_efforts"; +type ModelOverrideValue = number | string[]; type StatusTone = "success" | "error" | "info"; type ModelOverrideTarget = import("@/lib/modelCapabilityOverrideTargets").ModelOverrideTarget; @@ -22,7 +24,7 @@ interface PricingCatalogModel { interface ModelCapabilityOverride { target: string; key: ModelOverrideKey; - value: number; + value: ModelOverrideValue; } interface StatusMessage { @@ -70,7 +72,7 @@ function useModelCapabilityOverridesData() { }, [showStatus, t]); const saveOverride = useCallback( - async (target: string, key: ModelOverrideKey, value: number) => { + async (target: string, key: ModelOverrideKey, value: number | string) => { try { const response = await fetch("/api/model-capability-overrides", { method: "PATCH", @@ -150,7 +152,7 @@ function ModelCapabilityOverridesPanel({ }: { targets: ModelOverrideTarget[]; overrides: ModelCapabilityOverride[]; - onSave: (target: string, key: ModelOverrideKey, value: number) => void; + onSave: (target: string, key: ModelOverrideKey, value: number | string) => void; onRemove: (target: string, key: ModelOverrideKey) => void; }) { const [selectedTarget, setSelectedTarget] = useState(""); @@ -294,7 +296,7 @@ function ModelOverrideEditor({ activeOverrides: ModelCapabilityOverride[]; activeTarget: string; onRemove: (target: string, key: ModelOverrideKey) => void; - onSave: (target: string, key: ModelOverrideKey, value: number) => void; + onSave: (target: string, key: ModelOverrideKey, value: number | string) => void; }) { const t = useTranslations("settings"); return ( @@ -316,13 +318,18 @@ function ModelOverrideForm({ onSave, }: { activeTarget: string; - onSave: (target: string, key: ModelOverrideKey, value: number) => void; + onSave: (target: string, key: ModelOverrideKey, value: number | string) => void; }) { const t = useTranslations("settings"); const [key, setKey] = useState("context_length"); const [value, setValue] = useState(""); + const isReasoningEfforts = key === "reasoning_efforts"; const numericValue = Number(value); - const saveDisabled = !activeTarget || !Number.isInteger(numericValue) || numericValue <= 0; + const saveDisabled = + !activeTarget || + (isReasoningEfforts + ? value.length === 0 + : !Number.isInteger(numericValue) || numericValue <= 0); return (
@@ -334,14 +341,19 @@ function ModelOverrideForm({ + setValue(event.target.value)} - placeholder={t("modelOverrideValuePlaceholder")} + placeholder={t( + isReasoningEfforts + ? "modelOverrideReasoningEffortsPlaceholder" + : "modelOverrideValuePlaceholder" + )} className="flex-1 px-3 py-2 text-xs bg-bg-base border border-border rounded-md focus:outline-none focus:border-primary" />