Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fba39b35 | |||
| 5a75ff67c9 | |||
| 358828b617 | |||
| e080c4a16a | |||
| 04b7e38baf | |||
| 7ee23fbe19 | |||
| c49bdb4ebb | |||
| 0f7efed8d5 | |||
| d07bc6dcf3 | |||
| d607d46fa3 | |||
| 2225dd14aa | |||
| f6c0e7bbbe | |||
| c4675c5219 | |||
| 2d977a3c4d | |||
| 9405918258 | |||
| a69d7dd4b5 | |||
| 428e6cb53f | |||
| c9a2955d28 | |||
| 7aefcd3437 | |||
| 79f4f79c46 | |||
| c11c275678 | |||
| bbcd1d3a08 | |||
| 3342d5b931 |
@@ -31,14 +31,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: runner-base
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
cache: npm
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: electron-${{ matrix.platform }}
|
||||
path: release-assets/
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
@@ -172,6 +172,5 @@ jobs:
|
||||
release-assets/*.blockmap
|
||||
release-assets/*.source.tar.gz
|
||||
release-assets/*.source.zip
|
||||
release-assets/OmniRoute.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [2.0.6] — 2026-03-07
|
||||
|
||||
> ### 🐛 Bug Fix — Custom Model API Format Routing
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **#204 — Custom model `apiFormat` not used in routing** — Custom models configured with `apiFormat: "responses"` in the dashboard were still being routed through the Chat Completions translator. The `apiFormat` field was stored in the DB and displayed in the UI, but never consumed by the routing layer. Fix: `getModelInfo()` now returns `apiFormat` from the custom model DB, and both `resolveModelOrError()` functions override `targetFormat` to `openai-responses` when set. PR #233
|
||||
|
||||
### ✅ Issues Closed
|
||||
|
||||
- **#205** — Combo endpoint support — Already implemented in v2.0.2
|
||||
- **#206** — Manual model→endpoint mapping — Already implemented in v2.0.2
|
||||
- **#223** — CLI fingerprint parity — Responded with 4-phase roadmap
|
||||
|
||||
### 📁 Files Changed
|
||||
|
||||
| File | Change |
|
||||
| --------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `src/sse/services/model.ts` | Added `lookupCustomModelApiFormat()`, enriched `getModelInfo()` return |
|
||||
| `src/sse/handlers/chat.ts` | Override `targetFormat` when `apiFormat === "responses"` |
|
||||
| `src/sse/handlers/chatHelpers.ts` | Same override in duplicate `resolveModelOrError()` |
|
||||
|
||||
---
|
||||
|
||||
## [2.0.5] — 2026-03-06
|
||||
|
||||
> ### 🐛 Bug Fix, Electron Auto-Update & Dependency Bumps
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **#224 — Chat→Responses translation creates invalid reasoning IDs** — Removed synthetic reasoning item generation in `openaiToOpenAIResponsesRequest()`. The translator was creating reasoning items with IDs like `reasoning_15`, but OpenAI's Responses API requires server-generated `rs_*` IDs, causing `400 Invalid Request` errors from Responses-compatible upstreams. Fix: omit reasoning items entirely during translation
|
||||
- **CI: duplicate OmniRoute.exe in release workflow** — Removed redundant explicit `release-assets/OmniRoute.exe` entry that caused `softprops/action-gh-release` to fail with 404 on duplicate upload. PR #222 by @benzntech
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Electron Auto-Update** — Added auto-update functionality to the desktop app using `electron-updater`. Includes IPC handlers for check/download/install, "Check for Updates" in system tray menu, desktop notification when update is ready, and silent startup check (3s delay). PR #221 by @benzntech
|
||||
|
||||
### 📦 Dependencies
|
||||
|
||||
- Bump `actions/cache` from 4 to 5 (#225)
|
||||
- Bump `actions/download-artifact` from 4 to 8 (#226)
|
||||
- Bump `docker/login-action` from 3 to 4 (#227)
|
||||
- Bump `actions/upload-artifact` from 4 to 7 (#228)
|
||||
- Bump `docker/build-push-action` from 6 to 7 (#229)
|
||||
- Bump `express-rate-limit` from 8.2.1 to 8.3.0 (#230)
|
||||
|
||||
### 📁 Files Changed
|
||||
|
||||
| File | Change |
|
||||
| ------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `open-sse/translator/request/openai-responses.ts` | Remove synthetic reasoning item generation |
|
||||
| `.github/workflows/electron-release.yml` | Remove duplicate exe entry, bump GH Actions |
|
||||
| `.github/workflows/docker-publish.yml` | Bump docker/login-action and build-push-action |
|
||||
| `electron/main.js` | Auto-updater setup, IPC handlers, tray menu |
|
||||
| `electron/package.json` | Added electron-updater dep and GitHub publish config |
|
||||
| `electron/preload.js` | Exposed update APIs via contextBridge |
|
||||
| `package-lock.json` | Updated express-rate-limit |
|
||||
|
||||
---
|
||||
|
||||
## [2.0.4] — 2026-03-06
|
||||
|
||||
> ### 🐛 Bug Fixes — Round-Robin Persistence & Docker Compatibility
|
||||
|
||||
@@ -26,10 +26,12 @@ const {
|
||||
nativeImage,
|
||||
shell,
|
||||
session,
|
||||
Notification,
|
||||
} = require("electron");
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
|
||||
// ── Single Instance Lock ───────────────────────────────────
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -62,6 +64,11 @@ let serverPort = 20128;
|
||||
|
||||
const getServerUrl = () => `http://localhost:${serverPort}`;
|
||||
|
||||
// ── Auto-Updater Configuration ──────────────────────────────
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.logger = console;
|
||||
|
||||
// ── Helper: Send IPC event to renderer (#5) ────────────────
|
||||
function sendToRenderer(channel, data) {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
@@ -103,6 +110,77 @@ async function waitForServerExit(proc, timeoutMs = 5000) {
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Auto-Updater Event Handlers ─────────────────────────────
|
||||
function setupAutoUpdater() {
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
sendToRenderer("update-status", { status: "checking" });
|
||||
console.log("[Electron] Checking for updates...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
sendToRenderer("update-status", { status: "available", version: info.version });
|
||||
console.log("[Electron] Update available:", info.version);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
sendToRenderer("update-status", { status: "not-available", version: info.version });
|
||||
console.log("[Electron] No update available");
|
||||
});
|
||||
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
sendToRenderer("update-status", {
|
||||
status: "downloading",
|
||||
percent: Math.round(progress.percent),
|
||||
transferred: progress.transferred,
|
||||
total: progress.total,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
sendToRenderer("update-status", { status: "downloaded", version: info.version });
|
||||
console.log("[Electron] Update downloaded:", info.version);
|
||||
|
||||
if (Notification.isSupported()) {
|
||||
const notification = new Notification({
|
||||
title: "OmniRoute Update Ready",
|
||||
body: `Version ${info.version} is ready to install. Click to restart.`,
|
||||
});
|
||||
notification.on("click", () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
notification.show();
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
sendToRenderer("update-status", { status: "error", message: error.message });
|
||||
console.error("[Electron] Update error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForUpdates(silent = false) {
|
||||
if (isDev) {
|
||||
console.log("[Electron] Dev mode — skipping auto-update");
|
||||
if (!silent) {
|
||||
sendToRenderer("update-status", { status: "error", message: "Updates disabled in dev mode" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
await autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
async function downloadUpdate() {
|
||||
await autoUpdater.downloadUpdate();
|
||||
}
|
||||
|
||||
function installUpdate() {
|
||||
if (nextServer) {
|
||||
nextServer.kill("SIGTERM");
|
||||
nextServer = null;
|
||||
}
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
// ── Content Security Policy (#15) ──────────────────────────
|
||||
function setupContentSecurityPolicy() {
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
@@ -236,6 +314,11 @@ function createTray() {
|
||||
],
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Check for Updates",
|
||||
click: () => checkForUpdates(false),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => {
|
||||
@@ -391,6 +474,36 @@ function setupIpcHandlers() {
|
||||
});
|
||||
|
||||
ipcMain.on("window-close", () => mainWindow?.close());
|
||||
|
||||
// Auto-update IPC handlers
|
||||
ipcMain.handle("check-for-updates", async () => {
|
||||
try {
|
||||
await checkForUpdates(false);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[Electron] Check for updates failed:", error);
|
||||
sendToRenderer("update-status", { status: "error", message: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("download-update", async () => {
|
||||
try {
|
||||
await downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[Electron] Download update failed:", error);
|
||||
sendToRenderer("update-status", { status: "error", message: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("install-update", () => {
|
||||
installUpdate();
|
||||
// No return value — app will quit and restart
|
||||
});
|
||||
|
||||
ipcMain.handle("get-app-version", () => app.getVersion());
|
||||
}
|
||||
|
||||
// ── App Lifecycle ──────────────────────────────────────────
|
||||
@@ -407,6 +520,14 @@ app.whenReady().then(async () => {
|
||||
createWindow();
|
||||
createTray();
|
||||
setupIpcHandlers();
|
||||
setupAutoUpdater();
|
||||
|
||||
// Check for updates after a short delay (don't block startup)
|
||||
if (!isDev) {
|
||||
setTimeout(() => {
|
||||
checkForUpdates(true);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// macOS: recreate window when dock icon clicked
|
||||
app.on("activate", () => {
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"build:linux": "electron-builder --linux",
|
||||
"pack": "electron-builder --dir"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^40.6.1",
|
||||
"electron-builder": "^25.1.8"
|
||||
@@ -28,6 +30,11 @@
|
||||
"output": "dist-electron",
|
||||
"buildResources": "assets"
|
||||
},
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "diegosouzapw",
|
||||
"repo": "OmniRoute"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
|
||||
+18
-2
@@ -13,9 +13,18 @@ const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
// ── Channel Whitelist ──────────────────────────────────────
|
||||
const VALID_CHANNELS = {
|
||||
invoke: ["get-app-info", "open-external", "get-data-dir", "restart-server"],
|
||||
invoke: [
|
||||
"get-app-info",
|
||||
"open-external",
|
||||
"get-data-dir",
|
||||
"restart-server",
|
||||
"check-for-updates",
|
||||
"download-update",
|
||||
"install-update",
|
||||
"get-app-version",
|
||||
],
|
||||
send: ["window-minimize", "window-maximize", "window-close"],
|
||||
receive: ["server-status", "port-changed"],
|
||||
receive: ["server-status", "port-changed", "update-status"],
|
||||
};
|
||||
|
||||
// ── Fix #16: Generic IPC wrappers ──────────────────────────
|
||||
@@ -48,6 +57,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
openExternal: (url) => safeInvoke("open-external", url),
|
||||
getDataDir: () => safeInvoke("get-data-dir"),
|
||||
restartServer: () => safeInvoke("restart-server"),
|
||||
getAppVersion: () => safeInvoke("get-app-version"),
|
||||
|
||||
// ── Auto-Update ──────────────────────────────────────────
|
||||
checkForUpdates: () => safeInvoke("check-for-updates"),
|
||||
downloadUpdate: () => safeInvoke("download-update"),
|
||||
installUpdate: () => safeInvoke("install-update"),
|
||||
|
||||
// ── Send (fire-and-forget) ───────────────────────────────
|
||||
minimizeWindow: () => safeSend("window-minimize"),
|
||||
@@ -58,6 +73,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// Fix #6: Returns a disposer function for precise cleanup
|
||||
onServerStatus: (callback) => safeOn("server-status", callback),
|
||||
onPortChanged: (callback) => safeOn("port-changed", callback),
|
||||
onUpdateStatus: (callback) => safeOn("update-status", callback),
|
||||
|
||||
// ── Static Properties ────────────────────────────────────
|
||||
isElectron: true,
|
||||
|
||||
@@ -275,30 +275,11 @@ export function openaiToOpenAIResponsesRequest(
|
||||
|
||||
// Convert assistant messages
|
||||
if (role === "assistant") {
|
||||
// Add reasoning content before assistant output
|
||||
if (msg.reasoning_content) {
|
||||
input.push({
|
||||
type: "reasoning",
|
||||
id: `reasoning_${input.length}`,
|
||||
summary: [{ type: "summary_text", text: toString(msg.reasoning_content) }],
|
||||
});
|
||||
}
|
||||
// Skip reasoning_content — OpenAI Responses API requires server-generated
|
||||
// rs_* IDs for reasoning items. Synthesizing client-side IDs (e.g. reasoning_N)
|
||||
// causes 400 errors from Responses-compatible upstreams. (#224)
|
||||
|
||||
// Handle thinking blocks in array content
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const blockValue of msg.content) {
|
||||
const block = toRecord(blockValue);
|
||||
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
||||
input.push({
|
||||
type: "reasoning",
|
||||
id: `reasoning_${input.length}`,
|
||||
summary: [
|
||||
{ type: "summary_text", text: toString(block.thinking || block.data, "...") },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip thinking blocks in array content — same rs_* ID constraint applies
|
||||
|
||||
// Build assistant output content
|
||||
const outputContent: unknown[] = [];
|
||||
|
||||
Generated
+6
-15
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -6596,12 +6596,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz",
|
||||
"integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
"ip-address": "10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -6613,15 +6613,6 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit/node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.6",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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}`) {
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user