@studious-lms/server 1.4.2 → 1.4.3

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 (145) hide show
  1. package/.coderabbit.yaml +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +67 -28
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/fileUpload.d.ts +10 -2
  6. package/dist/lib/fileUpload.d.ts.map +1 -1
  7. package/dist/lib/fileUpload.js +38 -6
  8. package/dist/lib/fileUpload.js.map +1 -1
  9. package/dist/middleware/auth.d.ts.map +1 -1
  10. package/dist/middleware/auth.js +18 -5
  11. package/dist/middleware/auth.js.map +1 -1
  12. package/dist/models/auth.d.ts +19 -14
  13. package/dist/models/auth.d.ts.map +1 -1
  14. package/dist/models/auth.js +53 -30
  15. package/dist/models/auth.js.map +1 -1
  16. package/dist/models/class.d.ts +3 -1
  17. package/dist/models/class.d.ts.map +1 -1
  18. package/dist/models/class.js +16 -4
  19. package/dist/models/class.js.map +1 -1
  20. package/dist/models/notification.d.ts +11 -10
  21. package/dist/models/notification.d.ts.map +1 -1
  22. package/dist/models/notification.js +13 -9
  23. package/dist/models/notification.js.map +1 -1
  24. package/dist/models/worksheet.d.ts +378 -7
  25. package/dist/models/worksheet.d.ts.map +1 -1
  26. package/dist/models/worksheet.js +168 -2
  27. package/dist/models/worksheet.js.map +1 -1
  28. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  29. package/dist/pipelines/aiLabChat.js +11 -3
  30. package/dist/pipelines/aiLabChat.js.map +1 -1
  31. package/dist/pipelines/aiLabChatContract.d.ts +3 -3
  32. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  33. package/dist/pipelines/aiNewtonChat.js +25 -14
  34. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  35. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  36. package/dist/pipelines/gradeWorksheet.js +10 -3
  37. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  38. package/dist/routers/_app.d.ts +124 -18
  39. package/dist/routers/_app.d.ts.map +1 -1
  40. package/dist/routers/class.d.ts +2 -1
  41. package/dist/routers/class.d.ts.map +1 -1
  42. package/dist/routers/class.js +3 -3
  43. package/dist/routers/class.js.map +1 -1
  44. package/dist/routers/labChat.d.ts +6 -0
  45. package/dist/routers/labChat.d.ts.map +1 -1
  46. package/dist/routers/labChat.js +5 -3
  47. package/dist/routers/labChat.js.map +1 -1
  48. package/dist/routers/message.d.ts +31 -0
  49. package/dist/routers/message.d.ts.map +1 -1
  50. package/dist/routers/message.js +27 -4
  51. package/dist/routers/message.js.map +1 -1
  52. package/dist/routers/newtonChat.d.ts +6 -0
  53. package/dist/routers/newtonChat.d.ts.map +1 -1
  54. package/dist/routers/newtonChat.js +5 -4
  55. package/dist/routers/newtonChat.js.map +1 -1
  56. package/dist/routers/notifications.d.ts +9 -2
  57. package/dist/routers/notifications.d.ts.map +1 -1
  58. package/dist/routers/notifications.js +4 -4
  59. package/dist/routers/notifications.js.map +1 -1
  60. package/dist/routers/worksheet.d.ts +8 -6
  61. package/dist/routers/worksheet.d.ts.map +1 -1
  62. package/dist/routers/worksheet.js +18 -16
  63. package/dist/routers/worksheet.js.map +1 -1
  64. package/dist/services/auth.d.ts +1 -1
  65. package/dist/services/auth.d.ts.map +1 -1
  66. package/dist/services/auth.js +19 -12
  67. package/dist/services/auth.js.map +1 -1
  68. package/dist/services/class.d.ts +1 -1
  69. package/dist/services/class.d.ts.map +1 -1
  70. package/dist/services/class.js +6 -3
  71. package/dist/services/class.js.map +1 -1
  72. package/dist/services/file.d.ts.map +1 -1
  73. package/dist/services/file.js +7 -2
  74. package/dist/services/file.js.map +1 -1
  75. package/dist/services/labChat.d.ts +6 -0
  76. package/dist/services/labChat.d.ts.map +1 -1
  77. package/dist/services/labChat.js +30 -18
  78. package/dist/services/labChat.js.map +1 -1
  79. package/dist/services/message.d.ts +30 -0
  80. package/dist/services/message.d.ts.map +1 -1
  81. package/dist/services/message.js +97 -21
  82. package/dist/services/message.js.map +1 -1
  83. package/dist/services/newtonChat.d.ts +6 -1
  84. package/dist/services/newtonChat.d.ts.map +1 -1
  85. package/dist/services/newtonChat.js +36 -18
  86. package/dist/services/newtonChat.js.map +1 -1
  87. package/dist/services/notification.d.ts +12 -5
  88. package/dist/services/notification.d.ts.map +1 -1
  89. package/dist/services/notification.js +24 -9
  90. package/dist/services/notification.js.map +1 -1
  91. package/dist/services/studentProgress.d.ts.map +1 -1
  92. package/dist/services/studentProgress.js +6 -3
  93. package/dist/services/studentProgress.js.map +1 -1
  94. package/dist/services/worksheet.d.ts +15 -15
  95. package/dist/services/worksheet.d.ts.map +1 -1
  96. package/dist/services/worksheet.js +138 -54
  97. package/dist/services/worksheet.js.map +1 -1
  98. package/dist/trpc.d.ts.map +1 -1
  99. package/dist/trpc.js +11 -17
  100. package/dist/trpc.js.map +1 -1
  101. package/dist/utils/inference.d.ts +9 -1
  102. package/dist/utils/inference.d.ts.map +1 -1
  103. package/dist/utils/inference.js +138 -50
  104. package/dist/utils/inference.js.map +1 -1
  105. package/package.json +1 -1
  106. package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
  107. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
  108. package/prisma/schema.prisma +27 -1
  109. package/scripts/test-pre-push.ts +6 -2
  110. package/src/index.ts +80 -28
  111. package/src/lib/fileUpload.ts +55 -5
  112. package/src/middleware/auth.ts +19 -4
  113. package/src/models/auth.ts +59 -29
  114. package/src/models/class.ts +15 -4
  115. package/src/models/notification.ts +13 -7
  116. package/src/models/worksheet.ts +202 -0
  117. package/src/pipelines/aiLabChat.ts +9 -1
  118. package/src/pipelines/aiNewtonChat.ts +24 -14
  119. package/src/pipelines/gradeWorksheet.ts +8 -0
  120. package/src/routers/class.ts +2 -2
  121. package/src/routers/labChat.ts +3 -1
  122. package/src/routers/message.ts +38 -1
  123. package/src/routers/newtonChat.ts +4 -2
  124. package/src/routers/notifications.ts +2 -2
  125. package/src/routers/worksheet.ts +31 -19
  126. package/src/services/auth.ts +26 -12
  127. package/src/services/class.ts +4 -1
  128. package/src/services/file.ts +6 -0
  129. package/src/services/labChat.ts +42 -18
  130. package/src/services/message.ts +163 -18
  131. package/src/services/newtonChat.ts +49 -18
  132. package/src/services/notification.ts +27 -8
  133. package/src/services/studentProgress.ts +4 -1
  134. package/src/services/worksheet.ts +191 -51
  135. package/src/trpc.ts +12 -18
  136. package/src/utils/inference.ts +209 -55
  137. package/tests/globalSetup.ts +13 -4
  138. package/tests/routers/auth.test.ts +120 -17
  139. package/tests/routers/class.test.ts +7 -1
  140. package/tests/routers/message.test.ts +236 -9
  141. package/tests/routers/newtonChat.test.ts +81 -0
  142. package/tests/routers/notification.test.ts +12 -0
  143. package/tests/routers/worksheet.test.ts +194 -0
  144. package/tests/setup.ts +22 -5
  145. package/tests/utils/inference.test.ts +218 -0
@@ -0,0 +1,218 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ const {
5
+ chatCreateMock,
6
+ responseParseMock,
7
+ modelsListMock,
8
+ prismaMocks,
9
+ logger,
10
+ } = vi.hoisted(() => ({
11
+ chatCreateMock: vi.fn(),
12
+ responseParseMock: vi.fn(),
13
+ modelsListMock: vi.fn(),
14
+ prismaMocks: {
15
+ aiUsageLog: {
16
+ create: vi.fn(),
17
+ },
18
+ message: {
19
+ create: vi.fn(),
20
+ },
21
+ },
22
+ logger: {
23
+ info: vi.fn(),
24
+ error: vi.fn(),
25
+ },
26
+ }));
27
+
28
+ vi.mock('openai', () => ({
29
+ default: class OpenAI {
30
+ chat = {
31
+ completions: {
32
+ create: chatCreateMock,
33
+ },
34
+ };
35
+
36
+ responses = {
37
+ parse: responseParseMock,
38
+ };
39
+
40
+ models = {
41
+ list: modelsListMock,
42
+ };
43
+ },
44
+ }));
45
+
46
+ vi.mock('openai/helpers/zod', () => ({
47
+ zodTextFormat: vi.fn((schema, name) => ({ schema, name })),
48
+ }));
49
+
50
+ vi.mock('../../src/lib/prisma.js', () => ({
51
+ prisma: prismaMocks,
52
+ }));
53
+
54
+ vi.mock('../../src/utils/logger.js', () => ({
55
+ logger,
56
+ }));
57
+
58
+ vi.mock('../../src/lib/pusher.js', () => ({
59
+ chatChannel: vi.fn(),
60
+ pusher: {
61
+ trigger: vi.fn(),
62
+ },
63
+ }));
64
+
65
+ vi.mock('../../src/utils/aiUser.js', () => ({
66
+ ensureAIUserExists: vi.fn(),
67
+ getAIUserId: vi.fn(() => 'AI_ASSISTANT'),
68
+ }));
69
+
70
+ vi.mock('../../src/lib/config/env.js', () => ({
71
+ env: {
72
+ INFERENCE_API_KEY: 'test-key',
73
+ INFERENCE_API_BASE_URL: 'http://localhost:11434/v1',
74
+ OPENAI_API_KEY: 'test-openai-key',
75
+ },
76
+ }));
77
+
78
+ import { estimateTokenCount, inference } from '../../src/utils/inference.js';
79
+
80
+ describe('inference', () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ prismaMocks.aiUsageLog.create.mockResolvedValue({ id: 'log-1' });
84
+ });
85
+
86
+ test('logs chat completion usage with the provided reason', async () => {
87
+ chatCreateMock.mockResolvedValue({
88
+ choices: [{ message: { content: 'Hello back' } }],
89
+ usage: {
90
+ prompt_tokens: 12,
91
+ completion_tokens: 8,
92
+ total_tokens: 20,
93
+ },
94
+ });
95
+
96
+ const result = await inference<string>('Hello there', undefined, {
97
+ reason: 'student_progress_chat',
98
+ metadata: { classId: 'class-1', studentId: 'student-1' },
99
+ });
100
+
101
+ expect(result).toBe('Hello back');
102
+ expect(prismaMocks.aiUsageLog.create).toHaveBeenCalledWith({
103
+ data: expect.objectContaining({
104
+ reason: 'student_progress_chat',
105
+ operation: 'chat.completions',
106
+ success: true,
107
+ promptTokens: 12,
108
+ completionTokens: 8,
109
+ totalTokens: 20,
110
+ estimatedInputTokens: estimateTokenCount('Hello there'),
111
+ metadata: { classId: 'class-1', studentId: 'student-1' },
112
+ }),
113
+ });
114
+ expect(logger.info).toHaveBeenCalledWith(
115
+ 'AI usage logged',
116
+ expect.objectContaining({
117
+ reason: 'student_progress_chat',
118
+ totalTokens: 20,
119
+ }),
120
+ );
121
+ expect(chatCreateMock).toHaveBeenCalledWith({
122
+ model: 'gpt-5-nano',
123
+ messages: [{ role: 'user', content: 'Hello there' }],
124
+ });
125
+ });
126
+
127
+ test('passes optional chat completion settings through the helper', async () => {
128
+ chatCreateMock.mockResolvedValue({
129
+ choices: [{ message: { content: 'Configured response' } }],
130
+ usage: {
131
+ prompt_tokens: 10,
132
+ completion_tokens: 5,
133
+ total_tokens: 15,
134
+ },
135
+ });
136
+
137
+ const result = await inference<string>('Introduce yourself', undefined, {
138
+ reason: 'newton_chat_introduction',
139
+ metadata: { conversationId: 'conversation-1' },
140
+ model: 'command-a-03-2025',
141
+ maxTokens: 300,
142
+ temperature: 0.8,
143
+ });
144
+
145
+ expect(result).toBe('Configured response');
146
+ expect(chatCreateMock).toHaveBeenCalledWith({
147
+ model: 'command-a-03-2025',
148
+ messages: [{ role: 'user', content: 'Introduce yourself' }],
149
+ max_tokens: 300,
150
+ temperature: 0.8,
151
+ });
152
+ expect(prismaMocks.aiUsageLog.create).toHaveBeenCalledWith({
153
+ data: expect.objectContaining({
154
+ reason: 'newton_chat_introduction',
155
+ model: 'command-a-03-2025',
156
+ metadata: { conversationId: 'conversation-1' },
157
+ }),
158
+ });
159
+ });
160
+
161
+ test('logs structured response usage for parsed responses', async () => {
162
+ responseParseMock.mockResolvedValue({
163
+ output_parsed: { score: 4 },
164
+ usage: {
165
+ input_tokens: 30,
166
+ output_tokens: 10,
167
+ total_tokens: 40,
168
+ },
169
+ });
170
+
171
+ const schema = z.object({ score: z.number() });
172
+ const result = await inference<{ score: number }>('Grade this', schema, {
173
+ reason: 'worksheet_autograde',
174
+ metadata: { worksheetResponseId: 'response-1' },
175
+ });
176
+
177
+ expect(result).toEqual({ score: 4 });
178
+ expect(prismaMocks.aiUsageLog.create).toHaveBeenCalledWith({
179
+ data: expect.objectContaining({
180
+ reason: 'worksheet_autograde',
181
+ operation: 'responses.parse',
182
+ success: true,
183
+ promptTokens: 30,
184
+ completionTokens: 10,
185
+ totalTokens: 40,
186
+ metadata: { worksheetResponseId: 'response-1' },
187
+ }),
188
+ });
189
+ });
190
+
191
+ test('logs failed attempts before rethrowing', async () => {
192
+ chatCreateMock.mockRejectedValue(new Error('OpenAI unavailable'));
193
+
194
+ await expect(
195
+ inference<string>('Hello there', undefined, {
196
+ reason: 'newton_chat_response',
197
+ metadata: { conversationId: 'conversation-1' },
198
+ }),
199
+ ).rejects.toThrow('OpenAI unavailable');
200
+
201
+ expect(prismaMocks.aiUsageLog.create).toHaveBeenCalledWith({
202
+ data: expect.objectContaining({
203
+ reason: 'newton_chat_response',
204
+ operation: 'chat.completions',
205
+ success: false,
206
+ estimatedInputTokens: estimateTokenCount('Hello there'),
207
+ error: 'OpenAI unavailable',
208
+ metadata: { conversationId: 'conversation-1' },
209
+ }),
210
+ });
211
+ expect(logger.error).toHaveBeenCalledWith(
212
+ 'Failed to generate inference response',
213
+ expect.objectContaining({
214
+ reason: 'newton_chat_response',
215
+ }),
216
+ );
217
+ });
218
+ });