mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix model auto-sync startup and auth (#719)
This commit is contained in:
@@ -274,8 +274,9 @@ Domain State DB (SQLite):
|
||||
|
||||
## 5) Cloud Sync
|
||||
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`
|
||||
- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts`, `src/shared/services/modelSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/cloudSyncScheduler.ts`
|
||||
- Periodic task: `src/shared/services/modelSyncScheduler.ts`
|
||||
- Control route: `src/app/api/sync/cloud/route.ts`
|
||||
|
||||
## Request Lifecycle (`/v1/chat/completions`)
|
||||
|
||||
@@ -3,6 +3,10 @@ import { getProviderConnectionById } from "@/models";
|
||||
import { replaceCustomModels } from "@/lib/db/models";
|
||||
import { saveCallLog } from "@/lib/usage/callLogs";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
buildModelSyncInternalHeaders,
|
||||
isModelSyncInternalRequest,
|
||||
} from "@/shared/services/modelSyncScheduler";
|
||||
|
||||
/**
|
||||
* POST /api/providers/[id]/sync-models
|
||||
@@ -19,7 +23,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
if (!(await isAuthenticated(request))) {
|
||||
if (!(await isAuthenticated(request)) && !isModelSyncInternalRequest(request)) {
|
||||
return NextResponse.json(
|
||||
{ error: { message: "Authentication required", type: "invalid_api_key" } },
|
||||
{ status: 401 }
|
||||
@@ -41,7 +45,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
|
||||
method: "GET",
|
||||
headers: {
|
||||
cookie: request.headers.get("cookie") || "",
|
||||
"x-internal": "model-sync",
|
||||
...buildModelSyncInternalHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import initializeCloudSync from "@/shared/services/initializeCloudSync";
|
||||
import { startModelSyncScheduler } from "@/shared/services/modelSyncScheduler";
|
||||
import "@/lib/tokenHealthCheck"; // Proactive token health-check scheduler
|
||||
|
||||
// Initialize cloud sync when this module is imported
|
||||
// Initialize background sync services when this module is imported
|
||||
let initialized = false;
|
||||
|
||||
export async function ensureCloudSyncInitialized() {
|
||||
if (!initialized) {
|
||||
try {
|
||||
await initializeCloudSync();
|
||||
startModelSyncScheduler();
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error("[ServerInit] Error initializing cloud sync:", error);
|
||||
console.error("[ServerInit] Error initializing background sync services:", error);
|
||||
}
|
||||
}
|
||||
return initialized;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getSettings } from "./lib/localDb";
|
||||
import { isPublicRoute, verifyAuth, isAuthRequired } from "./shared/utils/apiAuth";
|
||||
import { checkBodySize, getBodySizeLimit } from "./shared/middleware/bodySizeGuard";
|
||||
import { isDraining } from "./lib/gracefulShutdown";
|
||||
import { isModelSyncInternalRequest } from "./shared/services/modelSyncScheduler";
|
||||
|
||||
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "");
|
||||
|
||||
@@ -43,6 +44,14 @@ export async function proxy(request) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Allow the model auto-sync scheduler to reach only its internal provider routes.
|
||||
if (
|
||||
isModelSyncInternalRequest(request) &&
|
||||
/^\/api\/providers\/[^/]+\/(sync-models|models)$/.test(pathname)
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Check if auth is required at all (respects requireLogin setting)
|
||||
const authRequired = await isAuthRequired();
|
||||
if (!authRequired) {
|
||||
|
||||
@@ -8,13 +8,53 @@
|
||||
* Pattern mirrors cloudSyncScheduler.ts for consistency.
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getSettings, updateSettings } from "@/lib/localDb";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const MODEL_SYNC_SETTING_KEY = "model_sync_last_run";
|
||||
const MODEL_SYNC_INTERNAL_AUTH_HEADER = "x-model-sync-internal-auth";
|
||||
|
||||
const { dashboardPort } = getRuntimePorts();
|
||||
|
||||
const INTERNAL_BASE_URL =
|
||||
process.env.BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
`http://localhost:${dashboardPort}`;
|
||||
|
||||
const globalState = globalThis as typeof globalThis & {
|
||||
__omnirouteModelSyncInternalAuthToken?: string;
|
||||
};
|
||||
|
||||
let schedulerTimer: NodeJS.Timeout | null = null;
|
||||
let isRunning = false;
|
||||
let internalAuthToken: string | null = null;
|
||||
|
||||
function getInternalAuthToken(): string {
|
||||
if (!internalAuthToken) {
|
||||
internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken || randomUUID();
|
||||
globalState.__omnirouteModelSyncInternalAuthToken = internalAuthToken;
|
||||
}
|
||||
return internalAuthToken;
|
||||
}
|
||||
|
||||
export function getModelSyncInternalAuthHeaderName(): string {
|
||||
return MODEL_SYNC_INTERNAL_AUTH_HEADER;
|
||||
}
|
||||
|
||||
export function buildModelSyncInternalHeaders(): Record<string, string> {
|
||||
return { [MODEL_SYNC_INTERNAL_AUTH_HEADER]: getInternalAuthToken() };
|
||||
}
|
||||
|
||||
export function isModelSyncInternalRequest(request: Request): boolean {
|
||||
if (!internalAuthToken && globalState.__omnirouteModelSyncInternalAuthToken) {
|
||||
internalAuthToken = globalState.__omnirouteModelSyncInternalAuthToken;
|
||||
}
|
||||
const headerToken = request.headers.get(MODEL_SYNC_INTERNAL_AUTH_HEADER);
|
||||
return Boolean(headerToken && internalAuthToken && headerToken === internalAuthToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all provider connections that have autoSync enabled.
|
||||
@@ -50,7 +90,10 @@ async function syncConnectionModels(
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/providers/${connectionId}/sync-models`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "x-internal": "model-sync-scheduler" },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...buildModelSyncInternalHeaders(),
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(
|
||||
@@ -121,7 +164,7 @@ async function runSyncCycle(apiBaseUrl: string): Promise<void> {
|
||||
* @param intervalMs — sync interval in milliseconds (default: 24h)
|
||||
*/
|
||||
export function startModelSyncScheduler(
|
||||
apiBaseUrl = "http://localhost:20128",
|
||||
apiBaseUrl = INTERNAL_BASE_URL,
|
||||
intervalMs = DEFAULT_INTERVAL_MS
|
||||
): void {
|
||||
if (schedulerTimer) {
|
||||
|
||||
39
tests/unit/model-sync-scheduler.test.mjs
Normal file
39
tests/unit/model-sync-scheduler.test.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
test("modelSyncScheduler: internal auth headers validate only for scheduler requests", async () => {
|
||||
const {
|
||||
buildModelSyncInternalHeaders,
|
||||
getModelSyncInternalAuthHeaderName,
|
||||
isModelSyncInternalRequest,
|
||||
} = await import("../../src/shared/services/modelSyncScheduler.ts");
|
||||
|
||||
const internalRequest = new Request("http://localhost/api/providers/test/sync-models", {
|
||||
method: "POST",
|
||||
headers: buildModelSyncInternalHeaders(),
|
||||
});
|
||||
assert.equal(isModelSyncInternalRequest(internalRequest), true);
|
||||
|
||||
const externalRequest = new Request("http://localhost/api/providers/test/sync-models", {
|
||||
method: "POST",
|
||||
headers: { [getModelSyncInternalAuthHeaderName()]: "invalid-token" },
|
||||
});
|
||||
assert.equal(isModelSyncInternalRequest(externalRequest), false);
|
||||
});
|
||||
|
||||
test("initCloudSync: startup initialization also starts model sync scheduler", () => {
|
||||
const filePath = path.join(process.cwd(), "src/lib/initCloudSync.ts");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
assert.match(source, /startModelSyncScheduler\s*\(/);
|
||||
});
|
||||
|
||||
test("proxy: internal model sync token is only allowed for provider model sync routes", () => {
|
||||
const filePath = path.join(process.cwd(), "src/proxy.ts");
|
||||
const source = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
assert.match(source, /isModelSyncInternalRequest/);
|
||||
assert.match(source, /sync-models\|models/);
|
||||
});
|
||||
Reference in New Issue
Block a user