@tuturuuu/ai 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,219 @@
1
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
2
+ import { getWorkspaceTier } from '@tuturuuu/utils/workspace-helper';
3
+ import {
4
+ matchesAllowedModel,
5
+ normalizeStableModelId,
6
+ resolveGatewayModelId,
7
+ } from './model-mapping';
8
+
9
+ export type PlanModelCapability = 'image' | 'language';
10
+
11
+ type WorkspaceProductTier = 'ENTERPRISE' | 'FREE' | 'PLUS' | 'PRO';
12
+
13
+ type AllocationRow = {
14
+ allowed_models: string[];
15
+ default_image_model?: string | null;
16
+ default_language_model?: string | null;
17
+ id: string;
18
+ tier: WorkspaceProductTier;
19
+ };
20
+
21
+ type GatewayModelRow = {
22
+ id: string;
23
+ is_enabled: boolean;
24
+ type: PlanModelCapability;
25
+ };
26
+
27
+ export class PlanModelResolutionError extends Error {
28
+ code:
29
+ | 'DEFAULT_MODEL_DISABLED'
30
+ | 'DEFAULT_MODEL_INVALID'
31
+ | 'NO_ALLOCATION'
32
+ | 'WORKSPACE_ID_REQUIRED';
33
+
34
+ constructor(code: PlanModelResolutionError['code'], message: string) {
35
+ super(message);
36
+ this.name = 'PlanModelResolutionError';
37
+ this.code = code;
38
+ }
39
+ }
40
+
41
+ export interface EffectivePlanModel {
42
+ allocationId: string;
43
+ modelId: string;
44
+ source: 'plan_default' | 'requested';
45
+ tier: AllocationRow['tier'];
46
+ }
47
+
48
+ function getDefaultModelField(capability: PlanModelCapability) {
49
+ return capability === 'language'
50
+ ? 'default_language_model'
51
+ : 'default_image_model';
52
+ }
53
+
54
+ function getFallbackPlanModelId(
55
+ tier: WorkspaceProductTier,
56
+ capability: PlanModelCapability
57
+ ) {
58
+ if (capability === 'language') {
59
+ return 'google/gemini-3.1-flash-lite';
60
+ }
61
+
62
+ switch (tier) {
63
+ case 'FREE':
64
+ return 'google/imagen-4.0-fast-generate-001';
65
+ case 'PLUS':
66
+ case 'PRO':
67
+ case 'ENTERPRISE':
68
+ return 'google/imagen-4.0-generate-001';
69
+ }
70
+ }
71
+
72
+ export function selectEffectivePlanModel(args: {
73
+ allocation: AllocationRow;
74
+ capability: PlanModelCapability;
75
+ modelsById: Map<string, GatewayModelRow>;
76
+ requestedModel?: string | null;
77
+ }): EffectivePlanModel {
78
+ const { allocation, capability, modelsById } = args;
79
+ const requestedModel = args.requestedModel?.trim()
80
+ ? resolveGatewayModelId(args.requestedModel)
81
+ : null;
82
+ const defaultModelId = normalizeStableModelId(
83
+ allocation[getDefaultModelField(capability)] ??
84
+ getFallbackPlanModelId(allocation.tier, capability)
85
+ );
86
+
87
+ if (requestedModel) {
88
+ const requestedRow = modelsById.get(requestedModel);
89
+ if (
90
+ requestedRow?.is_enabled &&
91
+ requestedRow.type === capability &&
92
+ matchesAllowedModel(requestedModel, allocation.allowed_models)
93
+ ) {
94
+ return {
95
+ allocationId: allocation.id,
96
+ modelId: requestedModel,
97
+ source: 'requested',
98
+ tier: allocation.tier,
99
+ };
100
+ }
101
+ }
102
+
103
+ const defaultRow = modelsById.get(defaultModelId);
104
+ if (!defaultRow) {
105
+ throw new PlanModelResolutionError(
106
+ 'DEFAULT_MODEL_INVALID',
107
+ `Default ${capability} model "${defaultModelId}" is missing from ai_gateway_models.`
108
+ );
109
+ }
110
+
111
+ if (!defaultRow.is_enabled) {
112
+ throw new PlanModelResolutionError(
113
+ 'DEFAULT_MODEL_DISABLED',
114
+ `Default ${capability} model "${defaultModelId}" is currently disabled.`
115
+ );
116
+ }
117
+
118
+ if (defaultRow.type !== capability) {
119
+ throw new PlanModelResolutionError(
120
+ 'DEFAULT_MODEL_INVALID',
121
+ `Default ${capability} model "${defaultModelId}" has incompatible type "${defaultRow.type}".`
122
+ );
123
+ }
124
+
125
+ if (!matchesAllowedModel(defaultModelId, allocation.allowed_models)) {
126
+ throw new PlanModelResolutionError(
127
+ 'DEFAULT_MODEL_INVALID',
128
+ `Default ${capability} model "${defaultModelId}" is not included in the allocation allowlist.`
129
+ );
130
+ }
131
+
132
+ return {
133
+ allocationId: allocation.id,
134
+ modelId: defaultModelId,
135
+ source: 'plan_default',
136
+ tier: allocation.tier,
137
+ };
138
+ }
139
+
140
+ export async function resolvePlanModel(args: {
141
+ capability: PlanModelCapability;
142
+ requestedModel?: string | null;
143
+ wsId?: string | null;
144
+ }): Promise<EffectivePlanModel> {
145
+ if (!args.wsId) {
146
+ throw new PlanModelResolutionError(
147
+ 'WORKSPACE_ID_REQUIRED',
148
+ 'Workspace ID is required to resolve a plan model.'
149
+ );
150
+ }
151
+
152
+ const sbAdmin = await createAdminClient();
153
+ const privateDb = sbAdmin.schema('private');
154
+ const tier = await getWorkspaceTier(args.wsId, { useAdmin: true });
155
+
156
+ const { data: allocationData, error: allocationError } = await sbAdmin
157
+ .from('ai_credit_plan_allocations')
158
+ .select('*')
159
+ .eq('tier', tier)
160
+ .eq('is_active', true)
161
+ .maybeSingle();
162
+
163
+ if (allocationError) {
164
+ throw new PlanModelResolutionError(
165
+ 'NO_ALLOCATION',
166
+ allocationError.message ||
167
+ `Failed to load AI credit allocation for ${tier}.`
168
+ );
169
+ }
170
+
171
+ const allocation = allocationData as AllocationRow | null;
172
+
173
+ if (!allocation) {
174
+ throw new PlanModelResolutionError(
175
+ 'NO_ALLOCATION',
176
+ `No AI credit allocation configured for the ${tier} plan.`
177
+ );
178
+ }
179
+
180
+ const candidateIds = new Set<string>([
181
+ normalizeStableModelId(
182
+ allocation.default_language_model ??
183
+ getFallbackPlanModelId(allocation.tier, 'language')
184
+ ),
185
+ normalizeStableModelId(
186
+ allocation.default_image_model ??
187
+ getFallbackPlanModelId(allocation.tier, 'image')
188
+ ),
189
+ ]);
190
+
191
+ if (args.requestedModel?.trim()) {
192
+ candidateIds.add(resolveGatewayModelId(args.requestedModel));
193
+ }
194
+
195
+ const { data: models, error: modelsError } = await privateDb
196
+ .from('ai_gateway_models')
197
+ .select('id, type, is_enabled')
198
+ .in('id', Array.from(candidateIds));
199
+
200
+ if (modelsError) {
201
+ throw new PlanModelResolutionError(
202
+ 'DEFAULT_MODEL_INVALID',
203
+ modelsError.message || 'Failed to load AI gateway model details.'
204
+ );
205
+ }
206
+
207
+ const modelsById = new Map(
208
+ ((models ?? []) as GatewayModelRow[]).map(
209
+ (model) => [model.id, model] as const
210
+ )
211
+ );
212
+
213
+ return selectEffectivePlanModel({
214
+ allocation,
215
+ capability: args.capability,
216
+ modelsById,
217
+ requestedModel: args.requestedModel,
218
+ });
219
+ }
@@ -0,0 +1,351 @@
1
+ import type { SupabaseClient } from '@tuturuuu/supabase';
2
+ import type { Json, TablesInsert } from '@tuturuuu/types';
3
+
4
+ export type GatewayModelSyncSource =
5
+ | 'tuturuuu-production-public'
6
+ | 'vercel-gateway';
7
+
8
+ type AIGatewayModelInsert = TablesInsert<
9
+ { schema: 'private' },
10
+ 'ai_gateway_models'
11
+ >;
12
+
13
+ interface GatewayPricingTier {
14
+ cost: string;
15
+ min: number;
16
+ max?: number;
17
+ }
18
+
19
+ interface GatewayVideoDurationPricing {
20
+ resolution?: string;
21
+ cost_per_second: string;
22
+ audio?: boolean;
23
+ mode?: string;
24
+ }
25
+
26
+ interface GatewayModelPricing {
27
+ // Vercel AI Gateway uses these field names (values are strings)
28
+ input?: string;
29
+ output?: string;
30
+ input_cache_read?: string;
31
+ input_cache_write?: string;
32
+ web_search?: string;
33
+ image?: string;
34
+ input_tiers?: GatewayPricingTier[];
35
+ output_tiers?: GatewayPricingTier[];
36
+ input_cache_read_tiers?: GatewayPricingTier[];
37
+ input_cache_write_tiers?: GatewayPricingTier[];
38
+ video_duration_pricing?: GatewayVideoDurationPricing[];
39
+ }
40
+
41
+ interface GatewayModel {
42
+ id: string;
43
+ object: string;
44
+ created: number;
45
+ released?: number;
46
+ owned_by: string;
47
+ name: string;
48
+ description?: string;
49
+ context_window?: number;
50
+ max_tokens?: number;
51
+ type: 'language' | 'embedding' | 'image' | 'video' | string;
52
+ tags?: string[];
53
+ pricing?: GatewayModelPricing;
54
+ }
55
+
56
+ interface GatewayModelsResponse {
57
+ object: string;
58
+ data: GatewayModel[];
59
+ }
60
+
61
+ interface PublicGatewayModel {
62
+ cache_read_price_per_token?: number | string | null;
63
+ cache_write_price_per_token?: number | string | null;
64
+ context_window?: number | null;
65
+ description?: string | null;
66
+ id: string;
67
+ image_gen_price?: number | string | null;
68
+ input_price_per_token?: number | string | null;
69
+ input_tiers?: unknown;
70
+ is_enabled?: boolean | null;
71
+ max_tokens?: number | null;
72
+ name?: string | null;
73
+ output_price_per_token?: number | string | null;
74
+ output_tiers?: unknown;
75
+ pricing_raw?: unknown;
76
+ provider?: string | null;
77
+ released_at?: string | null;
78
+ search_price?: number | string | null;
79
+ synced_at?: string | null;
80
+ tags?: string[] | null;
81
+ type?: string | null;
82
+ web_search_price?: number | string | null;
83
+ }
84
+
85
+ interface PublicModelsResponse {
86
+ data?: PublicGatewayModel[];
87
+ pagination?: {
88
+ limit: number;
89
+ page: number;
90
+ total: number;
91
+ };
92
+ }
93
+
94
+ interface SyncResult {
95
+ synced: number;
96
+ new: number;
97
+ updated: number;
98
+ errors: string[];
99
+ }
100
+
101
+ const GATEWAY_URL = 'https://ai-gateway.vercel.sh/v1/models';
102
+ const TUTURUUU_PRODUCTION_PUBLIC_MODELS_URL =
103
+ 'https://tuturuuu.com/api/v1/infrastructure/ai/models';
104
+ const PUBLIC_MODELS_PAGE_SIZE = 100;
105
+ const UPSERT_BATCH_SIZE = 100;
106
+ const SELECT_BATCH_SIZE = 1000;
107
+
108
+ function isImageGenModelId(id: string): boolean {
109
+ return (
110
+ id.startsWith('google/imagen-') || id === 'google/gemini-2.5-flash-image'
111
+ );
112
+ }
113
+
114
+ function toNumber(value: number | string | null | undefined): number {
115
+ if (value === null || value === undefined) return 0;
116
+ const parsed = Number(value);
117
+ return Number.isFinite(parsed) ? parsed : 0;
118
+ }
119
+
120
+ function toNullableNumber(
121
+ value: number | string | null | undefined
122
+ ): number | null {
123
+ if (value === null || value === undefined) return null;
124
+ const parsed = Number(value);
125
+ return Number.isFinite(parsed) ? parsed : null;
126
+ }
127
+
128
+ function toNullableJson(value: unknown): Json | null {
129
+ return value === null || value === undefined ? null : (value as Json);
130
+ }
131
+
132
+ function resolveGatewayModelMaxTokens(
133
+ id: string,
134
+ maxTokens: number | null | undefined
135
+ ): number | null {
136
+ if (id === 'google/gemini-embedding-2') {
137
+ return 3072;
138
+ }
139
+
140
+ return maxTokens ?? null;
141
+ }
142
+
143
+ async function fetchVercelGatewayModels(): Promise<GatewayModel[]> {
144
+ const response = await fetch(GATEWAY_URL);
145
+ if (!response.ok) {
146
+ throw new Error(
147
+ `Failed to fetch gateway models: ${response.status} ${response.statusText}`
148
+ );
149
+ }
150
+
151
+ const json = (await response.json()) as unknown;
152
+
153
+ return Array.isArray(json)
154
+ ? (json as GatewayModel[])
155
+ : ((json as GatewayModelsResponse | undefined)?.data ?? []);
156
+ }
157
+
158
+ async function fetchTuturuuuProductionPublicModels(): Promise<
159
+ PublicGatewayModel[]
160
+ > {
161
+ const models: PublicGatewayModel[] = [];
162
+
163
+ for (let page = 1; page <= 100; page++) {
164
+ const url = new URL(TUTURUUU_PRODUCTION_PUBLIC_MODELS_URL);
165
+ url.searchParams.set('format', 'paginated');
166
+ url.searchParams.set('limit', String(PUBLIC_MODELS_PAGE_SIZE));
167
+ url.searchParams.set('page', String(page));
168
+ url.searchParams.set('type', 'all');
169
+
170
+ const response = await fetch(url);
171
+ if (!response.ok) {
172
+ throw new Error(
173
+ `Failed to fetch Tuturuuu production public models: ${response.status} ${response.statusText}`
174
+ );
175
+ }
176
+
177
+ const json = (await response.json()) as
178
+ | PublicGatewayModel[]
179
+ | PublicModelsResponse;
180
+
181
+ if (Array.isArray(json)) {
182
+ return json;
183
+ }
184
+
185
+ const pageModels = Array.isArray(json.data) ? json.data : [];
186
+ models.push(...pageModels);
187
+
188
+ const total = json.pagination?.total;
189
+ if (
190
+ pageModels.length < PUBLIC_MODELS_PAGE_SIZE ||
191
+ (typeof total === 'number' && models.length >= total)
192
+ ) {
193
+ break;
194
+ }
195
+ }
196
+
197
+ return models;
198
+ }
199
+
200
+ async function fetchExistingModelIds(
201
+ sbAdmin: SupabaseClient
202
+ ): Promise<Set<string>> {
203
+ const existingIdSet = new Set<string>();
204
+ const privateDb = sbAdmin.schema('private');
205
+
206
+ for (let from = 0; ; from += SELECT_BATCH_SIZE) {
207
+ const { data, error } = await privateDb
208
+ .from('ai_gateway_models')
209
+ .select('id')
210
+ .range(from, from + SELECT_BATCH_SIZE - 1);
211
+
212
+ if (error) {
213
+ throw new Error(
214
+ `Failed to fetch existing gateway models: ${error.message}`
215
+ );
216
+ }
217
+
218
+ for (const row of (data ?? []) as Array<{ id: string }>) {
219
+ existingIdSet.add(row.id);
220
+ }
221
+
222
+ if (!data || data.length < SELECT_BATCH_SIZE) {
223
+ break;
224
+ }
225
+ }
226
+
227
+ return existingIdSet;
228
+ }
229
+
230
+ function mapVercelGatewayModel(m: GatewayModel): AIGatewayModelInsert {
231
+ const provider = m.owned_by || m.id.split('/')[0] || 'unknown';
232
+ const modelName = m.id.split('/').slice(1).join('/') || m.id;
233
+
234
+ return {
235
+ id: m.id,
236
+ name: m.name || modelName,
237
+ provider,
238
+ description: m.description || null,
239
+ type: m.type || 'language',
240
+ context_window: m.context_window ?? null,
241
+ max_tokens: resolveGatewayModelMaxTokens(m.id, m.max_tokens),
242
+ tags: m.tags ?? [],
243
+ input_price_per_token: parseFloat(m.pricing?.input ?? '0'),
244
+ output_price_per_token: parseFloat(m.pricing?.output ?? '0'),
245
+ input_tiers: toNullableJson(m.pricing?.input_tiers),
246
+ output_tiers: toNullableJson(m.pricing?.output_tiers),
247
+ cache_read_price_per_token: m.pricing?.input_cache_read
248
+ ? parseFloat(m.pricing.input_cache_read)
249
+ : null,
250
+ cache_write_price_per_token: m.pricing?.input_cache_write
251
+ ? parseFloat(m.pricing.input_cache_write)
252
+ : null,
253
+ web_search_price: m.pricing?.web_search
254
+ ? parseFloat(m.pricing.web_search)
255
+ : null,
256
+ // Use gateway image price when present; fallback for known image models so credit deduction stays correct
257
+ image_gen_price: m.pricing?.image
258
+ ? parseFloat(m.pricing.image)
259
+ : isImageGenModelId(m.id)
260
+ ? 0.0001
261
+ : null,
262
+ released_at: m.released ? new Date(m.released * 1000).toISOString() : null,
263
+ pricing_raw: toNullableJson(m.pricing),
264
+ synced_at: new Date().toISOString(),
265
+ };
266
+ }
267
+
268
+ function mapTuturuuuProductionPublicModel(
269
+ m: PublicGatewayModel
270
+ ): AIGatewayModelInsert {
271
+ const provider = m.provider || m.id.split('/')[0] || 'unknown';
272
+ const modelName = m.id.split('/').slice(1).join('/') || m.id;
273
+
274
+ return {
275
+ id: m.id,
276
+ name: m.name || modelName,
277
+ provider,
278
+ description: m.description ?? null,
279
+ type: m.type || 'language',
280
+ context_window: m.context_window ?? null,
281
+ max_tokens: resolveGatewayModelMaxTokens(m.id, m.max_tokens),
282
+ tags: m.tags ?? [],
283
+ input_price_per_token: toNumber(m.input_price_per_token),
284
+ output_price_per_token: toNumber(m.output_price_per_token),
285
+ input_tiers: toNullableJson(m.input_tiers),
286
+ output_tiers: toNullableJson(m.output_tiers),
287
+ cache_read_price_per_token: toNullableNumber(m.cache_read_price_per_token),
288
+ cache_write_price_per_token: toNullableNumber(
289
+ m.cache_write_price_per_token
290
+ ),
291
+ web_search_price: toNullableNumber(m.web_search_price),
292
+ image_gen_price:
293
+ toNullableNumber(m.image_gen_price) ??
294
+ (isImageGenModelId(m.id) ? 0.0001 : null),
295
+ released_at: m.released_at ?? null,
296
+ pricing_raw: toNullableJson(m.pricing_raw),
297
+ search_price: toNullableNumber(m.search_price),
298
+ synced_at: new Date().toISOString(),
299
+ ...(typeof m.is_enabled === 'boolean' ? { is_enabled: m.is_enabled } : {}),
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Fetches model data from the selected catalog and upserts into ai_gateway_models.
305
+ * Vercel Gateway sync preserves enablement; production-public sync mirrors it
306
+ * when the public catalog includes is_enabled.
307
+ */
308
+ export async function syncGatewayModels(
309
+ sbAdmin: SupabaseClient,
310
+ options: { source?: GatewayModelSyncSource } = {}
311
+ ): Promise<SyncResult> {
312
+ const result: SyncResult = { synced: 0, new: 0, updated: 0, errors: [] };
313
+ const source = options.source ?? 'vercel-gateway';
314
+
315
+ const rows =
316
+ source === 'tuturuuu-production-public'
317
+ ? (await fetchTuturuuuProductionPublicModels()).map(
318
+ mapTuturuuuProductionPublicModel
319
+ )
320
+ : (await fetchVercelGatewayModels()).map(mapVercelGatewayModel);
321
+
322
+ if (rows.length === 0) {
323
+ result.errors.push('No models returned from gateway');
324
+ return result;
325
+ }
326
+
327
+ const existingIdSet = await fetchExistingModelIds(sbAdmin);
328
+ const privateDb = sbAdmin.schema('private');
329
+
330
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH_SIZE) {
331
+ const batch = rows.slice(i, i + UPSERT_BATCH_SIZE);
332
+ const { error } = await privateDb
333
+ .from('ai_gateway_models')
334
+ .upsert(batch, { onConflict: 'id' });
335
+
336
+ if (error) {
337
+ result.errors.push(`Batch ${i / UPSERT_BATCH_SIZE}: ${error.message}`);
338
+ } else {
339
+ for (const row of batch) {
340
+ result.synced++;
341
+ if (existingIdSet.has(row.id)) {
342
+ result.updated++;
343
+ } else {
344
+ result.new++;
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ return result;
351
+ }
@@ -0,0 +1,109 @@
1
+ import type {
2
+ AiFeature,
3
+ CreditErrorCode,
4
+ } from '@tuturuuu/ai/credits/constants';
5
+
6
+ export type { AiFeature, CreditErrorCode };
7
+
8
+ export interface CreditCheckResult {
9
+ allowed: boolean;
10
+ remainingCredits: number;
11
+ tier: string;
12
+ maxOutputTokens: number | null;
13
+ errorCode: CreditErrorCode | null;
14
+ errorMessage: string | null;
15
+ }
16
+
17
+ export interface CreditDeductionResult {
18
+ success: boolean;
19
+ creditsDeducted: number;
20
+ remainingCredits: number;
21
+ errorCode: string | null;
22
+ }
23
+
24
+ export interface CreditReservationResult {
25
+ success: boolean;
26
+ reservationId: string | null;
27
+ remainingCredits: number;
28
+ errorCode: string | null;
29
+ }
30
+
31
+ export interface CreditReservationCommitResult {
32
+ success: boolean;
33
+ creditsDeducted: number;
34
+ remainingCredits: number;
35
+ errorCode: string | null;
36
+ }
37
+
38
+ export interface CreditReservationReleaseResult {
39
+ success: boolean;
40
+ remainingCredits: number;
41
+ errorCode: string | null;
42
+ }
43
+
44
+ export interface MeteredEmbeddingReservationResult {
45
+ success: boolean;
46
+ reservationId: string | null;
47
+ creditsReserved: number;
48
+ costUsd: number;
49
+ remainingCredits: number;
50
+ errorCode: string | null;
51
+ }
52
+
53
+ export interface AiCreditStatus {
54
+ totalAllocated: number;
55
+ totalUsed: number;
56
+ remaining: number;
57
+ bonusCredits: number;
58
+ percentUsed: number;
59
+ periodStart: string;
60
+ periodEnd: string;
61
+ tier: string;
62
+ allowedModels: string[];
63
+ allowedFeatures: string[];
64
+ defaultImageModel: string;
65
+ defaultLanguageModel: string;
66
+ dailyLimit: number | null;
67
+ dailyUsed: number;
68
+ maxOutputTokens: number | null;
69
+ balanceScope: 'user' | 'workspace';
70
+ seatCount: number | null;
71
+ }
72
+
73
+ export interface CreditAllocation {
74
+ tier: string;
75
+ monthlyCredits: number;
76
+ dailyLimit: number | null;
77
+ weeklyLimit: number | null;
78
+ maxCreditsPerRequest: number | null;
79
+ maxOutputTokensPerRequest: number | null;
80
+ markupMultiplier: number;
81
+ allowedModels: string[];
82
+ allowedFeatures: string[];
83
+ defaultImageModel: string;
84
+ defaultLanguageModel: string;
85
+ maxRequestsPerDay: number | null;
86
+ isActive: boolean;
87
+ }
88
+
89
+ export interface FeatureAccess {
90
+ tier: string;
91
+ feature: AiFeature;
92
+ enabled: boolean;
93
+ maxRequestsPerDay: number | null;
94
+ }
95
+
96
+ export interface DeductCreditsParams {
97
+ wsId?: string;
98
+ userId?: string;
99
+ modelId: string;
100
+ inputTokens: number;
101
+ outputTokens: number;
102
+ reasoningTokens?: number;
103
+ imageCount?: number;
104
+ searchCount?: number;
105
+ feature: AiFeature;
106
+ executionId?: string;
107
+ chatMessageId?: string;
108
+ metadata?: Record<string, unknown>;
109
+ }
@@ -0,0 +1,3 @@
1
+ // Client-side hooks for AI credits live in @tuturuuu/ui/hooks/use-ai-credits
2
+ // This file re-exports types only for backward compatibility
3
+ export type { AiCreditStatus } from './types';