File size: 4,152 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import { NextResponse } from "next/server";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { listMemories, createMemory, getMemoryTokensUsed } from "@/lib/memory/store";
import { memoryCache } from "@/lib/memory/cache";
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),
  key: z.string().min(1),
  type: z.nativeEnum(MemoryType).default(MemoryType.FACTUAL),
  sessionId: z.string().default(""),
  apiKeyId: z.string().default(""),
  metadata: z.record(z.string(), z.unknown()).default({}),
  expiresAt: z.coerce.date().nullable().default(null),
});

export async function GET(request: Request) {
  const authError = await requireManagementAuth(request);
  if (authError) return authError;

  try {
    const url = new URL(request.url);
    const { searchParams } = url;

    const paginationParams = parsePaginationParams(searchParams);
    const rawOffset = searchParams.get("offset");
    const offset =
      typeof rawOffset === "string" && rawOffset.trim().length > 0
        ? Math.max(0, Number.parseInt(rawOffset, 10) || 0)
        : undefined;
    const query = searchParams.get("q") || undefined;

    const apiKeyId = searchParams.get("apiKeyId") || undefined;
    const type = (searchParams.get("type") as any) || undefined;
    const sessionId = searchParams.get("sessionId") || undefined;

    const result = await listMemories({
      apiKeyId,
      type,
      sessionId,
      query,
      limit: paginationParams.limit,
      offset,
      page: offset === undefined ? paginationParams.page : undefined,
    });

    // Total tokens across all memories (computed in SQL inside the domain module
    // to avoid loading every memory's content into process memory).
    const tokensUsed = getMemoryTokensUsed(apiKeyId);

    // Compute hit rate from memory cache
    const cacheStats = memoryCache.stats();
    const totalCacheRequests = cacheStats.hits + cacheStats.misses;
    const hitRate = totalCacheRequests > 0 ? cacheStats.hits / totalCacheRequests : 0;

    const stats = {
      total: result.total,
      byType: result.byType ?? {},
      tokensUsed,
      hitRate,
      // Plan 21 / D18: MemoriesTab uses cacheStats to decide whether to render
      // the Hit Rate card (only when hits + misses > 0). Without this field
      // the card would never appear even when hitRate > 0.
      cacheStats: { hits: cacheStats.hits, misses: cacheStats.misses },
    };

    const responsePagination =
      offset === undefined
        ? paginationParams
        : {
            ...paginationParams,
            page: Math.floor(offset / paginationParams.limit) + 1,
          };

    const paginatedResponse = buildPaginatedResponse(result.data, result.total, responsePagination);

    return NextResponse.json({
      ...paginatedResponse,
      stats,
    });
  } catch (err: unknown) {
    const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
    return NextResponse.json({ error: { message } }, { status: 500 });
  }
}

export async function POST(request: Request) {
  const authError = await requireManagementAuth(request);
  if (authError) return authError;

  try {
    const rawBody = await request.json();
    const validation = validateBody(createMemorySchema, rawBody);
    if (isValidationFailure(validation)) {
      return NextResponse.json(validation.error, { status: 400 });
    }
    const memoryId = await createMemory(validation.data);
    return NextResponse.json({ success: true, id: memoryId });
  } catch (err: unknown) {
    const message = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
    return NextResponse.json({ error: { message } }, { status: 400 });
  }
}