@tuturuuu/ai 0.0.10

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 (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,368 @@
1
+ import { google } from '@ai-sdk/google';
2
+ import { createClient } from '@tuturuuu/supabase/next/server';
3
+ import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
4
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
5
+ import { extractIPFromHeaders } from '@tuturuuu/utils/abuse-protection';
6
+ import {
7
+ cascadeBackendRateLimitToProxyBan,
8
+ isBackendRateLimitError,
9
+ } from '@tuturuuu/utils/abuse-protection/backend-rate-limit';
10
+ import { validateAiTempAuthRequest } from '@tuturuuu/utils/ai-temp-auth';
11
+ import { generateText, type UIMessage } from 'ai';
12
+ import { NextResponse } from 'next/server';
13
+ import { normalizeStableModelId } from '../../../credits/model-mapping';
14
+ import {
15
+ resolveAiMemoryWorkspaceIdForUser,
16
+ withAiMemory,
17
+ } from '../../../memory';
18
+ import {
19
+ isInternalTuturuuuAiUser,
20
+ resolveSupabaseSessionUser,
21
+ } from '../route-auth';
22
+
23
+ const HUMAN_PROMPT = '\n\nHuman:';
24
+ const AI_PROMPT = '\n\nAssistant:';
25
+
26
+ /** Always use a lightweight model for title generation */
27
+ const TITLE_MODEL = 'gemini-3.1-flash-lite';
28
+
29
+ async function buildRateLimitResponse(
30
+ req: Request,
31
+ {
32
+ source,
33
+ userId,
34
+ }: {
35
+ source: 'auth' | 'database';
36
+ userId?: string | null;
37
+ }
38
+ ) {
39
+ const ipAddress = extractIPFromHeaders(req.headers);
40
+ const blockInfo = await cascadeBackendRateLimitToProxyBan({
41
+ endpoint: new URL(req.url).pathname,
42
+ ipAddress,
43
+ source,
44
+ userId,
45
+ });
46
+ const retryAfter = blockInfo
47
+ ? Math.max(
48
+ 1,
49
+ Math.ceil((blockInfo.expiresAt.getTime() - Date.now()) / 1000)
50
+ )
51
+ : 60;
52
+
53
+ return NextResponse.json(
54
+ { error: 'Too Many Requests', message: 'Rate limit exceeded' },
55
+ {
56
+ status: 429,
57
+ headers: {
58
+ 'Retry-After': `${retryAfter}`,
59
+ },
60
+ }
61
+ );
62
+ }
63
+
64
+ function getErrorMessage(error: unknown): string {
65
+ if (
66
+ error &&
67
+ typeof error === 'object' &&
68
+ 'message' in error &&
69
+ typeof error.message === 'string'
70
+ ) {
71
+ return error.message;
72
+ }
73
+
74
+ return 'Internal server error.';
75
+ }
76
+
77
+ type GatewayAuthenticatedClient = {
78
+ supabase: TypedSupabaseClient;
79
+ user: SupabaseUser;
80
+ };
81
+
82
+ type GatewayAuthResolution =
83
+ | {
84
+ auth: GatewayAuthenticatedClient;
85
+ ok: true;
86
+ }
87
+ | {
88
+ ok: false;
89
+ response: Response;
90
+ }
91
+ | null;
92
+
93
+ type CreatePostOptions = {
94
+ /** Resolves gateway-level JWT auth without making @tuturuuu/ai depend on auth. */
95
+ resolveGatewayAuth?: (request: Request) => Promise<GatewayAuthResolution>;
96
+ /** Gateway provider prefix for bare model names. Defaults to 'google'. */
97
+ defaultProvider?: string;
98
+ };
99
+
100
+ type ChatMessageInsertMode = 'direct' | 'rpc';
101
+
102
+ type AuthenticatedContext = GatewayAuthenticatedClient & {
103
+ messageInsertMode: ChatMessageInsertMode;
104
+ };
105
+
106
+ export function createPOST(options: CreatePostOptions = {}) {
107
+ return async function handler(req: Request) {
108
+ try {
109
+ const { id, model, message, isMiraMode } = (await req.json()) as {
110
+ id?: string;
111
+ model?: string;
112
+ message?: string;
113
+ isMiraMode?: boolean;
114
+ };
115
+
116
+ if (!message)
117
+ return NextResponse.json('No message provided', { status: 400 });
118
+
119
+ const tempAuth = await validateAiTempAuthRequest(req);
120
+ if (tempAuth.status === 'revoked') {
121
+ return NextResponse.json('Unauthorized', { status: 401 });
122
+ }
123
+
124
+ let auth: AuthenticatedContext | null = null;
125
+
126
+ if (tempAuth.status === 'valid') {
127
+ const supabase = (await createClient(req)) as TypedSupabaseClient;
128
+ auth = {
129
+ messageInsertMode: 'rpc',
130
+ supabase,
131
+ user: tempAuth.context.user as SupabaseUser,
132
+ };
133
+ } else {
134
+ const gatewayAuth = await options.resolveGatewayAuth?.(req);
135
+
136
+ if (gatewayAuth?.ok === false) {
137
+ return gatewayAuth.response;
138
+ }
139
+
140
+ if (gatewayAuth?.ok) {
141
+ auth = {
142
+ messageInsertMode: 'direct',
143
+ ...gatewayAuth.auth,
144
+ };
145
+ }
146
+ }
147
+
148
+ if (!auth) {
149
+ const supabase = (await createClient(req)) as TypedSupabaseClient;
150
+
151
+ const { user: sessionUser, authError } =
152
+ await resolveSupabaseSessionUser(supabase);
153
+
154
+ if (isBackendRateLimitError(authError)) {
155
+ return buildRateLimitResponse(req, { source: 'auth' });
156
+ }
157
+
158
+ if (sessionUser) {
159
+ auth = {
160
+ messageInsertMode: 'rpc',
161
+ supabase,
162
+ user: sessionUser,
163
+ };
164
+ }
165
+ }
166
+
167
+ if (!auth) return NextResponse.json('Unauthorized', { status: 401 });
168
+
169
+ const { messageInsertMode, supabase, user } = auth;
170
+
171
+ if (isMiraMode) {
172
+ const allowed = await isInternalTuturuuuAiUser({
173
+ ok: true,
174
+ supabase,
175
+ user,
176
+ ...(tempAuth.status === 'valid'
177
+ ? { tempAuthContext: tempAuth.context }
178
+ : {}),
179
+ });
180
+ if (!allowed) {
181
+ return NextResponse.json(
182
+ { error: 'Mira mode is limited to @tuturuuu.com accounts' },
183
+ { status: 403 }
184
+ );
185
+ }
186
+ }
187
+
188
+ const prompt = buildPrompt([
189
+ {
190
+ id: 'initial-message',
191
+ parts: [{ type: 'text', text: `"${message}"` }],
192
+ role: 'user',
193
+ },
194
+ ]);
195
+
196
+ const wsId = await resolveAiMemoryWorkspaceIdForUser({
197
+ supabase,
198
+ userId: user.id,
199
+ });
200
+
201
+ // Always use TITLE_MODEL for generating chat titles (cheap + fast)
202
+ const result = await generateText({
203
+ model: await withAiMemory({
204
+ addMemory: 'never',
205
+ customId: id ? `chat-title-${id}` : `chat-title-${Date.now()}`,
206
+ model: google(TITLE_MODEL),
207
+ product: isMiraMode ? 'mira' : 'ai_chat',
208
+ source: 'ai_chat_title',
209
+ surface: 'ai_chat_title',
210
+ userId: user.id,
211
+ wsId,
212
+ }),
213
+ prompt,
214
+ providerOptions: {
215
+ google: {
216
+ safetySettings: [
217
+ {
218
+ category: 'HARM_CATEGORY_HARASSMENT',
219
+ threshold: 'BLOCK_NONE',
220
+ },
221
+ {
222
+ category: 'HARM_CATEGORY_HATE_SPEECH',
223
+ threshold: 'BLOCK_NONE',
224
+ },
225
+ {
226
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
227
+ threshold: 'BLOCK_NONE',
228
+ },
229
+ {
230
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
231
+ threshold: 'BLOCK_NONE',
232
+ },
233
+ ],
234
+ },
235
+ },
236
+ });
237
+
238
+ const title = result.text;
239
+
240
+ if (!title) {
241
+ return NextResponse.json(
242
+ {
243
+ message: 'Internal server error.',
244
+ },
245
+ { status: 500 }
246
+ );
247
+ }
248
+
249
+ // Store bare model name for DB compatibility (ai_models FK)
250
+ const resolvedModel = model
251
+ ? normalizeStableModelId(
252
+ model.includes('/') ? model.split('/').pop()! : model
253
+ ).toLowerCase()
254
+ : TITLE_MODEL;
255
+
256
+ const { data: chat, error: chatError } = await supabase
257
+ .from('ai_chats')
258
+ .insert({
259
+ id,
260
+ title,
261
+ creator_id: user.id,
262
+ model: resolvedModel,
263
+ })
264
+ .select('id')
265
+ .single();
266
+
267
+ if (chatError) {
268
+ if (isBackendRateLimitError(chatError)) {
269
+ return buildRateLimitResponse(req, {
270
+ source: 'database',
271
+ userId: user.id,
272
+ });
273
+ }
274
+
275
+ console.log(chatError);
276
+ return NextResponse.json(getErrorMessage(chatError), { status: 500 });
277
+ }
278
+
279
+ const source = isMiraMode ? 'Mira' : 'Rewise';
280
+ const { error: msgError } =
281
+ messageInsertMode === 'direct'
282
+ ? await supabase.from('ai_chat_messages').insert({
283
+ chat_id: chat.id,
284
+ content: message,
285
+ creator_id: user.id,
286
+ role: 'USER',
287
+ metadata: { source },
288
+ })
289
+ : await supabase.rpc('insert_ai_chat_message', {
290
+ message: message,
291
+ chat_id: chat.id,
292
+ source,
293
+ });
294
+
295
+ if (msgError) {
296
+ if (isBackendRateLimitError(msgError)) {
297
+ return buildRateLimitResponse(req, {
298
+ source: 'database',
299
+ userId: user.id,
300
+ });
301
+ }
302
+
303
+ console.log(msgError);
304
+ return NextResponse.json(getErrorMessage(msgError), { status: 500 });
305
+ }
306
+
307
+ return NextResponse.json({ id: chat.id, title }, { status: 200 });
308
+ } catch (error: unknown) {
309
+ console.log(error);
310
+ return NextResponse.json(
311
+ {
312
+ message: `## Edge API Failure\nCould not complete the request. Please view the **Stack trace** below.\n\`\`\`bash\n${error instanceof Error ? error.stack : 'Unknown error'}`,
313
+ },
314
+ {
315
+ status: 500,
316
+ }
317
+ );
318
+ }
319
+ };
320
+ }
321
+
322
+ const normalize = (message: UIMessage) => {
323
+ const { parts, role } = message;
324
+ // Extract text from parts array
325
+ const content =
326
+ parts?.map((part) => (part.type === 'text' ? part.text : '')).join('') ||
327
+ '';
328
+ if (role === 'user') return `${HUMAN_PROMPT} ${content}`;
329
+ if (role === 'assistant') return `${AI_PROMPT} ${content}`;
330
+ return content;
331
+ };
332
+
333
+ const normalizeMessages = (messages: UIMessage[]) =>
334
+ [...leadingMessages, ...messages, ...trailingMessages]
335
+ .map(normalize)
336
+ .join('')
337
+ .trim();
338
+
339
+ function buildPrompt(messages: UIMessage[]) {
340
+ const normalizedMsgs = normalizeMessages(messages);
341
+ return normalizedMsgs + AI_PROMPT;
342
+ }
343
+
344
+ const leadingMessages: UIMessage[] = [
345
+ {
346
+ id: 'initial-message',
347
+ role: 'assistant',
348
+ parts: [
349
+ {
350
+ type: 'text',
351
+ text: 'Please provide an initial message so I can generate a short and comprehensive title for this chat conversation.',
352
+ },
353
+ ],
354
+ },
355
+ ];
356
+
357
+ const trailingMessages: UIMessage[] = [
358
+ {
359
+ id: 'final-message',
360
+ role: 'assistant',
361
+ parts: [
362
+ {
363
+ type: 'text',
364
+ text: 'Thank you, I will respond with a title in my next response that will briefly demonstrate what the chat conversation is about, and it will only contain the title without any quotation marks, markdown, and anything else but the title. The title will be in the language you provided the initial message in.',
365
+ },
366
+ ],
367
+ },
368
+ ];
@@ -0,0 +1,81 @@
1
+ import { resolveAuthenticatedSessionUser } from '@tuturuuu/supabase/next/auth-session-user';
2
+ import { createClient } from '@tuturuuu/supabase/next/server';
3
+ import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
4
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
5
+ import {
6
+ type AiTempAuthContext,
7
+ validateAiTempAuthRequest,
8
+ } from '@tuturuuu/utils/ai-temp-auth';
9
+ import { isExactTuturuuuDotComEmail } from '@tuturuuu/utils/email/client';
10
+ import { NextResponse } from 'next/server';
11
+
12
+ export type AiRouteAuthResult =
13
+ | {
14
+ ok: true;
15
+ supabase: TypedSupabaseClient;
16
+ user: SupabaseUser;
17
+ tempAuthContext?: AiTempAuthContext;
18
+ }
19
+ | {
20
+ ok: false;
21
+ response: Response;
22
+ };
23
+
24
+ export const resolveSupabaseSessionUser = resolveAuthenticatedSessionUser;
25
+
26
+ export async function resolveAiRouteAuth(
27
+ request: Request
28
+ ): Promise<AiRouteAuthResult> {
29
+ const supabase = (await createClient(request)) as TypedSupabaseClient;
30
+ const tempAuth = await validateAiTempAuthRequest(request);
31
+
32
+ if (tempAuth.status === 'revoked') {
33
+ return {
34
+ ok: false,
35
+ response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
36
+ };
37
+ }
38
+
39
+ if (tempAuth.status === 'valid') {
40
+ return {
41
+ ok: true,
42
+ supabase,
43
+ user: tempAuth.context.user as SupabaseUser,
44
+ tempAuthContext: tempAuth.context,
45
+ };
46
+ }
47
+
48
+ const { user } = await resolveSupabaseSessionUser(supabase);
49
+
50
+ if (!user) {
51
+ return {
52
+ ok: false,
53
+ response: new Response('Unauthorized', { status: 401 }),
54
+ };
55
+ }
56
+
57
+ return { ok: true, supabase, user };
58
+ }
59
+
60
+ export async function isInternalTuturuuuAiUser(
61
+ auth: Extract<AiRouteAuthResult, { ok: true }>
62
+ ) {
63
+ const tempEmail =
64
+ typeof auth.tempAuthContext?.user?.email === 'string'
65
+ ? auth.tempAuthContext.user.email
66
+ : null;
67
+ const sessionEmail =
68
+ typeof auth.user.email === 'string' ? auth.user.email : null;
69
+
70
+ if (isExactTuturuuuDotComEmail(tempEmail ?? sessionEmail)) {
71
+ return true;
72
+ }
73
+
74
+ const { data } = await auth.supabase
75
+ .from('user_private_details')
76
+ .select('email')
77
+ .eq('user_id', auth.user.id)
78
+ .maybeSingle();
79
+
80
+ return isExactTuturuuuDotComEmail(data?.email);
81
+ }
@@ -0,0 +1,98 @@
1
+ type AdminClientLike = {
2
+ message?: string;
3
+ };
4
+
5
+ export async function resolveChatIdForUser(
6
+ requestedChatId: string | undefined,
7
+ fetchLatestChatId: () => PromiseLike<{
8
+ data: { id: string } | null;
9
+ error: { message: string } | null;
10
+ }>
11
+ ): Promise<{ chatId: string } | { error: Response }> {
12
+ if (requestedChatId) {
13
+ return { chatId: requestedChatId };
14
+ }
15
+
16
+ const { data, error } = await fetchLatestChatId();
17
+
18
+ if (error) {
19
+ console.error(error.message);
20
+ return { error: new Response(error.message, { status: 500 }) };
21
+ }
22
+
23
+ if (!data) {
24
+ return { error: new Response('Internal Server Error', { status: 500 }) };
25
+ }
26
+
27
+ return { chatId: data.id };
28
+ }
29
+
30
+ type MoveTempFilesToThreadParams = {
31
+ loadThread: () => PromiseLike<{
32
+ data: { role?: string }[] | null;
33
+ error: { message: string } | null;
34
+ }>;
35
+ listFiles: (tempStoragePath: string) => PromiseLike<{
36
+ data: { name: string }[] | null;
37
+ error: AdminClientLike | null;
38
+ }>;
39
+ moveFile: (
40
+ fromPath: string,
41
+ toPath: string
42
+ ) => PromiseLike<{ error: AdminClientLike | null }>;
43
+ wsId?: string;
44
+ chatId: string;
45
+ userId: string;
46
+ };
47
+
48
+ export async function moveTempFilesToThread({
49
+ loadThread,
50
+ listFiles,
51
+ moveFile,
52
+ wsId,
53
+ chatId,
54
+ userId,
55
+ }: MoveTempFilesToThreadParams): Promise<Response | null> {
56
+ if (!wsId) {
57
+ return null;
58
+ }
59
+
60
+ const { data: thread, error: threadError } = await loadThread();
61
+
62
+ if (threadError) {
63
+ console.error('Error getting thread:', threadError);
64
+ return new Response(threadError.message, { status: 500 });
65
+ }
66
+
67
+ if (!thread || thread.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ const tempStoragePath = `${wsId}/chats/ai/resources/temp/${userId}`;
72
+ const { data: files, error: listError } = await listFiles(tempStoragePath);
73
+
74
+ if (listError) {
75
+ console.error('Error getting files:', listError);
76
+ return null;
77
+ }
78
+
79
+ if (!files?.length) {
80
+ return null;
81
+ }
82
+
83
+ await Promise.all(
84
+ files.map(async (file) => {
85
+ const fileName = file.name;
86
+ const { error: copyError } = await moveFile(
87
+ `${tempStoragePath}/${fileName}`,
88
+ `${wsId}/chats/ai/resources/${chatId}/${fileName}`
89
+ );
90
+
91
+ if (copyError) {
92
+ console.error('File copy error:', { fileName, copyError });
93
+ }
94
+ })
95
+ );
96
+
97
+ return null;
98
+ }
@@ -0,0 +1,61 @@
1
+ import { capMaxOutputTokensByCredits } from '@tuturuuu/ai/credits/cap-output-tokens';
2
+ import { checkAiCredits } from '@tuturuuu/ai/credits/check-credits';
3
+ import type { SupabaseClient } from '@tuturuuu/supabase';
4
+
5
+ type AdminRpcClientLike = SupabaseClient;
6
+
7
+ type CreditPreflightParams = {
8
+ wsId?: string;
9
+ model: string;
10
+ userId: string;
11
+ sbAdmin: AdminRpcClientLike;
12
+ };
13
+
14
+ export async function performCreditPreflight({
15
+ wsId,
16
+ model,
17
+ userId,
18
+ sbAdmin,
19
+ }: CreditPreflightParams): Promise<
20
+ { cappedMaxOutput: number | null } | { error: Response }
21
+ > {
22
+ const creditCheck = await checkAiCredits(wsId ?? undefined, model, 'chat', {
23
+ userId,
24
+ });
25
+
26
+ if (creditCheck && !creditCheck.allowed) {
27
+ return {
28
+ error: Response.json(
29
+ {
30
+ error: creditCheck.errorMessage || 'AI credits insufficient',
31
+ code: creditCheck.errorCode,
32
+ },
33
+ { status: 403 }
34
+ ),
35
+ };
36
+ }
37
+
38
+ const cappedMaxOutput = creditCheck
39
+ ? await capMaxOutputTokensByCredits(
40
+ sbAdmin,
41
+ model,
42
+ creditCheck.maxOutputTokens,
43
+ creditCheck.remainingCredits
44
+ )
45
+ : null;
46
+
47
+ if (
48
+ cappedMaxOutput === null &&
49
+ creditCheck &&
50
+ creditCheck.remainingCredits <= 0
51
+ ) {
52
+ return {
53
+ error: Response.json(
54
+ { error: 'AI credits insufficient', code: 'CREDITS_EXHAUSTED' },
55
+ { status: 403 }
56
+ ),
57
+ };
58
+ }
59
+
60
+ return { cappedMaxOutput };
61
+ }