@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,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
|
+
}
|