@mvriu5/payload-ai 1.3.2 → 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 (32) hide show
  1. package/README.md +76 -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 -0
  9. package/dist/components/Icons.js +15 -0
  10. package/dist/components/action-toast/ActionToast.d.ts +5 -1
  11. package/dist/components/action-toast/ActionToast.js +57 -21
  12. package/dist/components/ai-input/AIInput.d.ts +4 -1
  13. package/dist/components/ai-input/AIInput.js +165 -50
  14. package/dist/components/ai-input/AIInput.module.css +40 -2
  15. package/dist/components/audit-log-list/AuditLogList.js +9 -2
  16. package/dist/components/dashboard/Dashboard.js +3 -1
  17. package/dist/components/hooks/useAIChatStream.d.ts +12 -1
  18. package/dist/components/hooks/useAIChatStream.js +13 -4
  19. package/dist/components/hooks/useAISettings.d.ts +6 -4
  20. package/dist/components/hooks/useAISettings.js +62 -22
  21. package/dist/components/hooks/usePluginConfig.d.ts +8 -20
  22. package/dist/components/hooks/usePluginConfig.js +9 -2
  23. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  24. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  25. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  26. package/dist/exports/client.d.ts +2 -0
  27. package/dist/exports/client.js +2 -0
  28. package/dist/handlers/chatHandler.d.ts +4 -1
  29. package/dist/handlers/chatHandler.js +198 -17
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +92 -5
  32. package/package.json +18 -16
@@ -4,6 +4,7 @@ import { signAIActionProposal } from "../ai/proposalSigning.js";
4
4
  import { isAIProvider } from "../ai/providerOptions.js";
5
5
  import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
6
6
  import { containsSensitiveData } from "../ai/sensitiveData.js";
7
+ import { getExceededTokenUsageLimit, recordTokenUsage } from "../ai/tokenUsage.js";
7
8
  import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
8
9
  import { prepareProposalWriteData } from "../payload/proposalData.js";
9
10
  import { buildPromptWithMentionContext, collectBlocks, describeCollectionLikeConfig, describeCollectionLikeSummary, getAllowedCollectionSlugs, getMentionContext } from "../payload/schemaContext.js";
@@ -719,23 +720,67 @@ export const createChatHandler = (options = {})=>async (req)=>{
719
720
  });
720
721
  }
721
722
  const user = req.user;
722
- const requestedProvider = body?.provider || user.aiProvider || "openai";
723
- if (!isAIProvider(requestedProvider)) return Response.json({
724
- error: `Unsupported AI provider: ${requestedProvider}`
725
- }, {
726
- status: 400
723
+ const exceededTokenUsageLimit = await getExceededTokenUsageLimit({
724
+ maxTokenUsage: options.maxTokenUsage,
725
+ req,
726
+ userID: user.id
727
727
  });
728
- const provider = requestedProvider;
729
- const userApiKey = options.allowUserApiKeys === false ? null : user.aiApiKey;
728
+ if (exceededTokenUsageLimit) {
729
+ const scope = options.maxTokenUsage?.type === "site" ? "site" : "user";
730
+ const periodLabel = exceededTokenUsageLimit.period === "day" ? "Daily" : "Weekly";
731
+ logHandlerEvent(req, "warn", {
732
+ limit: exceededTokenUsageLimit.limit,
733
+ msg: "AI chat blocked: token usage limit reached",
734
+ period: exceededTokenUsageLimit.period,
735
+ scope,
736
+ used: exceededTokenUsageLimit.used,
737
+ userID: String(user.id)
738
+ });
739
+ return Response.json({
740
+ error: `${periodLabel} AI token limit reached for this ${scope}.`,
741
+ limit: exceededTokenUsageLimit.limit,
742
+ period: exceededTokenUsageLimit.period,
743
+ used: exceededTokenUsageLimit.used
744
+ }, {
745
+ status: 429
746
+ });
747
+ }
748
+ const managedProviders = options.providers?.length ? options.providers : null;
749
+ const requestedProvider = body?.provider || (managedProviders ? managedProviders[0].id : user.aiProvider || "openai");
750
+ const managedProvider = managedProviders?.find((providerConfig)=>providerConfig.id === requestedProvider);
751
+ if (managedProviders && !managedProvider) {
752
+ return Response.json({
753
+ error: `Unsupported AI provider: ${requestedProvider}`
754
+ }, {
755
+ status: 400
756
+ });
757
+ }
758
+ if (!managedProvider && !isAIProvider(requestedProvider)) {
759
+ return Response.json({
760
+ error: `Unsupported AI provider: ${requestedProvider}`
761
+ }, {
762
+ status: 400
763
+ });
764
+ }
765
+ const provider = managedProvider?.provider || requestedProvider;
766
+ const requestedModel = body?.model || managedProvider?.defaultModel;
767
+ if (managedProvider && requestedModel && !managedProvider.models.some((model)=>model.value === requestedModel)) {
768
+ return Response.json({
769
+ error: `Unsupported model "${requestedModel}" for AI provider "${managedProvider.id}".`
770
+ }, {
771
+ status: 400
772
+ });
773
+ }
774
+ const userApiKey = managedProvider ? managedProvider.apiKey : options.allowUserApiKeys === false ? null : user.aiApiKey;
730
775
  const providerConfig = getProviderConfig({
731
776
  apiKey: userApiKey,
732
777
  defaultModels: options.models?.defaults,
733
- model: body?.model,
778
+ model: requestedModel,
734
779
  provider
735
780
  });
736
781
  const debug = {
737
782
  model: providerConfig.modelID,
738
- provider,
783
+ provider: managedProvider?.id || provider,
739
784
  tools: [
740
785
  "getDoc",
741
786
  "getGlobal",
@@ -766,7 +811,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
766
811
  selectedLocales
767
812
  });
768
813
  return Response.json({
769
- error: options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
814
+ error: managedProvider ? `Configure a ${managedProvider?.id || provider} API key in the plugin config or server environment first.` : options.allowUserApiKeys === false ? `Configure a ${provider} API key in the server environment first.` : `Add a ${provider} API key to your account settings or configure it in the server environment first.`
770
815
  }, {
771
816
  status: 400
772
817
  });
@@ -819,9 +864,32 @@ export const createChatHandler = (options = {})=>async (req)=>{
819
864
  });
820
865
  return signedProposal;
821
866
  };
822
- const collectionSlugs = getAllowedCollectionSlugs(req, options.collections);
867
+ const requestedDocumentScope = body?.documentScope;
868
+ const configuredCollectionSlugs = getAllowedCollectionSlugs(req, options.collections);
869
+ const configuredCollectionSlugSet = new Set(configuredCollectionSlugs);
870
+ const allGlobalConfigs = req.payload.config.globals || [];
871
+ const requestedCollectionSlug = requestedDocumentScope?.type === "collection" && typeof requestedDocumentScope.collection === "string" ? requestedDocumentScope.collection.trim() : undefined;
872
+ const requestedGlobalSlug = requestedDocumentScope?.type === "global" && typeof requestedDocumentScope.slug === "string" ? requestedDocumentScope.slug.trim() : undefined;
873
+ const requestedDocumentID = requestedDocumentScope?.type === "collection" && typeof requestedDocumentScope.id === "string" ? requestedDocumentScope.id.trim() : undefined;
874
+ if (requestedDocumentScope?.type === "collection" && (!requestedCollectionSlug || !configuredCollectionSlugSet.has(requestedCollectionSlug))) {
875
+ return Response.json({
876
+ error: "The current collection is not available to the AI assistant."
877
+ }, {
878
+ status: 400
879
+ });
880
+ }
881
+ if (requestedDocumentScope?.type === "global" && (!requestedGlobalSlug || !allGlobalConfigs.some((global)=>global.slug === requestedGlobalSlug))) {
882
+ return Response.json({
883
+ error: "The current global is not available to the AI assistant."
884
+ }, {
885
+ status: 400
886
+ });
887
+ }
888
+ const collectionSlugs = requestedDocumentScope ? requestedCollectionSlug ? [
889
+ requestedCollectionSlug
890
+ ] : [] : configuredCollectionSlugs;
823
891
  const collectionSlugSet = new Set(collectionSlugs);
824
- const globalConfigs = req.payload.config.globals || [];
892
+ const globalConfigs = requestedDocumentScope ? requestedGlobalSlug ? allGlobalConfigs.filter((global)=>global.slug === requestedGlobalSlug) : [] : allGlobalConfigs;
825
893
  const globalSlugs = globalConfigs.map((global)=>global.slug);
826
894
  const globalConfigsBySlug = new Map(globalConfigs.map((global)=>[
827
895
  global.slug,
@@ -832,7 +900,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
832
900
  collection.slug,
833
901
  collection
834
902
  ]));
835
- if (collectionSlugs.length === 0) {
903
+ if (collectionSlugs.length === 0 && globalConfigs.length === 0) {
836
904
  logHandlerEvent(req, "warn", {
837
905
  debug,
838
906
  msg: "AI chat blocked: no AI-enabled collections configured"
@@ -861,6 +929,39 @@ export const createChatHandler = (options = {})=>async (req)=>{
861
929
  mentions: body?.mentions,
862
930
  req
863
931
  });
932
+ if (requestedCollectionSlug && requestedDocumentID) {
933
+ const currentDocument = await req.payload.findByID({
934
+ collection: requestedCollectionSlug,
935
+ depth: 2,
936
+ id: requestedDocumentID,
937
+ ...activeLocale ? {
938
+ locale: activeLocale
939
+ } : {},
940
+ overrideAccess: false,
941
+ req
942
+ });
943
+ mentionContext.push({
944
+ collection: requestedCollectionSlug,
945
+ document: currentDocument,
946
+ id: requestedDocumentID,
947
+ type: "currentDocument"
948
+ });
949
+ } else if (requestedGlobalSlug) {
950
+ const currentGlobal = await req.payload.findGlobal({
951
+ depth: 2,
952
+ ...activeLocale ? {
953
+ locale: activeLocale
954
+ } : {},
955
+ overrideAccess: false,
956
+ req,
957
+ slug: requestedGlobalSlug
958
+ });
959
+ mentionContext.push({
960
+ global: currentGlobal,
961
+ slug: requestedGlobalSlug,
962
+ type: "currentGlobal"
963
+ });
964
+ }
864
965
  const mediaAttachmentContext = await getMediaAttachmentContext({
865
966
  allowedCollectionsBySlug,
866
967
  attachments: body?.attachments,
@@ -908,7 +1009,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
908
1009
  prompt
909
1010
  });
910
1011
  const writeIntent = hasWriteIntent(prompt);
911
- const inferredCollectionSlug = mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined;
1012
+ const inferredCollectionSlug = requestedCollectionSlug || (mentionedCollectionSlugs.length === 1 ? mentionedCollectionSlugs[0] : mentionedCollectionSlugs.length === 0 && likelyCollectionMatches.length === 1 ? likelyCollectionMatches[0] : undefined);
912
1013
  const inferredCollectionConfig = inferredCollectionSlug ? allowedCollectionsBySlug.get(inferredCollectionSlug) : undefined;
913
1014
  if (inferredCollectionConfig && !mentionContext.some((item)=>item.type === "collection" && item.slug === inferredCollectionConfig.slug)) {
914
1015
  mentionContext.push({
@@ -920,7 +1021,16 @@ export const createChatHandler = (options = {})=>async (req)=>{
920
1021
  inferredFromPrompt: true
921
1022
  });
922
1023
  }
923
- const intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
1024
+ if (requestedGlobalSlug) {
1025
+ const currentGlobalConfig = globalConfigsBySlug.get(requestedGlobalSlug);
1026
+ if (currentGlobalConfig) {
1027
+ mentionContext.push(describeCollectionLikeConfig({
1028
+ config: currentGlobalConfig,
1029
+ type: "global"
1030
+ }));
1031
+ }
1032
+ }
1033
+ let intentToolChoice = inferredCollectionConfig ? getIntentToolChoice(prompt) : undefined;
924
1034
  logHandlerEvent(req, "info", {
925
1035
  activeLocale,
926
1036
  allowedCollectionCount: allowedCollections.length,
@@ -934,7 +1044,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
934
1044
  selectedLocales,
935
1045
  writeIntent
936
1046
  });
937
- const collectionSlugSchema = z.enum(collectionSlugs);
1047
+ const collectionSlugSchema = collectionSlugs.length > 0 ? z.enum(collectionSlugs) : z.string().refine(()=>false, "No collection is in scope.");
938
1048
  const getDisallowedCollectionActionError = (collection, action)=>{
939
1049
  if (isCollectionActionAllowed({
940
1050
  action,
@@ -948,7 +1058,7 @@ export const createChatHandler = (options = {})=>async (req)=>{
948
1058
  tool: "collectionPermissionCheck"
949
1059
  });
950
1060
  };
951
- const tools = {
1061
+ const allTools = {
952
1062
  getDoc: {
953
1063
  description: "Read a document by collection and id.",
954
1064
  inputSchema: z.object({
@@ -956,6 +1066,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
956
1066
  id: z.string().min(1)
957
1067
  }),
958
1068
  execute: async ({ collection, id })=>{
1069
+ if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
1070
+ return createToolError({
1071
+ collection,
1072
+ message: "Only the current document can be read in this context.",
1073
+ tool: "getDoc"
1074
+ });
1075
+ }
959
1076
  return req.payload.findByID({
960
1077
  collection: collection,
961
1078
  depth: 2,
@@ -1002,6 +1119,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
1002
1119
  slug: z.string().min(1)
1003
1120
  }),
1004
1121
  execute: async ({ slug })=>{
1122
+ if (requestedDocumentScope && slug !== requestedGlobalSlug) {
1123
+ return createToolError({
1124
+ message: "Only the current global can be read in this context.",
1125
+ slug,
1126
+ tool: "getGlobal"
1127
+ });
1128
+ }
1005
1129
  const globalConfig = globalConfigsBySlug.get(slug);
1006
1130
  if (!globalConfig) {
1007
1131
  return createToolError({
@@ -1196,6 +1320,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
1196
1320
  label: z.string().min(1)
1197
1321
  }),
1198
1322
  execute: async ({ collection, id, label })=>{
1323
+ if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
1324
+ return createToolError({
1325
+ collection,
1326
+ message: "Only the current document can be deleted in this context.",
1327
+ tool: "proposeDeleteDoc"
1328
+ });
1329
+ }
1199
1330
  const permissionError = getDisallowedCollectionActionError(collection, "delete");
1200
1331
  if (permissionError) return permissionError;
1201
1332
  const proposal = {
@@ -1222,6 +1353,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
1222
1353
  message: "Either data or localizedData is required."
1223
1354
  }),
1224
1355
  execute: async ({ collection, data, id, label, localizedData })=>{
1356
+ if (requestedDocumentScope && (collection !== requestedCollectionSlug || id !== requestedDocumentID)) {
1357
+ return createToolError({
1358
+ collection,
1359
+ message: "Only the current document can be updated in this context.",
1360
+ tool: "proposeUpdateDoc"
1361
+ });
1362
+ }
1225
1363
  const permissionError = getDisallowedCollectionActionError(collection, "update");
1226
1364
  if (permissionError) return permissionError;
1227
1365
  const collectionConfig = allowedCollectionsBySlug.get(collection);
@@ -1319,6 +1457,13 @@ export const createChatHandler = (options = {})=>async (req)=>{
1319
1457
  message: "Either data or localizedData is required."
1320
1458
  }),
1321
1459
  execute: async ({ data, label, localizedData, slug })=>{
1460
+ if (requestedDocumentScope && slug !== requestedGlobalSlug) {
1461
+ return createToolError({
1462
+ message: "Only the current global can be updated in this context.",
1463
+ slug,
1464
+ tool: "proposeUpdateGlobal"
1465
+ });
1466
+ }
1322
1467
  const globalConfig = globalConfigsBySlug.get(slug);
1323
1468
  if (!globalConfig) {
1324
1469
  return createToolError({
@@ -1404,12 +1549,31 @@ export const createChatHandler = (options = {})=>async (req)=>{
1404
1549
  }
1405
1550
  }
1406
1551
  };
1552
+ const scopedToolNames = requestedCollectionSlug ? requestedDocumentID ? new Set([
1553
+ "getDoc",
1554
+ "listCollections",
1555
+ "proposeUpdateDoc"
1556
+ ]) : new Set([
1557
+ "listCollections",
1558
+ "proposeCreateDoc"
1559
+ ]) : requestedGlobalSlug ? new Set([
1560
+ "getGlobal",
1561
+ "listGlobals",
1562
+ "proposeUpdateGlobal"
1563
+ ]) : null;
1564
+ const tools = scopedToolNames ? Object.fromEntries(Object.entries(allTools).filter(([name])=>scopedToolNames.has(name))) : allTools;
1565
+ if (intentToolChoice && scopedToolNames && !scopedToolNames.has(intentToolChoice.toolName)) {
1566
+ intentToolChoice = undefined;
1567
+ }
1407
1568
  const encoder = new TextEncoder();
1408
1569
  const sendEvent = (controller, event, data)=>{
1409
1570
  controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
1410
1571
  };
1411
1572
  const model = await getModel({
1412
1573
  apiKey: providerConfig.apiKey,
1574
+ ...managedProvider?.baseURL ? {
1575
+ baseURL: managedProvider.baseURL
1576
+ } : {},
1413
1577
  model: providerConfig.modelID,
1414
1578
  provider
1415
1579
  });
@@ -1468,6 +1632,23 @@ export const createChatHandler = (options = {})=>async (req)=>{
1468
1632
  if (part.type === "finish") {
1469
1633
  const finishPart = part;
1470
1634
  usage = finishPart.totalUsage || finishPart.usage || null;
1635
+ if (usage && options.maxTokenUsage) {
1636
+ try {
1637
+ await recordTokenUsage({
1638
+ model: providerConfig.modelID,
1639
+ provider: managedProvider?.id || provider,
1640
+ req,
1641
+ usage,
1642
+ userID: user.id
1643
+ });
1644
+ } catch (err) {
1645
+ req.payload.logger.error({
1646
+ err,
1647
+ msg: "AI token usage could not be recorded",
1648
+ userID: String(user.id)
1649
+ });
1650
+ }
1651
+ }
1471
1652
  const reason = getChatCompletionReason({
1472
1653
  proposalCount: proposals.length,
1473
1654
  toolFailures,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { Config } from "payload";
2
- import { type AIModelConfig } from "./ai/providerOptions.js";
2
+ import { type AIModelConfig, type AIProviderConfig } from "./ai/providerOptions.js";
3
+ import { type MaxTokenUsageOptions } from "./ai/tokenUsage.js";
3
4
  import { type CollectionPermissionMap } from "./payload/collectionPermissions.js";
5
+ export type { AIModelConfig, AIProviderConfig, AIProviderModelOption } from "./ai/providerOptions.js";
6
+ export type { MaxTokenUsageOptions } from "./ai/tokenUsage.js";
4
7
  export type PayloadAIPluginOptions = {
5
8
  allowUserApiKeys?: boolean;
6
9
  collections?: CollectionPermissionMap;
@@ -13,5 +16,7 @@ export type PayloadAIPluginOptions = {
13
16
  maxFileSize?: number;
14
17
  };
15
18
  models?: AIModelConfig;
19
+ maxTokenUsage?: MaxTokenUsageOptions;
20
+ providers?: AIProviderConfig[];
16
21
  };
17
22
  export declare const payloadAiPlugin: (pluginOptions: PayloadAIPluginOptions) => (config: Config) => Config;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { aiProviders, getResolvedAIModelConfig } from "./ai/providerOptions.js";
1
+ import { aiProviders, getResolvedAIModelConfig, resolveAIProviderConfigs, toClientAIProviderProfiles } from "./ai/providerOptions.js";
2
+ import { resolveMaxTokenUsageOptions, tokenUsageCollectionSlug } from "./ai/tokenUsage.js";
2
3
  import { createApplyActionHandler } from "./handlers/applyActionHandler.js";
3
4
  import { createChatHandler } from "./handlers/chatHandler.js";
4
5
  import { createMentionSuggestionHandler } from "./handlers/mentionSuggestionHandler.js";
@@ -137,6 +138,56 @@ const createAIChangesCollection = ()=>({
137
138
  ],
138
139
  timestamps: true
139
140
  });
141
+ const createAITokenUsageCollection = ()=>({
142
+ slug: tokenUsageCollectionSlug,
143
+ access: {
144
+ create: ()=>false,
145
+ delete: ()=>false,
146
+ read: ()=>false,
147
+ update: ()=>false
148
+ },
149
+ admin: {
150
+ hidden: true
151
+ },
152
+ fields: [
153
+ {
154
+ name: "userID",
155
+ type: "text",
156
+ index: true,
157
+ required: true
158
+ },
159
+ {
160
+ name: "provider",
161
+ type: "text",
162
+ required: true
163
+ },
164
+ {
165
+ name: "model",
166
+ type: "text",
167
+ required: true
168
+ },
169
+ {
170
+ name: "inputTokens",
171
+ type: "number"
172
+ },
173
+ {
174
+ name: "outputTokens",
175
+ type: "number"
176
+ },
177
+ {
178
+ name: "totalTokens",
179
+ type: "number",
180
+ required: true
181
+ },
182
+ {
183
+ name: "recordedAt",
184
+ type: "date",
185
+ index: true,
186
+ required: true
187
+ }
188
+ ],
189
+ timestamps: true
190
+ });
140
191
  const addAccountFields = ({ allowUserApiKeys, config })=>{
141
192
  const adminUserSlug = config.admin?.user;
142
193
  if (!adminUserSlug || !config.collections) return;
@@ -163,18 +214,49 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
163
214
  });
164
215
  }
165
216
  };
217
+ const aiField = {
218
+ name: "payloadAi",
219
+ type: "ui",
220
+ admin: {
221
+ components: {
222
+ Field: "@mvriu5/payload-ai/client#AIInput"
223
+ }
224
+ }
225
+ };
226
+ const addAIFieldToDocumentsAndGlobals = (config)=>{
227
+ for (const collection of config.collections || []){
228
+ if (isInternalCollection(collection.slug)) continue;
229
+ if (collection.slug === "payload-ai-auditlog") continue;
230
+ collection.fields = [
231
+ aiField,
232
+ ...collection.fields || []
233
+ ];
234
+ }
235
+ for (const global of config.globals || []){
236
+ global.fields = [
237
+ aiField,
238
+ ...global.fields || []
239
+ ];
240
+ }
241
+ };
166
242
  export const payloadAiPlugin = (pluginOptions)=>(config)=>{
167
243
  const incomingOnInit = config.onInit;
168
244
  const collectionPermissions = resolveCollectionPermissions(pluginOptions.collections);
169
- const allowUserApiKeys = pluginOptions.allowUserApiKeys !== false;
245
+ const providerConfigs = resolveAIProviderConfigs(pluginOptions.providers);
246
+ const managedProviders = providerConfigs.length > 0;
247
+ const allowUserApiKeys = !managedProviders && pluginOptions.allowUserApiKeys !== false;
170
248
  const modelConfig = getResolvedAIModelConfig(pluginOptions.models);
249
+ const maxTokenUsage = resolveMaxTokenUsageOptions(pluginOptions.maxTokenUsage);
171
250
  const mediaUploadOptions = resolveMediaUploadOptions(pluginOptions.media);
172
251
  const maxOutputTokens = typeof pluginOptions.maxOutputTokens === "number" && Number.isFinite(pluginOptions.maxOutputTokens) && pluginOptions.maxOutputTokens > 0 ? Math.floor(pluginOptions.maxOutputTokens) : undefined;
173
252
  if (!config.collections) config.collections = [];
174
253
  if (!config.collections.some((collection)=>collection.slug === "payload-ai-auditlog")) {
175
254
  config.collections.push(createAIChangesCollection());
176
255
  }
177
- addAccountFields({
256
+ if (maxTokenUsage && !config.collections.some((collection)=>collection.slug === tokenUsageCollectionSlug)) {
257
+ config.collections.push(createAITokenUsageCollection());
258
+ }
259
+ if (!managedProviders) addAccountFields({
178
260
  allowUserApiKeys,
179
261
  config
180
262
  });
@@ -194,13 +276,15 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
194
276
  ...config.admin.custom?.payloadAiPlugin || {},
195
277
  collectionSlugs: mentionCollectionSlugs,
196
278
  allowUserApiKeys,
279
+ managedProviders,
197
280
  ...mediaUploadOptions ? {
198
281
  media: {
199
282
  ...mediaUploadOptions,
200
283
  enabled: true
201
284
  }
202
285
  } : {},
203
- models: modelConfig
286
+ models: modelConfig,
287
+ providers: toClientAIProviderProfiles(providerConfigs)
204
288
  }
205
289
  };
206
290
  if (!config.admin.components) config.admin.components = {};
@@ -211,7 +295,9 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
211
295
  allowUserApiKeys,
212
296
  collections: collectionPermissions,
213
297
  maxOutputTokens,
214
- models: modelConfig
298
+ maxTokenUsage,
299
+ models: modelConfig,
300
+ providers: providerConfigs
215
301
  }),
216
302
  method: "post",
217
303
  path: "/ai-chat"
@@ -257,5 +343,6 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
257
343
  await incomingOnInit(payload);
258
344
  };
259
345
  }
346
+ addAIFieldToDocumentsAndGlobals(config);
260
347
  return config;
261
348
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mvriu5/payload-ai",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "AI assistant plugin for Payload CMS with provider selection, CMS mentions, and signed action proposals.",
5
5
  "keywords": [
6
6
  "payload",
@@ -68,16 +68,17 @@
68
68
  "devDependencies": {
69
69
  "@ai-sdk/anthropic": "^3.0.81",
70
70
  "@ai-sdk/google": "^3.0.80",
71
- "@ai-sdk/mistral": "^3.0.37",
71
+ "@ai-sdk/mistral": "^4.0.13",
72
72
  "@ai-sdk/openai": "^3.0.67",
73
- "@openrouter/ai-sdk-provider": "^2.9.1",
74
- "@payloadcms/db-postgres": "3.84.1",
75
- "@payloadcms/next": "3.84.1",
76
- "@payloadcms/richtext-lexical": "3.84.1",
77
- "@payloadcms/ui": "3.84.1",
73
+ "@floating-ui/react": "0.27.20",
74
+ "@openrouter/ai-sdk-provider": "^3.0.0",
75
+ "@payloadcms/db-postgres": "3.86.0",
76
+ "@payloadcms/next": "3.86.0",
77
+ "@payloadcms/richtext-lexical": "3.86.0",
78
+ "@payloadcms/ui": "3.86.0",
78
79
  "@playwright/test": "^1.56.1",
79
80
  "@swc/cli": "0.8.1",
80
- "@types/node": "25.9.3",
81
+ "@types/node": "26.1.1",
81
82
  "@types/react": "19.2.17",
82
83
  "@types/react-dom": "19.2.3",
83
84
  "copyfiles": "2.4.1",
@@ -86,22 +87,23 @@
86
87
  "graphql": "^17.0.1",
87
88
  "jsdom": "^29.1.1",
88
89
  "knip": "^6.18.0",
89
- "next": "16.2.9",
90
- "payload": "3.84.1",
90
+ "next": "16.2.10",
91
+ "payload": "3.86.0",
91
92
  "prettier": "^3.8.4",
92
93
  "react": "19.2.7",
93
94
  "react-dom": "19.2.7",
94
95
  "rimraf": "6.1.3",
95
- "sharp": "0.35.1",
96
- "typescript": "5.9.3",
97
- "vitest": "4.1.9"
96
+ "sharp": "0.35.3",
97
+ "typescript": "6.0.3",
98
+ "vitest": "4.1.10"
98
99
  },
99
100
  "peerDependencies": {
100
101
  "@ai-sdk/anthropic": "^3.0.81",
101
102
  "@ai-sdk/google": "^3.0.80",
102
- "@ai-sdk/mistral": "^3.0.37",
103
+ "@ai-sdk/mistral": "^4.0.13",
103
104
  "@ai-sdk/openai": "^3.0.67",
104
- "@openrouter/ai-sdk-provider": "^2.9.1",
105
+ "@openrouter/ai-sdk-provider": "^3.0.0",
106
+ "@payloadcms/ui": "^3.84.1",
105
107
  "payload": "^3.84.1"
106
108
  },
107
109
  "peerDependenciesMeta": {
@@ -147,7 +149,7 @@
147
149
  ]
148
150
  },
149
151
  "dependencies": {
150
- "ai": "^6.0.193",
152
+ "ai": "^7.0.31",
151
153
  "zod": "^4.4.3"
152
154
  }
153
155
  }