mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-30 20:05:40 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e06dc5ace | ||
|
|
bfd3e2c01b | ||
|
|
a1957f0923 | ||
|
|
11a02ba361 | ||
|
|
4643c19abc | ||
|
|
a3369df62f | ||
|
|
4297c42597 | ||
|
|
e06e7157ac | ||
|
|
22f9e6f4c0 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,6 +2,21 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [2.5.5] - 2026-03-15
|
||||
|
||||
> Model list dedup fix, Electron standalone build hardening, and Kiro credit tracking.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(models) #380**: `GET /api/models` now includes provider aliases when building the active-provider filter — models for `claude` (alias `cc`) and `github` (alias `gh`) were always shown regardless of whether a connection was configured, because `PROVIDER_MODELS` keys are aliases but DB connections are stored under provider IDs. Fixed by expanding each active provider ID to also include its alias via `PROVIDER_ID_TO_ALIAS`. Closes #353.
|
||||
- **fix(electron) #379**: New `scripts/prepare-electron-standalone.mjs` stages a dedicated `/.next/electron-standalone` bundle before Electron packaging. Aborts with a clear error if `node_modules` is a symlink (electron-builder would ship a runtime dependency on the build machine). Cross-platform path sanitization via `path.basename`. By @kfiramar.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(kiro) #381**: Kiro credit balance tracking — usage endpoint now returns credit data for Kiro accounts by calling `codewhisperer.us-east-1.amazonaws.com/getUserCredits` (same endpoint Kiro IDE uses internally). Returns remaining credits, total allowance, renewal date, and subscription tier. Closes #337.
|
||||
|
||||
## [2.5.4] - 2026-03-15
|
||||
|
||||
> Logger startup fix, login bootstrap security fix, and dev HMR reliability improvement. CI infrastructure hardened.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.5.4
|
||||
version: 2.5.5
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dev": "electron . --no-sandbox",
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:mac-x64": "electron-builder --mac --x64",
|
||||
"build:mac-arm64": "electron-builder --mac --arm64",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"pack": "electron-builder --dir"
|
||||
"prepare:bundle": "node ../scripts/prepare-electron-standalone.mjs",
|
||||
"build": "npm run prepare:bundle && electron-builder",
|
||||
"build:win": "npm run prepare:bundle && electron-builder --win",
|
||||
"build:mac": "npm run prepare:bundle && electron-builder --mac",
|
||||
"build:mac-x64": "npm run prepare:bundle && electron-builder --mac --x64",
|
||||
"build:mac-arm64": "npm run prepare:bundle && electron-builder --mac --arm64",
|
||||
"build:linux": "npm run prepare:bundle && electron-builder --linux",
|
||||
"pack": "npm run prepare:bundle && electron-builder --dir"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.8.3"
|
||||
@@ -47,24 +48,18 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../.next/standalone",
|
||||
"from": "../.next/electron-standalone",
|
||||
"to": "app",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../.next/static",
|
||||
"to": "app/.next/static",
|
||||
"from": "assets",
|
||||
"to": "assets",
|
||||
"filter": [
|
||||
"**/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../public",
|
||||
"to": "app/public",
|
||||
"filter": [
|
||||
"**/*"
|
||||
"icon.png",
|
||||
"tray-icon.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.5.4",
|
||||
"version": "2.5.5",
|
||||
"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": {
|
||||
|
||||
146
scripts/prepare-electron-standalone.mjs
Normal file
146
scripts/prepare-electron-standalone.mjs
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { basename, dirname, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
const STANDALONE_DIR = join(ROOT, ".next", "standalone");
|
||||
const ELECTRON_STANDALONE_DIR = join(ROOT, ".next", "electron-standalone");
|
||||
const STATIC_SRC = join(ROOT, ".next", "static");
|
||||
const STATIC_DEST = join(ELECTRON_STANDALONE_DIR, ".next", "static");
|
||||
const PUBLIC_SRC = join(ROOT, "public");
|
||||
const PUBLIC_DEST = join(ELECTRON_STANDALONE_DIR, "public");
|
||||
|
||||
function resolveStandaloneBundleDir() {
|
||||
const directServer = join(STANDALONE_DIR, "server.js");
|
||||
if (existsSync(directServer)) {
|
||||
return STANDALONE_DIR;
|
||||
}
|
||||
|
||||
const nestedCandidates = [
|
||||
join(STANDALONE_DIR, "projects", "OmniRoute"),
|
||||
join(STANDALONE_DIR, basename(ROOT)),
|
||||
];
|
||||
|
||||
for (const candidate of nestedCandidates) {
|
||||
if (existsSync(join(candidate, "server.js"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Standalone server bundle not found in ${STANDALONE_DIR}. Run \`npm run build\` first.`
|
||||
);
|
||||
}
|
||||
|
||||
function createPathPattern(filePath) {
|
||||
return filePath
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\//g, "[\\\\/]");
|
||||
}
|
||||
|
||||
function sanitizeBuildPaths(bundleDir) {
|
||||
const buildRoot = ROOT.replace(/\\/g, "/");
|
||||
const bundleRoot = bundleDir.replace(/\\/g, "/");
|
||||
const replacements = [buildRoot, bundleRoot];
|
||||
const targets = [
|
||||
join(ELECTRON_STANDALONE_DIR, "server.js"),
|
||||
join(ELECTRON_STANDALONE_DIR, ".next", "required-server-files.json"),
|
||||
];
|
||||
|
||||
for (const filePath of targets) {
|
||||
if (!existsSync(filePath)) continue;
|
||||
|
||||
let content = readFileSync(filePath, "utf8");
|
||||
let updated = content;
|
||||
|
||||
for (const original of replacements) {
|
||||
updated = updated.replace(new RegExp(createPathPattern(original), "g"), ".");
|
||||
}
|
||||
|
||||
if (updated !== content) {
|
||||
writeFileSync(filePath, updated, "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePackage(pkgPath, sourcePath) {
|
||||
if (existsSync(pkgPath) || !existsSync(sourcePath)) return;
|
||||
mkdirSync(dirname(pkgPath), { recursive: true });
|
||||
cpSync(sourcePath, pkgPath, { recursive: true, dereference: true });
|
||||
}
|
||||
|
||||
function assertBundleIsPackagable(bundleDir) {
|
||||
const nodeModulesPath = join(bundleDir, "node_modules");
|
||||
if (!existsSync(nodeModulesPath)) return;
|
||||
|
||||
if (lstatSync(nodeModulesPath).isSymbolicLink()) {
|
||||
throw new Error(
|
||||
[
|
||||
"Next standalone emitted app/node_modules as a symlink.",
|
||||
"electron-builder preserves extraResources symlinks, which would make the packaged app",
|
||||
"depend on the original build machine path at runtime.",
|
||||
"",
|
||||
`Offending path: ${nodeModulesPath}`,
|
||||
"Use a real node_modules directory in the build worktree before packaging Electron.",
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function logContextualError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[electron] failed to prepare standalone bundle: ${message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
process.on("uncaughtException", logContextualError);
|
||||
|
||||
const bundleDir = resolveStandaloneBundleDir();
|
||||
assertBundleIsPackagable(bundleDir);
|
||||
|
||||
rmSync(ELECTRON_STANDALONE_DIR, { recursive: true, force: true });
|
||||
mkdirSync(ELECTRON_STANDALONE_DIR, { recursive: true });
|
||||
|
||||
cpSync(bundleDir, ELECTRON_STANDALONE_DIR, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
});
|
||||
|
||||
sanitizeBuildPaths(bundleDir);
|
||||
|
||||
if (existsSync(STATIC_SRC)) {
|
||||
mkdirSync(dirname(STATIC_DEST), { recursive: true });
|
||||
cpSync(STATIC_SRC, STATIC_DEST, { recursive: true, dereference: true });
|
||||
}
|
||||
|
||||
if (existsSync(PUBLIC_SRC)) {
|
||||
cpSync(PUBLIC_SRC, PUBLIC_DEST, { recursive: true, dereference: true });
|
||||
}
|
||||
|
||||
ensurePackage(
|
||||
join(ELECTRON_STANDALONE_DIR, "node_modules", "@swc", "helpers"),
|
||||
join(ROOT, "node_modules", "@swc", "helpers")
|
||||
);
|
||||
|
||||
ensurePackage(
|
||||
join(ELECTRON_STANDALONE_DIR, "node_modules", "better-sqlite3"),
|
||||
join(ROOT, "node_modules", "better-sqlite3")
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[electron] prepared standalone bundle: ${relative(ROOT, ELECTRON_STANDALONE_DIR) || "."}`
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelAliases, setModelAlias, getProviderConnections } from "@/models";
|
||||
import { AI_MODELS } from "@/shared/constants/config";
|
||||
import { AI_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
||||
import { updateModelAliasSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
@@ -18,7 +18,17 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const connections = await getProviderConnections();
|
||||
const active = connections.filter((c: any) => c.isActive !== false);
|
||||
activeProviders = new Set(active.map((c: any) => c.provider));
|
||||
// Include both provider IDs and their aliases in the active set.
|
||||
// PROVIDER_MODELS keys are aliases (e.g. 'cc' for 'claude', 'gh' for 'github').
|
||||
// DB connections are stored under provider IDs ('claude', 'github').
|
||||
// Without this, models for aliased providers always appear unconfigured.
|
||||
activeProviders = new Set<string>();
|
||||
for (const c of active) {
|
||||
const pId = String((c as any).provider);
|
||||
activeProviders.add(pId);
|
||||
const alias = PROVIDER_ID_TO_ALIAS[pId];
|
||||
if (alias) activeProviders.add(alias);
|
||||
}
|
||||
} catch {
|
||||
// If DB unavailable, show all models
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export async function getUsageForProvider(connection) {
|
||||
return await getQwenUsage(accessToken, providerSpecificData);
|
||||
case "iflow":
|
||||
return await getIflowUsage(accessToken);
|
||||
case "kiro":
|
||||
return await getKiroUsage(accessToken);
|
||||
default:
|
||||
return { message: `Usage API not implemented for ${provider}` };
|
||||
}
|
||||
@@ -215,3 +217,56 @@ async function getIflowUsage(accessToken) {
|
||||
return { message: "Unable to fetch iFlow usage." };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kiro Credits
|
||||
* Fetches credit balance from Kiro's AWS CodeWhisperer backend.
|
||||
* The endpoint mirrors what Kiro IDE uses internally for the credit badge.
|
||||
*/
|
||||
async function getKiroUsage(accessToken: string) {
|
||||
try {
|
||||
const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com/getUserCredits", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "AWS-SDK-JS/3.0.0 kiro-ide/1.0.0",
|
||||
"X-Amz-User-Agent": "aws-sdk-js/3.0.0 kiro-ide/1.0.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
// 401/403 = expired token, show user-friendly message
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { message: "Kiro token expired. Please reconnect in Dashboard → Providers → Kiro." };
|
||||
}
|
||||
throw new Error(`Kiro credits API error (${response.status}): ${errText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Response shape: { remainingCredits, totalCredits, resetDate, subscriptionType }
|
||||
const remaining = data.remainingCredits ?? data.remaining_credits ?? null;
|
||||
const total = data.totalCredits ?? data.total_credits ?? null;
|
||||
const resetDate = data.resetDate ?? data.reset_date ?? null;
|
||||
const plan = data.subscriptionType ?? data.subscription_type ?? "unknown";
|
||||
|
||||
if (remaining === null) {
|
||||
return { message: "Kiro connected. Credit data unavailable — check Kiro IDE for balance." };
|
||||
}
|
||||
|
||||
return {
|
||||
plan,
|
||||
credits: {
|
||||
remaining,
|
||||
total: total ?? remaining,
|
||||
used: total != null ? total - remaining : 0,
|
||||
unlimited: total === null || total === 0,
|
||||
resetDate,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { message: `Unable to fetch Kiro credits: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user