@tuturuuu/ai 0.6.0 → 0.7.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/ai",
3
3
  "license": "MIT",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -22,53 +22,53 @@
22
22
  "type-check": "tsc --project tsconfig.typecheck.json"
23
23
  },
24
24
  "dependencies": {
25
- "@ai-sdk/amazon-bedrock": "^5.0.29",
26
- "@ai-sdk/anthropic": "^4.0.18",
27
- "@ai-sdk/azure": "^4.0.20",
28
- "@ai-sdk/cerebras": "^3.0.14",
29
- "@ai-sdk/cohere": "^4.0.12",
30
- "@ai-sdk/deepgram": "^3.0.12",
31
- "@ai-sdk/deepinfra": "^3.0.14",
32
- "@ai-sdk/deepseek": "^3.0.13",
33
- "@ai-sdk/elevenlabs": "^3.0.13",
34
- "@ai-sdk/fal": "^3.0.13",
35
- "@ai-sdk/fireworks": "^3.0.15",
36
- "@ai-sdk/gateway": "^4.0.27",
37
- "@ai-sdk/gladia": "^3.0.12",
38
- "@ai-sdk/google": "^4.0.23",
39
- "@ai-sdk/google-vertex": "^5.0.27",
40
- "@ai-sdk/groq": "^4.0.13",
41
- "@ai-sdk/hume": "^3.0.12",
42
- "@ai-sdk/lmnt": "^3.0.12",
43
- "@ai-sdk/luma": "^3.0.13",
44
- "@ai-sdk/mistral": "^4.0.14",
45
- "@ai-sdk/openai": "^4.0.19",
46
- "@ai-sdk/openai-compatible": "^3.0.14",
47
- "@ai-sdk/perplexity": "^4.0.13",
48
- "@ai-sdk/react": "^4.0.39",
49
- "@ai-sdk/replicate": "^3.0.13",
50
- "@ai-sdk/revai": "^3.0.12",
51
- "@ai-sdk/togetherai": "^3.0.15",
52
- "@ai-sdk/xai": "^4.0.18",
25
+ "@ai-sdk/amazon-bedrock": "^5.0.42",
26
+ "@ai-sdk/anthropic": "^4.0.29",
27
+ "@ai-sdk/azure": "^4.0.30",
28
+ "@ai-sdk/cerebras": "^3.0.22",
29
+ "@ai-sdk/cohere": "^4.0.20",
30
+ "@ai-sdk/deepgram": "^3.0.20",
31
+ "@ai-sdk/deepinfra": "^3.0.22",
32
+ "@ai-sdk/deepseek": "^3.0.21",
33
+ "@ai-sdk/elevenlabs": "^3.0.21",
34
+ "@ai-sdk/fal": "^3.0.21",
35
+ "@ai-sdk/fireworks": "^3.0.24",
36
+ "@ai-sdk/gateway": "^4.0.40",
37
+ "@ai-sdk/gladia": "^3.0.20",
38
+ "@ai-sdk/google": "^4.0.33",
39
+ "@ai-sdk/google-vertex": "^5.0.40",
40
+ "@ai-sdk/groq": "^4.0.21",
41
+ "@ai-sdk/hume": "^3.0.20",
42
+ "@ai-sdk/lmnt": "^3.0.20",
43
+ "@ai-sdk/luma": "^3.0.21",
44
+ "@ai-sdk/mistral": "^4.0.22",
45
+ "@ai-sdk/openai": "^4.0.29",
46
+ "@ai-sdk/openai-compatible": "^3.0.22",
47
+ "@ai-sdk/perplexity": "^4.0.22",
48
+ "@ai-sdk/react": "^4.0.54",
49
+ "@ai-sdk/replicate": "^3.0.21",
50
+ "@ai-sdk/revai": "^3.0.20",
51
+ "@ai-sdk/togetherai": "^3.0.23",
52
+ "@ai-sdk/xai": "^4.0.27",
53
53
  "@beeper/chat-adapter-matrix": "^0.2.0",
54
54
  "@bitbasti/chat-adapter-webex": "^0.1.0",
55
- "@chat-adapter/discord": "^4.34.0",
56
- "@chat-adapter/gchat": "^4.34.0",
57
- "@chat-adapter/github": "^4.34.0",
58
- "@chat-adapter/linear": "^4.34.0",
59
- "@chat-adapter/messenger": "^4.34.0",
60
- "@chat-adapter/slack": "^4.34.0",
61
- "@chat-adapter/state-ioredis": "^4.34.0",
62
- "@chat-adapter/state-memory": "^4.34.0",
63
- "@chat-adapter/state-pg": "^4.34.0",
64
- "@chat-adapter/state-redis": "^4.34.0",
65
- "@chat-adapter/teams": "^4.34.0",
66
- "@chat-adapter/telegram": "^4.34.0",
67
- "@chat-adapter/web": "^4.34.0",
68
- "@chat-adapter/whatsapp": "^4.34.0",
55
+ "@chat-adapter/discord": "^4.36.0",
56
+ "@chat-adapter/gchat": "^4.36.0",
57
+ "@chat-adapter/github": "^4.36.0",
58
+ "@chat-adapter/linear": "^4.36.0",
59
+ "@chat-adapter/messenger": "^4.36.0",
60
+ "@chat-adapter/slack": "^4.36.0",
61
+ "@chat-adapter/state-ioredis": "^4.36.0",
62
+ "@chat-adapter/state-memory": "^4.36.0",
63
+ "@chat-adapter/state-pg": "^4.36.0",
64
+ "@chat-adapter/state-redis": "^4.36.0",
65
+ "@chat-adapter/teams": "^4.36.0",
66
+ "@chat-adapter/telegram": "^4.36.0",
67
+ "@chat-adapter/web": "^4.36.0",
68
+ "@chat-adapter/whatsapp": "^4.36.0",
69
69
  "@json-render/core": "^0.19.0",
70
70
  "@json-render/react": "^0.19.0",
71
- "@liveblocks/chat-sdk-adapter": "^3.22.0",
71
+ "@liveblocks/chat-sdk-adapter": "^3.23.0",
72
72
  "@octokit/rest": "^22.0.1",
73
73
  "@resend/chat-sdk-adapter": "^0.2.2",
74
74
  "@streamdown/cjk": "^1.0.3",
@@ -76,14 +76,14 @@
76
76
  "@streamdown/math": "^1.0.2",
77
77
  "@streamdown/mermaid": "^1.0.2",
78
78
  "@tuturuuu/google": "0.1.0",
79
- "@tuturuuu/internal-api": "0.24.0",
79
+ "@tuturuuu/internal-api": "0.27.0",
80
80
  "@tuturuuu/supabase": "0.5.0",
81
- "@tuturuuu/utils": "0.20.0",
82
- "@vercel/sandbox": "^2.8.0",
83
- "@zernio/chat-sdk-adapter": "^0.4.0",
84
- "ai": "^7.0.36",
81
+ "@tuturuuu/utils": "0.22.0",
82
+ "@vercel/sandbox": "^2.9.2",
83
+ "@zernio/chat-sdk-adapter": "^0.5.0",
84
+ "ai": "^7.0.51",
85
85
  "bash-tool": "^1.3.18",
86
- "chat": "^4.34.0",
86
+ "chat": "^4.36.0",
87
87
  "chat-adapter-baileys": "^2.1.0",
88
88
  "chat-adapter-blooio": "^0.1.0",
89
89
  "chat-adapter-imessage": "^1.1.0",
@@ -93,7 +93,7 @@
93
93
  "chat-state-cloudflare-do": "^0.2.0",
94
94
  "chat-state-mysql": "^0.1.0",
95
95
  "dayjs": "^1.11.21",
96
- "next": "^16.3.0-preview.9",
96
+ "next": "^16.3.0",
97
97
  "qrcode": "^1.5.4",
98
98
  "react": "^19.2.8",
99
99
  "react-dom": "^19.2.8",
@@ -103,11 +103,11 @@
103
103
  "zod": "^4.4.3"
104
104
  },
105
105
  "devDependencies": {
106
- "@tuturuuu/types": "0.23.0",
106
+ "@tuturuuu/types": "0.26.0",
107
107
  "@tuturuuu/typescript-config": "0.1.1",
108
- "@types/node": "^26.1.1",
108
+ "@types/node": "^26.1.2",
109
109
  "@types/qrcode": "^1.5.6",
110
- "@types/react": "^19.2.17",
110
+ "@types/react": "^19.2.18",
111
111
  "typescript": "7.0.2"
112
112
  },
113
113
  "exports": {
@@ -56,6 +56,7 @@ export const ChatRequestBodySchema = z.object({
56
56
  creditSource: z.enum(['personal', 'workspace']).optional(),
57
57
  creditWsId: z.string().trim().min(1).max(MAX_ID_LENGTH).optional(),
58
58
  observabilityContext: z.array(z.record(z.string(), z.unknown())).optional(),
59
+ persistenceRequestId: z.string().trim().uuid().optional(),
59
60
  taskBoardContext: TaskBoardContextSchema.optional(),
60
61
  });
61
62
 
@@ -0,0 +1,218 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ type RpcError = { message: string };
4
+
5
+ type PrivateRpcClient = {
6
+ schema: (schema: 'private') => {
7
+ rpc: (
8
+ name: string,
9
+ args: Record<string, unknown>
10
+ ) => PromiseLike<{ data: unknown; error: RpcError | null }>;
11
+ };
12
+ };
13
+
14
+ type PersistenceSource = 'Mira' | 'Rewise';
15
+
16
+ export type PersistenceLease = {
17
+ leaseToken: string;
18
+ retryAfterSeconds: number;
19
+ state: 'active' | 'claimed' | 'completed';
20
+ };
21
+
22
+ export type ClaimedPersistenceLease = Pick<PersistenceLease, 'leaseToken'> & {
23
+ chatId: string;
24
+ requestId: string;
25
+ };
26
+
27
+ function parseClaimResult(data: unknown, leaseToken: string): PersistenceLease {
28
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
29
+ throw new Error('Invalid AI persistence lease response');
30
+ }
31
+
32
+ const record = data as Record<string, unknown>;
33
+ if (!['active', 'claimed', 'completed'].includes(String(record.state))) {
34
+ throw new Error('Invalid AI persistence lease state');
35
+ }
36
+
37
+ const retryAfterSeconds = Number(record.retryAfterSeconds);
38
+ return {
39
+ leaseToken,
40
+ retryAfterSeconds:
41
+ Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
42
+ ? Math.ceil(retryAfterSeconds)
43
+ : 0,
44
+ state: record.state as PersistenceLease['state'],
45
+ };
46
+ }
47
+
48
+ export async function claimAiPersistenceRequest({
49
+ chatId,
50
+ client,
51
+ content,
52
+ creatorId,
53
+ requestId,
54
+ source,
55
+ }: {
56
+ chatId: string;
57
+ client: unknown;
58
+ content: string;
59
+ creatorId: string;
60
+ requestId: string;
61
+ source: PersistenceSource;
62
+ }): Promise<PersistenceLease> {
63
+ const leaseToken = crypto.randomUUID();
64
+ const { data, error } = await (client as PrivateRpcClient)
65
+ .schema('private')
66
+ .rpc('ai_chat_claim_persistence_request', {
67
+ p_chat_id: chatId,
68
+ p_content: content,
69
+ p_creator_id: creatorId,
70
+ p_lease_token: leaseToken,
71
+ p_request_id: requestId,
72
+ p_source: source,
73
+ });
74
+
75
+ if (error) throw new Error(error.message);
76
+ return parseClaimResult(data, leaseToken);
77
+ }
78
+
79
+ export async function beginAiPersistenceRequest({
80
+ chatId,
81
+ client,
82
+ content,
83
+ creatorId,
84
+ requestId,
85
+ source,
86
+ }: {
87
+ chatId: string;
88
+ client: unknown;
89
+ content: string;
90
+ creatorId: string;
91
+ requestId?: string;
92
+ source: PersistenceSource;
93
+ }): Promise<
94
+ | { lease: ClaimedPersistenceLease; response: null }
95
+ | { lease: null; response: Response | null }
96
+ > {
97
+ if (!requestId) return { lease: null, response: null };
98
+
99
+ const result = await claimAiPersistenceRequest({
100
+ chatId,
101
+ client,
102
+ content,
103
+ creatorId,
104
+ requestId,
105
+ source,
106
+ });
107
+ if (result.state === 'active') {
108
+ return {
109
+ lease: null,
110
+ response: NextResponse.json(
111
+ {
112
+ code: 'ai_request_in_progress',
113
+ message: 'This AI request is already in progress.',
114
+ },
115
+ {
116
+ headers: {
117
+ 'Retry-After': String(Math.max(1, result.retryAfterSeconds)),
118
+ },
119
+ status: 409,
120
+ }
121
+ ),
122
+ };
123
+ }
124
+ if (result.state === 'completed') {
125
+ return {
126
+ lease: null,
127
+ response: NextResponse.json(
128
+ {
129
+ code: 'ai_request_completed',
130
+ message: 'This AI request has already completed.',
131
+ },
132
+ { status: 409 }
133
+ ),
134
+ };
135
+ }
136
+
137
+ return {
138
+ lease: { chatId, leaseToken: result.leaseToken, requestId },
139
+ response: null,
140
+ };
141
+ }
142
+
143
+ async function updateAiPersistenceRequest(
144
+ client: unknown,
145
+ rpcName:
146
+ | 'ai_chat_complete_persistence_request'
147
+ | 'ai_chat_release_persistence_request',
148
+ lease: ClaimedPersistenceLease
149
+ ): Promise<boolean> {
150
+ const { data, error } = await (client as PrivateRpcClient)
151
+ .schema('private')
152
+ .rpc(rpcName, {
153
+ p_chat_id: lease.chatId,
154
+ p_lease_token: lease.leaseToken,
155
+ p_request_id: lease.requestId,
156
+ });
157
+
158
+ if (error) throw new Error(error.message);
159
+ return data === true;
160
+ }
161
+
162
+ export function completeAiPersistenceRequest(
163
+ client: unknown,
164
+ lease: ClaimedPersistenceLease
165
+ ) {
166
+ return updateAiPersistenceRequest(
167
+ client,
168
+ 'ai_chat_complete_persistence_request',
169
+ lease
170
+ );
171
+ }
172
+
173
+ export function releaseAiPersistenceRequest(
174
+ client: unknown,
175
+ lease: ClaimedPersistenceLease
176
+ ) {
177
+ return updateAiPersistenceRequest(
178
+ client,
179
+ 'ai_chat_release_persistence_request',
180
+ lease
181
+ );
182
+ }
183
+
184
+ export function createAiPersistenceFinisher<Response>({
185
+ client,
186
+ lease,
187
+ onSettled,
188
+ persist,
189
+ }: {
190
+ client: unknown;
191
+ lease: ClaimedPersistenceLease | null;
192
+ onSettled: () => void;
193
+ persist: (response: Response) => Promise<boolean>;
194
+ }) {
195
+ let handled = false;
196
+
197
+ return async (response: Response): Promise<void> => {
198
+ if (handled) return;
199
+ handled = true;
200
+
201
+ try {
202
+ const persisted = await persist(response);
203
+ if (lease && persisted) {
204
+ const completed = await completeAiPersistenceRequest(client, lease);
205
+ if (!completed) {
206
+ throw new Error('Failed to complete AI persistence request lease');
207
+ }
208
+ } else if (lease) {
209
+ await releaseAiPersistenceRequest(client, lease);
210
+ }
211
+ onSettled();
212
+ } catch (error) {
213
+ if (lease) await releaseAiPersistenceRequest(client, lease);
214
+ onSettled();
215
+ throw error;
216
+ }
217
+ };
218
+ }
@@ -252,7 +252,7 @@ export async function prepareProcessedMessages(
252
252
  };
253
253
  }
254
254
 
255
- function extractLatestUserMessageContent(
255
+ export function extractLatestUserMessageContent(
256
256
  processedMessages: ModelMessage[]
257
257
  ): string {
258
258
  const userMessages = processedMessages.filter(
@@ -298,6 +298,54 @@ type PersistLatestUserMessageParams = {
298
298
  source: 'Mira' | 'Rewise';
299
299
  };
300
300
 
301
+ type RequestScopedPersistenceError = {
302
+ code?: string;
303
+ message: string;
304
+ };
305
+
306
+ export async function persistRequestScopedUserMessage({
307
+ chatId,
308
+ content,
309
+ creatorId,
310
+ findExistingMessage,
311
+ insertMessage,
312
+ requestId,
313
+ source,
314
+ }: {
315
+ chatId: string;
316
+ content: string;
317
+ creatorId: string;
318
+ findExistingMessage: () => PromiseLike<{
319
+ data: { content: string | null; creator_id: string | null } | null;
320
+ error: RequestScopedPersistenceError | null;
321
+ }>;
322
+ insertMessage: () => PromiseLike<{
323
+ error: RequestScopedPersistenceError | null;
324
+ }>;
325
+ requestId: string;
326
+ source: 'Mira' | 'Rewise';
327
+ }): Promise<{ error: RequestScopedPersistenceError | null }> {
328
+ const inserted = await insertMessage();
329
+ if (!inserted.error) return inserted;
330
+ if (inserted.error.code !== '23505') return inserted;
331
+
332
+ const existing = await findExistingMessage();
333
+ if (existing.error) return { error: existing.error };
334
+ if (
335
+ existing.data?.content === content &&
336
+ existing.data.creator_id === creatorId
337
+ ) {
338
+ return { error: null };
339
+ }
340
+
341
+ return {
342
+ error: {
343
+ code: '23505',
344
+ message: `Persistence request ${requestId} already belongs to another ${source} message in chat ${chatId}`,
345
+ },
346
+ };
347
+ }
348
+
301
349
  export async function persistLatestUserMessage({
302
350
  processedMessages,
303
351
  chatId,
@@ -32,6 +32,11 @@ import { ChatRequestBodySchema, mapToUIMessages } from './chat-request-schema';
32
32
  import { systemInstruction } from './default-system-instruction';
33
33
  import { prepareMiraToolStep } from './mira-step-preparation';
34
34
  import { resolveChatReasoningSettings } from './reasoning-settings';
35
+ import {
36
+ beginAiPersistenceRequest,
37
+ createAiPersistenceFinisher,
38
+ releaseAiPersistenceRequest,
39
+ } from './request-persistence-lease';
35
40
  import {
36
41
  type AiRouteAuthResult,
37
42
  isInternalTuturuuuAiUser,
@@ -43,7 +48,9 @@ import {
43
48
  } from './route-chat-resolution';
44
49
  import { performCreditPreflight } from './route-credits';
45
50
  import {
51
+ extractLatestUserMessageContent,
46
52
  persistLatestUserMessage,
53
+ persistRequestScopedUserMessage,
47
54
  prepareProcessedMessages,
48
55
  } from './route-message-preparation';
49
56
  import { prepareMiraRuntime } from './route-mira-runtime';
@@ -102,6 +109,7 @@ export function createPOST(
102
109
 
103
110
  // Higher-order function that returns the actual request handler
104
111
  return async function handler(req: NextRequest): Promise<Response> {
112
+ let releaseClaimedPersistenceLease: (() => Promise<void>) | null = null;
105
113
  try {
106
114
  const sbAdmin = await createAdminClient();
107
115
  let requestBody: unknown;
@@ -141,6 +149,7 @@ export function createPOST(
141
149
  creditSource: requestedCreditSourceRaw,
142
150
  creditWsId: rawCreditWsId,
143
151
  observabilityContext,
152
+ persistenceRequestId,
144
153
  taskBoardContext,
145
154
  } = parsedBody.data;
146
155
  const thinkingMode = rawThinkingMode === 'thinking' ? 'thinking' : 'fast';
@@ -404,6 +413,43 @@ export function createPOST(
404
413
  processedMessages,
405
414
  chatId,
406
415
  insertChatMessage: async (args) => {
416
+ if (persistenceRequestId) {
417
+ return persistRequestScopedUserMessage({
418
+ chatId,
419
+ content: args.message,
420
+ creatorId: user.id,
421
+ findExistingMessage: async () => {
422
+ const { data, error } = await sbAdmin
423
+ .from('ai_chat_messages')
424
+ .select('content,creator_id')
425
+ .eq('chat_id', chatId)
426
+ .eq('role', 'USER')
427
+ .contains('metadata', {
428
+ requestId: persistenceRequestId,
429
+ source: args.source,
430
+ })
431
+ .maybeSingle();
432
+ return { data, error };
433
+ },
434
+ insertMessage: async () => {
435
+ const { error } = await sbAdmin
436
+ .from('ai_chat_messages')
437
+ .insert({
438
+ chat_id: chatId,
439
+ content: args.message,
440
+ creator_id: user.id,
441
+ metadata: {
442
+ requestId: persistenceRequestId,
443
+ source: args.source,
444
+ },
445
+ role: 'USER',
446
+ });
447
+ return { error };
448
+ },
449
+ requestId: persistenceRequestId,
450
+ source: args.source,
451
+ });
452
+ }
407
453
  const { error } = await supabase.rpc(
408
454
  'insert_ai_chat_message' as never,
409
455
  args as never
@@ -427,8 +473,6 @@ export function createPOST(
427
473
  }
428
474
  const { cappedMaxOutput } = creditPreflight;
429
475
 
430
- // Mutable ref so the render_ui preprocessor can read current steps
431
- // at Zod-validation time (before the execute handler runs).
432
476
  const stepsRef: { current: unknown[] } = { current: [] };
433
477
 
434
478
  const { miraSystemPrompt, miraTools } = await prepareMiraRuntime({
@@ -480,15 +524,12 @@ export function createPOST(
480
524
  promptMessages.messages
481
525
  );
482
526
 
483
- // Provider-native Google Search tool for non-Mira mode.
484
527
  const googleSearchTool = createGoogleSearchToolSet();
485
528
 
486
529
  type PrepareStep = NonNullable<
487
530
  NonNullable<Parameters<typeof streamText>[0]>['prepareStep']
488
531
  >;
489
532
  const prepareStep: PrepareStep = ({ steps }) => {
490
- // Keep the mutable ref in sync so the render_ui preprocessor can
491
- // read current steps during Zod validation.
492
533
  stepsRef.current = steps;
493
534
  return prepareMiraToolStep({
494
535
  steps,
@@ -501,23 +542,43 @@ export function createPOST(
501
542
  });
502
543
  };
503
544
 
504
- let assistantResponsePersisted = false;
505
- const persistChatAssistantResponse = async (
506
- response: Parameters<typeof persistAssistantResponse>[0]['response']
507
- ) => {
508
- if (assistantResponsePersisted) return;
509
- assistantResponsePersisted = true;
510
- await persistAssistantResponse({
511
- response,
512
- sbAdmin,
545
+ const { lease: claimedLease, response: leaseResponse } =
546
+ await beginAiPersistenceRequest({
513
547
  chatId,
514
- userId: user.id,
515
- model: resolvedModelId,
516
- effectiveSource,
517
- wsId: billingWsId ?? normalizedWsId ?? undefined,
518
- observabilityContext,
548
+ client: sbAdmin,
549
+ content: extractLatestUserMessageContent(processedMessages),
550
+ creatorId: user.id,
551
+ requestId: persistenceRequestId,
552
+ source: effectiveSource,
519
553
  });
520
- };
554
+ if (leaseResponse) return leaseResponse;
555
+ if (claimedLease) {
556
+ releaseClaimedPersistenceLease = async () => {
557
+ await releaseAiPersistenceRequest(sbAdmin, claimedLease);
558
+ };
559
+ }
560
+
561
+ const persistChatAssistantResponse = createAiPersistenceFinisher<
562
+ Parameters<typeof persistAssistantResponse>[0]['response']
563
+ >({
564
+ client: sbAdmin,
565
+ lease: claimedLease,
566
+ onSettled: () => {
567
+ releaseClaimedPersistenceLease = null;
568
+ },
569
+ persist: (response) =>
570
+ persistAssistantResponse({
571
+ response,
572
+ sbAdmin,
573
+ chatId,
574
+ userId: user.id,
575
+ model: resolvedModelId,
576
+ effectiveSource,
577
+ wsId: billingWsId ?? normalizedWsId ?? undefined,
578
+ observabilityContext,
579
+ persistenceRequestId,
580
+ }),
581
+ });
521
582
 
522
583
  const result = streamText({
523
584
  abortSignal: req.signal,
@@ -612,6 +673,15 @@ export function createPOST(
612
673
  sendSources: true,
613
674
  });
614
675
  } catch (error) {
676
+ if (releaseClaimedPersistenceLease) {
677
+ try {
678
+ await releaseClaimedPersistenceLease();
679
+ } catch (releaseError) {
680
+ console.error('Failed to release AI persistence request lease.', {
681
+ error: releaseError,
682
+ });
683
+ }
684
+ }
615
685
  if (error instanceof Error) {
616
686
  console.log(error.message);
617
687
  return NextResponse.json(
@@ -106,6 +106,7 @@ type PersistAssistantResponseParams = {
106
106
  model: string;
107
107
  effectiveSource: 'Mira' | 'Rewise';
108
108
  observabilityContext?: unknown;
109
+ persistenceRequestId?: string;
109
110
  wsId?: string;
110
111
  };
111
112
 
@@ -470,6 +471,7 @@ function buildAssistantMessageMetadata({
470
471
  inputTokens,
471
472
  model,
472
473
  observabilityContext,
474
+ persistenceRequestId,
473
475
  outputTokens,
474
476
  parts,
475
477
  reasoningText,
@@ -485,6 +487,7 @@ function buildAssistantMessageMetadata({
485
487
  inputTokens: number;
486
488
  model: string;
487
489
  observabilityContext?: unknown;
490
+ persistenceRequestId?: string;
488
491
  outputTokens: number;
489
492
  parts: Record<string, unknown>[];
490
493
  reasoningText: string;
@@ -494,6 +497,7 @@ function buildAssistantMessageMetadata({
494
497
  }) {
495
498
  return {
496
499
  source: effectiveSource,
500
+ ...(persistenceRequestId ? { requestId: persistenceRequestId } : {}),
497
501
  ai: {
498
502
  finishReason: response.finishReason,
499
503
  model,
@@ -526,6 +530,7 @@ function compactAssistantMessageMetadata(
526
530
  const ai = metadata.ai;
527
531
  return {
528
532
  source: metadata.source,
533
+ ...(metadata.requestId ? { requestId: metadata.requestId } : {}),
529
534
  ai: {
530
535
  finishReason: ai.finishReason,
531
536
  metadataCompacted: true,
@@ -568,8 +573,9 @@ export async function persistAssistantResponse({
568
573
  model,
569
574
  effectiveSource,
570
575
  observabilityContext,
576
+ persistenceRequestId,
571
577
  wsId,
572
- }: PersistAssistantResponseParams): Promise<void> {
578
+ }: PersistAssistantResponseParams): Promise<boolean> {
573
579
  const steps = response.steps ?? [];
574
580
  const { allToolCalls, allToolResults } = collectToolData(steps);
575
581
 
@@ -579,7 +585,7 @@ export async function persistAssistantResponse({
579
585
  allToolResults.length === 0
580
586
  ) {
581
587
  console.warn('onFinish: no text and no tool calls — skipping DB save');
582
- return;
588
+ return false;
583
589
  }
584
590
 
585
591
  const reasoningText = collectReasoningText(response);
@@ -608,6 +614,7 @@ export async function persistAssistantResponse({
608
614
  inputTokens,
609
615
  model,
610
616
  observabilityContext,
617
+ persistenceRequestId,
611
618
  outputTokens,
612
619
  parts,
613
620
  reasoningText,
@@ -695,7 +702,7 @@ export async function persistAssistantResponse({
695
702
  ...(searchCount > 0 ? { searchCount } : {}),
696
703
  error,
697
704
  });
698
- return;
705
+ return true;
699
706
  }
700
707
 
701
708
  if (!deductionResult.success) {
@@ -707,7 +714,7 @@ export async function persistAssistantResponse({
707
714
  ...(searchCount > 0 ? { searchCount } : {}),
708
715
  deductionResult,
709
716
  });
710
- return;
717
+ return true;
711
718
  }
712
719
 
713
720
  console.info('AI credits deducted for assistant response.', {
@@ -719,4 +726,6 @@ export async function persistAssistantResponse({
719
726
  deductionResult,
720
727
  });
721
728
  }
729
+
730
+ return true;
722
731
  }
package/src/core.ts CHANGED
@@ -1,3 +1 @@
1
1
  export * from 'ai';
2
- export * as ChatSDK from 'chat';
3
- export * as ChatSDKAI from 'chat/ai';
@@ -9,6 +9,7 @@ export const AI_FEATURES = [
9
9
  'email_draft',
10
10
  'image_generation',
11
11
  'embeddings',
12
+ 'voice_live',
12
13
  ] as const;
13
14
 
14
15
  export type AiFeature = (typeof AI_FEATURES)[number];
@@ -0,0 +1,59 @@
1
+ import { CREDIT_UNIT_USD } from '../credits/constants';
2
+
3
+ export interface GeminiLiveUsageSnapshot {
4
+ inputAudioTokens: number;
5
+ inputImageTokens: number;
6
+ inputTextTokens: number;
7
+ inputVideoTokens: number;
8
+ outputAudioTokens: number;
9
+ outputTextTokens: number;
10
+ searchQueries: number;
11
+ thinkingTokens: number;
12
+ }
13
+
14
+ export interface GeminiLivePrice {
15
+ inputAudioPerMillion: number;
16
+ inputImageVideoPerMillion: number;
17
+ inputTextPerMillion: number;
18
+ outputAudioPerMillion: number;
19
+ outputTextPerMillion: number;
20
+ searchPerQuery: number;
21
+ }
22
+
23
+ export const GEMINI_3_1_FLASH_LIVE_PRICE: GeminiLivePrice = {
24
+ inputAudioPerMillion: 3,
25
+ inputImageVideoPerMillion: 1,
26
+ inputTextPerMillion: 0.75,
27
+ outputAudioPerMillion: 12,
28
+ outputTextPerMillion: 4.5,
29
+ searchPerQuery: 0.014,
30
+ };
31
+
32
+ export function calculateGeminiLiveCost(
33
+ usage: GeminiLiveUsageSnapshot,
34
+ price: GeminiLivePrice = GEMINI_3_1_FLASH_LIVE_PRICE
35
+ ) {
36
+ return (
37
+ (usage.inputTextTokens * price.inputTextPerMillion) / 1_000_000 +
38
+ (usage.inputAudioTokens * price.inputAudioPerMillion) / 1_000_000 +
39
+ ((usage.inputImageTokens + usage.inputVideoTokens) *
40
+ price.inputImageVideoPerMillion) /
41
+ 1_000_000 +
42
+ ((usage.outputTextTokens + usage.thinkingTokens) *
43
+ price.outputTextPerMillion) /
44
+ 1_000_000 +
45
+ (usage.outputAudioTokens * price.outputAudioPerMillion) / 1_000_000 +
46
+ usage.searchQueries * price.searchPerQuery
47
+ );
48
+ }
49
+
50
+ export function calculateGeminiLiveCredits(
51
+ usage: GeminiLiveUsageSnapshot,
52
+ markupMultiplier = 1,
53
+ price: GeminiLivePrice = GEMINI_3_1_FLASH_LIVE_PRICE
54
+ ) {
55
+ const costUsd = calculateGeminiLiveCost(usage, price);
56
+ return costUsd === 0
57
+ ? 0
58
+ : Math.max(1, (costUsd / CREDIT_UNIT_USD) * markupMultiplier);
59
+ }
@@ -16,6 +16,63 @@ export type AiStudioUsageCost = {
16
16
  providerCostUsd: number;
17
17
  };
18
18
 
19
+ export type AiStudioRunStepKind = 'grader' | 'model' | 'system' | 'tool';
20
+
21
+ export type RecordAiStudioRunStepInput = {
22
+ billedCredits?: number;
23
+ completedAt?: string | null;
24
+ errorClass?: string | null;
25
+ inputTokens?: number;
26
+ kind: AiStudioRunStepKind;
27
+ latencyMs?: number | null;
28
+ metadata?: Json;
29
+ modelId?: string | null;
30
+ name: string;
31
+ outputTokens?: number;
32
+ providerCostUsd?: number;
33
+ runId: string;
34
+ sequence: number;
35
+ startedAt?: string;
36
+ status: 'aborted' | 'failed' | 'running' | 'succeeded';
37
+ };
38
+
39
+ export async function recordAiStudioRunStep(
40
+ input: RecordAiStudioRunStepInput
41
+ ): Promise<void> {
42
+ const sbAdmin = await createAdminClient({ noCookie: true });
43
+ const { error } = await sbAdmin
44
+ .schema('private')
45
+ .from('ai_studio_run_steps')
46
+ .upsert(
47
+ {
48
+ billed_credits: input.billedCredits ?? 0,
49
+ completed_at: input.completedAt ?? new Date().toISOString(),
50
+ error_class: input.errorClass ?? null,
51
+ input_tokens: input.inputTokens ?? 0,
52
+ kind: input.kind,
53
+ latency_ms: input.latencyMs ?? null,
54
+ metadata: input.metadata ?? {},
55
+ model_id: input.modelId ?? null,
56
+ name: input.name,
57
+ output_tokens: input.outputTokens ?? 0,
58
+ provider_cost_usd: input.providerCostUsd ?? 0,
59
+ run_id: input.runId,
60
+ sequence: input.sequence,
61
+ started_at: input.startedAt ?? new Date().toISOString(),
62
+ status: input.status,
63
+ },
64
+ { onConflict: 'run_id,sequence' }
65
+ );
66
+
67
+ if (error) {
68
+ console.warn('Failed to record AI Studio run step', {
69
+ code: error.code,
70
+ runId: input.runId,
71
+ sequence: input.sequence,
72
+ });
73
+ }
74
+ }
75
+
19
76
  export type CalculateAiStudioUsageCostInput = {
20
77
  imageCount?: number;
21
78
  inputTokens?: number;
@@ -207,6 +264,11 @@ export async function settleAiStudioRun(
207
264
  });
208
265
 
209
266
  if (error || !data?.[0]?.success) {
267
+ console.error('AI Studio settlement RPC failed', {
268
+ code: error?.code ?? null,
269
+ errorCode: data?.[0]?.error_code ?? null,
270
+ runId: input.runId,
271
+ });
210
272
  throw new AiStudioError('AI usage could not be settled.', {
211
273
  code: 'server_error',
212
274
  status: 500,
@@ -238,6 +300,11 @@ export async function settleExternalAiStudioRun(
238
300
  });
239
301
 
240
302
  if (error || !data?.[0]?.success) {
303
+ console.error('External-app AI Studio settlement RPC failed', {
304
+ code: error?.code ?? null,
305
+ errorCode: data?.[0]?.error_code ?? null,
306
+ runId: input.runId,
307
+ });
241
308
  throw new AiStudioError('External-app AI usage could not be settled.', {
242
309
  code: 'server_error',
243
310
  status: 500,