From 358828b6173848d1bfe9a38e77249fad35bb2bd1 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 7 Mar 2026 01:26:59 -0300 Subject: [PATCH] fix: wire apiFormat from custom model DB into routing layer (#204) --- package-lock.json | 16 ++++++++++++-- src/sse/handlers/chat.ts | 9 +++++++- src/sse/handlers/chatHelpers.ts | 19 ++++++++++++++--- src/sse/services/model.ts | 37 +++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index adbc07c464..174dca4901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "2.0.4", + "version": "2.0.5", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -6905,6 +6905,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -9012,6 +9013,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 69d1bfc5bd..905e1cb6d9 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -369,7 +369,14 @@ async function resolveModelOrError(modelStr: string, body: any) { const { provider, model } = modelInfo; const sourceFormat = detectFormat(body); const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider; - const targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider); + + // If the custom model specifies apiFormat="responses", override targetFormat + // to route through the Responses API translator instead of Chat Completions + let targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider); + if ((modelInfo as any).apiFormat === "responses") { + targetFormat = "openai-responses"; + log.info("ROUTING", `Custom model apiFormat=responses → targetFormat=openai-responses`); + } if (modelStr !== `${provider}/${model}`) { log.info("ROUTING", `${modelStr} → ${provider}/${model}`); diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 019866dec7..15cd6d18d4 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -34,7 +34,12 @@ const HTTP_STATUS = { * @param {Function} errorResponse - Error response factory * @returns {Promise<{ error?: Response, provider: string, model: string, sourceFormat: string, targetFormat: string }>} */ -export async function resolveModelOrError(modelStr: string, body: any, log: any, errorResponse: Function) { +export async function resolveModelOrError( + modelStr: string, + body: any, + log: any, + errorResponse: Function +) { const modelInfo = await getModelInfo(modelStr); if (!modelInfo.provider) { @@ -44,7 +49,8 @@ export async function resolveModelOrError(modelStr: string, body: any, log: any, `Ambiguous model '${modelStr}'. Use provider/model prefix (ex: gh/${modelStr} or cc/${modelStr}).`; log.warn("CHAT", message, { model: modelStr, - candidates: (modelInfo as any).candidateAliases || (modelInfo as any).candidateProviders || [], + candidates: + (modelInfo as any).candidateAliases || (modelInfo as any).candidateProviders || [], }); return { error: errorResponse(HTTP_STATUS.BAD_REQUEST, message) }; } @@ -56,7 +62,14 @@ export async function resolveModelOrError(modelStr: string, body: any, log: any, const { provider, model } = modelInfo; const sourceFormat = detectFormat(body); const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider; - const targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider); + + // If the custom model specifies apiFormat="responses", override targetFormat + // to route through the Responses API translator instead of Chat Completions + let targetFormat = getModelTargetFormat(providerAlias, model) || getTargetFormat(provider); + if ((modelInfo as any).apiFormat === "responses") { + targetFormat = "openai-responses"; + log.info("ROUTING", `Custom model apiFormat=responses → targetFormat=openai-responses`); + } // Log routing if (modelStr !== `${provider}/${model}`) { diff --git a/src/sse/services/model.ts b/src/sse/services/model.ts index ec606b7da0..9b41c6077d 100644 --- a/src/sse/services/model.ts +++ b/src/sse/services/model.ts @@ -1,5 +1,5 @@ // Re-export from open-sse with localDb integration -import { getModelAliases, getComboByName, getProviderNodes } from "@/lib/localDb"; +import { getModelAliases, getComboByName, getProviderNodes, getCustomModels } from "@/lib/localDb"; import { parseModel, resolveModelAliasFromMap, @@ -16,13 +16,30 @@ export async function resolveModelAlias(alias) { return resolveModelAliasFromMap(alias, aliases); } +/** + * Look up the apiFormat for a custom model from the DB. + * Returns "responses" if the model is configured for the Responses API, otherwise undefined. + */ +async function lookupCustomModelApiFormat( + providerId: string, + modelId: string +): Promise { + try { + const models = await getCustomModels(providerId); + if (!Array.isArray(models)) return undefined; + const match = models.find((m: any) => m.id === modelId); + return match?.apiFormat === "responses" ? "responses" : undefined; + } catch { + return undefined; + } +} + /** * Get full model info (parse or resolve) */ export async function getModelInfo(modelStr) { const parsed = parseModel(modelStr); - // Check custom provider nodes first (for both alias and non-alias formats) // Check custom provider nodes first (for both alias and non-alias formats) if (parsed.providerAlias || parsed.provider) { // Ensure prefixToCheck is always a concise identifier, not a full model string @@ -32,14 +49,26 @@ export async function getModelInfo(modelStr) { const openaiNodes = await getProviderNodes({ type: "openai-compatible" }); const matchedOpenAI = openaiNodes.find((node) => node.prefix === prefixToCheck); if (matchedOpenAI) { - return { provider: matchedOpenAI.id, model: parsed.model }; + const apiFormat = await lookupCustomModelApiFormat( + matchedOpenAI.id as string, + parsed.model as string + ); + return { provider: matchedOpenAI.id, model: parsed.model, ...(apiFormat && { apiFormat }) }; } // Check Anthropic Compatible nodes const anthropicNodes = await getProviderNodes({ type: "anthropic-compatible" }); const matchedAnthropic = anthropicNodes.find((node) => node.prefix === prefixToCheck); if (matchedAnthropic) { - return { provider: matchedAnthropic.id, model: parsed.model }; + const apiFormat = await lookupCustomModelApiFormat( + matchedAnthropic.id as string, + parsed.model as string + ); + return { + provider: matchedAnthropic.id, + model: parsed.model, + ...(apiFormat && { apiFormat }), + }; } }