@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 +54 -54
- package/src/chat/google/chat-request-schema.ts +1 -0
- package/src/chat/google/request-persistence-lease.ts +218 -0
- package/src/chat/google/route-message-preparation.ts +49 -1
- package/src/chat/google/route.ts +90 -20
- package/src/chat/google/stream-finish-persistence.ts +13 -4
- package/src/core.ts +0 -2
- package/src/credits/constants.ts +1 -0
- package/src/live/usage.ts +59 -0
- package/src/studio/metering.ts +67 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/ai",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "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.
|
|
26
|
-
"@ai-sdk/anthropic": "^4.0.
|
|
27
|
-
"@ai-sdk/azure": "^4.0.
|
|
28
|
-
"@ai-sdk/cerebras": "^3.0.
|
|
29
|
-
"@ai-sdk/cohere": "^4.0.
|
|
30
|
-
"@ai-sdk/deepgram": "^3.0.
|
|
31
|
-
"@ai-sdk/deepinfra": "^3.0.
|
|
32
|
-
"@ai-sdk/deepseek": "^3.0.
|
|
33
|
-
"@ai-sdk/elevenlabs": "^3.0.
|
|
34
|
-
"@ai-sdk/fal": "^3.0.
|
|
35
|
-
"@ai-sdk/fireworks": "^3.0.
|
|
36
|
-
"@ai-sdk/gateway": "^4.0.
|
|
37
|
-
"@ai-sdk/gladia": "^3.0.
|
|
38
|
-
"@ai-sdk/google": "^4.0.
|
|
39
|
-
"@ai-sdk/google-vertex": "^5.0.
|
|
40
|
-
"@ai-sdk/groq": "^4.0.
|
|
41
|
-
"@ai-sdk/hume": "^3.0.
|
|
42
|
-
"@ai-sdk/lmnt": "^3.0.
|
|
43
|
-
"@ai-sdk/luma": "^3.0.
|
|
44
|
-
"@ai-sdk/mistral": "^4.0.
|
|
45
|
-
"@ai-sdk/openai": "^4.0.
|
|
46
|
-
"@ai-sdk/openai-compatible": "^3.0.
|
|
47
|
-
"@ai-sdk/perplexity": "^4.0.
|
|
48
|
-
"@ai-sdk/react": "^4.0.
|
|
49
|
-
"@ai-sdk/replicate": "^3.0.
|
|
50
|
-
"@ai-sdk/revai": "^3.0.
|
|
51
|
-
"@ai-sdk/togetherai": "^3.0.
|
|
52
|
-
"@ai-sdk/xai": "^4.0.
|
|
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.
|
|
56
|
-
"@chat-adapter/gchat": "^4.
|
|
57
|
-
"@chat-adapter/github": "^4.
|
|
58
|
-
"@chat-adapter/linear": "^4.
|
|
59
|
-
"@chat-adapter/messenger": "^4.
|
|
60
|
-
"@chat-adapter/slack": "^4.
|
|
61
|
-
"@chat-adapter/state-ioredis": "^4.
|
|
62
|
-
"@chat-adapter/state-memory": "^4.
|
|
63
|
-
"@chat-adapter/state-pg": "^4.
|
|
64
|
-
"@chat-adapter/state-redis": "^4.
|
|
65
|
-
"@chat-adapter/teams": "^4.
|
|
66
|
-
"@chat-adapter/telegram": "^4.
|
|
67
|
-
"@chat-adapter/web": "^4.
|
|
68
|
-
"@chat-adapter/whatsapp": "^4.
|
|
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.
|
|
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.
|
|
79
|
+
"@tuturuuu/internal-api": "0.27.0",
|
|
80
80
|
"@tuturuuu/supabase": "0.5.0",
|
|
81
|
-
"@tuturuuu/utils": "0.
|
|
82
|
-
"@vercel/sandbox": "^2.
|
|
83
|
-
"@zernio/chat-sdk-adapter": "^0.
|
|
84
|
-
"ai": "^7.0.
|
|
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.
|
|
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
|
|
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.
|
|
106
|
+
"@tuturuuu/types": "0.26.0",
|
|
107
107
|
"@tuturuuu/typescript-config": "0.1.1",
|
|
108
|
-
"@types/node": "^26.1.
|
|
108
|
+
"@types/node": "^26.1.2",
|
|
109
109
|
"@types/qrcode": "^1.5.6",
|
|
110
|
-
"@types/react": "^19.2.
|
|
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,
|
package/src/chat/google/route.ts
CHANGED
|
@@ -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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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<
|
|
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
package/src/credits/constants.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/studio/metering.ts
CHANGED
|
@@ -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,
|