Compare commits

...

23 Commits

Author SHA1 Message Date
diegosouzapw 30fba39b35 feat(release): v2.0.6 — custom model apiFormat routing fix
Build Electron Desktop App / Validate version (push) Failing after 33s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-07 01:36:21 -03:00
Diego Rodrigues de Sa e Souza 5a75ff67c9 Merge pull request #233 from diegosouzapw/fix/issue-204-apiformat-routing
fix: wire apiFormat from custom model DB into routing layer (#204)
2026-03-07 01:35:30 -03:00
diegosouzapw 358828b617 fix: wire apiFormat from custom model DB into routing layer (#204) 2026-03-07 01:26:59 -03:00
diegosouzapw e080c4a16a feat(release): v2.0.5 — fix Chat→Responses reasoning IDs, electron auto-update, dependency bumps
Build Electron Desktop App / Validate version (push) Failing after 31s
Build Electron Desktop App / Build Electron (macos-arm64) (push) Has been skipped
Build Electron Desktop App / Build Electron (linux) (push) Has been skipped
Build Electron Desktop App / Build Electron (macos-intel) (push) Has been skipped
Build Electron Desktop App / Build Electron (windows) (push) Has been skipped
Build Electron Desktop App / Create Release (push) Has been skipped
2026-03-06 18:51:24 -03:00
Diego Rodrigues de Sa e Souza 04b7e38baf Merge pull request #221 from benzntech/feat/electron-auto-update
feat(electron): add auto-update functionality with electron-updater
2026-03-06 18:49:54 -03:00
Diego Rodrigues de Sa e Souza 7ee23fbe19 Merge pull request #230 from diegosouzapw/dependabot/npm_and_yarn/express-rate-limit-8.3.0
deps: bump express-rate-limit from 8.2.1 to 8.3.0
2026-03-06 18:49:51 -03:00
Diego Rodrigues de Sa e Souza c49bdb4ebb Merge pull request #229 from diegosouzapw/dependabot/github_actions/docker/build-push-action-7
chore(deps): bump docker/build-push-action from 6 to 7
2026-03-06 18:49:48 -03:00
Diego Rodrigues de Sa e Souza 0f7efed8d5 Merge pull request #228 from diegosouzapw/dependabot/github_actions/actions/upload-artifact-7
chore(deps): bump actions/upload-artifact from 4 to 7
2026-03-06 18:49:46 -03:00
Diego Rodrigues de Sa e Souza d07bc6dcf3 Merge pull request #227 from diegosouzapw/dependabot/github_actions/docker/login-action-4
chore(deps): bump docker/login-action from 3 to 4
2026-03-06 18:49:43 -03:00
Diego Rodrigues de Sa e Souza d607d46fa3 Merge pull request #226 from diegosouzapw/dependabot/github_actions/actions/download-artifact-8
chore(deps): bump actions/download-artifact from 4 to 8
2026-03-06 18:49:40 -03:00
Diego Rodrigues de Sa e Souza 2225dd14aa Merge pull request #225 from diegosouzapw/dependabot/github_actions/actions/cache-5
chore(deps): bump actions/cache from 4 to 5
2026-03-06 18:49:37 -03:00
Diego Rodrigues de Sa e Souza f6c0e7bbbe Merge pull request #222 from benzntech/fix/electron-release-duplicate-asset
fix(ci): remove duplicate OmniRoute.exe entry in electron release workflow
2026-03-06 18:49:28 -03:00
Diego Rodrigues de Sa e Souza c4675c5219 Merge pull request #231 from diegosouzapw/fix/issue-224-reasoning-ids
fix: omit synthesized reasoning items in Chat→Responses translation (#224)
2026-03-06 18:49:25 -03:00
diegosouzapw 2d977a3c4d fix: omit synthesized reasoning items in Chat→Responses translation (#224) 2026-03-06 18:48:34 -03:00
dependabot[bot] 9405918258 deps: bump express-rate-limit from 8.2.1 to 8.3.0
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.3.0.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.3.0)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:46:36 +00:00
dependabot[bot] a69d7dd4b5 chore(deps): bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:27:03 +00:00
dependabot[bot] 428e6cb53f chore(deps): bump actions/upload-artifact from 4 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:59 +00:00
dependabot[bot] c9a2955d28 chore(deps): bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:54 +00:00
dependabot[bot] 7aefcd3437 chore(deps): bump actions/download-artifact from 4 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:51 +00:00
dependabot[bot] 79f4f79c46 chore(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 18:26:46 +00:00
benzntech c11c275678 fix(electron): address auto-updater review issues
- Remove unused dialog import
- Stop Next.js server before quitAndInstall() to prevent data loss
- Propagate errors from checkForUpdates/downloadUpdate to IPC handlers
  so renderer can distinguish success from failure
- Remove meaningless return value from install-update handler
2026-03-06 19:22:41 +05:30
benzntech bbcd1d3a08 fix(ci): remove duplicate OmniRoute.exe entry in electron release workflow
Duplicate release-assets/OmniRoute.exe glob caused softprops/action-gh-release
to attempt a second upload of the same asset, triggering a 404 Not Found error
on the GitHub release asset update API. The file is already covered by the
*.exe glob pattern above it.
2026-03-06 19:18:41 +05:30
benzntech 3342d5b931 feat(electron): add auto-update functionality with electron-updater 2026-03-06 18:54:00 +05:30
12 changed files with 281 additions and 57 deletions
+3 -3
View File
@@ -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 }}
+3 -4
View File
@@ -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 }}
+60
View File
@@ -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
+121
View File
@@ -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", () => {
+8 -1
View File
@@ -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
View File
@@ -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[] = [];
+6 -15
View File
@@ -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
View File
@@ -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": {
+8 -1
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}`);
+16 -3
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}`) {
+33 -4
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 }),
};
}
}