@mvriu5/payload-ai 0.5.9 → 1.1.1

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 (73) hide show
  1. package/README.md +62 -6
  2. package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
  3. package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
  4. package/dist/ai/providerOptions.d.ts +17 -1
  5. package/dist/ai/providerOptions.js +35 -7
  6. package/dist/ai/providerRuntime.d.ts +2 -2
  7. package/dist/ai/providerRuntime.js +50 -16
  8. package/dist/ai/sensitiveData.js +2 -6
  9. package/dist/components/AIInput.d.ts +2 -1
  10. package/dist/components/AIInput.js +363 -135
  11. package/dist/components/AIInput.module.css +15 -1
  12. package/dist/components/APIKeyField.d.ts +3 -0
  13. package/dist/components/APIKeyField.js +63 -0
  14. package/dist/components/ActionToast.d.ts +30 -0
  15. package/dist/components/ActionToast.js +263 -0
  16. package/dist/components/AuditLogList.d.ts +32 -0
  17. package/dist/components/AuditLogList.js +104 -0
  18. package/dist/components/{RecentChangesList.module.css → AuditLogList.module.css} +18 -1
  19. package/dist/components/DiffDialog.d.ts +10 -3
  20. package/dist/components/DiffDialog.js +284 -97
  21. package/dist/components/DiffDialog.module.css +86 -2
  22. package/dist/components/Icons.d.ts +5 -0
  23. package/dist/components/Icons.js +436 -214
  24. package/dist/components/MentionPopover.d.ts +19 -0
  25. package/dist/components/MentionPopover.js +95 -0
  26. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  27. package/dist/components/hooks/useAISettings.d.ts +6 -4
  28. package/dist/components/hooks/useAISettings.js +18 -3
  29. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  31. package/dist/exports/client.d.ts +4 -2
  32. package/dist/exports/client.js +4 -2
  33. package/dist/handlers/applyActionHandler.d.ts +8 -0
  34. package/dist/handlers/applyActionHandler.js +563 -0
  35. package/dist/handlers/auditLogHandler.d.ts +6 -0
  36. package/dist/{endpoints/aiRecentChangesEndpointHandler.js → handlers/auditLogHandler.js} +12 -8
  37. package/dist/handlers/chatHandler.d.ts +42 -0
  38. package/dist/handlers/chatHandler.js +721 -0
  39. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  41. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  42. package/dist/handlers/proposalDiffHandler.js +208 -0
  43. package/dist/index.d.ts +6 -6
  44. package/dist/index.js +52 -20
  45. package/dist/payload/collectionPermissions.d.ts +14 -13
  46. package/dist/payload/collectionPermissions.js +3 -2
  47. package/dist/payload/normalizeData.d.ts +8 -0
  48. package/dist/payload/normalizeData.js +25 -3
  49. package/dist/payload/schemaContext.d.ts +43 -16
  50. package/dist/payload/schemaContext.js +139 -18
  51. package/dist/payload/shared.d.ts +33 -0
  52. package/dist/payload/shared.js +114 -0
  53. package/package.json +45 -9
  54. package/dist/components/AIActionProposalList.d.ts +0 -26
  55. package/dist/components/AIActionProposalList.js +0 -162
  56. package/dist/components/AIApiKeyField.d.ts +0 -2
  57. package/dist/components/AIApiKeyField.js +0 -47
  58. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  59. package/dist/components/CollectionMentionPopover.js +0 -77
  60. package/dist/components/RecentChangesList.d.ts +0 -18
  61. package/dist/components/RecentChangesList.js +0 -66
  62. package/dist/components/hooks/utils.d.ts +0 -1
  63. package/dist/components/hooks/utils.js +0 -3
  64. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -8
  65. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -365
  66. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  67. package/dist/endpoints/aiChatEndpointHandler.js +0 -382
  68. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  69. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
  70. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +0 -7
  71. package/dist/endpoints/aiProposalDiffEndpointHandler.js +0 -138
  72. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +0 -6
  73. /package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +0 -0
package/README.md CHANGED
@@ -5,14 +5,28 @@ AI assistant plugin for Payload CMS. It adds an admin dashboard assistant that c
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm add @mvriu5/payload-ai
8
+ npm add @mvriu5/payload-ai @ai-sdk/openai
9
+ ```
10
+
11
+ Install only the provider SDKs you actually use:
12
+
13
+ ```bash
14
+ npm add @mvriu5/payload-ai @ai-sdk/anthropic
15
+ npm add @mvriu5/payload-ai @ai-sdk/google
16
+ npm add @mvriu5/payload-ai @ai-sdk/mistral
17
+ ```
18
+
19
+ OpenRouter uses the community OpenRouter provider:
20
+
21
+ ```bash
22
+ npm add @mvriu5/payload-ai @openrouter/ai-sdk-provider
9
23
  ```
10
24
 
11
25
  ## Usage
12
26
 
13
27
  ```ts
14
28
  import { buildConfig } from "payload";
15
- import { payloadAiPlugin } from "payload-ai-plugin";
29
+ import { payloadAiPlugin } from "@mvriu5/payload-ai";
16
30
 
17
31
  export default buildConfig({
18
32
  plugins: [
@@ -35,7 +49,7 @@ Users can select their provider and optionally store their own API key in accoun
35
49
  ## Options
36
50
 
37
51
  ```ts
38
- import type { PayloadAiPluginOptions } from "payload-ai-plugin";
52
+ import type { PayloadAiPluginOptions } from "@mvriu5/payload-ai";
39
53
 
40
54
  const options: PayloadAiPluginOptions = {
41
55
  allowUserApiKeys: false,
@@ -46,10 +60,12 @@ const options: PayloadAiPluginOptions = {
46
60
  update: true,
47
61
  delete: false,
48
62
  },
63
+ users: true,
49
64
  },
50
65
  models: {
51
66
  defaults: {
52
67
  openai: "gpt-4.1-mini",
68
+ openrouter: "openai/gpt-oss-120b",
53
69
  },
54
70
  providers: {
55
71
  openai: [
@@ -58,6 +74,7 @@ const options: PayloadAiPluginOptions = {
58
74
  ],
59
75
  },
60
76
  },
77
+ maxOutputTokens: 1200,
61
78
  };
62
79
  ```
63
80
 
@@ -90,12 +107,47 @@ payloadAiPlugin({
90
107
  })
91
108
  ```
92
109
 
110
+ You can mix both forms in the same object:
111
+
112
+ ```ts
113
+ payloadAiPlugin({
114
+ collections: {
115
+ posts: true,
116
+ pages: true,
117
+ users: {
118
+ read: true,
119
+ update: true,
120
+ },
121
+ },
122
+ })
123
+ ```
124
+
93
125
  `read` controls schema/context access, document search, and mentions. `create`, `update`, and `delete` control AI action proposals and server-side apply permissions.
94
126
 
95
127
  ### `models`
96
128
 
97
129
  Overrides the model list shown in the admin UI and the default model per provider.
98
130
 
131
+ Built-in providers are `openai`, `openrouter`, `claude`, `mistral`, and `google`.
132
+
133
+ OpenRouter includes these built-in model options:
134
+
135
+ - `openrouter/auto`
136
+ - `openai/gpt-oss-120b`
137
+ - `openai/gpt-4o-mini`
138
+ - `anthropic/claude-3.5-sonnet`
139
+ - `google/gemini-2.0-flash-001`
140
+
141
+ ### `maxOutputTokens`
142
+
143
+ Controls the maximum number of output tokens the chat endpoint may generate per request. If omitted, the plugin uses `700`.
144
+
145
+ ```ts
146
+ payloadAiPlugin({
147
+ maxOutputTokens: 1200,
148
+ })
149
+ ```
150
+
99
151
  ### `allowUserApiKeys`
100
152
 
101
153
  Controls whether the plugin adds an `aiApiKey` field to the admin user collection.
@@ -114,12 +166,16 @@ Disables endpoint and UI registration while keeping the plugin call in your conf
114
166
 
115
167
  ## Provider Environment Variables
116
168
 
169
+ The package lazy-loads provider SDKs at runtime. If a user selects `claude`, `google`, `mistral`, `openai`, or `openrouter`, the matching provider package must be installed in the host app.
170
+ `openrouter` uses `@openrouter/ai-sdk-provider`.
171
+
117
172
  API key priority is:
118
173
 
119
174
  1. account-level API key, unless `allowUserApiKeys: false`
120
175
  2. provider environment variables
121
176
 
122
177
  - `OPENAI_API_KEY`, `OPENAI_MODEL`
178
+ - `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`
123
179
  - `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
124
180
  - `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_GENERATIVE_AI_MODEL`
125
181
  - `MISTRAL_API_KEY`, `MISTRAL_MODEL`
@@ -135,12 +191,12 @@ The apply endpoint returns only minimal status/doc references and does not retur
135
191
  ## Exports
136
192
 
137
193
  ```ts
138
- import { payloadAiPlugin } from "payload-ai-plugin";
139
- import type { PayloadAiPluginOptions } from "payload-ai-plugin";
194
+ import { payloadAiPlugin } from "@mvriu5/payload-ai";
195
+ import type { PayloadAiPluginOptions } from "@mvriu5/payload-ai";
140
196
  ```
141
197
 
142
198
  Client components are exported through:
143
199
 
144
200
  ```ts
145
- import { AIInput, AIApiKeyField } from "payload-ai-plugin/client";
201
+ import { AIInput, AIApiKeyField } from "@mvriu5/payload-ai/client";
146
202
  ```
@@ -8,5 +8,5 @@ type SignableProposal = Record<string, unknown> & {
8
8
  export declare const signAIActionProposal: <Proposal extends SignableProposal>(proposal: Proposal) => Proposal & {
9
9
  _aiSignature: AIActionSignature;
10
10
  };
11
- export declare const verifyAIActionProposal: (proposal: SignableProposal) => boolean;
11
+ export declare const verifyActionProposal: (proposal: SignableProposal) => boolean;
12
12
  export {};
@@ -1,10 +1,9 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { isRecord } from "../payload/shared.js";
2
3
  const signatureTTL = 10 * 60 * 1000;
3
4
  const stableStringify = (value)=>{
4
- if (Array.isArray(value)) {
5
- return `[${value.map(stableStringify).join(",")}]`;
6
- }
7
- if (value && typeof value === "object") {
5
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
6
+ if (isRecord(value)) {
8
7
  const entries = Object.entries(value).filter(([key])=>key !== "_aiSignature").sort(([a], [b])=>a.localeCompare(b));
9
8
  return `{${entries.map(([key, entryValue])=>`${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
10
9
  }
@@ -12,9 +11,7 @@ const stableStringify = (value)=>{
12
11
  };
13
12
  const getSigningSecret = ()=>{
14
13
  const secret = process.env.PAYLOAD_SECRET;
15
- if (!secret) {
16
- throw new Error("PAYLOAD_SECRET is required to sign AI proposals.");
17
- }
14
+ if (!secret) throw new Error("PAYLOAD_SECRET is required to sign AI proposals.");
18
15
  return secret;
19
16
  };
20
17
  const getSignaturePayload = (proposal, expiresAt)=>{
@@ -23,9 +20,7 @@ const getSignaturePayload = (proposal, expiresAt)=>{
23
20
  proposal
24
21
  });
25
22
  };
26
- const signPayload = (payload)=>{
27
- return createHmac("sha256", getSigningSecret()).update(payload).digest("hex");
28
- };
23
+ const signPayload = (payload)=>createHmac("sha256", getSigningSecret()).update(payload).digest("hex");
29
24
  export const signAIActionProposal = (proposal)=>{
30
25
  const expiresAt = new Date(Date.now() + signatureTTL).toISOString();
31
26
  const value = signPayload(getSignaturePayload(proposal, expiresAt));
@@ -37,7 +32,7 @@ export const signAIActionProposal = (proposal)=>{
37
32
  }
38
33
  };
39
34
  };
40
- export const verifyAIActionProposal = (proposal)=>{
35
+ export const verifyActionProposal = (proposal)=>{
41
36
  const signature = proposal._aiSignature;
42
37
  if (!signature?.expiresAt || !signature.value) return false;
43
38
  if (Number.isNaN(new Date(signature.expiresAt).getTime())) return false;
@@ -52,6 +52,22 @@ declare const aiProviderModels: {
52
52
  readonly label: "GPT-4o Mini";
53
53
  readonly value: "gpt-4o-mini";
54
54
  }];
55
+ readonly openrouter: readonly [{
56
+ readonly label: "OpenRouter Auto";
57
+ readonly value: "openrouter/auto";
58
+ }, {
59
+ readonly label: "GPT-OSS-120B";
60
+ readonly value: "openai/gpt-oss-120b";
61
+ }, {
62
+ readonly label: "GPT-4o Mini";
63
+ readonly value: "openai/gpt-4o-mini";
64
+ }, {
65
+ readonly label: "Claude 3.5 Sonnet";
66
+ readonly value: "anthropic/claude-3.5-sonnet";
67
+ }, {
68
+ readonly label: "Gemini 2.0 Flash";
69
+ readonly value: "google/gemini-2.0-flash-001";
70
+ }];
55
71
  };
56
72
  export type AIProvider = keyof typeof aiProviderModels;
57
73
  type AIProviderModels = Record<AIProvider, AIProviderModelOption[]>;
@@ -65,7 +81,7 @@ export type AIModelConfig = {
65
81
  providers?: Partial<Record<AIProvider, AIProviderModelOption[]>>;
66
82
  };
67
83
  export declare const getResolvedAIModelConfig: (modelConfig?: AIModelConfig) => {
68
- defaults: Record<"claude" | "google" | "mistral" | "openai", string>;
84
+ defaults: Record<"claude" | "google" | "mistral" | "openai" | "openrouter", string>;
69
85
  providers: AIProviderModels;
70
86
  };
71
87
  export declare const isAIProvider: (provider: string) => provider is AIProvider;
@@ -66,6 +66,28 @@ const aiProviderModels = {
66
66
  label: "GPT-4o Mini",
67
67
  value: "gpt-4o-mini"
68
68
  }
69
+ ],
70
+ openrouter: [
71
+ {
72
+ label: "OpenRouter Auto",
73
+ value: "openrouter/auto"
74
+ },
75
+ {
76
+ label: "GPT-OSS-120B",
77
+ value: "openai/gpt-oss-120b"
78
+ },
79
+ {
80
+ label: "GPT-4o Mini",
81
+ value: "openai/gpt-4o-mini"
82
+ },
83
+ {
84
+ label: "Claude 3.5 Sonnet",
85
+ value: "anthropic/claude-3.5-sonnet"
86
+ },
87
+ {
88
+ label: "Gemini 2.0 Flash",
89
+ value: "google/gemini-2.0-flash-001"
90
+ }
69
91
  ]
70
92
  };
71
93
  export const aiProviders = [
@@ -84,21 +106,29 @@ export const aiProviders = [
84
106
  {
85
107
  label: "OpenAI",
86
108
  value: "openai"
109
+ },
110
+ {
111
+ label: "OpenRouter",
112
+ value: "openrouter"
87
113
  }
88
114
  ];
89
115
  export const defaultAIModels = {
90
116
  claude: aiProviderModels.claude[0].value,
91
117
  google: aiProviderModels.google[0].value,
92
118
  mistral: aiProviderModels.mistral[0].value,
93
- openai: aiProviderModels.openai[0].value
119
+ openai: aiProviderModels.openai[0].value,
120
+ openrouter: aiProviderModels.openrouter[0].value
94
121
  };
95
122
  export const getResolvedAIModelConfig = (modelConfig)=>{
96
- const providers = Object.fromEntries(Object.entries(aiProviderModels).map(([provider, models])=>[
123
+ const providers = Object.fromEntries(Object.entries(aiProviderModels).map(([provider, models])=>{
124
+ const providerKey = provider;
125
+ return [
97
126
  provider,
98
- modelConfig?.providers?.[provider] || [
127
+ modelConfig?.providers?.[providerKey] || [
99
128
  ...models
100
129
  ]
101
- ]));
130
+ ];
131
+ }));
102
132
  const defaults = Object.fromEntries(Object.entries(defaultAIModels).map(([provider, defaultModel])=>{
103
133
  const providerKey = provider;
104
134
  const configuredDefault = modelConfig?.defaults?.[providerKey];
@@ -113,6 +143,4 @@ export const getResolvedAIModelConfig = (modelConfig)=>{
113
143
  providers
114
144
  };
115
145
  };
116
- export const isAIProvider = (provider)=>{
117
- return provider in aiProviderModels;
118
- };
146
+ export const isAIProvider = (provider)=>provider in aiProviderModels;
@@ -11,9 +11,9 @@ type ModelConfig = {
11
11
  model: string;
12
12
  provider: AIProvider;
13
13
  };
14
- export declare const getProviderConfig: ({ apiKey, defaultModels, model, provider, }: ProviderConfig) => {
14
+ export declare const getProviderConfig: ({ apiKey, defaultModels, model, provider }: ProviderConfig) => {
15
15
  apiKey: string | undefined;
16
16
  modelID: string;
17
17
  };
18
- export declare const getModel: ({ apiKey, model, provider }: ModelConfig) => LanguageModel;
18
+ export declare const getModel: ({ apiKey, model, provider }: ModelConfig) => Promise<LanguageModel>;
19
19
  export {};
@@ -1,7 +1,3 @@
1
- import { createAnthropic } from "@ai-sdk/anthropic";
2
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
3
- import { createMistral } from "@ai-sdk/mistral";
4
- import { createOpenAI } from "@ai-sdk/openai";
5
1
  import { defaultAIModels } from "./providerOptions.js";
6
2
  export const getProviderConfig = ({ apiKey, defaultModels, model, provider })=>{
7
3
  const defaultModel = defaultModels?.[provider] || defaultAIModels[provider];
@@ -23,22 +19,60 @@ export const getProviderConfig = ({ apiKey, defaultModels, model, provider })=>{
23
19
  modelID: model || process.env.MISTRAL_MODEL || defaultModel
24
20
  };
25
21
  }
22
+ if (provider === "openrouter") {
23
+ return {
24
+ apiKey: apiKey || process.env.OPENROUTER_API_KEY,
25
+ modelID: model || process.env.OPENROUTER_MODEL || defaultModel
26
+ };
27
+ }
26
28
  return {
27
29
  apiKey: apiKey || process.env.OPENAI_API_KEY,
28
30
  modelID: model || process.env.OPENAI_MODEL || defaultModel
29
31
  };
30
32
  };
31
- export const getModel = ({ apiKey, model, provider })=>{
32
- if (provider === "claude") return createAnthropic({
33
- apiKey
34
- })(model);
35
- if (provider === "google") return createGoogleGenerativeAI({
36
- apiKey
37
- })(model);
38
- if (provider === "mistral") return createMistral({
39
- apiKey
40
- })(model);
41
- return createOpenAI({
33
+ const getMissingProviderDependencyError = (packageName, provider)=>{
34
+ return new Error(`Missing optional dependency ${packageName}. Install it to use the ${provider} provider.`);
35
+ };
36
+ export const getModel = async ({ apiKey, model, provider })=>{
37
+ const providerOptions = {
42
38
  apiKey
43
- })(model);
39
+ };
40
+ if (provider === "claude") {
41
+ try {
42
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
43
+ return createAnthropic(providerOptions)(model);
44
+ } catch (error) {
45
+ throw getMissingProviderDependencyError("@ai-sdk/anthropic", provider);
46
+ }
47
+ }
48
+ if (provider === "google") {
49
+ try {
50
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
51
+ return createGoogleGenerativeAI(providerOptions)(model);
52
+ } catch (error) {
53
+ throw getMissingProviderDependencyError("@ai-sdk/google", provider);
54
+ }
55
+ }
56
+ if (provider === "mistral") {
57
+ try {
58
+ const { createMistral } = await import("@ai-sdk/mistral");
59
+ return createMistral(providerOptions)(model);
60
+ } catch (error) {
61
+ throw getMissingProviderDependencyError("@ai-sdk/mistral", provider);
62
+ }
63
+ }
64
+ if (provider === "openrouter") {
65
+ try {
66
+ const { createOpenRouter } = await import("@openrouter/ai-sdk-provider");
67
+ return createOpenRouter(providerOptions)(model);
68
+ } catch (error) {
69
+ throw getMissingProviderDependencyError("@openrouter/ai-sdk-provider", provider);
70
+ }
71
+ }
72
+ try {
73
+ const { createOpenAI } = await import("@ai-sdk/openai");
74
+ return createOpenAI(providerOptions)(model);
75
+ } catch (error) {
76
+ throw getMissingProviderDependencyError("@ai-sdk/openai", provider);
77
+ }
44
78
  };
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "../payload/shared.js";
1
2
  const sensitiveKeyPatterns = [
2
3
  /^apiKey$/i,
3
4
  /^api_key$/i,
@@ -7,12 +8,7 @@ const sensitiveKeyPatterns = [
7
8
  /^refreshToken$/i,
8
9
  /^secret$/i
9
10
  ];
10
- const isRecord = (value)=>{
11
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
- };
13
- const isSensitiveKey = (key)=>{
14
- return sensitiveKeyPatterns.some((pattern)=>pattern.test(key));
15
- };
11
+ const isSensitiveKey = (key)=>sensitiveKeyPatterns.some((pattern)=>pattern.test(key));
16
12
  export const containsSensitiveData = (value)=>{
17
13
  if (Array.isArray(value)) return value.some(containsSensitiveData);
18
14
  if (!isRecord(value)) return false;
@@ -1 +1,2 @@
1
- export declare const AIInput: () => import("react").JSX.Element;
1
+ declare const AIInput: () => import("react").JSX.Element;
2
+ export default AIInput;