@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,632 @@
1
+ import { google } from '@ai-sdk/google';
2
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
3
+ import {
4
+ normalizeWorkspaceId,
5
+ verifyWorkspaceMembershipType,
6
+ } from '@tuturuuu/utils/workspace-helper';
7
+ import {
8
+ consumeStream,
9
+ type ModelMessage,
10
+ smoothStream,
11
+ stepCountIs,
12
+ streamText,
13
+ } from 'ai';
14
+ import { type NextRequest, NextResponse } from 'next/server';
15
+ import { normalizeStableModelId } from '../../credits/model-mapping';
16
+ import {
17
+ PlanModelResolutionError,
18
+ resolvePlanModel,
19
+ } from '../../credits/resolve-plan-model';
20
+ import { withAiMemory } from '../../memory';
21
+ import type { CreditSource as SharedCreditSource } from '../credit-source';
22
+ import {
23
+ shouldForceGoogleSearchForLatestUserMessage,
24
+ shouldForceRenderUiForLatestUserMessage,
25
+ shouldForceWorkspaceMembersForLatestUserMessage,
26
+ shouldPreferMarkdownTablesForLatestUserMessage,
27
+ shouldResolveWorkspaceContextForLatestUserMessage,
28
+ shouldUseParallelChecksForLatestUserMessage,
29
+ } from '../mira-render-ui-policy';
30
+ import { ChatRequestBodySchema, mapToUIMessages } from './chat-request-schema';
31
+ import { systemInstruction } from './default-system-instruction';
32
+ import { prepareMiraToolStep } from './mira-step-preparation';
33
+ import {
34
+ type AiRouteAuthResult,
35
+ isInternalTuturuuuAiUser,
36
+ resolveAiRouteAuth,
37
+ } from './route-auth';
38
+ import {
39
+ moveTempFilesToThread,
40
+ resolveChatIdForUser,
41
+ } from './route-chat-resolution';
42
+ import { performCreditPreflight } from './route-credits';
43
+ import {
44
+ persistLatestUserMessage,
45
+ prepareProcessedMessages,
46
+ } from './route-message-preparation';
47
+ import { prepareMiraRuntime } from './route-mira-runtime';
48
+ import {
49
+ buildAbortedStreamFinishResponse,
50
+ persistAssistantResponse,
51
+ } from './stream-finish-persistence';
52
+
53
+ type ThinkingMode = 'fast' | 'thinking';
54
+
55
+ function splitSystemMessages(messages: ModelMessage[]) {
56
+ const systemMessages: string[] = [];
57
+ const nonSystemMessages: ModelMessage[] = [];
58
+
59
+ for (const message of messages) {
60
+ if (message.role === 'system') {
61
+ const systemMessage = getTextContent(message.content);
62
+ if (systemMessage) systemMessages.push(systemMessage);
63
+ continue;
64
+ }
65
+
66
+ nonSystemMessages.push(message);
67
+ }
68
+
69
+ return {
70
+ messages: nonSystemMessages,
71
+ system: systemMessages.join('\n\n').trim(),
72
+ };
73
+ }
74
+
75
+ function getTextContent(content: ModelMessage['content']) {
76
+ if (typeof content === 'string') return content.trim();
77
+ if (!Array.isArray(content)) return '';
78
+
79
+ return content
80
+ .map((part) => (part.type === 'text' ? part.text.trim() : ''))
81
+ .filter(Boolean)
82
+ .join('\n')
83
+ .trim();
84
+ }
85
+
86
+ function mergeSystemInstructions(...instructions: Array<string | null>) {
87
+ return instructions
88
+ .map((instruction) => instruction?.trim())
89
+ .filter(Boolean)
90
+ .join('\n\n');
91
+ }
92
+
93
+ export function createPOST(
94
+ _options: {
95
+ serverAPIKeyFallback?: boolean;
96
+ /** Gateway provider prefix for bare model names (e.g., 'openai', 'anthropic', 'vertex'). Defaults to 'google'. */
97
+ defaultProvider?: string;
98
+ resolveAuth?: (request: NextRequest) => Promise<AiRouteAuthResult | null>;
99
+ } = {}
100
+ ) {
101
+ const defaultProvider = _options.defaultProvider ?? 'google';
102
+
103
+ // Higher-order function that returns the actual request handler
104
+ return async function handler(req: NextRequest): Promise<Response> {
105
+ try {
106
+ const sbAdmin = await createAdminClient();
107
+ let requestBody: unknown;
108
+ try {
109
+ requestBody = await req.json();
110
+ } catch (error) {
111
+ return NextResponse.json(
112
+ {
113
+ error: 'Invalid JSON payload',
114
+ message:
115
+ error instanceof Error ? error.message : 'Malformed JSON body',
116
+ },
117
+ { status: 400 }
118
+ );
119
+ }
120
+
121
+ const parsedBody = ChatRequestBodySchema.safeParse(requestBody);
122
+ if (!parsedBody.success) {
123
+ return NextResponse.json(
124
+ {
125
+ error: 'Invalid request body',
126
+ issues: parsedBody.error.issues,
127
+ },
128
+ { status: 400 }
129
+ );
130
+ }
131
+
132
+ const {
133
+ id,
134
+ model,
135
+ messages,
136
+ wsId,
137
+ workspaceContextId,
138
+ isMiraMode,
139
+ timezone,
140
+ thinkingMode: rawThinkingMode,
141
+ creditSource: requestedCreditSourceRaw,
142
+ creditWsId: rawCreditWsId,
143
+ observabilityContext,
144
+ taskBoardContext,
145
+ } = parsedBody.data;
146
+ const thinkingMode: ThinkingMode =
147
+ rawThinkingMode === 'thinking' ? 'thinking' : 'fast';
148
+
149
+ if (!messages) {
150
+ console.error('Missing messages');
151
+ return new Response('Missing messages', { status: 400 });
152
+ }
153
+
154
+ const auth =
155
+ (await _options.resolveAuth?.(req)) ?? (await resolveAiRouteAuth(req));
156
+ if (!auth.ok) return auth.response;
157
+ const { supabase, user } = auth;
158
+
159
+ if (isMiraMode && !(await isInternalTuturuuuAiUser(auth))) {
160
+ return NextResponse.json(
161
+ { error: 'Mira mode is limited to @tuturuuu.com accounts' },
162
+ { status: 403 }
163
+ );
164
+ }
165
+
166
+ // Normalize both workspace identifiers so slugs like 'personal' resolve to UUIDs.
167
+ let normalizedWsId: string | null = null;
168
+ let requestedCreditWsId: string | undefined;
169
+ try {
170
+ normalizedWsId = wsId
171
+ ? await normalizeWorkspaceId(wsId, supabase, req)
172
+ : null;
173
+ requestedCreditWsId = rawCreditWsId
174
+ ? await normalizeWorkspaceId(rawCreditWsId, supabase, req)
175
+ : undefined;
176
+ } catch (normError) {
177
+ console.error(
178
+ 'Workspace ID normalization failed:',
179
+ normError instanceof Error ? normError.message : normError
180
+ );
181
+ return NextResponse.json(
182
+ { error: 'Invalid workspace identifier' },
183
+ { status: 422 }
184
+ );
185
+ }
186
+
187
+ if (normalizedWsId) {
188
+ const contextMembership = await verifyWorkspaceMembershipType({
189
+ wsId: normalizedWsId,
190
+ userId: user.id,
191
+ supabase: sbAdmin,
192
+ requiredType: 'MEMBER',
193
+ });
194
+
195
+ if (contextMembership.error === 'membership_lookup_failed') {
196
+ console.error('DB error checking workspace membership');
197
+ return NextResponse.json(
198
+ { error: 'Internal error verifying workspace access' },
199
+ { status: 500 }
200
+ );
201
+ }
202
+
203
+ if (!contextMembership.ok) {
204
+ return NextResponse.json(
205
+ { error: 'Workspace access denied' },
206
+ { status: 403 }
207
+ );
208
+ }
209
+ }
210
+
211
+ const requestedCreditSource: SharedCreditSource =
212
+ requestedCreditSourceRaw ?? 'workspace';
213
+ let billingWsId: string | null = normalizedWsId ?? null;
214
+
215
+ if (requestedCreditSource === 'personal') {
216
+ const { data: personalWorkspace, error: personalWorkspaceError } =
217
+ await sbAdmin
218
+ .from('workspaces')
219
+ .select('id, workspace_members!inner(user_id)')
220
+ .eq('personal', true)
221
+ .eq('workspace_members.user_id', user.id)
222
+ .maybeSingle();
223
+
224
+ if (personalWorkspaceError) {
225
+ console.error(
226
+ 'DB error looking up personal workspace:',
227
+ personalWorkspaceError.message
228
+ );
229
+ return NextResponse.json(
230
+ { error: 'Internal error resolving personal workspace' },
231
+ { status: 500 }
232
+ );
233
+ }
234
+
235
+ if (!personalWorkspace?.id) {
236
+ return NextResponse.json(
237
+ {
238
+ error:
239
+ 'Personal workspace not found. Please ensure your account has a personal workspace.',
240
+ code: 'PERSONAL_WORKSPACE_NOT_FOUND',
241
+ },
242
+ { status: 403 }
243
+ );
244
+ }
245
+
246
+ if (
247
+ requestedCreditWsId &&
248
+ requestedCreditWsId !== personalWorkspace.id
249
+ ) {
250
+ return NextResponse.json(
251
+ {
252
+ error:
253
+ 'Invalid credit workspace for personal credit source selection.',
254
+ code: 'INVALID_CREDIT_SOURCE',
255
+ },
256
+ { status: 403 }
257
+ );
258
+ }
259
+
260
+ billingWsId = personalWorkspace.id;
261
+ } else if (requestedCreditWsId) {
262
+ if (normalizedWsId && requestedCreditWsId !== normalizedWsId) {
263
+ return NextResponse.json(
264
+ {
265
+ error: 'Invalid credit workspace for workspace source selection.',
266
+ code: 'INVALID_CREDIT_SOURCE',
267
+ },
268
+ { status: 403 }
269
+ );
270
+ }
271
+
272
+ if (!normalizedWsId) {
273
+ const billingMembership = await verifyWorkspaceMembershipType({
274
+ wsId: requestedCreditWsId,
275
+ userId: user.id,
276
+ supabase: sbAdmin,
277
+ requiredType: 'MEMBER',
278
+ });
279
+
280
+ if (billingMembership.error === 'membership_lookup_failed') {
281
+ console.error('Failed to check billing workspace membership');
282
+ return NextResponse.json(
283
+ { error: 'Internal server error' },
284
+ { status: 500 }
285
+ );
286
+ }
287
+
288
+ if (!billingMembership.ok) {
289
+ return NextResponse.json(
290
+ {
291
+ error: 'Workspace access denied for selected credit workspace.',
292
+ code: 'INVALID_CREDIT_SOURCE',
293
+ },
294
+ { status: 403 }
295
+ );
296
+ }
297
+
298
+ billingWsId = requestedCreditWsId;
299
+ }
300
+ }
301
+
302
+ let resolvedModelId: string;
303
+ try {
304
+ if (billingWsId) {
305
+ const resolvedPlanModel = await resolvePlanModel({
306
+ capability: 'language',
307
+ requestedModel: model,
308
+ wsId: billingWsId,
309
+ });
310
+ resolvedModelId = resolvedPlanModel.modelId;
311
+ } else if (model) {
312
+ resolvedModelId = normalizeStableModelId(
313
+ model.includes('/') ? model : `${defaultProvider}/${model}`
314
+ );
315
+ } else {
316
+ return NextResponse.json(
317
+ {
318
+ error: 'Model is required when billing workspace is unavailable.',
319
+ },
320
+ { status: 400 }
321
+ );
322
+ }
323
+ } catch (error) {
324
+ if (error instanceof PlanModelResolutionError) {
325
+ const status =
326
+ error.code === 'WORKSPACE_ID_REQUIRED'
327
+ ? 400
328
+ : error.code === 'NO_ALLOCATION'
329
+ ? 503
330
+ : 500;
331
+ return NextResponse.json(
332
+ { error: error.message, code: error.code },
333
+ { status }
334
+ );
335
+ }
336
+
337
+ console.error('Failed to resolve chat model:', error);
338
+ return NextResponse.json(
339
+ { error: 'Failed to resolve chat model' },
340
+ { status: 500 }
341
+ );
342
+ }
343
+
344
+ const resolvedChatId = await resolveChatIdForUser(id, () =>
345
+ sbAdmin
346
+ .from('ai_chats')
347
+ .select('id')
348
+ .eq('creator_id', user.id)
349
+ .order('created_at', { ascending: false })
350
+ .limit(1)
351
+ .single()
352
+ );
353
+ if ('error' in resolvedChatId) {
354
+ return resolvedChatId.error;
355
+ }
356
+ const chatId = resolvedChatId.chatId;
357
+
358
+ const moveFilesError = await moveTempFilesToThread({
359
+ loadThread: () =>
360
+ sbAdmin
361
+ .from('ai_chat_messages')
362
+ .select('role, ai_chats!chat_id!inner(creator_id)')
363
+ .eq('chat_id', chatId)
364
+ .eq('ai_chats.creator_id', user.id),
365
+ listFiles: (tempStoragePath) =>
366
+ sbAdmin.storage.from('workspaces').list(tempStoragePath),
367
+ moveFile: (fromPath, toPath) =>
368
+ sbAdmin.storage.from('workspaces').move(fromPath, toPath),
369
+ wsId: normalizedWsId ?? undefined,
370
+ chatId,
371
+ userId: user.id,
372
+ });
373
+ if (moveFilesError) {
374
+ return moveFilesError;
375
+ }
376
+
377
+ const normalizedMessages = mapToUIMessages(messages);
378
+ const preparedMessages = await prepareProcessedMessages(
379
+ normalizedMessages,
380
+ normalizedWsId ?? undefined,
381
+ chatId,
382
+ req,
383
+ {
384
+ attachYoutubeVideoInput:
385
+ resolvedModelId.toLowerCase().startsWith('google/') ||
386
+ resolvedModelId.toLowerCase().startsWith('google-vertex/'),
387
+ }
388
+ );
389
+ if ('error' in preparedMessages) {
390
+ return preparedMessages.error;
391
+ }
392
+ const { processedMessages } = preparedMessages;
393
+ const promptMessages = splitSystemMessages(processedMessages);
394
+
395
+ const persistUserMessageError = await persistLatestUserMessage({
396
+ processedMessages,
397
+ chatId,
398
+ insertChatMessage: async (args) => {
399
+ const { error } = await supabase.rpc(
400
+ 'insert_ai_chat_message' as never,
401
+ args as never
402
+ );
403
+ return { error };
404
+ },
405
+ source: isMiraMode ? 'Mira' : 'Rewise',
406
+ });
407
+ if (persistUserMessageError) {
408
+ return persistUserMessageError;
409
+ }
410
+
411
+ const creditPreflight = await performCreditPreflight({
412
+ wsId: billingWsId ?? normalizedWsId ?? undefined,
413
+ model: resolvedModelId,
414
+ userId: user.id,
415
+ sbAdmin,
416
+ });
417
+ if ('error' in creditPreflight) {
418
+ return creditPreflight.error;
419
+ }
420
+ const { cappedMaxOutput } = creditPreflight;
421
+
422
+ // Mutable ref so the render_ui preprocessor can read current steps
423
+ // at Zod-validation time (before the execute handler runs).
424
+ const stepsRef: { current: unknown[] } = { current: [] };
425
+
426
+ const { miraSystemPrompt, miraTools } = await prepareMiraRuntime({
427
+ isMiraMode,
428
+ wsId: normalizedWsId ?? undefined,
429
+ workspaceContextId,
430
+ creditWsId: billingWsId ?? normalizedWsId ?? undefined,
431
+ request: req,
432
+ user,
433
+ userId: user.id,
434
+ chatId,
435
+ supabase,
436
+ toolSupabase: sbAdmin as typeof supabase,
437
+ timezone,
438
+ taskBoardContext,
439
+ getSteps: () => stepsRef.current,
440
+ });
441
+
442
+ const effectiveSource = isMiraMode ? 'Mira' : 'Rewise';
443
+
444
+ const resolvedGatewayModel = await withAiMemory({
445
+ customId: chatId,
446
+ model: google(resolvedModelId.split('/').slice(-1)[0]!),
447
+ product: isMiraMode ? 'mira' : 'rewise',
448
+ source: effectiveSource,
449
+ surface: 'shared_chat',
450
+ userId: user.id,
451
+ wsId: billingWsId ?? normalizedWsId,
452
+ });
453
+
454
+ // Reasoning mode: default to fast unless the client explicitly requests thinking.
455
+ const modelLower = resolvedModelId.toLowerCase();
456
+ const supportsThinking =
457
+ modelLower.includes('gemini-2.5') || modelLower.includes('gemini-3');
458
+ const thinkingConfig = supportsThinking
459
+ ? thinkingMode === 'thinking'
460
+ ? { thinkingConfig: { includeThoughts: true } }
461
+ : {
462
+ thinkingConfig: {
463
+ thinkingBudget: 0,
464
+ includeThoughts: false,
465
+ },
466
+ }
467
+ : {};
468
+ const forceRenderUi = shouldForceRenderUiForLatestUserMessage(
469
+ promptMessages.messages
470
+ );
471
+ const forceGoogleSearch = shouldForceGoogleSearchForLatestUserMessage(
472
+ promptMessages.messages
473
+ );
474
+ const preferMarkdownTables =
475
+ shouldPreferMarkdownTablesForLatestUserMessage(promptMessages.messages);
476
+ const needsWorkspaceContextResolution =
477
+ shouldResolveWorkspaceContextForLatestUserMessage(
478
+ promptMessages.messages
479
+ );
480
+ const needsWorkspaceMembersTool =
481
+ shouldForceWorkspaceMembersForLatestUserMessage(
482
+ promptMessages.messages
483
+ );
484
+ const needsParallelChecks = shouldUseParallelChecksForLatestUserMessage(
485
+ promptMessages.messages
486
+ );
487
+
488
+ // Provider-native Google Search tool for non-Mira mode.
489
+ const googleSearchTool = {
490
+ google_search: google.tools.googleSearch({}),
491
+ };
492
+
493
+ type PrepareStep = NonNullable<
494
+ NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
495
+ >;
496
+ const prepareStep: PrepareStep = ({ steps }) => {
497
+ // Keep the mutable ref in sync so the render_ui preprocessor can
498
+ // read current steps during Zod validation.
499
+ stepsRef.current = steps;
500
+ return prepareMiraToolStep({
501
+ steps,
502
+ forceGoogleSearch,
503
+ forceRenderUi,
504
+ needsParallelChecks,
505
+ needsWorkspaceContextResolution,
506
+ needsWorkspaceMembersTool,
507
+ preferMarkdownTables,
508
+ });
509
+ };
510
+
511
+ let assistantResponsePersisted = false;
512
+ const persistChatAssistantResponse = async (
513
+ response: Parameters<typeof persistAssistantResponse>[0]['response']
514
+ ) => {
515
+ if (assistantResponsePersisted) return;
516
+ assistantResponsePersisted = true;
517
+ await persistAssistantResponse({
518
+ response,
519
+ sbAdmin,
520
+ chatId,
521
+ userId: user.id,
522
+ model: resolvedModelId,
523
+ effectiveSource,
524
+ wsId: billingWsId ?? normalizedWsId ?? undefined,
525
+ observabilityContext,
526
+ });
527
+ };
528
+
529
+ const result = streamText({
530
+ abortSignal: req.signal,
531
+ experimental_transform: smoothStream({ delayInMs: null }),
532
+ maxRetries: 0,
533
+ model: resolvedGatewayModel,
534
+ messages: promptMessages.messages,
535
+ system: mergeSystemInstructions(
536
+ isMiraMode && miraSystemPrompt ? miraSystemPrompt : systemInstruction,
537
+ promptMessages.system
538
+ ),
539
+ ...(cappedMaxOutput ? { maxOutputTokens: cappedMaxOutput } : {}),
540
+ ...(miraTools
541
+ ? {
542
+ tools: { ...miraTools, ...googleSearchTool } as NonNullable<
543
+ Parameters<typeof streamText>[0]
544
+ >['tools'],
545
+ stopWhen: stepCountIs(25),
546
+ toolChoice: 'auto' as const,
547
+ prepareStep: prepareStep as NonNullable<
548
+ NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
549
+ >,
550
+ }
551
+ : {
552
+ tools: googleSearchTool,
553
+ }),
554
+ providerOptions: {
555
+ google: {
556
+ ...thinkingConfig,
557
+ safetySettings: [
558
+ {
559
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
560
+ threshold: 'BLOCK_NONE',
561
+ },
562
+ {
563
+ category: 'HARM_CATEGORY_HATE_SPEECH',
564
+ threshold: 'BLOCK_NONE',
565
+ },
566
+ {
567
+ category: 'HARM_CATEGORY_HARASSMENT',
568
+ threshold: 'BLOCK_NONE',
569
+ },
570
+ {
571
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
572
+ threshold: 'BLOCK_NONE',
573
+ },
574
+ ],
575
+ },
576
+ vertex: {
577
+ ...thinkingConfig,
578
+ safetySettings: [
579
+ {
580
+ category: 'HARM_CATEGORY_HARASSMENT',
581
+ threshold: 'BLOCK_NONE',
582
+ },
583
+ {
584
+ category: 'HARM_CATEGORY_HATE_SPEECH',
585
+ threshold: 'BLOCK_NONE',
586
+ },
587
+ {
588
+ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
589
+ threshold: 'BLOCK_NONE',
590
+ },
591
+ {
592
+ category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
593
+ threshold: 'BLOCK_NONE',
594
+ },
595
+ ],
596
+ },
597
+ },
598
+ onAbort: async ({ steps }) =>
599
+ persistChatAssistantResponse(buildAbortedStreamFinishResponse(steps)),
600
+ onFinish: persistChatAssistantResponse,
601
+ timeout: {
602
+ chunkMs: 20_000,
603
+ stepMs: 45_000,
604
+ totalMs: 120_000,
605
+ },
606
+ });
607
+
608
+ // Per https://ai-sdk.dev/docs/advanced/stopping-streams: consumeSseStream ensures
609
+ // the stream is consumed on abort so cleanup can run; use onFinish in toUIMessageStreamResponse
610
+ // to handle isAborted when needed.
611
+ return result.toUIMessageStreamResponse({
612
+ consumeSseStream: consumeStream,
613
+ sendReasoning: true,
614
+ sendSources: true,
615
+ });
616
+ } catch (error) {
617
+ if (error instanceof Error) {
618
+ console.log(error.message);
619
+ return NextResponse.json(
620
+ {
621
+ 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'}\n\`\`\``,
622
+ },
623
+ {
624
+ status: 500,
625
+ }
626
+ );
627
+ }
628
+ console.log(error);
629
+ return new Response('Internal Server Error', { status: 500 });
630
+ }
631
+ };
632
+ }