Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/docs/components/DocsSidebarClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
63 changes: 62 additions & 1 deletion src/sse/handlers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -488,6 +502,53 @@ async function handleSingleModelChat(
const resolved = await resolveModelOrError(modelStr, body, clientRawRequest?.endpoint);
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 =
Expand Down
42 changes: 41 additions & 1 deletion src/sse/handlers/chatHelpers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -79,6 +79,46 @@ export async function resolveModelOrError(modelStr: string, body: any, endpointP
}
}

// "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
Expand Down