@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.
- package/README.md +76 -13
- package/dist/ai/providerOptions.d.ts +24 -1
- package/dist/ai/providerOptions.js +81 -0
- package/dist/ai/providerRuntime.d.ts +2 -1
- package/dist/ai/providerRuntime.js +5 -2
- package/dist/ai/tokenUsage.d.ts +38 -0
- package/dist/ai/tokenUsage.js +106 -0
- package/dist/components/Icons.d.ts +1 -0
- package/dist/components/Icons.js +15 -0
- package/dist/components/action-toast/ActionToast.d.ts +5 -1
- package/dist/components/action-toast/ActionToast.js +57 -21
- package/dist/components/ai-input/AIInput.d.ts +4 -1
- package/dist/components/ai-input/AIInput.js +165 -50
- package/dist/components/ai-input/AIInput.module.css +40 -2
- package/dist/components/audit-log-list/AuditLogList.js +9 -2
- package/dist/components/dashboard/Dashboard.js +3 -1
- package/dist/components/hooks/useAIChatStream.d.ts +12 -1
- package/dist/components/hooks/useAIChatStream.js +13 -4
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +62 -22
- package/dist/components/hooks/usePluginConfig.d.ts +8 -20
- package/dist/components/hooks/usePluginConfig.js +9 -2
- package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
- package/dist/components/text-shimmer/TextShimmer.js +34 -0
- package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +2 -0
- package/dist/handlers/chatHandler.d.ts +4 -1
- package/dist/handlers/chatHandler.js +198 -17
- package/dist/index.d.ts +6 -1
- package/dist/index.js +92 -5
- 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
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
status: 400
|
|
723
|
+
const exceededTokenUsageLimit = await getExceededTokenUsageLimit({
|
|
724
|
+
maxTokenUsage: options.maxTokenUsage,
|
|
725
|
+
req,
|
|
726
|
+
userID: user.id
|
|
727
727
|
});
|
|
728
|
-
|
|
729
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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": "^
|
|
71
|
+
"@ai-sdk/mistral": "^4.0.13",
|
|
72
72
|
"@ai-sdk/openai": "^3.0.67",
|
|
73
|
-
"@
|
|
74
|
-
"@
|
|
75
|
-
"@payloadcms/
|
|
76
|
-
"@payloadcms/
|
|
77
|
-
"@payloadcms/
|
|
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": "
|
|
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.
|
|
90
|
-
"payload": "3.
|
|
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.
|
|
96
|
-
"typescript": "
|
|
97
|
-
"vitest": "4.1.
|
|
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": "^
|
|
103
|
+
"@ai-sdk/mistral": "^4.0.13",
|
|
103
104
|
"@ai-sdk/openai": "^3.0.67",
|
|
104
|
-
"@openrouter/ai-sdk-provider": "^
|
|
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": "^
|
|
152
|
+
"ai": "^7.0.31",
|
|
151
153
|
"zod": "^4.4.3"
|
|
152
154
|
}
|
|
153
155
|
}
|