Merge pull request #233 from diegosouzapw/fix/issue-204-apiformat-routing

fix: wire apiFormat from custom model DB into routing layer (#204)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-07 01:35:30 -03:00
committed by GitHub
4 changed files with 71 additions and 10 deletions

16
package-lock.json generated
View File

@@ -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",

View File

@@ -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}`);

View File

@@ -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}`) {

View File

@@ -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<string | undefined> {
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 }),
};
}
}