From 7214efa86e22f066a6c159fe3d7533808f4e30bc Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Thu, 7 May 2026 18:49:16 +0700 Subject: [PATCH] fix: add fuzzy auto-combo routing for 'auto/*' model prefix (#2010) Integrated into release/v3.8.0 --- src/app/docs/components/DocsSidebarClient.tsx | 2 +- src/sse/handlers/chat.ts | 63 ++++++++++++++++++- src/sse/handlers/chatHelpers.ts | 42 ++++++++++++- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/app/docs/components/DocsSidebarClient.tsx b/src/app/docs/components/DocsSidebarClient.tsx index d50464af6b..d3955b53b7 100644 --- a/src/app/docs/components/DocsSidebarClient.tsx +++ b/src/app/docs/components/DocsSidebarClient.tsx @@ -12,7 +12,7 @@ export function DocsSidebarClient({ mobileOnly = false }: { mobileOnly?: boolean const [isOpen, setIsOpen] = useState(false); // Extract slug from pathname (e.g., /docs/setup-guide -> setup-guide) - const currentSlug = pathname.split("/").filter(Boolean).pop() || ""; + const currentSlug = pathname.split("/").filter(Boolean).pop(); const isActive = (slug: string) => currentSlug === slug; diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 7ff8e16167..1cb83f69a1 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -278,7 +278,21 @@ export async function handleChat(request: any, clientRawRequest: any = null) { // Check if model is a combo (has multiple models with fallback) telemetry.startPhase("resolve"); - const combo: any = await getComboForModel(resolvedModelStr); + let combo: any = await getComboForModel(resolvedModelStr); + + // "auto" prefix fuzzy matching: "auto/fast" → "auto/best-fast", etc. + // parseModel splits "auto/fast" into provider="auto" which isn't a real provider. + if (!combo && resolvedModelStr.startsWith("auto/")) { + const suffix = resolvedModelStr.slice(5); + for (const candidate of [`auto/best-${suffix}`, `auto/${suffix}`]) { + combo = await getComboForModel(candidate); + if (combo) { + log.info("ROUTING", `"${resolvedModelStr}" → combo "${candidate}" (auto fuzzy)`); + break; + } + } + } + if (combo) { log.info( "CHAT", @@ -493,6 +507,53 @@ async function handleSingleModelChat( ); if (resolved.error) return resolved.error; + // Safety net: if auto-combo resolution returned a combo object, redirect + // to combo flow. This handles the case where the auto-fuzzy match in + // resolveModelOrError found a combo but the main handler's combo lookup missed it. + if ((resolved as any).combo) { + const redirectCombo = (resolved as any).combo; + log.info("ROUTING", `Auto-combo redirect from handleSingleModelChat for "${modelStr}"`); + log.info("ROUTING", `Auto-combo redirect to combo flow for "${modelStr}"`); + return handleComboChat({ + body, + combo: redirectCombo, + handleSingleModel: ( + b: any, + m: string, + target?: { + connectionId?: string | null; + executionKey?: string | null; + stepId?: string | null; + } + ) => + handleSingleModelChat( + b, + m, + clientRawRequest, + request, + redirectCombo.name ?? modelStr, + apiKeyInfo, + telemetry, + { + sessionId: "", // safety-net redirect doesn't have session context + forceLiveComboTest: false, + forcedConnectionId: null, + allowedConnectionIds: null, + comboStepId: null, + comboExecutionKey: null, + }, + redirectCombo.strategy ?? "priority", + false + ), + isModelAvailable: async () => true, + log, + settings: {}, + allCombos: [], + relayOptions: undefined, + signal: request?.signal ?? null, + }); + } + const { provider, model, sourceFormat, targetFormat, extendedContext } = resolved; const forceLiveComboTest = runtimeOptions.forceLiveComboTest === true; const hasForcedConnection = diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 95fdfb73de..33bbea24b8 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -1,4 +1,4 @@ -import { getModelInfo } from "../services/model"; +import { getModelInfo, getComboForModel } from "../services/model"; import { clearAccountError, markAccountUnavailable } from "../services/auth"; import * as log from "../utils/logger"; import { updateProviderCredentials } from "../services/tokenRefresh"; @@ -130,6 +130,46 @@ export async function resolveModelOrError( } } + // "auto" is a combo prefix, not a provider. parseModel("auto/fast") splits it into + // provider="auto" model="fast" — redirect to matching combo before credential lookup fails. + if (modelInfo.provider === "auto") { + const exactCombo = await getComboForModel(modelStr); + if (exactCombo) { + log.info("ROUTING", `"auto" provider → combo "${modelStr}"`); + return { combo: exactCombo, provider: "auto", model: modelInfo.model }; + } + + // Fuzzy: "fast" → "auto/best-fast", "chat" → "auto/best-chat" + const suffix = modelInfo.model || ""; + for (const candidate of [`auto/best-${suffix}`, `auto/${suffix}`]) { + const fuzzyCombo = await getComboForModel(candidate); + if (fuzzyCombo) { + log.info("ROUTING", `"auto/${suffix}" → combo "${candidate}" (fuzzy)`); + return { combo: fuzzyCombo, provider: "auto", model: suffix }; + } + } + + // List available auto/* combos in error + const available: string[] = []; + try { + const { getCombos } = await import("@/lib/localDb"); + const all = await getCombos(); + for (const c of all) { + if (c.name?.startsWith("auto/")) available.push(c.name); + } + } catch { + /* DB unavailable */ + } + + const hint = + available.length > 0 + ? ` Available auto combos: ${available.join(", ")}` + : " No auto combos configured — create one in the Dashboard."; + const message = `Model '${modelStr}' is not a valid combo or provider.${hint}`; + log.warn("CHAT", message, { model: modelStr }); + return { error: errorResponse(HTTP_STATUS.BAD_REQUEST, message) }; + } + if (!modelInfo.provider) { if ((modelInfo as any).errorType === "ambiguous_model") { // Family disambiguation: if the model name begins with a known