@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +30 -4
  3. package/dist/frontend/components/AiChatButton.js +3 -2
  4. package/dist/frontend/components/AiChatButton.js.map +2 -2
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
  11. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
  12. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
  14. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
  15. package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
  16. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  17. package/dist/modules/ai_assistant/api/settings/route.js +4 -3
  18. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  19. package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
  20. package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
  21. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
  22. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
  23. package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
  24. package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
  25. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
  29. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
  30. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  31. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
  32. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
  33. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
  34. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
  35. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
  36. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
  37. package/dist/modules/ai_assistant/cli.js +12 -0
  38. package/dist/modules/ai_assistant/cli.js.map +2 -2
  39. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
  40. package/dist/modules/ai_assistant/data/entities.js +177 -1
  41. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  42. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
  43. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  44. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
  45. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
  46. package/dist/modules/ai_assistant/events.js +8 -0
  47. package/dist/modules/ai_assistant/events.js.map +2 -2
  48. package/dist/modules/ai_assistant/i18n/de.json +74 -1
  49. package/dist/modules/ai_assistant/i18n/en.json +74 -1
  50. package/dist/modules/ai_assistant/i18n/es.json +75 -2
  51. package/dist/modules/ai_assistant/i18n/pl.json +74 -1
  52. package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
  53. package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
  54. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
  55. package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
  56. package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
  57. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  58. package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
  59. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  60. package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
  61. package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
  62. package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
  63. package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
  64. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
  65. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
  66. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
  67. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
  68. package/dist/modules/ai_assistant/setup.js +34 -0
  69. package/dist/modules/ai_assistant/setup.js.map +2 -2
  70. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
  71. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
  72. package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
  73. package/generated/entities/ai_token_usage_daily/index.ts +16 -0
  74. package/generated/entities/ai_token_usage_event/index.ts +19 -0
  75. package/generated/entities.ids.generated.ts +2 -0
  76. package/generated/entity-fields-registry.ts +47 -1
  77. package/package.json +15 -7
  78. package/src/frontend/components/AiChatButton.tsx +3 -2
  79. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
  80. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
  81. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
  82. package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
  83. package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
  84. package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
  85. package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
  86. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
  87. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
  88. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
  89. package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
  90. package/src/modules/ai_assistant/api/settings/route.ts +5 -3
  91. package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
  92. package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
  93. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
  94. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
  95. package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
  96. package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
  97. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
  98. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
  99. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
  100. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
  101. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
  103. package/src/modules/ai_assistant/cli.ts +18 -0
  104. package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
  105. package/src/modules/ai_assistant/data/entities.ts +237 -0
  106. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
  107. package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
  108. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
  109. package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
  110. package/src/modules/ai_assistant/events.ts +8 -0
  111. package/src/modules/ai_assistant/i18n/de.json +74 -1
  112. package/src/modules/ai_assistant/i18n/en.json +74 -1
  113. package/src/modules/ai_assistant/i18n/es.json +75 -2
  114. package/src/modules/ai_assistant/i18n/pl.json +74 -1
  115. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
  116. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
  117. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
  118. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
  119. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
  120. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
  121. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
  122. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
  123. package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
  124. package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
  125. package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
  126. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
  127. package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
  128. package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
  129. package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
  130. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
  131. package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
  132. package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
  133. package/src/modules/ai_assistant/setup.ts +49 -0
  134. package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
  135. package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
@@ -0,0 +1,111 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { AiTokenUsageRepository } from "../../../data/repositories/AiTokenUsageRepository.js";
6
+ import { hasRequiredFeatures } from "../../../lib/auth.js";
7
+ import { toDateString, toIntegerString, toIsoString } from "../../../lib/usage-serialization.js";
8
+ const REQUIRED_FEATURE = "ai_assistant.settings.manage";
9
+ const querySchema = z.object({
10
+ from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "from must be a date in YYYY-MM-DD format"),
11
+ to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "to must be a date in YYYY-MM-DD format"),
12
+ agentId: z.string().min(1).max(256).optional(),
13
+ modelId: z.string().min(1).max(256).optional()
14
+ });
15
+ const openApi = {
16
+ tag: "AI Assistant",
17
+ summary: "Token usage daily rollup",
18
+ methods: {
19
+ GET: {
20
+ operationId: "aiAssistantUsageDaily",
21
+ summary: "Fetch daily token-usage rollup rows for a date window.",
22
+ description: "Returns aggregated token-usage data from `ai_token_usage_daily` for the given date window. Tenant-scoped. Optionally filtered by `agentId` and/or `modelId`. Requires `ai_assistant.settings.manage`.",
23
+ query: querySchema,
24
+ responses: [
25
+ {
26
+ status: 200,
27
+ description: "Array of daily rollup rows.",
28
+ mediaType: "application/json"
29
+ }
30
+ ],
31
+ errors: [
32
+ { status: 400, description: "Invalid query parameters." },
33
+ { status: 401, description: "Unauthenticated caller." },
34
+ { status: 403, description: "Caller lacks `ai_assistant.settings.manage`." },
35
+ { status: 500, description: "Internal failure." }
36
+ ]
37
+ }
38
+ }
39
+ };
40
+ const metadata = {
41
+ path: "/ai_assistant/usage/daily",
42
+ GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
43
+ };
44
+ function jsonError(status, message, code, extra) {
45
+ return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
46
+ }
47
+ async function GET(req) {
48
+ const auth = await getAuthFromRequest(req);
49
+ if (!auth) {
50
+ return jsonError(401, "Unauthorized", "unauthenticated");
51
+ }
52
+ const { searchParams } = new URL(req.url);
53
+ const rawQuery = {
54
+ from: searchParams.get("from") ?? void 0,
55
+ to: searchParams.get("to") ?? void 0,
56
+ agentId: searchParams.get("agentId") ?? void 0,
57
+ modelId: searchParams.get("modelId") ?? void 0
58
+ };
59
+ const queryResult = querySchema.safeParse(rawQuery);
60
+ if (!queryResult.success) {
61
+ return jsonError(400, "Invalid query parameters.", "validation_error", {
62
+ issues: queryResult.error.issues
63
+ });
64
+ }
65
+ const { from, to, agentId, modelId } = queryResult.data;
66
+ try {
67
+ const container = await createRequestContainer();
68
+ const rbacService = container.resolve("rbacService");
69
+ const acl = await rbacService.loadAcl(auth.sub, {
70
+ tenantId: auth.tenantId,
71
+ organizationId: auth.orgId
72
+ });
73
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
74
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
75
+ }
76
+ if (!auth.tenantId) {
77
+ return NextResponse.json({ rows: [], total: 0 });
78
+ }
79
+ const em = container.resolve("em");
80
+ const repo = new AiTokenUsageRepository(em);
81
+ const rows = await repo.listDailyRollup(auth.tenantId, from, to, { agentId, modelId });
82
+ const serialized = rows.map((row) => ({
83
+ id: row.id,
84
+ tenantId: row.tenantId,
85
+ organizationId: row.organizationId ?? null,
86
+ day: toDateString(row.day),
87
+ agentId: row.agentId,
88
+ modelId: row.modelId,
89
+ providerId: row.providerId,
90
+ inputTokens: toIntegerString(row.inputTokens),
91
+ outputTokens: toIntegerString(row.outputTokens),
92
+ cachedInputTokens: toIntegerString(row.cachedInputTokens),
93
+ reasoningTokens: toIntegerString(row.reasoningTokens),
94
+ stepCount: toIntegerString(row.stepCount),
95
+ turnCount: toIntegerString(row.turnCount),
96
+ sessionCount: toIntegerString(row.sessionCount),
97
+ createdAt: toIsoString(row.createdAt),
98
+ updatedAt: toIsoString(row.updatedAt)
99
+ }));
100
+ return NextResponse.json({ rows: serialized, total: serialized.length });
101
+ } catch (error) {
102
+ console.error("[AI Usage Daily] GET error:", error);
103
+ return jsonError(500, "Failed to fetch daily usage data.", "internal_error");
104
+ }
105
+ }
106
+ export {
107
+ GET,
108
+ metadata,
109
+ openApi
110
+ };
111
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/ai_assistant/api/usage/daily/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { AiTokenUsageRepository } from '../../../data/repositories/AiTokenUsageRepository'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toDateString, toIntegerString, toIsoString } from '../../../lib/usage-serialization'\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\n\nconst querySchema = z.object({\n from: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'from must be a date in YYYY-MM-DD format'),\n to: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'to must be a date in YYYY-MM-DD format'),\n agentId: z.string().min(1).max(256).optional(),\n modelId: z.string().min(1).max(256).optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Token usage daily rollup',\n methods: {\n GET: {\n operationId: 'aiAssistantUsageDaily',\n summary: 'Fetch daily token-usage rollup rows for a date window.',\n description:\n 'Returns aggregated token-usage data from `ai_token_usage_daily` for the given ' +\n 'date window. Tenant-scoped. Optionally filtered by `agentId` and/or `modelId`. ' +\n 'Requires `ai_assistant.settings.manage`.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Array of daily rollup rows.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 500, description: 'Internal failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n path: '/ai_assistant/usage/daily',\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(status: number, message: string, code: string, extra?: Record<string, unknown>): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const { searchParams } = new URL(req.url)\n const rawQuery = {\n from: searchParams.get('from') ?? undefined,\n to: searchParams.get('to') ?? undefined,\n agentId: searchParams.get('agentId') ?? undefined,\n modelId: searchParams.get('modelId') ?? undefined,\n }\n\n const queryResult = querySchema.safeParse(rawQuery)\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n const { from, to, agentId, modelId } = queryResult.data\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return NextResponse.json({ rows: [], total: 0 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTokenUsageRepository(em)\n const rows = await repo.listDailyRollup(auth.tenantId, from, to, { agentId, modelId })\n\n const serialized = rows.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId ?? null,\n day: toDateString(row.day),\n agentId: row.agentId,\n modelId: row.modelId,\n providerId: row.providerId,\n inputTokens: toIntegerString(row.inputTokens),\n outputTokens: toIntegerString(row.outputTokens),\n cachedInputTokens: toIntegerString(row.cachedInputTokens),\n reasoningTokens: toIntegerString(row.reasoningTokens),\n stepCount: toIntegerString(row.stepCount),\n turnCount: toIntegerString(row.turnCount),\n sessionCount: toIntegerString(row.sessionCount),\n createdAt: toIsoString(row.createdAt),\n updatedAt: toIsoString(row.updatedAt),\n }))\n\n return NextResponse.json({ rows: serialized, total: serialized.length })\n } catch (error) {\n console.error('[AI Usage Daily] GET error:', error)\n return jsonError(500, 'Failed to fetch daily usage data.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,108 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { AiTokenUsageRepository } from "../../../../data/repositories/AiTokenUsageRepository.js";
6
+ import { hasRequiredFeatures } from "../../../../lib/auth.js";
7
+ import { toInteger, toIsoString } from "../../../../lib/usage-serialization.js";
8
+ const REQUIRED_FEATURE = "ai_assistant.settings.manage";
9
+ const sessionIdParamSchema = z.object({
10
+ sessionId: z.string().trim().uuid("sessionId must be a valid UUID")
11
+ });
12
+ const openApi = {
13
+ tag: "AI Assistant",
14
+ summary: "Per-step token usage events for a session",
15
+ methods: {
16
+ GET: {
17
+ operationId: "aiAssistantUsageSessionDetail",
18
+ summary: "Fetch per-step token usage event rows for a single session.",
19
+ description: "Returns up to 200 raw `ai_token_usage_events` rows for the given `sessionId`, ordered by `created_at ASC, step_index ASC`. Tenant-scoped. Requires `ai_assistant.settings.manage`.",
20
+ responses: [
21
+ {
22
+ status: 200,
23
+ description: "Array of per-step event rows for the session.",
24
+ mediaType: "application/json"
25
+ }
26
+ ],
27
+ errors: [
28
+ { status: 400, description: "Invalid session id (must be a UUID)." },
29
+ { status: 401, description: "Unauthenticated caller." },
30
+ { status: 403, description: "Caller lacks `ai_assistant.settings.manage`." },
31
+ { status: 404, description: "No events found for the given session id in the caller's tenant." },
32
+ { status: 500, description: "Internal failure." }
33
+ ]
34
+ }
35
+ }
36
+ };
37
+ const metadata = {
38
+ path: "/ai_assistant/usage/sessions/[sessionId]",
39
+ GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
40
+ };
41
+ function jsonError(status, message, code, extra) {
42
+ return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
43
+ }
44
+ async function GET(req, context) {
45
+ const auth = await getAuthFromRequest(req);
46
+ if (!auth) {
47
+ return jsonError(401, "Unauthorized", "unauthenticated");
48
+ }
49
+ const rawParams = await context.params;
50
+ const paramResult = sessionIdParamSchema.safeParse(rawParams);
51
+ if (!paramResult.success) {
52
+ return jsonError(400, "Invalid session id.", "validation_error", {
53
+ issues: paramResult.error.issues
54
+ });
55
+ }
56
+ const { sessionId } = paramResult.data;
57
+ try {
58
+ const container = await createRequestContainer();
59
+ const rbacService = container.resolve("rbacService");
60
+ const acl = await rbacService.loadAcl(auth.sub, {
61
+ tenantId: auth.tenantId,
62
+ organizationId: auth.orgId
63
+ });
64
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
65
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
66
+ }
67
+ if (!auth.tenantId) {
68
+ return jsonError(404, `No events found for session "${sessionId}".`, "session_not_found");
69
+ }
70
+ const em = container.resolve("em");
71
+ const repo = new AiTokenUsageRepository(em);
72
+ const events = await repo.listEventsForSession(auth.tenantId, sessionId);
73
+ if (events.length === 0) {
74
+ return jsonError(404, `No events found for session "${sessionId}".`, "session_not_found");
75
+ }
76
+ const serialized = events.map((event) => ({
77
+ id: event.id,
78
+ tenantId: event.tenantId,
79
+ organizationId: event.organizationId ?? null,
80
+ userId: event.userId,
81
+ agentId: event.agentId,
82
+ moduleId: event.moduleId,
83
+ sessionId: event.sessionId,
84
+ turnId: event.turnId,
85
+ stepIndex: toInteger(event.stepIndex),
86
+ providerId: event.providerId,
87
+ modelId: event.modelId,
88
+ inputTokens: toInteger(event.inputTokens),
89
+ outputTokens: toInteger(event.outputTokens),
90
+ cachedInputTokens: event.cachedInputTokens == null ? null : toInteger(event.cachedInputTokens),
91
+ reasoningTokens: event.reasoningTokens == null ? null : toInteger(event.reasoningTokens),
92
+ finishReason: event.finishReason ?? null,
93
+ loopAbortReason: event.loopAbortReason ?? null,
94
+ createdAt: toIsoString(event.createdAt),
95
+ updatedAt: toIsoString(event.updatedAt)
96
+ }));
97
+ return NextResponse.json({ events: serialized, total: serialized.length, sessionId });
98
+ } catch (error) {
99
+ console.error("[AI Usage Session Detail] GET error:", error);
100
+ return jsonError(500, "Failed to fetch session event data.", "internal_error");
101
+ }
102
+ }
103
+ export {
104
+ GET,
105
+ metadata,
106
+ openApi
107
+ };
108
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/ai_assistant/api/usage/sessions/%5BsessionId%5D/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { AiTokenUsageRepository } from '../../../../data/repositories/AiTokenUsageRepository'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport { toInteger, toIsoString } from '../../../../lib/usage-serialization'\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\n\nconst sessionIdParamSchema = z.object({\n sessionId: z\n .string()\n .trim()\n .uuid('sessionId must be a valid UUID'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-step token usage events for a session',\n methods: {\n GET: {\n operationId: 'aiAssistantUsageSessionDetail',\n summary: 'Fetch per-step token usage event rows for a single session.',\n description:\n 'Returns up to 200 raw `ai_token_usage_events` rows for the given `sessionId`, ' +\n 'ordered by `created_at ASC, step_index ASC`. Tenant-scoped. ' +\n 'Requires `ai_assistant.settings.manage`.',\n responses: [\n {\n status: 200,\n description: 'Array of per-step event rows for the session.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid session id (must be a UUID).' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 404, description: 'No events found for the given session id in the caller\\'s tenant.' },\n { status: 500, description: 'Internal failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n path: '/ai_assistant/usage/sessions/[sessionId]',\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ sessionId: string }>\n}\n\nfunction jsonError(status: number, message: string, code: string, extra?: Record<string, unknown>): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const rawParams = await context.params\n const paramResult = sessionIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid session id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n\n const { sessionId } = paramResult.data\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return jsonError(404, `No events found for session \"${sessionId}\".`, 'session_not_found')\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiTokenUsageRepository(em)\n const events = await repo.listEventsForSession(auth.tenantId, sessionId)\n\n if (events.length === 0) {\n return jsonError(404, `No events found for session \"${sessionId}\".`, 'session_not_found')\n }\n\n const serialized = events.map((event) => ({\n id: event.id,\n tenantId: event.tenantId,\n organizationId: event.organizationId ?? null,\n userId: event.userId,\n agentId: event.agentId,\n moduleId: event.moduleId,\n sessionId: event.sessionId,\n turnId: event.turnId,\n stepIndex: toInteger(event.stepIndex),\n providerId: event.providerId,\n modelId: event.modelId,\n inputTokens: toInteger(event.inputTokens),\n outputTokens: toInteger(event.outputTokens),\n cachedInputTokens: event.cachedInputTokens == null ? null : toInteger(event.cachedInputTokens),\n reasoningTokens: event.reasoningTokens == null ? null : toInteger(event.reasoningTokens),\n finishReason: event.finishReason ?? null,\n loopAbortReason: event.loopAbortReason ?? null,\n createdAt: toIsoString(event.createdAt),\n updatedAt: toIsoString(event.updatedAt),\n }))\n\n return NextResponse.json({ events: serialized, total: serialized.length, sessionId })\n } catch (error) {\n console.error('[AI Usage Session Detail] GET error:', error)\n return jsonError(500, 'Failed to fetch session event data.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,WAAW,mBAAmB;AAEvC,MAAM,mBAAmB;AAEzB,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,WAAW,EACR,OAAO,EACP,KAAK,EACL,KAAK,gCAAgC;AAC1C,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAGF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,uCAAuC;AAAA,QACnE,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,+CAA+C;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,mEAAoE;AAAA,QAChG,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAChE;AAMA,SAAS,UAAU,QAAgB,SAAiB,MAAc,OAA+C;AAC/G,SAAO,aAAa,KAAK,EAAE,OAAO,SAAS,MAAM,GAAI,SAAS,CAAC,EAAG,GAAG,EAAE,OAAO,CAAC;AACjF;AAEA,eAAsB,IAAI,KAAkB,SAA0C;AACpF,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAAA,EACzD;AAEA,QAAM,YAAY,MAAM,QAAQ;AAChC,QAAM,cAAc,qBAAqB,UAAU,SAAS;AAC5D,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,uBAAuB,oBAAoB;AAAA,MAC/D,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,QAAM,EAAE,UAAU,IAAI,YAAY;AAElC,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAED,QAAI,CAAC,oBAAoB,CAAC,gBAAgB,GAAG,IAAI,UAAU,IAAI,cAAc,WAAW,GAAG;AACzF,aAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,IAC3F;AAEA,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,UAAU,KAAK,gCAAgC,SAAS,MAAM,mBAAmB;AAAA,IAC1F;AAEA,UAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,UAAM,OAAO,IAAI,uBAAuB,EAAE;AAC1C,UAAM,SAAS,MAAM,KAAK,qBAAqB,KAAK,UAAU,SAAS;AAEvE,QAAI,OAAO,WAAW,GAAG;AACvB,aAAO,UAAU,KAAK,gCAAgC,SAAS,MAAM,mBAAmB;AAAA,IAC1F;AAEA,UAAM,aAAa,OAAO,IAAI,CAAC,WAAW;AAAA,MACxC,IAAI,MAAM;AAAA,MACV,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM;AAAA,MACd,WAAW,UAAU,MAAM,SAAS;AAAA,MACpC,YAAY,MAAM;AAAA,MAClB,SAAS,MAAM;AAAA,MACf,aAAa,UAAU,MAAM,WAAW;AAAA,MACxC,cAAc,UAAU,MAAM,YAAY;AAAA,MAC1C,mBAAmB,MAAM,qBAAqB,OAAO,OAAO,UAAU,MAAM,iBAAiB;AAAA,MAC7F,iBAAiB,MAAM,mBAAmB,OAAO,OAAO,UAAU,MAAM,eAAe;AAAA,MACvF,cAAc,MAAM,gBAAgB;AAAA,MACpC,iBAAiB,MAAM,mBAAmB;AAAA,MAC1C,WAAW,YAAY,MAAM,SAAS;AAAA,MACtC,WAAW,YAAY,MAAM,SAAS;AAAA,IACxC,EAAE;AAEF,WAAO,aAAa,KAAK,EAAE,QAAQ,YAAY,OAAO,WAAW,QAAQ,UAAU,CAAC;AAAA,EACtF,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC,KAAK;AAC3D,WAAO,UAAU,KAAK,uCAAuC,gBAAgB;AAAA,EAC/E;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,153 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { hasRequiredFeatures } from "../../../lib/auth.js";
6
+ import { toInteger, toIsoString } from "../../../lib/usage-serialization.js";
7
+ const REQUIRED_FEATURE = "ai_assistant.settings.manage";
8
+ const MAX_PAGE_SIZE = 100;
9
+ const querySchema = z.object({
10
+ from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "from must be a date in YYYY-MM-DD format"),
11
+ to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "to must be a date in YYYY-MM-DD format"),
12
+ agentId: z.string().min(1).max(256).optional(),
13
+ limit: z.string().optional().transform((val) => val !== void 0 ? parseInt(val, 10) : MAX_PAGE_SIZE).refine((val) => !isNaN(val) && val > 0 && val <= MAX_PAGE_SIZE, {
14
+ message: `limit must be between 1 and ${MAX_PAGE_SIZE}`
15
+ }),
16
+ offset: z.string().optional().transform((val) => val !== void 0 ? parseInt(val, 10) : 0).refine((val) => !isNaN(val) && val >= 0, { message: "offset must be a non-negative integer" })
17
+ });
18
+ const openApi = {
19
+ tag: "AI Assistant",
20
+ summary: "Per-session token usage totals",
21
+ methods: {
22
+ GET: {
23
+ operationId: "aiAssistantUsageSessions",
24
+ summary: "List per-session token usage totals for a date window.",
25
+ description: "Returns aggregated token-usage data grouped by `session_id` from `ai_token_usage_events` for the given date window. Tenant-scoped. Optionally filtered by `agentId`. Paginated via `limit` / `offset`. Requires `ai_assistant.settings.manage`.",
26
+ query: querySchema,
27
+ responses: [
28
+ {
29
+ status: 200,
30
+ description: "Array of session-level usage summaries plus pagination metadata.",
31
+ mediaType: "application/json"
32
+ }
33
+ ],
34
+ errors: [
35
+ { status: 400, description: "Invalid query parameters." },
36
+ { status: 401, description: "Unauthenticated caller." },
37
+ { status: 403, description: "Caller lacks `ai_assistant.settings.manage`." },
38
+ { status: 500, description: "Internal failure." }
39
+ ]
40
+ }
41
+ }
42
+ };
43
+ const metadata = {
44
+ path: "/ai_assistant/usage/sessions",
45
+ GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
46
+ };
47
+ function jsonError(status, message, code, extra) {
48
+ return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
49
+ }
50
+ async function GET(req) {
51
+ const auth = await getAuthFromRequest(req);
52
+ if (!auth) {
53
+ return jsonError(401, "Unauthorized", "unauthenticated");
54
+ }
55
+ const { searchParams } = new URL(req.url);
56
+ const rawQuery = {
57
+ from: searchParams.get("from") ?? void 0,
58
+ to: searchParams.get("to") ?? void 0,
59
+ agentId: searchParams.get("agentId") ?? void 0,
60
+ limit: searchParams.get("limit") ?? void 0,
61
+ offset: searchParams.get("offset") ?? void 0
62
+ };
63
+ const queryResult = querySchema.safeParse(rawQuery);
64
+ if (!queryResult.success) {
65
+ return jsonError(400, "Invalid query parameters.", "validation_error", {
66
+ issues: queryResult.error.issues
67
+ });
68
+ }
69
+ const { from, to, agentId, limit, offset } = queryResult.data;
70
+ try {
71
+ const container = await createRequestContainer();
72
+ const rbacService = container.resolve("rbacService");
73
+ const acl = await rbacService.loadAcl(auth.sub, {
74
+ tenantId: auth.tenantId,
75
+ organizationId: auth.orgId
76
+ });
77
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
78
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
79
+ }
80
+ if (!auth.tenantId) {
81
+ return NextResponse.json({ sessions: [], total: 0, limit, offset });
82
+ }
83
+ const em = container.resolve("em");
84
+ const connection = em.getConnection();
85
+ const params = [auth.tenantId, from, to];
86
+ let agentFilter = "";
87
+ if (agentId) {
88
+ agentFilter = "and agent_id = ?";
89
+ params.push(agentId);
90
+ }
91
+ const countParams = [...params];
92
+ const countSql = `
93
+ select count(distinct session_id)::bigint as total
94
+ from ai_token_usage_events
95
+ where tenant_id = ?
96
+ and created_at >= ?::date
97
+ and created_at < (?::date + interval '1 day')
98
+ ${agentFilter}
99
+ `;
100
+ const countRows = await connection.execute(countSql, countParams, "all");
101
+ const totalRaw = Array.isArray(countRows) && countRows.length > 0 ? countRows[0].total : "0";
102
+ const total = toInteger(totalRaw);
103
+ params.push(limit, offset);
104
+ const dataSql = `
105
+ select
106
+ session_id,
107
+ agent_id,
108
+ module_id,
109
+ user_id,
110
+ min(created_at) as started_at,
111
+ max(created_at) as last_event_at,
112
+ count(*)::bigint as step_count,
113
+ count(distinct turn_id)::bigint as turn_count,
114
+ sum(input_tokens)::bigint as input_tokens,
115
+ sum(output_tokens)::bigint as output_tokens,
116
+ sum(coalesce(cached_input_tokens, 0))::bigint as cached_input_tokens,
117
+ sum(coalesce(reasoning_tokens, 0))::bigint as reasoning_tokens
118
+ from ai_token_usage_events
119
+ where tenant_id = ?
120
+ and created_at >= ?::date
121
+ and created_at < (?::date + interval '1 day')
122
+ ${agentFilter}
123
+ group by session_id, agent_id, module_id, user_id
124
+ order by started_at desc
125
+ limit ? offset ?
126
+ `;
127
+ const dataRows = await connection.execute(dataSql, params, "all");
128
+ const sessions = Array.isArray(dataRows) ? dataRows.map((row) => ({
129
+ sessionId: row.session_id,
130
+ agentId: row.agent_id,
131
+ moduleId: row.module_id,
132
+ userId: row.user_id,
133
+ startedAt: toIsoString(row.started_at),
134
+ lastEventAt: toIsoString(row.last_event_at),
135
+ stepCount: toInteger(row.step_count),
136
+ turnCount: toInteger(row.turn_count),
137
+ inputTokens: toInteger(row.input_tokens),
138
+ outputTokens: toInteger(row.output_tokens),
139
+ cachedInputTokens: toInteger(row.cached_input_tokens),
140
+ reasoningTokens: toInteger(row.reasoning_tokens)
141
+ })) : [];
142
+ return NextResponse.json({ sessions, total, limit, offset });
143
+ } catch (error) {
144
+ console.error("[AI Usage Sessions] GET error:", error);
145
+ return jsonError(500, "Failed to fetch session usage data.", "internal_error");
146
+ }
147
+ }
148
+ export {
149
+ GET,
150
+ metadata,
151
+ openApi
152
+ };
153
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/ai_assistant/api/usage/sessions/route.ts"],
4
+ "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toInteger, toIsoString } from '../../../lib/usage-serialization'\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\n\nconst MAX_PAGE_SIZE = 100\n\nconst querySchema = z.object({\n from: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'from must be a date in YYYY-MM-DD format'),\n to: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'to must be a date in YYYY-MM-DD format'),\n agentId: z.string().min(1).max(256).optional(),\n limit: z\n .string()\n .optional()\n .transform((val) => (val !== undefined ? parseInt(val, 10) : MAX_PAGE_SIZE))\n .refine((val) => !isNaN(val) && val > 0 && val <= MAX_PAGE_SIZE, {\n message: `limit must be between 1 and ${MAX_PAGE_SIZE}`,\n }),\n offset: z\n .string()\n .optional()\n .transform((val) => (val !== undefined ? parseInt(val, 10) : 0))\n .refine((val) => !isNaN(val) && val >= 0, { message: 'offset must be a non-negative integer' }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-session token usage totals',\n methods: {\n GET: {\n operationId: 'aiAssistantUsageSessions',\n summary: 'List per-session token usage totals for a date window.',\n description:\n 'Returns aggregated token-usage data grouped by `session_id` from `ai_token_usage_events` ' +\n 'for the given date window. Tenant-scoped. Optionally filtered by `agentId`. ' +\n 'Paginated via `limit` / `offset`. Requires `ai_assistant.settings.manage`.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Array of session-level usage summaries plus pagination metadata.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 500, description: 'Internal failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n path: '/ai_assistant/usage/sessions',\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(status: number, message: string, code: string, extra?: Record<string, unknown>): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const { searchParams } = new URL(req.url)\n const rawQuery = {\n from: searchParams.get('from') ?? undefined,\n to: searchParams.get('to') ?? undefined,\n agentId: searchParams.get('agentId') ?? undefined,\n limit: searchParams.get('limit') ?? undefined,\n offset: searchParams.get('offset') ?? undefined,\n }\n\n const queryResult = querySchema.safeParse(rawQuery)\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n const { from, to, agentId, limit, offset } = queryResult.data\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return NextResponse.json({ sessions: [], total: 0, limit, offset })\n }\n\n const em = container.resolve<EntityManager>('em')\n const connection = em.getConnection()\n\n const params: unknown[] = [auth.tenantId, from, to]\n let agentFilter = ''\n if (agentId) {\n agentFilter = 'and agent_id = ?'\n params.push(agentId)\n }\n\n const countParams = [...params]\n const countSql = `\n select count(distinct session_id)::bigint as total\n from ai_token_usage_events\n where tenant_id = ?\n and created_at >= ?::date\n and created_at < (?::date + interval '1 day')\n ${agentFilter}\n `\n const countRows = await connection.execute(countSql, countParams, 'all')\n const totalRaw = Array.isArray(countRows) && countRows.length > 0\n ? (countRows[0] as Record<string, unknown>).total\n : '0'\n const total = toInteger(totalRaw)\n\n params.push(limit, offset)\n const dataSql = `\n select\n session_id,\n agent_id,\n module_id,\n user_id,\n min(created_at) as started_at,\n max(created_at) as last_event_at,\n count(*)::bigint as step_count,\n count(distinct turn_id)::bigint as turn_count,\n sum(input_tokens)::bigint as input_tokens,\n sum(output_tokens)::bigint as output_tokens,\n sum(coalesce(cached_input_tokens, 0))::bigint as cached_input_tokens,\n sum(coalesce(reasoning_tokens, 0))::bigint as reasoning_tokens\n from ai_token_usage_events\n where tenant_id = ?\n and created_at >= ?::date\n and created_at < (?::date + interval '1 day')\n ${agentFilter}\n group by session_id, agent_id, module_id, user_id\n order by started_at desc\n limit ? offset ?\n `\n const dataRows = await connection.execute(dataSql, params, 'all')\n\n const sessions = Array.isArray(dataRows)\n ? (dataRows as Array<Record<string, unknown>>).map((row) => ({\n sessionId: row.session_id as string,\n agentId: row.agent_id as string,\n moduleId: row.module_id as string,\n userId: row.user_id as string,\n startedAt: toIsoString(row.started_at),\n lastEventAt: toIsoString(row.last_event_at),\n stepCount: toInteger(row.step_count),\n turnCount: toInteger(row.turn_count),\n inputTokens: toInteger(row.input_tokens),\n outputTokens: toInteger(row.output_tokens),\n cachedInputTokens: toInteger(row.cached_input_tokens),\n reasoningTokens: toInteger(row.reasoning_tokens),\n }))\n : []\n\n return NextResponse.json({ sessions, total, limit, offset })\n } catch (error) {\n console.error('[AI Usage Sessions] GET error:', error)\n return jsonError(500, 'Failed to fetch session usage data.', 'internal_error')\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }