@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,722 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { deductAiCredits } from '@tuturuuu/ai/credits/check-credits';
|
|
3
|
+
import type { CreditDeductionResult } from '../../credits/types';
|
|
4
|
+
|
|
5
|
+
type UsageLike = {
|
|
6
|
+
inputTokens?: number;
|
|
7
|
+
inputTokenDetails?: {
|
|
8
|
+
cacheReadTokens?: number;
|
|
9
|
+
cacheWriteTokens?: number;
|
|
10
|
+
cachedTokens?: number;
|
|
11
|
+
noCacheTokens?: number;
|
|
12
|
+
};
|
|
13
|
+
outputTokens?: number;
|
|
14
|
+
outputTokenDetails?: {
|
|
15
|
+
cacheReadTokens?: number;
|
|
16
|
+
cacheWriteTokens?: number;
|
|
17
|
+
cachedTokens?: number;
|
|
18
|
+
noCacheTokens?: number;
|
|
19
|
+
reasoningTokens?: number;
|
|
20
|
+
textTokens?: number;
|
|
21
|
+
};
|
|
22
|
+
reasoningTokens?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type GroundingMetadataLike = {
|
|
26
|
+
webSearchQueries?: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type GoogleProviderMetadataLike = {
|
|
30
|
+
groundingMetadata?: GroundingMetadataLike;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ProviderMetadataLike = {
|
|
34
|
+
google?: GoogleProviderMetadataLike;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ToolCallLike = {
|
|
38
|
+
toolCallId?: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
} & Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
type ToolResultLike = {
|
|
43
|
+
toolCallId?: string;
|
|
44
|
+
toolName?: string;
|
|
45
|
+
} & Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
type SourceLike = {
|
|
48
|
+
sourceId?: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type StepLike = {
|
|
54
|
+
text?: string;
|
|
55
|
+
toolCalls?: ReadonlyArray<ToolCallLike>;
|
|
56
|
+
toolResults?: ReadonlyArray<ToolResultLike>;
|
|
57
|
+
usage?: UsageLike;
|
|
58
|
+
providerMetadata?: ProviderMetadataLike;
|
|
59
|
+
sources?: ReadonlyArray<unknown>;
|
|
60
|
+
reasoningText?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type StreamFinishResponseLike = {
|
|
64
|
+
text?: string;
|
|
65
|
+
finishReason?: string;
|
|
66
|
+
usage?: UsageLike;
|
|
67
|
+
totalUsage?: UsageLike;
|
|
68
|
+
steps?: StepLike[];
|
|
69
|
+
providerMetadata?: ProviderMetadataLike;
|
|
70
|
+
reasoningText?: string;
|
|
71
|
+
sources?: SourceLike[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type UsageTotals = {
|
|
75
|
+
cachedInputTokens: number;
|
|
76
|
+
cachedOutputTokens: number;
|
|
77
|
+
inputTokens: number;
|
|
78
|
+
outputTokens: number;
|
|
79
|
+
reasoningTokens: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type InsertedMessage = {
|
|
83
|
+
id?: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type InsertResult = PromiseLike<{
|
|
87
|
+
data: InsertedMessage | null;
|
|
88
|
+
error: { code?: string; message: string } | null;
|
|
89
|
+
}>;
|
|
90
|
+
|
|
91
|
+
type AdminClientLike = {
|
|
92
|
+
from: (table: 'ai_chat_messages') => {
|
|
93
|
+
insert: (payload: Record<string, unknown>) => {
|
|
94
|
+
select: (columns: string) => {
|
|
95
|
+
single: () => InsertResult;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type PersistAssistantResponseParams = {
|
|
102
|
+
response: StreamFinishResponseLike;
|
|
103
|
+
sbAdmin: AdminClientLike;
|
|
104
|
+
chatId: string;
|
|
105
|
+
userId: string;
|
|
106
|
+
model: string;
|
|
107
|
+
effectiveSource: 'Mira' | 'Rewise';
|
|
108
|
+
observabilityContext?: unknown;
|
|
109
|
+
wsId?: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function collectToolData(steps: StepLike[]) {
|
|
113
|
+
const allToolCalls = steps.flatMap((step) => step.toolCalls ?? []);
|
|
114
|
+
const allToolResults = steps.flatMap((step) => step.toolResults ?? []);
|
|
115
|
+
return { allToolCalls, allToolResults };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectReasoningText(response: StreamFinishResponseLike): string {
|
|
119
|
+
const allReasoning = (response.steps ?? [])
|
|
120
|
+
.map((step) => step.reasoningText)
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join('\n\n');
|
|
123
|
+
|
|
124
|
+
return allReasoning || response.reasoningText || '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectUsageTotals(response: StreamFinishResponseLike): UsageTotals {
|
|
128
|
+
const usage = response.totalUsage ?? response.usage ?? {};
|
|
129
|
+
let cachedInputTokens = getCachedTokenCount(usage.inputTokenDetails);
|
|
130
|
+
let cachedOutputTokens = getCachedTokenCount(usage.outputTokenDetails);
|
|
131
|
+
let inputTokens = usage.inputTokens ?? 0;
|
|
132
|
+
let outputTokens = usage.outputTokens ?? 0;
|
|
133
|
+
let reasoningTokens = usage.reasoningTokens ?? 0;
|
|
134
|
+
|
|
135
|
+
if ((response.steps?.length ?? 0) > 0) {
|
|
136
|
+
let stepInputSum = 0;
|
|
137
|
+
let stepOutputSum = 0;
|
|
138
|
+
let stepReasoningSum = 0;
|
|
139
|
+
let stepCachedInputSum = 0;
|
|
140
|
+
let stepCachedOutputSum = 0;
|
|
141
|
+
|
|
142
|
+
for (const step of response.steps ?? []) {
|
|
143
|
+
const stepUsage = step.usage;
|
|
144
|
+
if (!stepUsage) continue;
|
|
145
|
+
|
|
146
|
+
stepInputSum += stepUsage.inputTokens ?? 0;
|
|
147
|
+
stepOutputSum += stepUsage.outputTokens ?? 0;
|
|
148
|
+
stepReasoningSum += stepUsage.reasoningTokens ?? 0;
|
|
149
|
+
stepCachedInputSum += getCachedTokenCount(stepUsage.inputTokenDetails);
|
|
150
|
+
stepCachedOutputSum += getCachedTokenCount(stepUsage.outputTokenDetails);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (inputTokens === 0) {
|
|
154
|
+
inputTokens = stepInputSum;
|
|
155
|
+
}
|
|
156
|
+
if (outputTokens === 0) {
|
|
157
|
+
outputTokens = stepOutputSum;
|
|
158
|
+
}
|
|
159
|
+
if (reasoningTokens === 0) {
|
|
160
|
+
reasoningTokens = stepReasoningSum;
|
|
161
|
+
}
|
|
162
|
+
if (cachedInputTokens === 0) {
|
|
163
|
+
cachedInputTokens = stepCachedInputSum;
|
|
164
|
+
}
|
|
165
|
+
if (cachedOutputTokens === 0) {
|
|
166
|
+
cachedOutputTokens = stepCachedOutputSum;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
cachedInputTokens,
|
|
172
|
+
cachedOutputTokens,
|
|
173
|
+
inputTokens,
|
|
174
|
+
outputTokens,
|
|
175
|
+
reasoningTokens,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getCachedTokenCount(
|
|
180
|
+
details: UsageLike['inputTokenDetails'] | UsageLike['outputTokenDetails']
|
|
181
|
+
) {
|
|
182
|
+
if (!details) return 0;
|
|
183
|
+
return (
|
|
184
|
+
details.cachedTokens ??
|
|
185
|
+
(details.cacheReadTokens ?? 0) + (details.cacheWriteTokens ?? 0)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collectSerializableSources(response: StreamFinishResponseLike) {
|
|
190
|
+
const sources = [
|
|
191
|
+
...(response.sources ?? []),
|
|
192
|
+
...(response.steps ?? []).flatMap((step) =>
|
|
193
|
+
(step.sources ?? []).filter((source): source is SourceLike =>
|
|
194
|
+
Boolean(
|
|
195
|
+
source &&
|
|
196
|
+
typeof source === 'object' &&
|
|
197
|
+
'url' in source &&
|
|
198
|
+
typeof source.url === 'string'
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
if (!sources.length) return [];
|
|
205
|
+
|
|
206
|
+
const seen = new Set<string>();
|
|
207
|
+
return sources
|
|
208
|
+
.map((source) => ({
|
|
209
|
+
sourceId: source.sourceId,
|
|
210
|
+
url: source.url,
|
|
211
|
+
title: source.title,
|
|
212
|
+
}))
|
|
213
|
+
.filter((source) => {
|
|
214
|
+
if (!source.url || seen.has(source.url)) return false;
|
|
215
|
+
seen.add(source.url);
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function logGoogleSearchDebug(response: StreamFinishResponseLike): void {
|
|
221
|
+
if (process.env.GOOGLE_SEARCH_DEBUG !== 'true') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log('[Google Search Debug] response keys:', Object.keys(response));
|
|
226
|
+
console.log(
|
|
227
|
+
'[Google Search Debug] providerMetadata:',
|
|
228
|
+
JSON.stringify(response.providerMetadata, null, 2)?.slice(0, 500)
|
|
229
|
+
);
|
|
230
|
+
console.log(
|
|
231
|
+
'[Google Search Debug] sources:',
|
|
232
|
+
JSON.stringify(response.sources, null, 2)?.slice(0, 500)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (!response.steps?.length) return;
|
|
236
|
+
|
|
237
|
+
for (let stepIndex = 0; stepIndex < response.steps.length; stepIndex++) {
|
|
238
|
+
const step = response.steps[stepIndex];
|
|
239
|
+
console.log(
|
|
240
|
+
`[Google Search Debug] step[${stepIndex}] providerMetadata:`,
|
|
241
|
+
JSON.stringify(step?.providerMetadata, null, 2)?.slice(0, 500)
|
|
242
|
+
);
|
|
243
|
+
console.log(
|
|
244
|
+
`[Google Search Debug] step[${stepIndex}] sources:`,
|
|
245
|
+
JSON.stringify(step?.sources, null, 2)?.slice(0, 500)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function countGoogleSearchQueries(
|
|
251
|
+
response: StreamFinishResponseLike,
|
|
252
|
+
allToolCalls: ToolCallLike[]
|
|
253
|
+
): number {
|
|
254
|
+
const customGoogleSearchCalls = allToolCalls.filter(
|
|
255
|
+
(toolCall) => toolCall.toolName === 'google_search'
|
|
256
|
+
).length;
|
|
257
|
+
if (customGoogleSearchCalls > 0) return customGoogleSearchCalls;
|
|
258
|
+
|
|
259
|
+
const topQueries =
|
|
260
|
+
response.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
|
|
261
|
+
if (topQueries?.length) return topQueries.length;
|
|
262
|
+
|
|
263
|
+
let perStepQueriesCount = 0;
|
|
264
|
+
for (const step of response.steps ?? []) {
|
|
265
|
+
const stepQueries =
|
|
266
|
+
step.providerMetadata?.google?.groundingMetadata?.webSearchQueries;
|
|
267
|
+
if (stepQueries?.length) {
|
|
268
|
+
perStepQueriesCount += stepQueries.length;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (perStepQueriesCount > 0) return perStepQueriesCount;
|
|
272
|
+
|
|
273
|
+
if (Array.isArray(response.sources) && response.sources.length > 0) {
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function collectUiMessageParts({
|
|
281
|
+
allToolCalls,
|
|
282
|
+
allToolResults,
|
|
283
|
+
reasoningText,
|
|
284
|
+
response,
|
|
285
|
+
serializedSources,
|
|
286
|
+
}: {
|
|
287
|
+
allToolCalls: ToolCallLike[];
|
|
288
|
+
allToolResults: ToolResultLike[];
|
|
289
|
+
reasoningText: string;
|
|
290
|
+
response: StreamFinishResponseLike;
|
|
291
|
+
serializedSources: ReturnType<typeof collectSerializableSources>;
|
|
292
|
+
}) {
|
|
293
|
+
const parts: Record<string, unknown>[] = [];
|
|
294
|
+
|
|
295
|
+
if (response.text) {
|
|
296
|
+
parts.push({ type: 'text', text: response.text });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (reasoningText) {
|
|
300
|
+
parts.push({ type: 'reasoning', text: reasoningText });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const toolCall of allToolCalls) {
|
|
304
|
+
const toolCallId =
|
|
305
|
+
toolCall.toolCallId ?? readString(toolCall.toolCallId) ?? undefined;
|
|
306
|
+
const matchingResult = allToolResults.find(
|
|
307
|
+
(result) => toolCallId && readString(result.toolCallId) === toolCallId
|
|
308
|
+
);
|
|
309
|
+
parts.push({
|
|
310
|
+
type: 'dynamic-tool',
|
|
311
|
+
toolName: toolCall.toolName ?? matchingResult?.toolName ?? 'tool',
|
|
312
|
+
toolCallId: toolCallId ?? randomUUID(),
|
|
313
|
+
state: 'output-available',
|
|
314
|
+
input: extractToolInput(toolCall),
|
|
315
|
+
output: matchingResult ? extractToolOutput(matchingResult) : null,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const toolResult of allToolResults) {
|
|
320
|
+
const hasCall = allToolCalls.some(
|
|
321
|
+
(toolCall) =>
|
|
322
|
+
readString(toolCall.toolCallId) &&
|
|
323
|
+
readString(toolCall.toolCallId) === readString(toolResult.toolCallId)
|
|
324
|
+
);
|
|
325
|
+
if (hasCall) continue;
|
|
326
|
+
|
|
327
|
+
parts.push({
|
|
328
|
+
type: 'dynamic-tool',
|
|
329
|
+
toolName: toolResult.toolName ?? 'tool',
|
|
330
|
+
toolCallId: toolResult.toolCallId ?? randomUUID(),
|
|
331
|
+
state: 'output-available',
|
|
332
|
+
output: extractToolOutput(toolResult),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const source of serializedSources) {
|
|
337
|
+
if (!source.url) continue;
|
|
338
|
+
parts.push({
|
|
339
|
+
type: 'source-url',
|
|
340
|
+
sourceId: source.sourceId ?? source.url,
|
|
341
|
+
title: source.title,
|
|
342
|
+
url: source.url,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return parts;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function buildAbortedStreamFinishResponse(
|
|
350
|
+
steps: ReadonlyArray<StepLike>
|
|
351
|
+
): StreamFinishResponseLike {
|
|
352
|
+
return {
|
|
353
|
+
finishReason: 'abort',
|
|
354
|
+
steps: [...steps],
|
|
355
|
+
text: steps
|
|
356
|
+
.map((step) => step.text)
|
|
357
|
+
.filter((text): text is string => Boolean(text?.trim()))
|
|
358
|
+
.join('\n\n'),
|
|
359
|
+
totalUsage: sumStepUsage(steps),
|
|
360
|
+
sources: steps.flatMap((step) =>
|
|
361
|
+
(step.sources ?? []).filter((source): source is SourceLike =>
|
|
362
|
+
Boolean(
|
|
363
|
+
source &&
|
|
364
|
+
typeof source === 'object' &&
|
|
365
|
+
'url' in source &&
|
|
366
|
+
typeof source.url === 'string'
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function sumStepUsage(steps: ReadonlyArray<StepLike>): UsageLike {
|
|
374
|
+
return steps.reduce<UsageLike>(
|
|
375
|
+
(total, step) => {
|
|
376
|
+
const usage = step.usage ?? {};
|
|
377
|
+
total.inputTokens = (total.inputTokens ?? 0) + (usage.inputTokens ?? 0);
|
|
378
|
+
total.outputTokens =
|
|
379
|
+
(total.outputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
380
|
+
total.reasoningTokens =
|
|
381
|
+
(total.reasoningTokens ?? 0) + (usage.reasoningTokens ?? 0);
|
|
382
|
+
return total;
|
|
383
|
+
},
|
|
384
|
+
{ inputTokens: 0, outputTokens: 0, reasoningTokens: 0 }
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractToolInput(toolCall: ToolCallLike) {
|
|
389
|
+
return toolCall.input ?? toolCall.args ?? toolCall.arguments ?? {};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function extractToolOutput(toolResult: ToolResultLike) {
|
|
393
|
+
return toolResult.output ?? toolResult.result ?? toolResult;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function readString(value: unknown) {
|
|
397
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function truncateString(value: string, maxLength: number) {
|
|
401
|
+
if (value.length <= maxLength) return value;
|
|
402
|
+
return `${value.slice(0, Math.max(0, maxLength - 14))}... [truncated]`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function compactJsonValue(value: unknown, maxLength = 400): unknown {
|
|
406
|
+
if (typeof value === 'string') return truncateString(value, maxLength);
|
|
407
|
+
if (value === null || typeof value !== 'object') return value;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const serialized = JSON.stringify(value);
|
|
411
|
+
if (serialized.length <= maxLength) return value;
|
|
412
|
+
return {
|
|
413
|
+
truncated: true,
|
|
414
|
+
preview: truncateString(serialized, maxLength),
|
|
415
|
+
};
|
|
416
|
+
} catch {
|
|
417
|
+
return { truncated: true, preview: '[unserializable]' };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function compactAiMessagePart(part: Record<string, unknown>) {
|
|
422
|
+
const type = readString(part.type) ?? 'unknown';
|
|
423
|
+
|
|
424
|
+
if (type === 'text' || type === 'reasoning') {
|
|
425
|
+
return {
|
|
426
|
+
...part,
|
|
427
|
+
text:
|
|
428
|
+
typeof part.text === 'string'
|
|
429
|
+
? truncateString(part.text, 4000)
|
|
430
|
+
: part.text,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (type === 'dynamic-tool') {
|
|
435
|
+
return {
|
|
436
|
+
type,
|
|
437
|
+
toolName: part.toolName,
|
|
438
|
+
toolCallId: part.toolCallId,
|
|
439
|
+
state: part.state,
|
|
440
|
+
input: compactJsonValue(part.input),
|
|
441
|
+
output: compactJsonValue(part.output),
|
|
442
|
+
errorText:
|
|
443
|
+
typeof part.errorText === 'string'
|
|
444
|
+
? truncateString(part.errorText, 1200)
|
|
445
|
+
: part.errorText,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return compactJsonValue(part, 1200);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function isPayloadMetadataLimitError(error: {
|
|
453
|
+
code?: string;
|
|
454
|
+
message: string;
|
|
455
|
+
}) {
|
|
456
|
+
return (
|
|
457
|
+
error.code === '22001' &&
|
|
458
|
+
/PAYLOAD_FIELD_BYTES_EXCEEDED: ai_chat_messages\.metadata/u.test(
|
|
459
|
+
error.message
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildAssistantMessageMetadata({
|
|
465
|
+
allToolCalls,
|
|
466
|
+
allToolResults,
|
|
467
|
+
cachedInputTokens,
|
|
468
|
+
cachedOutputTokens,
|
|
469
|
+
effectiveSource,
|
|
470
|
+
inputTokens,
|
|
471
|
+
model,
|
|
472
|
+
observabilityContext,
|
|
473
|
+
outputTokens,
|
|
474
|
+
parts,
|
|
475
|
+
reasoningText,
|
|
476
|
+
reasoningTokens,
|
|
477
|
+
response,
|
|
478
|
+
serializedSources,
|
|
479
|
+
}: {
|
|
480
|
+
allToolCalls: ToolCallLike[];
|
|
481
|
+
allToolResults: ToolResultLike[];
|
|
482
|
+
cachedInputTokens: number;
|
|
483
|
+
cachedOutputTokens: number;
|
|
484
|
+
effectiveSource: 'Mira' | 'Rewise';
|
|
485
|
+
inputTokens: number;
|
|
486
|
+
model: string;
|
|
487
|
+
observabilityContext?: unknown;
|
|
488
|
+
outputTokens: number;
|
|
489
|
+
parts: Record<string, unknown>[];
|
|
490
|
+
reasoningText: string;
|
|
491
|
+
reasoningTokens: number;
|
|
492
|
+
response: StreamFinishResponseLike;
|
|
493
|
+
serializedSources: ReturnType<typeof collectSerializableSources>;
|
|
494
|
+
}) {
|
|
495
|
+
return {
|
|
496
|
+
source: effectiveSource,
|
|
497
|
+
ai: {
|
|
498
|
+
finishReason: response.finishReason,
|
|
499
|
+
model,
|
|
500
|
+
observability: {
|
|
501
|
+
contextBreakdown: observabilityContext ?? [],
|
|
502
|
+
},
|
|
503
|
+
parts,
|
|
504
|
+
usage: {
|
|
505
|
+
cachedInputTokens,
|
|
506
|
+
cachedOutputTokens,
|
|
507
|
+
inputTokens,
|
|
508
|
+
outputTokens,
|
|
509
|
+
reasoningTokens,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
...(reasoningText ? { reasoning: reasoningText } : {}),
|
|
513
|
+
...(allToolCalls.length
|
|
514
|
+
? { toolCalls: structuredClone(allToolCalls) }
|
|
515
|
+
: {}),
|
|
516
|
+
...(allToolResults.length
|
|
517
|
+
? { toolResults: structuredClone(allToolResults) }
|
|
518
|
+
: {}),
|
|
519
|
+
...(serializedSources.length ? { sources: serializedSources } : {}),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function compactAssistantMessageMetadata(
|
|
524
|
+
metadata: ReturnType<typeof buildAssistantMessageMetadata>
|
|
525
|
+
) {
|
|
526
|
+
const ai = metadata.ai;
|
|
527
|
+
return {
|
|
528
|
+
source: metadata.source,
|
|
529
|
+
ai: {
|
|
530
|
+
finishReason: ai.finishReason,
|
|
531
|
+
metadataCompacted: true,
|
|
532
|
+
model: ai.model,
|
|
533
|
+
observability: {
|
|
534
|
+
contextBreakdown: Array.isArray(ai.observability.contextBreakdown)
|
|
535
|
+
? ai.observability.contextBreakdown.slice(-20)
|
|
536
|
+
: [],
|
|
537
|
+
},
|
|
538
|
+
omittedPartCount: Math.max(0, ai.parts.length - 8),
|
|
539
|
+
parts: ai.parts
|
|
540
|
+
.slice(0, 8)
|
|
541
|
+
.map((part) =>
|
|
542
|
+
part && typeof part === 'object' && !Array.isArray(part)
|
|
543
|
+
? compactAiMessagePart(part as Record<string, unknown>)
|
|
544
|
+
: part
|
|
545
|
+
),
|
|
546
|
+
usage: ai.usage,
|
|
547
|
+
},
|
|
548
|
+
...(typeof metadata.reasoning === 'string'
|
|
549
|
+
? { reasoning: truncateString(metadata.reasoning, 3000) }
|
|
550
|
+
: {}),
|
|
551
|
+
...(Array.isArray(metadata.sources)
|
|
552
|
+
? { sources: metadata.sources.slice(0, 20) }
|
|
553
|
+
: {}),
|
|
554
|
+
toolCallCount: Array.isArray(metadata.toolCalls)
|
|
555
|
+
? metadata.toolCalls.length
|
|
556
|
+
: 0,
|
|
557
|
+
toolResultCount: Array.isArray(metadata.toolResults)
|
|
558
|
+
? metadata.toolResults.length
|
|
559
|
+
: 0,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function persistAssistantResponse({
|
|
564
|
+
response,
|
|
565
|
+
sbAdmin,
|
|
566
|
+
chatId,
|
|
567
|
+
userId,
|
|
568
|
+
model,
|
|
569
|
+
effectiveSource,
|
|
570
|
+
observabilityContext,
|
|
571
|
+
wsId,
|
|
572
|
+
}: PersistAssistantResponseParams): Promise<void> {
|
|
573
|
+
const steps = response.steps ?? [];
|
|
574
|
+
const { allToolCalls, allToolResults } = collectToolData(steps);
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
!response.text &&
|
|
578
|
+
allToolCalls.length === 0 &&
|
|
579
|
+
allToolResults.length === 0
|
|
580
|
+
) {
|
|
581
|
+
console.warn('onFinish: no text and no tool calls — skipping DB save');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const reasoningText = collectReasoningText(response);
|
|
586
|
+
const {
|
|
587
|
+
cachedInputTokens,
|
|
588
|
+
cachedOutputTokens,
|
|
589
|
+
inputTokens,
|
|
590
|
+
outputTokens,
|
|
591
|
+
reasoningTokens,
|
|
592
|
+
} = collectUsageTotals(response);
|
|
593
|
+
const serializedSources = collectSerializableSources(response);
|
|
594
|
+
const parts = collectUiMessageParts({
|
|
595
|
+
allToolCalls,
|
|
596
|
+
allToolResults,
|
|
597
|
+
reasoningText,
|
|
598
|
+
response,
|
|
599
|
+
serializedSources,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const metadata = buildAssistantMessageMetadata({
|
|
603
|
+
allToolCalls,
|
|
604
|
+
allToolResults,
|
|
605
|
+
cachedInputTokens,
|
|
606
|
+
cachedOutputTokens,
|
|
607
|
+
effectiveSource,
|
|
608
|
+
inputTokens,
|
|
609
|
+
model,
|
|
610
|
+
observabilityContext,
|
|
611
|
+
outputTokens,
|
|
612
|
+
parts,
|
|
613
|
+
reasoningText,
|
|
614
|
+
reasoningTokens,
|
|
615
|
+
response,
|
|
616
|
+
serializedSources,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const insertPayload = {
|
|
620
|
+
chat_id: chatId,
|
|
621
|
+
creator_id: userId,
|
|
622
|
+
content: response.text || '',
|
|
623
|
+
role: 'ASSISTANT',
|
|
624
|
+
model: (model.includes('/')
|
|
625
|
+
? model.split('/').pop()!
|
|
626
|
+
: model
|
|
627
|
+
).toLowerCase(),
|
|
628
|
+
finish_reason: response.finishReason,
|
|
629
|
+
prompt_tokens: inputTokens,
|
|
630
|
+
completion_tokens: outputTokens,
|
|
631
|
+
metadata,
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
let { data: messageData, error } = await sbAdmin
|
|
635
|
+
.from('ai_chat_messages')
|
|
636
|
+
.insert(insertPayload)
|
|
637
|
+
.select('id')
|
|
638
|
+
.single();
|
|
639
|
+
|
|
640
|
+
if (error && isPayloadMetadataLimitError(error)) {
|
|
641
|
+
const retry = await sbAdmin
|
|
642
|
+
.from('ai_chat_messages')
|
|
643
|
+
.insert({
|
|
644
|
+
...insertPayload,
|
|
645
|
+
metadata: compactAssistantMessageMetadata(metadata),
|
|
646
|
+
})
|
|
647
|
+
.select('id')
|
|
648
|
+
.single();
|
|
649
|
+
messageData = retry.data;
|
|
650
|
+
error = retry.error;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (error) {
|
|
654
|
+
console.log('ERROR ORIGIN: ROOT COMPLETION');
|
|
655
|
+
console.log(error);
|
|
656
|
+
throw new Error(error.message);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log('AI Response saved to database');
|
|
660
|
+
logGoogleSearchDebug(response);
|
|
661
|
+
|
|
662
|
+
const searchCount = countGoogleSearchQueries(response, allToolCalls);
|
|
663
|
+
if (searchCount > 0) {
|
|
664
|
+
console.log(
|
|
665
|
+
`Google Search grounding detected: ${searchCount} search quer${searchCount === 1 ? 'y' : 'ies'}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (
|
|
670
|
+
(wsId || userId) &&
|
|
671
|
+
(inputTokens > 0 ||
|
|
672
|
+
outputTokens > 0 ||
|
|
673
|
+
reasoningTokens > 0 ||
|
|
674
|
+
searchCount > 0)
|
|
675
|
+
) {
|
|
676
|
+
let deductionResult: CreditDeductionResult;
|
|
677
|
+
try {
|
|
678
|
+
deductionResult = await deductAiCredits({
|
|
679
|
+
wsId: wsId ?? undefined,
|
|
680
|
+
userId,
|
|
681
|
+
modelId: model,
|
|
682
|
+
inputTokens,
|
|
683
|
+
outputTokens,
|
|
684
|
+
reasoningTokens,
|
|
685
|
+
feature: 'chat',
|
|
686
|
+
chatMessageId: messageData?.id,
|
|
687
|
+
...(searchCount > 0 ? { searchCount } : {}),
|
|
688
|
+
});
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.error('Failed to deduct AI credits after assistant response.', {
|
|
691
|
+
wsId,
|
|
692
|
+
userId,
|
|
693
|
+
chatMessageId: messageData?.id,
|
|
694
|
+
model,
|
|
695
|
+
...(searchCount > 0 ? { searchCount } : {}),
|
|
696
|
+
error,
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!deductionResult.success) {
|
|
702
|
+
console.error('AI credit deduction returned unsuccessful result.', {
|
|
703
|
+
wsId,
|
|
704
|
+
userId,
|
|
705
|
+
chatMessageId: messageData?.id,
|
|
706
|
+
model,
|
|
707
|
+
...(searchCount > 0 ? { searchCount } : {}),
|
|
708
|
+
deductionResult,
|
|
709
|
+
});
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
console.info('AI credits deducted for assistant response.', {
|
|
714
|
+
wsId,
|
|
715
|
+
userId,
|
|
716
|
+
chatMessageId: messageData?.id,
|
|
717
|
+
model,
|
|
718
|
+
...(searchCount > 0 ? { searchCount } : {}),
|
|
719
|
+
deductionResult,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|