@tuturuuu/ai 0.0.11 → 0.1.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.
@@ -1,10 +1,27 @@
1
1
  import { google } from '@ai-sdk/google';
2
- import { stepCountIs, ToolLoopAgent } from 'ai';
2
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
3
+ import {
4
+ gateway,
5
+ type LanguageModelUsage,
6
+ stepCountIs,
7
+ ToolLoopAgent,
8
+ } from 'ai';
3
9
  import { z } from 'zod';
10
+ import { capMaxOutputTokensByCredits } from '../../credits/cap-output-tokens';
11
+ import { checkAiCredits, deductAiCredits } from '../../credits/check-credits';
12
+ import {
13
+ GEMINI_31_FLASH_LITE_MODEL,
14
+ isGoogleModelId,
15
+ toBareModelName,
16
+ } from '../../credits/model-mapping';
17
+ import {
18
+ PlanModelResolutionError,
19
+ resolvePlanModel,
20
+ } from '../../credits/resolve-plan-model';
4
21
  import { withAiMemory } from '../../memory';
5
22
  import type { MiraToolContext } from '../mira-tools';
6
23
 
7
- const PARALLEL_CHECKS_MODEL = 'gemini-3.1-flash-lite';
24
+ const PARALLEL_CHECKS_MODEL = GEMINI_31_FLASH_LITE_MODEL;
8
25
 
9
26
  const ParallelChecksArgsSchema = z.object({
10
27
  question: z
@@ -37,6 +54,12 @@ type ParallelCheckResult = {
37
54
  finding: string;
38
55
  };
39
56
 
57
+ type MeteredUsage = {
58
+ inputTokens: number;
59
+ outputTokens: number;
60
+ reasoningTokens: number;
61
+ };
62
+
40
63
  const CHECK_INSTRUCTIONS: Record<CheckKind, string> = {
41
64
  assumptions:
42
65
  'You identify hidden assumptions, missing premises, and unclear decision points. Return concise findings only.',
@@ -69,26 +92,79 @@ ${context ? `Relevant context:\n${context}\n\n` : ''}Return:
69
92
  - "No material issues" if this perspective has nothing important`;
70
93
  }
71
94
 
95
+ function getCreditCheckErrorMessage(creditCheck: {
96
+ errorCode?: string | null;
97
+ errorMessage?: string | null;
98
+ }) {
99
+ const errorMessages: Record<string, string> = {
100
+ CREDIT_CHECK_FAILED: 'AI credit check failed. Please try again.',
101
+ CREDITS_EXHAUSTED: 'You have run out of AI credits for parallel checks.',
102
+ FEATURE_NOT_ALLOWED:
103
+ 'Parallel checks are not available on your current plan.',
104
+ MODEL_NOT_ALLOWED:
105
+ 'The parallel-checks model is not enabled for your workspace.',
106
+ NO_ALLOCATION: 'AI credits are not configured for your workspace.',
107
+ };
108
+
109
+ return (
110
+ creditCheck.errorMessage ??
111
+ errorMessages[creditCheck.errorCode ?? ''] ??
112
+ 'Parallel checks are not available. Please check your AI credit settings.'
113
+ );
114
+ }
115
+
116
+ function normalizeTokenCount(value: number | undefined) {
117
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
118
+ return Math.max(0, Math.trunc(value));
119
+ }
120
+
121
+ function extractMeteredUsage(usage: LanguageModelUsage): MeteredUsage {
122
+ return {
123
+ inputTokens: normalizeTokenCount(usage.inputTokens),
124
+ outputTokens: normalizeTokenCount(usage.outputTokens),
125
+ reasoningTokens: normalizeTokenCount(
126
+ usage.outputTokenDetails.reasoningTokens ?? usage.reasoningTokens
127
+ ),
128
+ };
129
+ }
130
+
131
+ function getPerCheckMaxOutputTokens(
132
+ cappedMaxOutput: number | null,
133
+ checkCount: number
134
+ ) {
135
+ if (cappedMaxOutput === null) return undefined;
136
+ return Math.max(1, Math.floor(cappedMaxOutput / checkCount));
137
+ }
138
+
72
139
  async function runCheck({
73
140
  abortSignal,
141
+ billingWsId,
74
142
  check,
75
143
  context,
144
+ maxOutputTokens,
145
+ modelId,
76
146
  question,
77
147
  toolContext,
78
148
  }: {
79
149
  abortSignal?: AbortSignal;
150
+ billingWsId: string;
80
151
  check: CheckKind;
81
152
  context?: string;
153
+ maxOutputTokens?: number;
154
+ modelId: string;
82
155
  question: string;
83
156
  toolContext: MiraToolContext;
84
157
  }): Promise<ParallelCheckResult> {
158
+ const useGoogleNativeModel = isGoogleModelId(modelId);
85
159
  const agent = new ToolLoopAgent({
86
160
  model: await withAiMemory({
87
161
  addMemory: 'never',
88
162
  customId: toolContext.chatId
89
163
  ? `${toolContext.chatId}-parallel-checks-${check}`
90
164
  : `parallel-checks-${check}`,
91
- model: google(PARALLEL_CHECKS_MODEL),
165
+ model: useGoogleNativeModel
166
+ ? google(toBareModelName(modelId))
167
+ : gateway(modelId),
92
168
  product: 'mira',
93
169
  source: 'mira_parallel_checks_tool',
94
170
  surface: 'mira_parallel_checks_tool',
@@ -97,21 +173,51 @@ async function runCheck({
97
173
  }),
98
174
  instructions: CHECK_INSTRUCTIONS[check],
99
175
  stopWhen: stepCountIs(2),
100
- providerOptions: {
101
- google: {
102
- thinkingConfig: {
103
- thinkingBudget: 0,
104
- includeThoughts: false,
105
- },
106
- },
107
- },
176
+ ...(useGoogleNativeModel
177
+ ? {
178
+ providerOptions: {
179
+ google: {
180
+ thinkingConfig: {
181
+ thinkingBudget: 0,
182
+ includeThoughts: false,
183
+ },
184
+ },
185
+ },
186
+ }
187
+ : {}),
108
188
  });
109
189
 
110
190
  const result = await agent.generate({
111
191
  prompt: buildPrompt({ check, context, question }),
112
192
  abortSignal,
193
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
113
194
  });
114
195
 
196
+ const usage = extractMeteredUsage(result.totalUsage);
197
+ const deduction = await deductAiCredits({
198
+ wsId: billingWsId,
199
+ userId: toolContext.userId,
200
+ modelId,
201
+ inputTokens: usage.inputTokens,
202
+ outputTokens: usage.outputTokens,
203
+ reasoningTokens: usage.reasoningTokens,
204
+ feature: 'chat',
205
+ metadata: {
206
+ source: 'mira_parallel_checks_tool',
207
+ check,
208
+ creditWsId: billingWsId,
209
+ routeWsId: toolContext.wsId,
210
+ ...(toolContext.chatId ? { chatId: toolContext.chatId } : {}),
211
+ ...(toolContext.workspaceContext?.wsId
212
+ ? { workspaceContextWsId: toolContext.workspaceContext.wsId }
213
+ : {}),
214
+ },
215
+ });
216
+
217
+ if (!deduction.success) {
218
+ throw new Error('Failed to deduct AI credits for parallel checks.');
219
+ }
220
+
115
221
  return {
116
222
  label: check,
117
223
  finding: result.text.trim() || 'No material issues.',
@@ -133,14 +239,55 @@ export async function executeParallelChecks(
133
239
 
134
240
  const { context, question } = parsed.data;
135
241
  const checks = parsed.data.checks ?? DEFAULT_CHECKS;
242
+ const billingWsId = ctx.creditWsId ?? ctx.wsId;
136
243
 
137
244
  try {
245
+ const resolvedModel = await resolvePlanModel({
246
+ capability: 'language',
247
+ requestedModel: PARALLEL_CHECKS_MODEL,
248
+ wsId: billingWsId,
249
+ });
250
+ const modelId = resolvedModel.modelId;
251
+ const creditCheck = await checkAiCredits(billingWsId, modelId, 'chat', {
252
+ userId: ctx.userId,
253
+ });
254
+
255
+ if (!creditCheck.allowed) {
256
+ return {
257
+ ok: false,
258
+ error: getCreditCheckErrorMessage(creditCheck),
259
+ };
260
+ }
261
+
262
+ const sbAdmin = await createAdminClient();
263
+ const cappedMaxOutput = await capMaxOutputTokensByCredits(
264
+ sbAdmin,
265
+ modelId,
266
+ creditCheck.maxOutputTokens,
267
+ creditCheck.remainingCredits
268
+ );
269
+
270
+ if (cappedMaxOutput === null && creditCheck.remainingCredits <= 0) {
271
+ return {
272
+ ok: false,
273
+ error: 'You have run out of AI credits for parallel checks.',
274
+ };
275
+ }
276
+
277
+ const maxOutputTokens = getPerCheckMaxOutputTokens(
278
+ cappedMaxOutput,
279
+ checks.length
280
+ );
281
+
138
282
  const results = await Promise.all(
139
283
  checks.map((check) =>
140
284
  runCheck({
141
285
  abortSignal: options?.abortSignal,
286
+ billingWsId,
142
287
  check,
143
288
  context,
289
+ maxOutputTokens,
290
+ modelId,
144
291
  question,
145
292
  toolContext: ctx,
146
293
  })
@@ -160,6 +307,13 @@ export async function executeParallelChecks(
160
307
  checks: results,
161
308
  };
162
309
  } catch (error) {
310
+ if (error instanceof PlanModelResolutionError) {
311
+ return {
312
+ ok: false,
313
+ error: error.message,
314
+ };
315
+ }
316
+
163
317
  if (error instanceof DOMException && error.name === 'AbortError') {
164
318
  return {
165
319
  ok: false,