fix model auto-sync startup and auth (#719)

This commit is contained in:
Randi
2026-03-28 19:39:02 -04:00
committed by GitHub
parent 437cf9bab0
commit eba9d854a9
6 changed files with 105 additions and 7 deletions

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View 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/);
});