Files
OmniRoute/open-sse/config/providerModels.ts
Randi ebf06b5e6c 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>
2026-06-14 08:43:45 -03:00

186 lines
7.3 KiB
TypeScript

import { generateModels, generateAliasMap, type RegistryModel } from "./providerRegistry.ts";
// Provider models - Generated from providerRegistry.js (single source of truth)
export const PROVIDER_MODELS = generateModels();
// Provider ID to alias mapping - Generated from providerRegistry.js
export const PROVIDER_ID_TO_ALIAS = generateAliasMap();
// Helper functions
export function getProviderModels(aliasOrId: string): RegistryModel[] {
return PROVIDER_MODELS[aliasOrId] || [];
}
export function getDefaultModel(aliasOrId: string): string | null {
const models = PROVIDER_MODELS[aliasOrId];
return models?.[0]?.id || null;
}
export function getProviderModel(aliasOrId: string, modelId: string): RegistryModel | undefined {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return undefined;
return models.find((model) => model.id === modelId);
}
export function isValidModel(
aliasOrId: string,
modelId: string,
passthroughProviders = new Set<string>()
): boolean {
if (passthroughProviders.has(aliasOrId)) return true;
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return false;
return models.some((m) => m.id === modelId);
}
export function findModelName(aliasOrId: string, modelId: string): string {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return modelId;
const found = models.find((m) => m.id === modelId);
return found?.name || modelId;
}
export function getModelTargetFormat(aliasOrId: string, modelId: string): string | null {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return null;
const found = models.find((m) => m.id === modelId);
return found?.targetFormat || null;
}
export function getModelStripTypes(aliasOrId: string, modelId: string): string[] {
const models = PROVIDER_MODELS[aliasOrId];
if (!models) return [];
const found = models.find((m) => m.id === modelId);
return Array.isArray(found?.strip) ? [...found.strip] : [];
}
export function getModelsByProviderId(providerId: string): RegistryModel[] {
const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId;
return PROVIDER_MODELS[alias] || [];
}
const CLAUDE_MODEL_PATTERN = /(?:^|[\/._-])claude(?:[._-]|$)/;
const CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS = [/(?:^|[\/._-])haiku(?:[._-]|$)/] as const;
const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-";
export function supportsClaudeMaxEffort(modelId: string | null | undefined): boolean {
if (typeof modelId !== "string" || modelId.length === 0) return false;
const normalized = modelId.toLowerCase();
const claudeMatch = normalized.match(CLAUDE_MODEL_PATTERN);
if (!claudeMatch) return false;
const claudeScopedId = normalized.slice(claudeMatch.index ?? 0);
return !CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS.some((pattern) =>
pattern.test(claudeScopedId)
);
}
// Reasoning-effort suffixes the Claude/Claude-Code model picker appends to a base
// model id (an "Effort" slider: Low/Medium/High/Extra-High/Max). Longest/most
// specific token first so the `-${level}` match below picks "xhigh" before "high".
export const CLAUDE_EFFORT_SUFFIXES = ["xhigh", "max", "high", "medium", "low"] as const;
export type ClaudeEffortSuffix = (typeof CLAUDE_EFFORT_SUFFIXES)[number];
/**
* Split a trailing reasoning-effort suffix off a Claude model id, e.g.
* "claude-opus-4-8-high" -> { baseModel: "claude-opus-4-8", effort: "high" }.
*
* VS Code (and other clients) advertise claude-...-{low,medium,high,xhigh,max} via
* the model catalog; Anthropic has no such model id, so the suffixed string must be
* stripped before it is sent upstream (otherwise the relay returns HTTP 404) and
* surfaced as reasoning_effort so the translator / Claude-Code bridge convert it into
* Claude thinking/effort config. Mirrors codex's splitCodexReasoningSuffix but also
* covers "max" (codex's EFFORT_ORDER intentionally omits it). The `-${level}` anchor
* keeps "xhigh" from colliding with "high".
*/
export function splitClaudeEffortSuffix(model: unknown): {
baseModel: string;
effort: ClaudeEffortSuffix | null;
} {
const id = typeof model === "string" ? model : "";
const lower = id.toLowerCase();
for (const level of CLAUDE_EFFORT_SUFFIXES) {
if (lower.endsWith(`-${level}`)) {
return { baseModel: id.slice(0, -(level.length + 1)), effort: level };
}
}
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;
} {
const resolvedId = aliasOrId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX) ? "claude" : aliasOrId;
const alias = PROVIDER_ID_TO_ALIAS[resolvedId] || resolvedId;
const models = PROVIDER_MODELS[alias] || PROVIDER_MODELS[resolvedId] || null;
return { alias, models };
}
export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean {
const { models: providerModels } = resolveProviderModelList(aliasOrId);
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. 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 {
return supportsXHighEffort(aliasOrId, modelId);
}