@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,90 @@
1
+ import type { SupabaseClient } from '@tuturuuu/supabase';
2
+ import { resolveGatewayModelId } from './model-mapping';
3
+
4
+ /**
5
+ * Query the gateway model's output price per token from the database.
6
+ * Returns the flat output_price_per_token. For tiered pricing, returns
7
+ * the lowest tier cost as a conservative estimate.
8
+ */
9
+ async function getOutputPricePerToken(
10
+ sbAdmin: SupabaseClient,
11
+ modelId: string
12
+ ): Promise<number | null> {
13
+ const gatewayId = resolveGatewayModelId(modelId);
14
+ const privateDb = sbAdmin.schema('private');
15
+
16
+ const { data, error } = await privateDb
17
+ .from('ai_gateway_models')
18
+ .select('output_price_per_token, output_tiers')
19
+ .or(`id.eq.${gatewayId},id.eq.google/${modelId}`)
20
+ .eq('is_enabled', true)
21
+ .limit(1)
22
+ .maybeSingle();
23
+
24
+ if (error || !data) return null;
25
+
26
+ // If tiered pricing exists, use the first (lowest) tier cost
27
+ if (
28
+ data.output_tiers &&
29
+ Array.isArray(data.output_tiers) &&
30
+ data.output_tiers.length > 0
31
+ ) {
32
+ const firstTier = data.output_tiers[0] as { cost?: string };
33
+ if (firstTier?.cost) {
34
+ return parseFloat(firstTier.cost);
35
+ }
36
+ }
37
+
38
+ return data.output_price_per_token ?? null;
39
+ }
40
+
41
+ /**
42
+ * Application-level safety cap: ensure maxOutputTokens won't exceed
43
+ * what the user can afford based on remaining credits and real gateway pricing.
44
+ *
45
+ * This is a defense-in-depth layer — the database function
46
+ * `check_ai_credit_allowance` applies the same cap, but this catches
47
+ * cases where the DB function is stale or fails open.
48
+ *
49
+ * @returns capped maxOutputTokens, or `null` if no output can be afforded
50
+ */
51
+ export async function capMaxOutputTokensByCredits(
52
+ sbAdmin: SupabaseClient,
53
+ modelId: string,
54
+ maxOutputTokens: number | null,
55
+ remainingCredits: number,
56
+ markupMultiplier = 1.0
57
+ ): Promise<number | null> {
58
+ if (remainingCredits <= 0) return null;
59
+
60
+ const outputPricePerToken = await getOutputPricePerToken(sbAdmin, modelId);
61
+ if (!outputPricePerToken || outputPricePerToken <= 0) {
62
+ // Can't determine pricing — fall through to DB-layer cap
63
+ return maxOutputTokens;
64
+ }
65
+
66
+ // credits = (tokens * pricePerToken / 0.0001) * markup
67
+ // → tokens = (credits * 0.0001 / markup) / pricePerToken
68
+ const affordableTokens = Math.floor(
69
+ (remainingCredits * 0.0001) / markupMultiplier / outputPricePerToken
70
+ );
71
+
72
+ if (affordableTokens < 1) return null;
73
+ if (!maxOutputTokens) return affordableTokens;
74
+ return Math.min(maxOutputTokens, affordableTokens);
75
+ }
76
+
77
+ /**
78
+ * Pure computation version for unit testing (no DB dependency).
79
+ * Same formula as the async version, but takes price directly.
80
+ */
81
+ export function computeAffordableTokens(
82
+ remainingCredits: number,
83
+ outputPricePerToken: number,
84
+ markupMultiplier = 1.0
85
+ ): number {
86
+ if (remainingCredits <= 0 || outputPricePerToken <= 0) return 0;
87
+ return Math.floor(
88
+ (remainingCredits * 0.0001) / markupMultiplier / outputPricePerToken
89
+ );
90
+ }
@@ -0,0 +1,232 @@
1
+ import type {
2
+ AiFeature,
3
+ CreditErrorCode,
4
+ } from '@tuturuuu/ai/credits/constants';
5
+ import {
6
+ matchesAllowedModel,
7
+ resolveGatewayModelId,
8
+ } from '@tuturuuu/ai/credits/model-mapping';
9
+ import type {
10
+ CreditCheckResult,
11
+ CreditDeductionResult,
12
+ DeductCreditsParams,
13
+ } from '@tuturuuu/ai/credits/types';
14
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
15
+ import {
16
+ decrementAiCreditChargeInFlight,
17
+ hasAiCreditChargeInFlight,
18
+ incrementAiCreditChargeInFlight,
19
+ invalidateAiCreditSnapshot,
20
+ isAiCreditSnapshotUsable,
21
+ readAiCreditSnapshot,
22
+ } from '@tuturuuu/utils/ai-temp-auth';
23
+
24
+ type DeductAiCreditsRpcRow = {
25
+ success?: boolean;
26
+ credits_deducted?: number | string;
27
+ remaining_credits?: number | string;
28
+ error_code?: string | null;
29
+ };
30
+
31
+ type RpcError = { message?: string } | null;
32
+
33
+ /**
34
+ * Pre-flight check: can this workspace use AI credits for the given model/feature?
35
+ * Returns allowance info including remaining credits and effective maxOutputTokens.
36
+ */
37
+ export async function checkAiCredits(
38
+ wsId: string | undefined,
39
+ modelId: string,
40
+ feature: AiFeature,
41
+ opts?: { userId?: string; estimatedInputTokens?: number }
42
+ ): Promise<CreditCheckResult> {
43
+ if (!wsId) {
44
+ return {
45
+ allowed: false,
46
+ remainingCredits: 0,
47
+ tier: 'FREE',
48
+ maxOutputTokens: null,
49
+ errorCode: 'CREDIT_CHECK_FAILED',
50
+ errorMessage: 'Workspace ID is missing.',
51
+ };
52
+ }
53
+
54
+ const gatewayModelId = resolveGatewayModelId(modelId);
55
+
56
+ if (opts?.userId) {
57
+ const snapshot = await readAiCreditSnapshot({
58
+ wsId,
59
+ userId: opts.userId,
60
+ });
61
+ const inFlight = await hasAiCreditChargeInFlight({
62
+ wsId,
63
+ userId: opts.userId,
64
+ });
65
+ if (
66
+ isAiCreditSnapshotUsable(snapshot, { inFlight }) &&
67
+ (snapshot.allowedFeatures.length === 0 ||
68
+ snapshot.allowedFeatures.includes(feature)) &&
69
+ matchesAllowedModel(gatewayModelId, snapshot.allowedModels)
70
+ ) {
71
+ return {
72
+ allowed: snapshot.remainingCredits > 0,
73
+ remainingCredits: snapshot.remainingCredits,
74
+ tier: snapshot.tier,
75
+ maxOutputTokens: snapshot.maxOutputTokens,
76
+ errorCode: snapshot.remainingCredits > 0 ? null : 'CREDITS_EXHAUSTED',
77
+ errorMessage:
78
+ snapshot.remainingCredits > 0
79
+ ? null
80
+ : 'AI credits exhausted. Please upgrade your plan or purchase more credits.',
81
+ };
82
+ }
83
+ }
84
+
85
+ const sbAdmin = await createAdminClient();
86
+ const { data, error } = await sbAdmin.rpc('check_ai_credit_allowance', {
87
+ p_ws_id: wsId,
88
+ p_model_id: gatewayModelId,
89
+ p_feature: feature,
90
+ ...(opts?.estimatedInputTokens != null
91
+ ? { p_estimated_input_tokens: opts.estimatedInputTokens }
92
+ : {}),
93
+ ...(opts?.userId ? { p_user_id: opts.userId } : {}),
94
+ });
95
+
96
+ if (error) {
97
+ console.error('Error checking AI credits:', error);
98
+ return {
99
+ allowed: false,
100
+ remainingCredits: 0,
101
+ tier: 'FREE',
102
+ maxOutputTokens: null,
103
+ errorCode: 'CREDIT_CHECK_FAILED',
104
+ errorMessage: 'AI credit check failed. Please try again.',
105
+ };
106
+ }
107
+
108
+ const row = Array.isArray(data) ? data[0] : data;
109
+ if (!row) {
110
+ return {
111
+ allowed: false,
112
+ remainingCredits: 0,
113
+ tier: 'FREE',
114
+ maxOutputTokens: null,
115
+ errorCode: 'CREDIT_CHECK_FAILED',
116
+ errorMessage: 'AI credit check returned no result. Please try again.',
117
+ };
118
+ }
119
+
120
+ return {
121
+ allowed: row.allowed ?? true,
122
+ remainingCredits: Number(row.remaining_credits ?? 0),
123
+ tier: row.tier ?? 'FREE',
124
+ maxOutputTokens: row.max_output_tokens ?? null,
125
+ errorCode: (row.error_code as CreditErrorCode) ?? null,
126
+ errorMessage: row.error_message ?? null,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Deduct credits after a successful AI execution.
132
+ * Should be called in the `onFinish` callback or after `generateObject` completes.
133
+ */
134
+ export async function deductAiCredits(
135
+ params: DeductCreditsParams
136
+ ): Promise<CreditDeductionResult> {
137
+ if (!params.wsId) {
138
+ return {
139
+ success: false,
140
+ creditsDeducted: 0,
141
+ remainingCredits: 0,
142
+ errorCode: 'DEDUCTION_FAILED',
143
+ };
144
+ }
145
+
146
+ const wsId = params.wsId;
147
+ const sbAdmin = await createAdminClient();
148
+ const gatewayModelId = resolveGatewayModelId(params.modelId);
149
+ let inFlightMarked = false;
150
+
151
+ if (params.userId) {
152
+ inFlightMarked = await incrementAiCreditChargeInFlight({
153
+ wsId,
154
+ userId: params.userId,
155
+ });
156
+ }
157
+
158
+ let data: DeductAiCreditsRpcRow[] | DeductAiCreditsRpcRow | null = null;
159
+ let error: RpcError = null;
160
+
161
+ try {
162
+ const response = await sbAdmin.rpc('deduct_ai_credits', {
163
+ p_ws_id: wsId,
164
+ p_model_id: gatewayModelId,
165
+ p_input_tokens: params.inputTokens,
166
+ p_output_tokens: params.outputTokens,
167
+ p_reasoning_tokens: params.reasoningTokens ?? 0,
168
+ p_feature: params.feature,
169
+ ...(params.executionId ? { p_execution_id: params.executionId } : {}),
170
+ ...(params.chatMessageId
171
+ ? { p_chat_message_id: params.chatMessageId }
172
+ : {}),
173
+ ...(params.metadata
174
+ ? {
175
+ p_metadata:
176
+ params.metadata as unknown as import('@tuturuuu/types').Json,
177
+ }
178
+ : {}),
179
+ ...(params.userId ? { p_user_id: params.userId } : {}),
180
+ ...(params.imageCount ? { p_image_count: params.imageCount } : {}),
181
+ ...(params.searchCount ? { p_search_count: params.searchCount } : {}),
182
+ });
183
+ data = response.data as
184
+ | DeductAiCreditsRpcRow[]
185
+ | DeductAiCreditsRpcRow
186
+ | null;
187
+ error = response.error as RpcError;
188
+ } finally {
189
+ if (inFlightMarked && params.userId) {
190
+ await decrementAiCreditChargeInFlight({
191
+ wsId,
192
+ userId: params.userId,
193
+ });
194
+ }
195
+ }
196
+
197
+ if (error) {
198
+ console.error('Error deducting AI credits:', error);
199
+ return {
200
+ success: false,
201
+ creditsDeducted: 0,
202
+ remainingCredits: 0,
203
+ errorCode: 'DEDUCTION_FAILED',
204
+ };
205
+ }
206
+
207
+ const row = Array.isArray(data) ? data[0] : data;
208
+ if (!row) {
209
+ return {
210
+ success: false,
211
+ creditsDeducted: 0,
212
+ remainingCredits: 0,
213
+ errorCode: 'NO_RESULT',
214
+ };
215
+ }
216
+
217
+ const result = {
218
+ success: row.success ?? false,
219
+ creditsDeducted: Number(row.credits_deducted ?? 0),
220
+ remainingCredits: Number(row.remaining_credits ?? 0),
221
+ errorCode: row.error_code ?? null,
222
+ };
223
+
224
+ if (result.success && params.wsId && params.userId) {
225
+ await invalidateAiCreditSnapshot({
226
+ wsId,
227
+ userId: params.userId,
228
+ });
229
+ }
230
+
231
+ return result;
232
+ }
@@ -0,0 +1,30 @@
1
+ /** 1 credit = this many USD */
2
+ export const CREDIT_UNIT_USD = 0.0001;
3
+
4
+ /** AI feature slugs used throughout the credit system */
5
+ export const AI_FEATURES = [
6
+ 'chat',
7
+ 'generate',
8
+ 'task_journal',
9
+ 'email_draft',
10
+ 'image_generation',
11
+ 'embeddings',
12
+ ] as const;
13
+
14
+ export type AiFeature = (typeof AI_FEATURES)[number];
15
+
16
+ /** Error codes returned by credit checks */
17
+ export const CREDIT_ERROR_CODES = {
18
+ CREDITS_EXHAUSTED: 'CREDITS_EXHAUSTED',
19
+ MODEL_NOT_ALLOWED: 'MODEL_NOT_ALLOWED',
20
+ MODEL_DISABLED: 'MODEL_DISABLED',
21
+ MODEL_PRICING_UNAVAILABLE: 'MODEL_PRICING_UNAVAILABLE',
22
+ FEATURE_NOT_ALLOWED: 'FEATURE_NOT_ALLOWED',
23
+ DAILY_LIMIT_REACHED: 'DAILY_LIMIT_REACHED',
24
+ NO_ALLOCATION: 'NO_ALLOCATION',
25
+ NO_BALANCE: 'NO_BALANCE',
26
+ CREDIT_CHECK_FAILED: 'CREDIT_CHECK_FAILED',
27
+ } as const;
28
+
29
+ export type CreditErrorCode =
30
+ (typeof CREDIT_ERROR_CODES)[keyof typeof CREDIT_ERROR_CODES];
@@ -0,0 +1,46 @@
1
+ export {
2
+ capMaxOutputTokensByCredits,
3
+ computeAffordableTokens,
4
+ } from './cap-output-tokens';
5
+ export { checkAiCredits, deductAiCredits } from './check-credits';
6
+ export {
7
+ AI_FEATURES,
8
+ type AiFeature,
9
+ CREDIT_ERROR_CODES,
10
+ CREDIT_UNIT_USD,
11
+ type CreditErrorCode,
12
+ } from './constants';
13
+ export {
14
+ matchesAllowedModel,
15
+ resolveGatewayModelId,
16
+ toBareModelName,
17
+ toGatewayModelId,
18
+ } from './model-mapping';
19
+ export {
20
+ commitFixedAiCreditReservation,
21
+ commitMeteredEmbeddingCredits,
22
+ releaseFixedAiCreditReservation,
23
+ releaseMeteredEmbeddingCredits,
24
+ reserveFixedAiCredits,
25
+ reserveMeteredEmbeddingCredits,
26
+ } from './reservations';
27
+ export {
28
+ type EffectivePlanModel,
29
+ type PlanModelCapability,
30
+ PlanModelResolutionError,
31
+ resolvePlanModel,
32
+ selectEffectivePlanModel,
33
+ } from './resolve-plan-model';
34
+ export { syncGatewayModels } from './sync-gateway-models';
35
+ export type {
36
+ AiCreditStatus,
37
+ CreditAllocation,
38
+ CreditCheckResult,
39
+ CreditDeductionResult,
40
+ CreditReservationCommitResult,
41
+ CreditReservationReleaseResult,
42
+ CreditReservationResult,
43
+ DeductCreditsParams,
44
+ FeatureAccess,
45
+ MeteredEmbeddingReservationResult,
46
+ } from './types';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Maps bare model names used in existing code to gateway model IDs.
3
+ * The gateway uses `provider/model-name` format (e.g., `google/gemini-2.5-flash`),
4
+ * while existing code uses bare names (e.g., `gemini-2.5-flash`).
5
+ */
6
+
7
+ export const GEMINI_31_FLASH_LITE_MODEL = 'gemini-3.1-flash-lite';
8
+ export const GEMINI_31_FLASH_LITE_GATEWAY_MODEL =
9
+ 'google/gemini-3.1-flash-lite';
10
+ export const GEMINI_31_FLASH_LITE_PREVIEW_MODEL =
11
+ 'gemini-3.1-flash-lite-preview';
12
+ export const GEMINI_31_FLASH_LITE_PREVIEW_GATEWAY_MODEL =
13
+ 'google/gemini-3.1-flash-lite-preview';
14
+ export const GEMINI_3_FLASH_MODEL = 'gemini-3-flash';
15
+ export const GEMINI_3_FLASH_GATEWAY_MODEL = 'google/gemini-3-flash';
16
+
17
+ /** Normalize retired model aliases to the current stable model ids. */
18
+ export function normalizeStableModelId(modelId: string): string {
19
+ const slashIndex = modelId.indexOf('/');
20
+ const providerPrefix =
21
+ slashIndex === -1 ? '' : `${modelId.slice(0, slashIndex + 1)}`;
22
+ const bareModelId =
23
+ slashIndex === -1 ? modelId : modelId.slice(slashIndex + 1);
24
+
25
+ if (
26
+ bareModelId === GEMINI_31_FLASH_LITE_PREVIEW_MODEL ||
27
+ bareModelId === GEMINI_3_FLASH_MODEL
28
+ ) {
29
+ return `${providerPrefix}${GEMINI_31_FLASH_LITE_MODEL}`;
30
+ }
31
+
32
+ return modelId;
33
+ }
34
+
35
+ /** Convert a bare model name + provider to a gateway model ID */
36
+ export function toGatewayModelId(provider: string, modelName: string): string {
37
+ return `${provider}/${normalizeStableModelId(modelName)}`;
38
+ }
39
+
40
+ /**
41
+ * Resolve a model name to a gateway model ID.
42
+ * If the name already contains '/', it's assumed to be in gateway format.
43
+ * Otherwise, prepend the provider (defaults to 'google').
44
+ */
45
+ export function resolveGatewayModelId(
46
+ modelName: string,
47
+ provider?: string
48
+ ): string {
49
+ const stableModelName = normalizeStableModelId(modelName);
50
+ if (stableModelName.includes('/')) return stableModelName;
51
+ return `${provider || 'google'}/${stableModelName}`;
52
+ }
53
+
54
+ /**
55
+ * Checks whether a model is allowed by an allocation list.
56
+ * Empty lists mean "all models".
57
+ */
58
+ export function matchesAllowedModel(
59
+ modelName: string,
60
+ allowedModels: string[]
61
+ ): boolean {
62
+ if (allowedModels.length === 0) return true;
63
+
64
+ const gatewayModelId = resolveGatewayModelId(modelName);
65
+ const bareModelName = toBareModelName(gatewayModelId);
66
+
67
+ return allowedModels.some((allowedModel) => {
68
+ const normalizedAllowedModel = resolveGatewayModelId(allowedModel);
69
+ return (
70
+ normalizedAllowedModel === gatewayModelId ||
71
+ toBareModelName(normalizedAllowedModel) === bareModelName
72
+ );
73
+ });
74
+ }
75
+
76
+ export function isGoogleModelId(modelId: string): boolean {
77
+ const slashIndex = modelId.indexOf('/');
78
+ if (slashIndex === -1) return true;
79
+
80
+ const provider = modelId.slice(0, slashIndex);
81
+ return provider === 'google' || provider === 'google-vertex';
82
+ }
83
+
84
+ /**
85
+ * Extract the bare model name from a gateway model ID.
86
+ * e.g., 'google/gemini-2.5-flash' → 'gemini-2.5-flash'
87
+ */
88
+ export function toBareModelName(gatewayModelId: string): string {
89
+ const slashIndex = gatewayModelId.indexOf('/');
90
+ if (slashIndex === -1) return gatewayModelId;
91
+ return gatewayModelId.slice(slashIndex + 1);
92
+ }