fix(reasoning): map max effort to xhigh by default (#3826)

Reuse the xhigh opt-out policy for OpenAI-compatible `max` normalization: non-Claude
providers map `max` to `xhigh` unless the target model explicitly opts out (DeepSeek
via OpenRouter supports xhigh), downgrading to `high` only on explicit opt-outs.
Resolves common Claude aliases (anthropic/claude-opus-4.6, anthropic.claude-opus-4-6,
short/dated/-thinking variants) back to the canonical Claude xhigh-support list, and
keeps literal `max` pass-through for native Claude/CC providers that support it. Also
moves the mistral/github reasoning-effort rejection ahead of normalization (it was
previously dead code for `max`). Includes the v3.8.25 release file-size re-baseline.

Integrated into release/v3.8.25.

Co-authored-by: R.D. <rogerproself@gmail.com>
This commit is contained in:
Randi
2026-06-14 07:43:45 -04:00
committed by GitHub
parent 9f2d062083
commit ebf06b5e6c
6 changed files with 183 additions and 48 deletions

View File

@@ -19,6 +19,7 @@
- **fix(sse): retry once on an early stream close (`STREAM_EARLY_EOF`) for single-model requests** — flaky OpenAI-compatible upstreams (e.g. NVIDIA NIM with minimax-m3 / qwen3.5 / glm-5.1) intermittently send HTTP 200 then close the SSE with zero useful frames, surfacing as a 502 "Stream ended before producing useful content". Only Antigravity got an early-close retry; every other provider returned the 502 immediately on the non-combo single-model path. A bounded one-retry (early-close only — not readiness-timeout — and without marking the account unavailable) now generalizes it. (The separate qwen-web validation SSRF part of the same report was already fixed in v3.8.24, [#3767](https://github.com/diegosouzapw/OmniRoute/pull/3767).) ([#3758](https://github.com/diegosouzapw/OmniRoute/issues/3758) — thanks @Svatosalav)
- **fix(models): preserve eye-hidden models across auto-sync / import** — hiding models via the visibility (eye) toggle to keep only a combo's models was undone on every model import or auto-sync, which re-showed all of them. The sync re-import treated "hidden" identically to "deleted" and dropped both; a distinct `isDeleted` marker now separates the trash/delete path (still dropped on re-import, #3199) from the eye toggle (preserved as listed-but-hidden), and eye-hidden models are no longer re-aliased into the routable catalog on sync. ([#3782](https://github.com/diegosouzapw/OmniRoute/issues/3782) — thanks @xenstar)
- **fix(providers): correct the lmarena cookie hint (`session``arena-auth-prod-v1`)** — the lmarena credential hint asked for a cookie named `session`, but lmarena.ai's real auth cookie is `arena-auth-prod-v1`, so users who pasted only `session=…` hit validation failures. The credential name, placeholder and storage keys now use the correct name (the legacy `session` key is retained for back-compat with already-saved credentials). ([#3810](https://github.com/diegosouzapw/OmniRoute/issues/3810) — thanks @xspylol)
- **fix(reasoning): normalize OpenAI-compatible `max` effort to `xhigh` by default** — OpenAI-compatible providers do not accept literal `max`, but some upstreams (for example DeepSeek through OpenRouter) support `xhigh`; `max` now maps to `xhigh` unless the target model explicitly opts out of `xhigh`, with Claude alias variants still honoring the canonical Claude opt-out list. (thanks @rdself)
---

View File

@@ -64,7 +64,7 @@
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 932,
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 880,
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1072,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1089,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 983,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1580,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
@@ -99,9 +99,9 @@
"src/shared/constants/cliTools.ts": 875,
"src/shared/constants/pricing.ts": 1470,
"src/shared/constants/providers.ts": 3147,
"src/shared/constants/sidebarVisibility.ts": 990,
"src/shared/constants/sidebarVisibility.ts": 1006,
"src/shared/services/cliRuntime.ts": 1090,
"src/shared/validation/schemas.ts": 2519,
"src/shared/validation/schemas.ts": 2522,
"src/sse/handlers/chat.ts": 1425,
"src/sse/services/auth.ts": 2207
},
@@ -121,5 +121,6 @@
"_rebaseline_2026_06_13_3474_grok_403": "Re-baseline #3474 (mensagem clara no 403 anti-bot do Grok): validation.ts 4302→4348 (+46). Crescimento = helper isGrokAntiBotBlock() + branch 403 de 3 tiers (auth-shaped / anti-bot-IP-reputation / upstream-error). Lógica coesa de classificação no validator; não-extraível.",
"_rebaseline_2026_06_13_3324_windsurf_devin": "Re-baseline #3324 (windsurf auth text + devin error propagation): route.ts 897→903 (+6, texto da instrução windsurf→fluxo command-palette) + sseParser.ts ADICIONADO como frozen 812 (era 746, +66 = helper extractSSEErrorMessage que faz surface do erro real SSE em vez do 502 genérico). 812 fica 12 acima do cap 800 — helper coeso no parser de SSE, congelado com justificativa (precedente providerLimits/useProviderConnections).",
"_rebaseline_2026_06_13_2743d_skipbreaker": "Re-baseline #2743 gap-d (testar consumer do skipProviderBreaker): combo.ts 5131→5162 (+31). Crescimento = extração do boolean inline da decisão de circuit-breaker para o predicado puro EXPORTADO shouldRecordProviderBreakerFailure() (byte-idêntico) + JSDoc, para torná-lo unit-testável sem o harness completo de combo. Shrink estrutural segue com #3501.",
"_rebaseline_2026_06_13_v3825_prettier_reconcile": "Reconciliação tardia: o prettier do pre-commit reformatou 3 arquivos DEPOIS da medição de file-size dos PRs, inflando linhas além do baseline setado — OAuthModal.tsx 956→960 e providers.ts 3146→3147 (#3324), combo.ts 5162→5164 (#2743d). Bumps de reformatação automática (sem lógica nova). LIÇÃO: medir file-size pós-commit (pós-prettier), não antes."
"_rebaseline_2026_06_13_v3825_prettier_reconcile": "Reconciliação tardia: o prettier do pre-commit reformatou 3 arquivos DEPOIS da medição de file-size dos PRs, inflando linhas além do baseline setado — OAuthModal.tsx 956→960 e providers.ts 3146→3147 (#3324), combo.ts 5162→5164 (#2743d). Bumps de reformatação automática (sem lógica nova). LIÇÃO: medir file-size pós-commit (pós-prettier), não antes.",
"_rebaseline_2026_06_14_3826_release_drift": "Re-baseline release/v3.8.25 drift already documented from #3809 owner changes: ProxyRegistryManager.tsx 1072→1089, sidebarVisibility.ts 990→1006, schemas.ts 2519→2522. This PR does not touch those source files; updating the frozen values restores Fast Quality Gates on the current release branch."
}

View File

@@ -106,6 +106,48 @@ export function splitClaudeEffortSuffix(model: unknown): {
return { baseModel: id, effort: null };
}
function getDatedClaudeAliasDate(candidate: string, modelId: string): number | null {
if (!modelId.startsWith(`${candidate}-`)) return null;
const suffix = modelId.slice(candidate.length + 1);
if (!/^\d{8}$/.test(suffix)) return null;
return Number(suffix);
}
function findCanonicalClaudeEffortModel(modelId: string): RegistryModel | undefined {
const id = splitClaudeEffortSuffix(modelId).baseModel.toLowerCase();
const claudeMatch = id.match(CLAUDE_MODEL_PATTERN);
if (!claudeMatch) return undefined;
const claudeOffset = claudeMatch[0]?.indexOf("claude") ?? 0;
const claudeStart = (claudeMatch.index ?? 0) + Math.max(claudeOffset, 0);
const claudeScopedId = id.slice(claudeStart).replace(/\.(?=\d)/g, "-");
const candidates = [claudeScopedId];
if (claudeScopedId.endsWith("-thinking")) {
candidates.push(claudeScopedId.slice(0, -"-thinking".length));
}
const claudeModels = getModelsByProviderId("claude");
for (const candidate of candidates) {
const exact = claudeModels.find((entry) => entry.id.toLowerCase() === candidate);
if (exact) return exact;
if (!/-\d+-\d+$/.test(candidate)) continue;
const datedAliases = claudeModels
.map((entry) => ({
entry,
date: getDatedClaudeAliasDate(candidate, entry.id.toLowerCase()),
}))
.filter(
(item): item is { entry: RegistryModel; date: number } =>
item.date !== null && item.entry.supportsXHighEffort !== undefined
)
.sort((a, b) => b.date - a.date || a.entry.id.localeCompare(b.entry.id));
if (datedAliases[0]) return datedAliases[0].entry;
}
return undefined;
}
function resolveProviderModelList(aliasOrId: string): {
alias: string;
models: RegistryModel[] | null;
@@ -118,25 +160,26 @@ function resolveProviderModelList(aliasOrId: string): {
export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean {
const { models: providerModels } = resolveProviderModelList(aliasOrId);
// Unknown provider (not in registry) — pass through unchanged.
if (!providerModels) return true;
const model = providerModels.find((entry) => entry.id === modelId);
const model = providerModels?.find((entry) => entry.id === modelId);
if (model?.supportsXHighEffort !== undefined) {
return model.supportsXHighEffort !== false;
}
const canonicalClaudeModel = findCanonicalClaudeEffortModel(modelId);
if (canonicalClaudeModel?.supportsXHighEffort !== undefined) {
return canonicalClaudeModel.supportsXHighEffort !== false;
}
// Keep explicit false entries as the unsupported-model list. Unlisted models
// and models without an explicit flag pass through unchanged.
return model?.supportsXHighEffort !== false;
// and models without an explicit flag pass through unchanged. Unknown
// providers follow the same rule except for canonical Claude aliases above.
return true;
}
/** @deprecated Use supportsXHighEffort(); max normalization now follows the same opt-out policy. */
export function supportsXHighEffortForMaxNormalization(
aliasOrId: string,
modelId: string
): boolean {
const { alias, models: providerModels } = resolveProviderModelList(aliasOrId);
if (!providerModels) return true;
const model = providerModels.find((entry) => entry.id === modelId);
if (alias === "cc") {
return model?.supportsXHighEffort !== false;
}
return model?.supportsXHighEffort === true;
return supportsXHighEffort(aliasOrId, modelId);
}

View File

@@ -1,10 +1,6 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
import {
supportsClaudeMaxEffort,
supportsXHighEffort,
supportsXHighEffortForMaxNormalization,
} from "../config/providerModels.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
import type { PoolConfig } from "../services/sessionPool/types.ts";
import type { Session } from "../services/sessionPool/session.ts";
import { SessionPool } from "../services/sessionPool/sessionPool.ts";
@@ -232,10 +228,10 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
* provider. Apply provider-aware sanitation here (after transformRequest, so
* reintroductions by per-provider transforms are also caught) before fetch.
* xhigh support is opt-out: pass through unchanged unless the registry marks
* a model as unsupported. max support is Claude/CC-compatible only and
* a model as unsupported. Literal max support is Claude/CC-compatible only and
* intentionally separate: older Opus/Sonnet models may support max even when
* they do not support xhigh. For OpenAI-shape providers, keep the existing
* max normalization behavior.
* they do not support xhigh. For OpenAI-shape providers, max normalizes to
* xhigh by default and falls back to high only for explicit xhigh opt-outs.
*/
const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i;
@@ -265,9 +261,28 @@ export function sanitizeReasoningEffortForProvider(
const effortStr = typeof effort === "string" ? effort.toLowerCase() : "";
const modelStr = model || "";
const rejecting =
(provider === "mistral" && MISTRAL_NO_REASONING_EFFORT_PATTERN.test(modelStr)) ||
(provider === "github" && GITHUB_NO_REASONING_EFFORT_PATTERN.test(modelStr));
if (rejecting) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: removed unsupported reasoning_effort`
);
const next: Record<string, unknown> = { ...b };
delete next.reasoning_effort;
if (reasoning) {
const r = { ...reasoning };
delete r.effort;
if (Object.keys(r).length === 0) delete next.reasoning;
else next.reasoning = r;
}
return next;
}
const supportsXHigh = supportsXHighEffort(provider, modelStr);
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh;
const supportsXHighForMax = supportsXHighEffortForMaxNormalization(provider, modelStr);
const supportsXHighForMax = supportsXHigh;
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);
const shouldNormalizeMaxToXHigh = effortStr === "max" && !supportsMax && supportsXHighForMax;
const shouldDowngradeMax = effortStr === "max" && !supportsMax && !supportsXHighForMax;
@@ -302,25 +317,6 @@ export function sanitizeReasoningEffortForProvider(
return next;
}
const rejecting =
(provider === "mistral" && MISTRAL_NO_REASONING_EFFORT_PATTERN.test(modelStr)) ||
(provider === "github" && GITHUB_NO_REASONING_EFFORT_PATTERN.test(modelStr));
if (rejecting) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: removed unsupported reasoning_effort`
);
const next: Record<string, unknown> = { ...b };
delete next.reasoning_effort;
if (reasoning) {
const r = { ...reasoning };
delete r.effort;
if (Object.keys(r).length === 0) delete next.reasoning;
else next.reasoning = r;
}
return next;
}
return body;
}

View File

@@ -73,7 +73,7 @@ test("sanitizeReasoningEffortForProvider: Anthropic-compatible dynamic provider
assert.equal((result as any).reasoning_effort, "high");
});
test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades max → high", () => {
test("sanitizeReasoningEffortForProvider: xiaomi-mimo normalizes max → xhigh by default", () => {
const log = makeLog();
const body = {
model: "mimo-v2.5-pro",
@@ -81,6 +81,48 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades max → high",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", log);
assert.equal((result as any).reasoning_effort, "xhigh");
assert.ok(
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
"logs the normalization"
);
});
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek normalizes max → xhigh", () => {
const log = makeLog();
const body = {
model: "deepseek/deepseek-v4-pro",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(
body,
"openrouter",
"deepseek/deepseek-v4-pro",
log
);
assert.notEqual(result, body, "must return a new object when mutating");
assert.equal((result as any).reasoning_effort, "xhigh");
assert.ok(
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
"logs the normalization"
);
});
test("sanitizeReasoningEffortForProvider: OpenRouter Claude opt-out aliases downgrade max → high", () => {
const log = makeLog();
const body = {
model: "anthropic/claude-opus-4.6",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(
body,
"openrouter",
"anthropic/claude-opus-4.6",
log
);
assert.notEqual(result, body, "must return a new object when mutating");
assert.equal((result as any).reasoning_effort, "high");
assert.ok(
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → high/.test(m)),
@@ -218,6 +260,34 @@ test("sanitizeReasoningEffortForProvider: github/claude-opus strips reasoning_ef
assert.equal((result as any).reasoning_effort, undefined);
});
test("sanitizeReasoningEffortForProvider: rejecting providers strip max before normalization", () => {
const mistralBody = {
model: "devstral-2512",
reasoning_effort: "max",
messages: [],
};
const mistralResult = sanitizeReasoningEffortForProvider(
mistralBody,
"mistral",
"devstral-2512",
null
);
assert.equal((mistralResult as any).reasoning_effort, undefined);
const githubBody = {
model: "claude-opus-4-6",
reasoning_effort: "max",
messages: [],
};
const githubResult = sanitizeReasoningEffortForProvider(
githubBody,
"github",
"claude-opus-4-6",
null
);
assert.equal((githubResult as any).reasoning_effort, undefined);
});
test("sanitizeReasoningEffortForProvider: mistral/devstral strips reasoning object when only effort present", () => {
const body = {
model: "devstral-2512",

View File

@@ -121,15 +121,27 @@ test("xhigh effort support defaults to pass-through and opts out explicit false
assert.equal(supportsXHighEffort("anthropic-compatible-cc-test", "claude-opus-4-6"), false);
assert.equal(supportsXHighEffort("anthropic-compatible-cc-test", "claude-opus-4-7"), true);
assert.equal(supportsXHighEffort("openrouter", "deepseek/deepseek-v4-pro"), true);
assert.equal(supportsXHighEffort("openrouter", "anthropic/claude-opus-4.6"), false);
assert.equal(supportsXHighEffort("openrouter", "anthropic/claude-opus-4.7"), true);
assert.equal(supportsXHighEffort("openrouter", "anthropic/claude-opus-4.5"), false);
assert.equal(supportsXHighEffort("bedrock", "anthropic.claude-opus-4-6"), false);
assert.equal(supportsXHighEffort("bedrock", "anthropic.claude-opus-4-7"), true);
assert.equal(supportsXHighEffort("github", "claude-opus-4.6"), false);
assert.equal(supportsXHighEffort("github", "claude-opus-4.7"), true);
assert.equal(supportsXHighEffort("unknown-provider", "vendor/claude-opus-4.6"), false);
assert.equal(
supportsXHighEffort("openrouter", "anthropic/claude-opus-4.6-thinking-xhigh"),
false
);
assert.equal(supportsXHighEffort("deepseek", "deepseek-v4-pro"), true);
});
test("max normalization keeps existing xhigh opt-in behavior", () => {
test("max normalization follows xhigh opt-out behavior", () => {
assert.equal(
supportsXHighEffortForMaxNormalization("openai-compatible-free1", "gemini-3.1-pro-preview"),
true
);
assert.equal(supportsXHighEffortForMaxNormalization("xiaomi-mimo", "mimo-v2.5-pro"), false);
assert.equal(supportsXHighEffortForMaxNormalization("xiaomi-mimo", "mimo-v2.5-pro"), true);
assert.equal(
supportsXHighEffortForMaxNormalization("anthropic-compatible-cc-test", "claude-opus-4-6"),
false
@@ -148,6 +160,18 @@ test("max normalization keeps existing xhigh opt-in behavior", () => {
);
assert.equal(
supportsXHighEffortForMaxNormalization("openrouter", "deepseek/deepseek-v4-pro"),
true
);
assert.equal(
supportsXHighEffortForMaxNormalization("openrouter", "anthropic/claude-opus-4.6"),
false
);
assert.equal(
supportsXHighEffortForMaxNormalization("openrouter", "anthropic/claude-opus-4.7"),
true
);
assert.equal(
supportsXHighEffortForMaxNormalization("bedrock", "anthropic.claude-opus-4-6"),
false
);
});