@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.
- package/README.md +76 -0
- package/package.json +106 -0
- package/src/api-key-hash.ts +28 -0
- package/src/calendar/events.ts +34 -0
- package/src/calendar/route.ts +114 -0
- package/src/chat/credit-source.ts +1 -0
- package/src/chat/google/chat-request-schema.ts +150 -0
- package/src/chat/google/default-system-instruction.ts +198 -0
- package/src/chat/google/message-file-processing.ts +212 -0
- package/src/chat/google/mira-step-preparation.ts +221 -0
- package/src/chat/google/new/route.ts +368 -0
- package/src/chat/google/route-auth.ts +81 -0
- package/src/chat/google/route-chat-resolution.ts +98 -0
- package/src/chat/google/route-credits.ts +61 -0
- package/src/chat/google/route-message-preparation.ts +331 -0
- package/src/chat/google/route-mira-runtime.ts +206 -0
- package/src/chat/google/route.ts +632 -0
- package/src/chat/google/stream-finish-persistence.ts +722 -0
- package/src/chat/google/summary/route.ts +153 -0
- package/src/chat/mira-render-ui-policy.ts +540 -0
- package/src/chat/mira-system-instruction.ts +484 -0
- package/src/chat-sdk/adapters.ts +389 -0
- package/src/chat-sdk/registry.ts +197 -0
- package/src/chat-sdk.ts +33 -0
- package/src/core.ts +3 -0
- package/src/credits/cap-output-tokens.ts +90 -0
- package/src/credits/check-credits.ts +232 -0
- package/src/credits/constants.ts +30 -0
- package/src/credits/index.ts +46 -0
- package/src/credits/model-mapping.ts +92 -0
- package/src/credits/reservations.ts +514 -0
- package/src/credits/resolve-plan-model.ts +219 -0
- package/src/credits/sync-gateway-models.ts +351 -0
- package/src/credits/types.ts +109 -0
- package/src/credits/use-ai-credits.ts +3 -0
- package/src/embeddings/metered.ts +283 -0
- package/src/executions/route.ts +137 -0
- package/src/generate/route.ts +411 -0
- package/src/hooks.ts +7 -0
- package/src/meetings/summary/route.ts +7 -0
- package/src/meetings/transcription/route.ts +134 -0
- package/src/memory/client.ts +158 -0
- package/src/memory/config.ts +38 -0
- package/src/memory/index.ts +32 -0
- package/src/memory/ingest.ts +51 -0
- package/src/memory/middleware.ts +35 -0
- package/src/memory/operations.ts +480 -0
- package/src/memory/scope.ts +102 -0
- package/src/memory/settings.ts +121 -0
- package/src/memory/types.ts +101 -0
- package/src/memory/workspace.ts +36 -0
- package/src/memory.ts +1 -0
- package/src/mind/patch.ts +146 -0
- package/src/mind/route.ts +687 -0
- package/src/mind/tools.ts +1500 -0
- package/src/mind/types.ts +20 -0
- package/src/object/core.ts +3 -0
- package/src/object/flashcards/route.ts +140 -0
- package/src/object/quizzes/explanation/route.ts +145 -0
- package/src/object/quizzes/route.ts +142 -0
- package/src/object/types.ts +187 -0
- package/src/object/year-plan/route.ts +196 -0
- package/src/react.ts +1 -0
- package/src/scheduling/algorithm.ts +791 -0
- package/src/scheduling/default.ts +36 -0
- package/src/scheduling/duration-optimizer.ts +689 -0
- package/src/scheduling/index.ts +79 -0
- package/src/scheduling/priority-calculator.ts +187 -0
- package/src/scheduling/recurrence-calculator.ts +621 -0
- package/src/scheduling/templates.ts +892 -0
- package/src/scheduling/types.ts +136 -0
- package/src/scheduling/web-adapter.ts +308 -0
- package/src/scheduling.ts +6 -0
- package/src/supported-actions.ts +1 -0
- package/src/supported-providers.ts +6 -0
- package/src/tools/context-builder.ts +372 -0
- package/src/tools/core.ts +1 -0
- package/src/tools/definitions/calendar.ts +106 -0
- package/src/tools/definitions/finance.ts +197 -0
- package/src/tools/definitions/image.ts +74 -0
- package/src/tools/definitions/memory.ts +83 -0
- package/src/tools/definitions/meta.ts +154 -0
- package/src/tools/definitions/render-ui.ts +81 -0
- package/src/tools/definitions/tasks.ts +343 -0
- package/src/tools/definitions/time-tracking.ts +381 -0
- package/src/tools/definitions/workspace-context.ts +45 -0
- package/src/tools/definitions/workspace-user-chat.ts +111 -0
- package/src/tools/executors/calendar.ts +371 -0
- package/src/tools/executors/chat.ts +15 -0
- package/src/tools/executors/finance.ts +638 -0
- package/src/tools/executors/helpers/encryption.ts +107 -0
- package/src/tools/executors/image.ts +247 -0
- package/src/tools/executors/markitdown.ts +684 -0
- package/src/tools/executors/memory.ts +277 -0
- package/src/tools/executors/parallel-checks.ts +176 -0
- package/src/tools/executors/qr.ts +170 -0
- package/src/tools/executors/scope-helpers.ts +192 -0
- package/src/tools/executors/search.ts +149 -0
- package/src/tools/executors/settings.ts +40 -0
- package/src/tools/executors/tasks.ts +1087 -0
- package/src/tools/executors/theme.ts +23 -0
- package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
- package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
- package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
- package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
- package/src/tools/executors/timer/timer-helpers.ts +372 -0
- package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
- package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
- package/src/tools/executors/timer/timer-mutations.ts +19 -0
- package/src/tools/executors/timer/timer-queries.ts +18 -0
- package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
- package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
- package/src/tools/executors/timer/timer-session-queries.ts +153 -0
- package/src/tools/executors/timer/timer-session-updates.ts +200 -0
- package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
- package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
- package/src/tools/executors/timer.ts +22 -0
- package/src/tools/executors/user.ts +60 -0
- package/src/tools/executors/workspace.ts +135 -0
- package/src/tools/json-render-catalog.ts +875 -0
- package/src/tools/mira-tool-definitions.ts +55 -0
- package/src/tools/mira-tool-dispatcher.ts +265 -0
- package/src/tools/mira-tool-metadata.ts +164 -0
- package/src/tools/mira-tool-names.ts +95 -0
- package/src/tools/mira-tool-render-ui.ts +54 -0
- package/src/tools/mira-tool-types.ts +17 -0
- package/src/tools/mira-tools.ts +167 -0
- package/src/tools/normalize-render-ui-input.ts +321 -0
- package/src/tools/workspace-context.ts +233 -0
- 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
|
+
}
|