@mvriu5/payload-ai 1.2.0 → 1.4.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.
Files changed (63) hide show
  1. package/README.md +118 -13
  2. package/dist/ai/providerOptions.d.ts +24 -1
  3. package/dist/ai/providerOptions.js +81 -0
  4. package/dist/ai/providerRuntime.d.ts +2 -1
  5. package/dist/ai/providerRuntime.js +5 -2
  6. package/dist/ai/tokenUsage.d.ts +38 -0
  7. package/dist/ai/tokenUsage.js +106 -0
  8. package/dist/components/Icons.d.ts +1 -8
  9. package/dist/components/Icons.js +10 -207
  10. package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +5 -2
  11. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +81 -54
  12. package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
  13. package/dist/components/ai-input/AIInput.d.ts +5 -0
  14. package/dist/components/ai-input/AIInput.js +548 -0
  15. package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +150 -44
  16. package/dist/components/ai-input/badge.d.ts +14 -0
  17. package/dist/components/ai-input/badge.js +85 -0
  18. package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
  19. package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +56 -21
  20. package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
  21. package/dist/components/dashboard/Dashboard.d.ts +2 -0
  22. package/dist/components/dashboard/Dashboard.js +17 -0
  23. package/dist/components/dashboard/Dashboard.module.css +13 -0
  24. package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
  25. package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
  26. package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
  27. package/dist/components/hooks/useAIChatStream.d.ts +50 -0
  28. package/dist/components/hooks/useAIChatStream.js +226 -0
  29. package/dist/components/hooks/useAISettings.d.ts +6 -4
  30. package/dist/components/hooks/useAISettings.js +68 -27
  31. package/dist/components/hooks/useAuditLog.d.ts +11 -0
  32. package/dist/components/hooks/useAuditLog.js +41 -0
  33. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
  34. package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
  35. package/dist/components/hooks/useMentions.d.ts +58 -0
  36. package/dist/components/hooks/useMentions.js +276 -0
  37. package/dist/components/hooks/usePluginConfig.d.ts +40 -0
  38. package/dist/components/hooks/usePluginConfig.js +27 -0
  39. package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
  40. package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
  41. package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
  42. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  43. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  44. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  45. package/dist/exports/client.d.ts +4 -2
  46. package/dist/exports/client.js +4 -2
  47. package/dist/handlers/applyActionHandler.js +27 -7
  48. package/dist/handlers/chatHandler.d.ts +4 -1
  49. package/dist/handlers/chatHandler.js +499 -74
  50. package/dist/handlers/mediaUploadHandler.d.ts +7 -0
  51. package/dist/handlers/mediaUploadHandler.js +99 -0
  52. package/dist/handlers/mentionSuggestionHandler.js +16 -14
  53. package/dist/handlers/proposalDiffHandler.js +41 -29
  54. package/dist/index.d.ts +12 -1
  55. package/dist/index.js +131 -12
  56. package/dist/payload/collectionPermissions.js +3 -1
  57. package/dist/payload/normalizeData.d.ts +0 -6
  58. package/dist/payload/normalizeData.js +25 -13
  59. package/dist/payload/proposalData.js +4 -1
  60. package/dist/payload/schemaContext.js +98 -43
  61. package/package.json +22 -20
  62. package/dist/components/AIInput.d.ts +0 -2
  63. package/dist/components/AIInput.js +0 -896
package/README.md CHANGED
@@ -39,21 +39,27 @@ export default buildConfig({
39
39
  });
40
40
  ```
41
41
 
42
- The plugin adds two fields to the configured Payload admin user collection:
42
+ Without centrally configured providers, the plugin adds two fields to the configured Payload admin user collection:
43
43
 
44
44
  - `aiProvider`
45
45
  - `aiApiKey`
46
46
 
47
47
  Users can select their provider and optionally store their own API key in account settings. If no account-level key is set, the chat endpoint uses provider environment variables.
48
48
 
49
+ When `providers` is configured, provider selection and API keys are managed centrally. The plugin does not add either field to the user collection. All configured provider models are grouped by provider in the AI input.
50
+
49
51
  ## Options
50
52
 
51
53
  ```ts
52
- import type { PayloadAiPluginOptions } from "@mvriu5/payload-ai";
54
+ import type { PayloadAIPluginOptions } from "@mvriu5/payload-ai";
53
55
 
54
- const options: PayloadAiPluginOptions = {
56
+ const options: PayloadAIPluginOptions = {
55
57
  allowUserApiKeys: false,
56
58
  collections: {
59
+ media: {
60
+ read: true,
61
+ update: true,
62
+ },
57
63
  posts: {
58
64
  read: true,
59
65
  create: true,
@@ -62,22 +68,47 @@ const options: PayloadAiPluginOptions = {
62
68
  },
63
69
  users: true,
64
70
  },
65
- models: {
66
- defaults: {
67
- openai: "gpt-4.1-mini",
68
- openrouter: "openai/gpt-oss-120b",
69
- },
70
- providers: {
71
- openai: [
71
+ media: {
72
+ enabled: true,
73
+ collectionSlug: "media",
74
+ acceptedMimeTypes: ["image/*"],
75
+ maxFileSize: 10 * 1024 * 1024,
76
+ },
77
+ providers: [
78
+ {
79
+ id: "company-openai",
80
+ label: "Company OpenAI",
81
+ provider: "openai",
82
+ apiKey: process.env.COMPANY_OPENAI_API_KEY,
83
+ models: [
72
84
  { label: "GPT-4.1 Mini", value: "gpt-4.1-mini" },
73
85
  { label: "GPT-4.1", value: "gpt-4.1" },
74
86
  ],
87
+ defaultModel: "gpt-4.1-mini",
75
88
  },
76
- },
89
+ {
90
+ id: "ollama",
91
+ label: "Local Ollama",
92
+ provider: "openai",
93
+ baseURL: "http://localhost:11434/v1",
94
+ apiKey: "ollama",
95
+ models: [
96
+ { label: "Llama 3.3", value: "llama3.3" },
97
+ { label: "Qwen 3", value: "qwen3" },
98
+ ],
99
+ },
100
+ ],
77
101
  maxOutputTokens: 1200,
102
+ maxTokenUsage: {
103
+ type: "user",
104
+ perDay: 50_000,
105
+ perWeek: 250_000,
106
+ },
78
107
  };
79
108
  ```
80
109
 
110
+ `models` configures model choices for the user-selected provider mode. Use `providers` instead when provider selection, credentials, and endpoints should be managed centrally.
111
+
81
112
  ### `collections`
82
113
 
83
114
  Restricts AI read and write proposals to enabled collection slugs. If omitted, all non-internal Payload collections are available.
@@ -124,6 +155,38 @@ payloadAiPlugin({
124
155
 
125
156
  `read` controls schema/context access, document search, and mentions. `create`, `update`, and `delete` control AI action proposals and server-side apply permissions.
126
157
 
158
+ ### `media`
159
+
160
+ Enables media uploads from the AI assistant. Uploaded files are created through the configured Payload upload collection and then passed to the chat endpoint as media attachments.
161
+
162
+ ```ts
163
+ payloadAiPlugin({
164
+ collections: {
165
+ media: {
166
+ read: true,
167
+ update: true,
168
+ },
169
+ posts: true,
170
+ },
171
+ media: {
172
+ enabled: true,
173
+ collectionSlug: "media",
174
+ acceptedMimeTypes: ["image/*"],
175
+ maxFileSize: 10 * 1024 * 1024,
176
+ },
177
+ })
178
+ ```
179
+
180
+ `collectionSlug` defaults to `"media"`. The target collection must be configured with Payload `upload` support.
181
+
182
+ `acceptedMimeTypes` accepts exact MIME types such as `"image/png"` and wildcard groups such as `"image/*"`. If omitted, all file types accepted by the upload collection can be sent to the endpoint.
183
+
184
+ `maxFileSize` is checked before creating the upload document. The value is in bytes.
185
+
186
+ If you want the AI to use uploaded files in other documents, enable `read` on the media collection so the chat endpoint can validate and inspect the uploaded media document. If the media collection has editable fields such as `alt`, `caption`, or `credit` and you want the AI to fill them, also enable `update` for that collection.
187
+
188
+ Upload references in AI proposals are restricted to the uploaded attachments for the current request. This prevents the model from inventing arbitrary media IDs for upload fields.
189
+
127
190
  ### `models`
128
191
 
129
192
  Overrides the model list shown in the admin UI and the default model per provider.
@@ -138,6 +201,27 @@ OpenRouter includes these built-in model options:
138
201
  - `anthropic/claude-3.5-sonnet`
139
202
  - `google/gemini-2.0-flash-001`
140
203
 
204
+ ### `providers`
205
+
206
+ Enables centrally managed provider profiles. Each profile supports:
207
+
208
+ - `id`: unique provider profile identifier
209
+ - `label`: provider group label shown in the model select
210
+ - `provider`: SDK adapter (`openai`, `openrouter`, `claude`, `mistral`, or `google`)
211
+ - `models`: allowed model list
212
+ - `defaultModel`: optional default; otherwise the first configured model is used
213
+ - `baseURL`: optional custom provider URL
214
+ - `apiKey`: optional server-side API key
215
+
216
+ When at least one profile is configured:
217
+
218
+ - `aiProvider` and `aiApiKey` are not added to the admin user collection.
219
+ - Models from every configured profile are displayed in provider groups.
220
+ - The chat endpoint validates both provider IDs and model IDs against this configuration.
221
+ - User-level provider and API key values are ignored.
222
+
223
+ `apiKey` and `baseURL` remain server-side and are not included in Payload's public admin configuration. Custom endpoints must implement the protocol expected by their selected SDK adapter. For Ollama, vLLM, and similar APIs, use the `openai` adapter with an OpenAI-compatible `/v1` endpoint.
224
+
141
225
  ### `maxOutputTokens`
142
226
 
143
227
  Controls the maximum number of output tokens the chat endpoint may generate per request. If omitted, the plugin uses `700`.
@@ -148,6 +232,24 @@ payloadAiPlugin({
148
232
  })
149
233
  ```
150
234
 
235
+ ### `maxTokenUsage`
236
+
237
+ Limits total AI tokens across rolling 24-hour and 7-day windows.
238
+
239
+ ```ts
240
+ payloadAiPlugin({
241
+ maxTokenUsage: {
242
+ type: "user",
243
+ perDay: 50_000,
244
+ perWeek: 250_000,
245
+ },
246
+ })
247
+ ```
248
+
249
+ Use `type: "user"` to enforce separate budgets per authenticated user, or `type: "site"` to share one budget across the entire Payload installation. `perDay` and `perWeek` are optional individually, but at least one must be configured.
250
+
251
+ Completed model usage is stored in the hidden `payload-ai-usage` collection. Requests made after a limit is reached return HTTP `429`. Because providers report token usage after completion, the request that crosses a limit is allowed to finish and subsequent requests are blocked.
252
+
151
253
  ### `allowUserApiKeys`
152
254
 
153
255
  Controls whether the plugin adds an `aiApiKey` field to the admin user collection.
@@ -160,6 +262,8 @@ payloadAiPlugin({
160
262
 
161
263
  When disabled, users can still select an AI provider, but API keys must come from environment variables.
162
264
 
265
+ This option only applies when `providers` is not configured. Managed provider mode never adds user-level AI settings.
266
+
163
267
  ### `disabled`
164
268
 
165
269
  Disables endpoint and UI registration while keeping the plugin call in your config.
@@ -171,8 +275,9 @@ The package lazy-loads provider SDKs at runtime. If a user selects `claude`, `go
171
275
 
172
276
  API key priority is:
173
277
 
174
- 1. account-level API key, unless `allowUserApiKeys: false`
175
- 2. provider environment variables
278
+ 1. managed provider `apiKey`, when `providers` is configured
279
+ 2. account-level API key, unless `allowUserApiKeys: false`
280
+ 3. provider environment variables
176
281
 
177
282
  - `OPENAI_API_KEY`, `OPENAI_MODEL`
178
283
  - `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
@@ -1,4 +1,4 @@
1
- type AIProviderModelOption = {
1
+ export type AIProviderModelOption = {
2
2
  label: string;
3
3
  value: string;
4
4
  };
@@ -71,6 +71,26 @@ declare const aiProviderModels: {
71
71
  };
72
72
  export type AIProvider = keyof typeof aiProviderModels;
73
73
  type AIProviderModels = Record<AIProvider, AIProviderModelOption[]>;
74
+ export type AIProviderConfig = {
75
+ apiKey?: string;
76
+ baseURL?: string;
77
+ defaultModel?: string;
78
+ id: string;
79
+ label: string;
80
+ models: AIProviderModelOption[];
81
+ provider: AIProvider;
82
+ };
83
+ export type AIProviderProfile = {
84
+ defaultModel: string;
85
+ id: string;
86
+ label: string;
87
+ models: AIProviderModelOption[];
88
+ provider: AIProvider;
89
+ };
90
+ export type ResolvedAIProviderConfig = AIProviderProfile & {
91
+ apiKey?: string;
92
+ baseURL?: string;
93
+ };
74
94
  export declare const aiProviders: {
75
95
  label: string;
76
96
  value: AIProvider;
@@ -85,4 +105,7 @@ export declare const getResolvedAIModelConfig: (modelConfig?: AIModelConfig) =>
85
105
  providers: AIProviderModels;
86
106
  };
87
107
  export declare const isAIProvider: (provider: string) => provider is AIProvider;
108
+ export declare const getLegacyAIProviderProfiles: (modelConfig?: AIModelConfig) => AIProviderProfile[];
109
+ export declare const resolveAIProviderConfigs: (providers?: AIProviderConfig[]) => ResolvedAIProviderConfig[];
110
+ export declare const toClientAIProviderProfiles: (providers: ResolvedAIProviderConfig[]) => AIProviderProfile[];
88
111
  export {};
@@ -144,3 +144,84 @@ export const getResolvedAIModelConfig = (modelConfig)=>{
144
144
  };
145
145
  };
146
146
  export const isAIProvider = (provider)=>provider in aiProviderModels;
147
+ export const getLegacyAIProviderProfiles = (modelConfig)=>{
148
+ const resolvedModels = getResolvedAIModelConfig(modelConfig);
149
+ return aiProviders.map(({ label, value })=>({
150
+ defaultModel: resolvedModels.defaults[value],
151
+ id: value,
152
+ label,
153
+ models: resolvedModels.providers[value],
154
+ provider: value
155
+ }));
156
+ };
157
+ export const resolveAIProviderConfigs = (providers)=>{
158
+ if (!providers?.length) return [];
159
+ const providerIDs = new Set();
160
+ return providers.map((providerConfig, index)=>{
161
+ const path = `providers[${index}]`;
162
+ const id = providerConfig.id.trim();
163
+ const label = providerConfig.label.trim();
164
+ if (!id || !/^[a-z0-9][a-z0-9_-]*$/i.test(id)) {
165
+ throw new Error(`${path}.id must contain only letters, numbers, hyphens, or underscores.`);
166
+ }
167
+ if (providerIDs.has(id)) throw new Error(`Duplicate AI provider id: ${id}`);
168
+ providerIDs.add(id);
169
+ if (!label) throw new Error(`${path}.label is required.`);
170
+ if (!isAIProvider(providerConfig.provider)) {
171
+ throw new Error(`${path}.provider is unsupported: ${String(providerConfig.provider)}`);
172
+ }
173
+ if (!providerConfig.models.length) throw new Error(`${path}.models must contain at least one model.`);
174
+ const modelValues = new Set();
175
+ const models = providerConfig.models.map((model, modelIndex)=>{
176
+ const modelPath = `${path}.models[${modelIndex}]`;
177
+ const modelLabel = model.label.trim();
178
+ const value = model.value.trim();
179
+ if (!modelLabel) throw new Error(`${modelPath}.label is required.`);
180
+ if (!value) throw new Error(`${modelPath}.value is required.`);
181
+ if (modelValues.has(value)) throw new Error(`Duplicate model value "${value}" in AI provider "${id}".`);
182
+ modelValues.add(value);
183
+ return {
184
+ label: modelLabel,
185
+ value
186
+ };
187
+ });
188
+ const defaultModel = providerConfig.defaultModel?.trim() || models[0].value;
189
+ if (!modelValues.has(defaultModel)) {
190
+ throw new Error(`${path}.defaultModel must match a configured model value.`);
191
+ }
192
+ if (providerConfig.baseURL) {
193
+ let parsedURL;
194
+ try {
195
+ parsedURL = new URL(providerConfig.baseURL);
196
+ } catch {
197
+ throw new Error(`${path}.baseURL must be a valid URL.`);
198
+ }
199
+ if (![
200
+ "http:",
201
+ "https:"
202
+ ].includes(parsedURL.protocol)) {
203
+ throw new Error(`${path}.baseURL must use http or https.`);
204
+ }
205
+ }
206
+ return {
207
+ ...providerConfig.apiKey ? {
208
+ apiKey: providerConfig.apiKey
209
+ } : {},
210
+ ...providerConfig.baseURL ? {
211
+ baseURL: providerConfig.baseURL
212
+ } : {},
213
+ defaultModel,
214
+ id,
215
+ label,
216
+ models,
217
+ provider: providerConfig.provider
218
+ };
219
+ });
220
+ };
221
+ export const toClientAIProviderProfiles = (providers)=>providers.map(({ defaultModel, id, label, models, provider })=>({
222
+ defaultModel,
223
+ id,
224
+ label,
225
+ models,
226
+ provider
227
+ }));
@@ -8,6 +8,7 @@ type ProviderConfig = {
8
8
  };
9
9
  type ModelConfig = {
10
10
  apiKey: string;
11
+ baseURL?: string;
11
12
  model: string;
12
13
  provider: AIProvider;
13
14
  };
@@ -15,5 +16,5 @@ export declare const getProviderConfig: ({ apiKey, defaultModels, model, provide
15
16
  apiKey: string | undefined;
16
17
  modelID: string;
17
18
  };
18
- export declare const getModel: ({ apiKey, model, provider }: ModelConfig) => Promise<LanguageModel>;
19
+ export declare const getModel: ({ apiKey, baseURL, model, provider }: ModelConfig) => Promise<LanguageModel>;
19
20
  export {};
@@ -33,9 +33,12 @@ export const getProviderConfig = ({ apiKey, defaultModels, model, provider })=>{
33
33
  const getMissingProviderDependencyError = (packageName, provider)=>{
34
34
  return new Error(`Missing optional dependency ${packageName}. Install it to use the ${provider} provider.`);
35
35
  };
36
- export const getModel = async ({ apiKey, model, provider })=>{
36
+ export const getModel = async ({ apiKey, baseURL, model, provider })=>{
37
37
  const providerOptions = {
38
- apiKey
38
+ apiKey,
39
+ ...baseURL ? {
40
+ baseURL
41
+ } : {}
39
42
  };
40
43
  if (provider === "claude") {
41
44
  try {
@@ -0,0 +1,38 @@
1
+ import type { PayloadHandler } from "payload";
2
+ export type MaxTokenUsageOptions = {
3
+ perDay?: number;
4
+ perWeek?: number;
5
+ type: "site" | "user";
6
+ };
7
+ export type ResolvedMaxTokenUsageOptions = {
8
+ perDay?: number;
9
+ perWeek?: number;
10
+ type: "site" | "user";
11
+ };
12
+ export type TokenUsageData = {
13
+ inputTokens?: number;
14
+ outputTokens?: number;
15
+ totalTokens?: number;
16
+ };
17
+ type TokenUsageLimit = {
18
+ limit: number;
19
+ period: "day" | "week";
20
+ used: number;
21
+ };
22
+ export declare const tokenUsageCollectionSlug = "payload-ai-usage";
23
+ export declare const resolveMaxTokenUsageOptions: (options?: MaxTokenUsageOptions) => ResolvedMaxTokenUsageOptions | undefined;
24
+ export declare const getExceededTokenUsageLimit: ({ maxTokenUsage, now, req, userID, }: {
25
+ maxTokenUsage?: ResolvedMaxTokenUsageOptions;
26
+ now?: Date;
27
+ req: Parameters<PayloadHandler>[0];
28
+ userID: number | string;
29
+ }) => Promise<TokenUsageLimit | null>;
30
+ export declare const recordTokenUsage: ({ model, now, provider, req, usage, userID, }: {
31
+ model: string;
32
+ now?: Date;
33
+ provider: string;
34
+ req: Parameters<PayloadHandler>[0];
35
+ usage: TokenUsageData;
36
+ userID: number | string;
37
+ }) => Promise<void>;
38
+ export {};
@@ -0,0 +1,106 @@
1
+ export const tokenUsageCollectionSlug = "payload-ai-usage";
2
+ const dayInMilliseconds = 24 * 60 * 60 * 1000;
3
+ const weekInMilliseconds = 7 * dayInMilliseconds;
4
+ const resolvePositiveInteger = (value, path)=>{
5
+ if (value === undefined) return undefined;
6
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${path} must be a positive number.`);
7
+ return Math.floor(value);
8
+ };
9
+ export const resolveMaxTokenUsageOptions = (options)=>{
10
+ if (!options) return undefined;
11
+ if (![
12
+ "site",
13
+ "user"
14
+ ].includes(options.type)) throw new Error('maxTokenUsage.type must be either "user" or "site".');
15
+ const perDay = resolvePositiveInteger(options.perDay, "maxTokenUsage.perDay");
16
+ const perWeek = resolvePositiveInteger(options.perWeek, "maxTokenUsage.perWeek");
17
+ if (!perDay && !perWeek) throw new Error("maxTokenUsage must configure perDay, perWeek, or both.");
18
+ return {
19
+ ...perDay ? {
20
+ perDay
21
+ } : {},
22
+ ...perWeek ? {
23
+ perWeek
24
+ } : {},
25
+ type: options.type
26
+ };
27
+ };
28
+ const normalizeTokenCount = (value)=>typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
29
+ const getTokenCount = (usage)=>typeof usage.totalTokens === "number" ? normalizeTokenCount(usage.totalTokens) : normalizeTokenCount(usage.inputTokens) + normalizeTokenCount(usage.outputTokens);
30
+ export const getExceededTokenUsageLimit = async ({ maxTokenUsage, now = new Date(), req, userID })=>{
31
+ if (!maxTokenUsage) return null;
32
+ const dayStart = new Date(now.getTime() - dayInMilliseconds);
33
+ const weekStart = new Date(now.getTime() - weekInMilliseconds);
34
+ const queryStart = maxTokenUsage.perWeek ? weekStart : dayStart;
35
+ let page = 1;
36
+ let usagePerDay = 0;
37
+ let usagePerWeek = 0;
38
+ while(true){
39
+ const result = await req.payload.find({
40
+ collection: tokenUsageCollectionSlug,
41
+ depth: 0,
42
+ limit: 500,
43
+ overrideAccess: true,
44
+ page,
45
+ req,
46
+ where: {
47
+ and: [
48
+ {
49
+ recordedAt: {
50
+ greater_than_equal: queryStart.toISOString()
51
+ }
52
+ },
53
+ ...maxTokenUsage.type === "user" ? [
54
+ {
55
+ userID: {
56
+ equals: String(userID)
57
+ }
58
+ }
59
+ ] : []
60
+ ]
61
+ }
62
+ });
63
+ for (const document of result.docs || []){
64
+ const tokenCount = getTokenCount(document);
65
+ usagePerWeek += tokenCount;
66
+ if (document.recordedAt && new Date(document.recordedAt).getTime() >= dayStart.getTime()) {
67
+ usagePerDay += tokenCount;
68
+ }
69
+ }
70
+ if (!result.hasNextPage || !result.nextPage) break;
71
+ page = result.nextPage;
72
+ }
73
+ if (maxTokenUsage.perDay && usagePerDay >= maxTokenUsage.perDay) {
74
+ return {
75
+ limit: maxTokenUsage.perDay,
76
+ period: "day",
77
+ used: usagePerDay
78
+ };
79
+ }
80
+ if (maxTokenUsage.perWeek && usagePerWeek >= maxTokenUsage.perWeek) {
81
+ return {
82
+ limit: maxTokenUsage.perWeek,
83
+ period: "week",
84
+ used: usagePerWeek
85
+ };
86
+ }
87
+ return null;
88
+ };
89
+ export const recordTokenUsage = async ({ model, now = new Date(), provider, req, usage, userID })=>{
90
+ const totalTokens = getTokenCount(usage);
91
+ if (!totalTokens) return;
92
+ await req.payload.create({
93
+ collection: tokenUsageCollectionSlug,
94
+ data: {
95
+ inputTokens: typeof usage.inputTokens === "number" ? normalizeTokenCount(usage.inputTokens) : undefined,
96
+ model,
97
+ outputTokens: typeof usage.outputTokens === "number" ? normalizeTokenCount(usage.outputTokens) : undefined,
98
+ provider,
99
+ recordedAt: now.toISOString(),
100
+ totalTokens,
101
+ userID: String(userID)
102
+ },
103
+ overrideAccess: true,
104
+ req
105
+ });
106
+ };
@@ -4,11 +4,4 @@ export declare function OpenaiIcon(props: SVGProps<SVGSVGElement>): import("reac
4
4
  export declare function ClaudeIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
5
5
  export declare function GoogleGeminiIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
6
6
  export declare function OpenrouterIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
7
- export declare function FileDiff(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
8
- export declare function Send(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
9
- export declare function Apply(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
10
- export declare function Reject(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
11
- export declare function Collection(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
12
- export declare function Document(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
13
- export declare function Global(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
14
- export declare function Block(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
7
+ export declare function PaperclipIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;