merge(F6): backend REST routes (memory + settings/qdrant)

This commit is contained in:
diegosouzapw
2026-05-28 11:02:16 -03:00
19 changed files with 1433 additions and 34 deletions

View File

@@ -1,6 +1,9 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { deleteMemory, getMemory } from "@/lib/memory/store";
import { deleteMemory, getMemory, updateMemory } from "@/lib/memory/store";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { MemoryUpdatePutSchema } from "@/shared/schemas/memory";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function DELETE(request: Request, props: { params: Promise<{ id: string }> }) {
const authError = await requireManagementAuth(request);
@@ -14,8 +17,8 @@ export async function DELETE(request: Request, props: { params: Promise<{ id: st
}
return NextResponse.json({ success: true });
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error }, { status: 500 });
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}
@@ -31,7 +34,41 @@ export async function GET(request: Request, props: { params: Promise<{ id: strin
}
return NextResponse.json({ memory });
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error }, { status: 500 });
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}
export async function PUT(request: Request, props: { params: Promise<{ id: string }> }) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(MemoryUpdatePutSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
try {
const { id } = await props.params;
const existing = await getMemory(id);
if (!existing) {
return NextResponse.json({ error: { message: "Memory not found" } }, { status: 404 });
}
await updateMemory(id, validation.data);
return NextResponse.json({ success: true });
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { listEmbeddingProviders } from "@/lib/memory/embedding";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function GET(request: Request) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const providers = await listEmbeddingProviders();
return NextResponse.json({ providers });
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { engineStatus } from "@/lib/memory/retrieval";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function GET(request: Request) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const status = await engineStatus();
return NextResponse.json(status);
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { MemoryReindexSchema } from "@/shared/schemas/memory";
import { runReindexBatch, getReindexPending } from "@/lib/memory/reindex";
import { markAllMemoriesNeedReindex } from "@/lib/localDb";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { logger } from "@omniroute/open-sse/utils/logger.ts";
const log = logger("MEMORY_REINDEX_ROUTE");
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(MemoryReindexSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
const { force } = validation.data;
try {
if (force) {
markAllMemoriesNeedReindex();
}
const pending = getReindexPending();
// Dispatch batch in background — do NOT await (returns immediate response).
setImmediate(() => {
runReindexBatch(100).catch((err: unknown) => {
log.error("memory.reindex.background.fail", {
error: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)),
});
});
});
return NextResponse.json({ started: true, pending });
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,58 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { RetrievePreviewSchema } from "@/shared/schemas/memory";
import { retrievePreview } from "@/lib/memory/retrieval";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(RetrievePreviewSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
const { query, strategy, maxTokens, apiKeyId, limit } = validation.data;
try {
const bundle = await retrievePreview(apiKeyId ?? null, query, {
strategy,
maxTokens,
limit,
});
const memories = bundle.items.map((item) => ({
id: item.memory.id,
type: item.memory.type,
key: item.memory.key ?? "",
content: item.memory.content,
score: item.score,
tokens: item.tokens,
tier: item.tier,
vecScore: item.vecScore,
ftsScore: item.ftsScore,
}));
return NextResponse.json({
memories,
resolution: bundle.resolution,
totalTokensUsed: bundle.totalTokens,
budgetMaxTokens: bundle.budgetMaxTokens,
});
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -6,6 +6,7 @@ import { MemoryType } from "@/lib/memory/types";
import { parsePaginationParams, buildPaginatedResponse } from "@/shared/types/pagination";
import { z } from "zod";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
const createMemorySchema = z.object({
content: z.string().min(1),
@@ -78,8 +79,8 @@ export async function GET(request: Request) {
stats,
});
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error }, { status: 500 });
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}
@@ -96,7 +97,7 @@ export async function POST(request: Request) {
const memoryId = await createMemory(validation.data);
return NextResponse.json({ success: true, id: memoryId });
} catch (err: unknown) {
const error = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error }, { status: 400 });
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 400 });
}
}

View File

@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { MemorySummarizeSchema } from "@/shared/schemas/memory";
import { summarizeMemoriesOlderThan } from "@/lib/memory/summarization";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(MemorySummarizeSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
const { apiKeyId, olderThanDays, dryRun } = validation.data;
try {
const result = await summarizeMemoriesOlderThan(apiKeyId, olderThanDays, dryRun);
return NextResponse.json({
candidates: result.candidates,
totalTokens: result.totalTokens,
deletedCount: result.deletedCount,
summaryId: result.summaryId,
dryRun: result.dryRun,
});
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -1,23 +1,14 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { getSettings, updateSettings } from "@/lib/localDb";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { MemorySettingsExtendedSchema } from "@/shared/schemas/memory";
import {
invalidateMemorySettingsCache,
normalizeMemorySettings,
toMemorySettingsUpdates,
} from "@/lib/memory/settings";
const memorySettingsUpdateSchema = z
.object({
enabled: z.boolean().optional(),
maxTokens: z.number().int().min(0).max(16000).optional(),
retentionDays: z.number().int().min(1).max(365).optional(),
strategy: z.enum(["recent", "semantic", "hybrid"]).optional(),
skillsEnabled: z.boolean().optional(),
})
.strict();
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function GET(request: NextRequest) {
if (!(await isAuthenticated(request))) {
@@ -27,8 +18,9 @@ export async function GET(request: NextRequest) {
try {
const settings = (await getSettings()) as Record<string, unknown>;
return NextResponse.json(normalizeMemorySettings(settings));
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}
@@ -37,25 +29,29 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let rawBody: unknown;
try {
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(memorySettingsUpdateSchema, rawBody);
if (isValidationFailure(validation)) {
return validation.response;
}
const validation = validateBody(MemorySettingsExtendedSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
try {
const updates = toMemorySettingsUpdates(validation.data);
const settings = (await updateSettings(updates)) as Record<string, unknown>;
invalidateMemorySettingsCache();
return NextResponse.json(normalizeMemorySettings(settings));
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { cleanupSemanticMemoryPoints } from "@/lib/memory/qdrant";
import { getMemorySettings } from "@/lib/memory/settings";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function POST(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const memorySettings = await getMemorySettings();
const result = await cleanupSemanticMemoryPoints({
retentionDays: memorySettings.retentionDays,
});
return NextResponse.json({
ok: result.ok,
deletedCount: result.deletedCount,
retentionDays: memorySettings.retentionDays,
});
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { checkQdrantHealth } from "@/lib/memory/qdrant";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function GET(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const result = await checkQdrantHealth();
return NextResponse.json(result);
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { QdrantSettingsUpdateSchema } from "@/shared/schemas/qdrant";
import { getQdrantConfig, normalizeQdrantConfig } from "@/lib/memory/qdrant";
import { updateSettings, getSettings } from "@/lib/localDb";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
function maskApiKey(apiKey: string | null): { hasApiKey: boolean; apiKeyMasked: string | null } {
if (!apiKey || apiKey.trim().length === 0) {
return { hasApiKey: false, apiKeyMasked: null };
}
const trimmed = apiKey.trim();
const last4 = trimmed.slice(-4);
return { hasApiKey: true, apiKeyMasked: `***${last4}` };
}
function buildQdrantSettingsResponse(settings: Record<string, unknown>) {
const cfg = normalizeQdrantConfig(settings);
const { hasApiKey, apiKeyMasked } = maskApiKey(cfg.apiKey);
return {
enabled: cfg.enabled,
host: cfg.host,
port: cfg.port,
collection: cfg.collection,
embeddingModel: cfg.embeddingModel,
hasApiKey,
apiKeyMasked,
};
}
export async function GET(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const settings = (await getSettings()) as Record<string, unknown>;
return NextResponse.json(buildQdrantSettingsResponse(settings));
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}
export async function PUT(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(QdrantSettingsUpdateSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
const body = validation.data;
try {
const updates: Record<string, unknown> = {};
if (body.enabled !== undefined) updates.qdrantEnabled = body.enabled;
if (body.host !== undefined) updates.qdrantHost = body.host;
if (body.port !== undefined) updates.qdrantPort = body.port;
if (body.collection !== undefined) updates.qdrantCollection = body.collection;
if (body.embeddingModel !== undefined) updates.qdrantEmbeddingModel = body.embeddingModel;
if (body.apiKey !== undefined) {
// Empty string = remove key
updates.qdrantApiKey = body.apiKey === "" ? null : body.apiKey;
}
const newSettings = (await updateSettings(updates)) as Record<string, unknown>;
return NextResponse.json(buildQdrantSettingsResponse(newSettings));
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
import { QdrantSearchSchema } from "@/shared/schemas/qdrant";
import { searchSemanticMemory } from "@/lib/memory/qdrant";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
export async function POST(request: NextRequest) {
if (!(await isAuthenticated(request))) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid JSON body", details: [] } },
{ status: 400 },
);
}
const validation = validateBody(QdrantSearchSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json(validation.error, { status: 400 });
}
const { query, topK } = validation.data;
try {
const result = await searchSemanticMemory(query, topK);
return NextResponse.json({
ok: result.ok,
results: result.results ?? [],
});
} catch (err: unknown) {
const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
return NextResponse.json({ error: { message } }, { status: 500 });
}
}