@mvriu5/payload-ai 1.6.0 → 1.6.2

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 CHANGED
@@ -25,18 +25,18 @@ npm add @mvriu5/payload-ai @openrouter/ai-sdk-provider
25
25
  ## Usage
26
26
 
27
27
  ```ts
28
- import { buildConfig } from "payload";
29
- import { payloadAiPlugin } from "@mvriu5/payload-ai";
28
+ import { buildConfig } from "payload"
29
+ import { payloadAiPlugin } from "@mvriu5/payload-ai"
30
30
 
31
31
  export default buildConfig({
32
- plugins: [
33
- payloadAiPlugin({
34
- collections: {
35
- posts: true,
36
- },
37
- }),
38
- ],
39
- });
32
+ plugins: [
33
+ payloadAiPlugin({
34
+ collections: {
35
+ posts: true,
36
+ },
37
+ }),
38
+ ],
39
+ })
40
40
  ```
41
41
 
42
42
  Without centrally configured providers, the plugin adds two fields to the configured Payload admin user collection:
@@ -51,64 +51,72 @@ When `providers` is configured, provider selection and API keys are managed cent
51
51
  ## Options
52
52
 
53
53
  ```ts
54
- import type { PayloadAIPluginOptions } from "@mvriu5/payload-ai";
54
+ import type { PayloadAIPluginOptions } from "@mvriu5/payload-ai"
55
55
 
56
56
  const options: PayloadAIPluginOptions = {
57
- aiInput: true,
58
- allowUserApiKeys: false,
59
- collections: {
60
- media: {
61
- read: true,
62
- update: true,
57
+ aiInput: true,
58
+ allowUserApiKeys: false,
59
+ authCollections: {
60
+ aiInput: false,
61
+ generateFields: false,
63
62
  },
64
- posts: {
65
- read: true,
66
- create: true,
67
- update: true,
68
- delete: false,
63
+ collections: {
64
+ media: {
65
+ read: true,
66
+ update: true,
67
+ },
68
+ posts: {
69
+ read: true,
70
+ create: true,
71
+ update: true,
72
+ delete: false,
73
+ },
74
+ users: true,
69
75
  },
70
- users: true,
71
- },
72
- generateFields: true,
73
- media: {
74
- enabled: true,
75
- collectionSlug: "media",
76
- acceptedMimeTypes: ["image/*"],
77
- maxFileSize: 10 * 1024 * 1024,
78
- },
79
- providers: [
80
- {
81
- id: "company-openai",
82
- label: "Company OpenAI",
83
- provider: "openai",
84
- apiKey: process.env.COMPANY_OPENAI_API_KEY,
85
- models: [
86
- { label: "GPT-4.1 Mini", value: "gpt-4.1-mini" },
87
- { label: "GPT-4.1", value: "gpt-4.1" },
88
- ],
89
- defaultModel: "gpt-4.1-mini",
76
+ generateFields: true,
77
+ media: {
78
+ enabled: true,
79
+ collectionSlug: "media",
80
+ acceptedMimeTypes: ["image/*"],
81
+ maxFileSize: 10 * 1024 * 1024,
82
+ },
83
+ providers: [
84
+ {
85
+ id: "company-openai",
86
+ label: "Company OpenAI",
87
+ provider: "openai",
88
+ apiKey: process.env.COMPANY_OPENAI_API_KEY,
89
+ models: [
90
+ { label: "GPT-4.1 Mini", value: "gpt-4.1-mini" },
91
+ { label: "GPT-4.1", value: "gpt-4.1" },
92
+ ],
93
+ defaultModel: "gpt-4.1-mini",
94
+ },
95
+ {
96
+ id: "ollama",
97
+ label: "Local Ollama",
98
+ provider: "openai",
99
+ baseURL: "http://localhost:11434/v1",
100
+ apiKey: "ollama",
101
+ models: [
102
+ { label: "Llama 3.3", value: "llama3.3" },
103
+ { label: "Qwen 3", value: "qwen3" },
104
+ ],
105
+ },
106
+ ],
107
+ maxOutputTokens: 1200,
108
+ promptCaching: true,
109
+ translate: true,
110
+ uploadCollections: {
111
+ aiInput: true,
112
+ generateFields: false,
90
113
  },
91
- {
92
- id: "ollama",
93
- label: "Local Ollama",
94
- provider: "openai",
95
- baseURL: "http://localhost:11434/v1",
96
- apiKey: "ollama",
97
- models: [
98
- { label: "Llama 3.3", value: "llama3.3" },
99
- { label: "Qwen 3", value: "qwen3" },
100
- ],
114
+ maxTokenUsage: {
115
+ type: "user",
116
+ perDay: 50_000,
117
+ perWeek: 250_000,
101
118
  },
102
- ],
103
- maxOutputTokens: 1200,
104
- promptCaching: true,
105
- translate: true,
106
- maxTokenUsage: {
107
- type: "user",
108
- perDay: 50_000,
109
- perWeek: 250_000,
110
- },
111
- };
119
+ }
112
120
  ```
113
121
 
114
122
  `models` configures model choices for the user-selected provider mode. Use `providers` instead when provider selection, credentials, and endpoints should be managed centrally.
@@ -119,7 +127,7 @@ Controls whether the collapsible AI Assistant input is added to collection and g
119
127
 
120
128
  ```ts
121
129
  payloadAiPlugin({
122
- aiInput: false,
130
+ aiInput: false,
123
131
  })
124
132
  ```
125
133
 
@@ -131,19 +139,38 @@ Controls whether supported fields receive a Generate control. It defaults to `tr
131
139
 
132
140
  ```ts
133
141
  payloadAiPlugin({
134
- generateFields: false,
142
+ generateFields: false,
135
143
  })
136
144
  ```
137
145
 
138
146
  Generate controls are added to `text`, `textarea`, `richText`, and `json` fields in collection and global edit views. When disabled, the plugin also omits the `/ai-generate-field` endpoint. The embedded AI Assistant and dashboard remain available.
139
147
 
148
+ ### `authCollections` and `uploadCollections`
149
+
150
+ Auth and upload collections do not receive the embedded AI Assistant or Generate controls by default. Enable either feature explicitly for the respective collection type:
151
+
152
+ ```ts
153
+ payloadAiPlugin({
154
+ authCollections: {
155
+ aiInput: true,
156
+ generateFields: true,
157
+ },
158
+ uploadCollections: {
159
+ aiInput: true,
160
+ generateFields: true,
161
+ },
162
+ })
163
+ ```
164
+
165
+ The global `aiInput` and `generateFields` options still take precedence when set to `false`.
166
+
140
167
  ### `translate`
141
168
 
142
169
  Controls document translation for localized collections and globals. It defaults to `true`.
143
170
 
144
171
  ```ts
145
172
  payloadAiPlugin({
146
- translate: false,
173
+ translate: false,
147
174
  })
148
175
  ```
149
176
 
@@ -165,9 +192,9 @@ Use `true` to enable all AI actions for a collection:
165
192
 
166
193
  ```ts
167
194
  payloadAiPlugin({
168
- collections: {
169
- posts: true,
170
- },
195
+ collections: {
196
+ posts: true,
197
+ },
171
198
  })
172
199
  ```
173
200
 
@@ -175,14 +202,14 @@ Use granular permissions to control each action:
175
202
 
176
203
  ```ts
177
204
  payloadAiPlugin({
178
- collections: {
179
- posts: {
180
- read: true,
181
- create: true,
182
- update: true,
183
- delete: false,
205
+ collections: {
206
+ posts: {
207
+ read: true,
208
+ create: true,
209
+ update: true,
210
+ delete: false,
211
+ },
184
212
  },
185
- },
186
213
  })
187
214
  ```
188
215
 
@@ -190,14 +217,14 @@ You can mix both forms in the same object:
190
217
 
191
218
  ```ts
192
219
  payloadAiPlugin({
193
- collections: {
194
- posts: true,
195
- pages: true,
196
- users: {
197
- read: true,
198
- update: true,
220
+ collections: {
221
+ posts: true,
222
+ pages: true,
223
+ users: {
224
+ read: true,
225
+ update: true,
226
+ },
199
227
  },
200
- },
201
228
  })
202
229
  ```
203
230
 
@@ -209,19 +236,19 @@ Enables media uploads from the AI assistant. Uploaded files are created through
209
236
 
210
237
  ```ts
211
238
  payloadAiPlugin({
212
- collections: {
239
+ collections: {
240
+ media: {
241
+ read: true,
242
+ update: true,
243
+ },
244
+ posts: true,
245
+ },
213
246
  media: {
214
- read: true,
215
- update: true,
247
+ enabled: true,
248
+ collectionSlug: "media",
249
+ acceptedMimeTypes: ["image/*"],
250
+ maxFileSize: 10 * 1024 * 1024,
216
251
  },
217
- posts: true,
218
- },
219
- media: {
220
- enabled: true,
221
- collectionSlug: "media",
222
- acceptedMimeTypes: ["image/*"],
223
- maxFileSize: 10 * 1024 * 1024,
224
- },
225
252
  })
226
253
  ```
227
254
 
@@ -276,7 +303,7 @@ Controls the maximum number of output tokens the chat endpoint may generate per
276
303
 
277
304
  ```ts
278
305
  payloadAiPlugin({
279
- maxOutputTokens: 1200,
306
+ maxOutputTokens: 1200,
280
307
  })
281
308
  ```
282
309
 
@@ -286,11 +313,11 @@ Limits total AI tokens across rolling 24-hour and 7-day windows.
286
313
 
287
314
  ```ts
288
315
  payloadAiPlugin({
289
- maxTokenUsage: {
290
- type: "user",
291
- perDay: 50_000,
292
- perWeek: 250_000,
293
- },
316
+ maxTokenUsage: {
317
+ type: "user",
318
+ perDay: 50_000,
319
+ perWeek: 250_000,
320
+ },
294
321
  })
295
322
  ```
296
323
 
@@ -304,7 +331,7 @@ Enables provider prompt-caching hints and defaults to `true`. The chat endpoint
304
331
 
305
332
  ```ts
306
333
  payloadAiPlugin({
307
- promptCaching: false,
334
+ promptCaching: false,
308
335
  })
309
336
  ```
310
337
 
@@ -316,7 +343,7 @@ Controls whether the plugin adds an `aiApiKey` field to the admin user collectio
316
343
 
317
344
  ```ts
318
345
  payloadAiPlugin({
319
- allowUserApiKeys: false,
346
+ allowUserApiKeys: false,
320
347
  })
321
348
  ```
322
349
 
@@ -356,12 +383,12 @@ The apply endpoint returns only minimal status/doc references and does not retur
356
383
  ## Exports
357
384
 
358
385
  ```ts
359
- import { payloadAiPlugin } from "@mvriu5/payload-ai";
360
- import type { PayloadAiPluginOptions } from "@mvriu5/payload-ai";
386
+ import { payloadAiPlugin } from "@mvriu5/payload-ai"
387
+ import type { PayloadAiPluginOptions } from "@mvriu5/payload-ai"
361
388
  ```
362
389
 
363
390
  Client components are exported through:
364
391
 
365
392
  ```ts
366
- import { AIInput, AIApiKeyField } from "@mvriu5/payload-ai/client";
393
+ import { AIInput, AIApiKeyField } from "@mvriu5/payload-ai/client"
367
394
  ```
@@ -1,5 +1,5 @@
1
1
  type AIInputProps = {
2
2
  isDashboard?: boolean;
3
3
  };
4
- declare const AIInput: ({ isDashboard }: AIInputProps) => import("react").JSX.Element;
4
+ declare const AIInput: ({ isDashboard }: AIInputProps) => import("react").JSX.Element | null;
5
5
  export default AIInput;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Button, ChevronIcon, useConfig, useDocumentForm, useDocumentInfo, useLocale } from "@payloadcms/ui";
3
+ import { Button, ChevronIcon, useAuth, useConfig, useDocumentForm, useDocumentInfo, useLocale } from "@payloadcms/ui";
4
4
  import { formatAdminURL } from "payload/shared";
5
5
  import { useEffect, useRef, useState } from "react";
6
6
  import { mergeData } from "../../payload/shared.js";
@@ -540,6 +540,8 @@ const DocumentAIInput = ()=>{
540
540
  });
541
541
  };
542
542
  const AIInput = ({ isDashboard = false })=>{
543
+ const { user } = useAuth();
544
+ if (!user) return null;
543
545
  if (isDashboard) return /*#__PURE__*/ _jsx(AIInputCore, {
544
546
  isDashboard: true
545
547
  });
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useConfig, useDocumentForm, useDocumentInfo, useField, useLocale } from "@payloadcms/ui";
3
+ import { useAuth, useConfig, useDocumentForm, useDocumentInfo, useField, useLocale } from "@payloadcms/ui";
4
4
  import { formatAdminURL } from "payload/shared";
5
5
  import { useState } from "react";
6
6
  import { createGeneratedRichTextValue } from "./richText.js";
@@ -27,6 +27,7 @@ const requestGeneratedValue = ({ apiRoute, body, cacheKey })=>{
27
27
  return request;
28
28
  };
29
29
  const GenerateField = ({ field, generationFieldKey, generationFieldType, path, readOnly })=>{
30
+ const { user } = useAuth();
30
31
  const configContext = useConfig();
31
32
  const documentForm = useDocumentForm();
32
33
  const documentInfo = useDocumentInfo();
@@ -87,7 +88,7 @@ const GenerateField = ({ field, generationFieldKey, generationFieldType, path, r
87
88
  setIsGenerating(false);
88
89
  }
89
90
  };
90
- if (!scope) return null;
91
+ if (!user || !scope) return null;
91
92
  return /*#__PURE__*/ _jsxs("div", {
92
93
  className: styles.generateField,
93
94
  children: [
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import { generateText } from "ai";
2
3
  import { isAIProvider } from "../ai/providerOptions.js";
3
4
  import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
@@ -70,17 +71,25 @@ export const createTranslateDocumentHandler = (options)=>async (req)=>{
70
71
  locale: defaultLocale,
71
72
  req
72
73
  });
74
+ const sourceValues = getTranslationValues(pageContext.fields, sourceDocument);
75
+ if (sourceValues.length === 0) return Response.json({
76
+ available: false
77
+ });
73
78
  const targetDocument = await loadDocument({
74
79
  context: pageContext,
75
80
  id: body.id,
76
81
  locale,
77
82
  req
78
83
  });
79
- const sourceValues = getTranslationValues(pageContext.fields, sourceDocument);
80
- const targetValues = getTranslationValues(pageContext.fields, targetDocument);
81
- const populatedTargetPaths = new Set(targetValues.map((entry)=>entry.path));
82
- const missingSourceValues = sourceValues.filter((entry)=>!populatedTargetPaths.has(entry.path));
83
- const available = missingSourceValues.length > 0;
84
+ const targetValuesByPath = new Map(getTranslationValues(pageContext.fields, targetDocument).map((entry)=>[
85
+ entry.path,
86
+ entry.value
87
+ ]));
88
+ const translationSourceValues = sourceValues.filter((entry)=>{
89
+ const targetValue = targetValuesByPath.get(entry.path);
90
+ return targetValue === undefined || isDeepStrictEqual(targetValue, entry.value);
91
+ });
92
+ const available = translationSourceValues.length > 0;
84
93
  if (body.action === "status" || !available) return Response.json({
85
94
  available
86
95
  });
@@ -139,7 +148,7 @@ export const createTranslateDocumentHandler = (options)=>async (req)=>{
139
148
  prompt: JSON.stringify({
140
149
  sourceLocale: defaultLocale,
141
150
  targetLocale: locale,
142
- values: missingSourceValues.map((entry, index)=>({
151
+ values: translationSourceValues.map((entry, index)=>({
143
152
  id: String(index),
144
153
  value: entry.value
145
154
  }))
@@ -151,7 +160,7 @@ export const createTranslateDocumentHandler = (options)=>async (req)=>{
151
160
  "Return no Markdown or explanation."
152
161
  ].join(" ")
153
162
  });
154
- const translatedValues = parseTranslations(result.text, missingSourceValues.length);
163
+ const translatedValues = parseTranslations(result.text, translationSourceValues.length);
155
164
  if (translatedValues.some((value)=>value === undefined)) throw new Error("Translation response is incomplete.");
156
165
  if (result.usage && options.maxTokenUsage) {
157
166
  await recordTokenUsage({
@@ -164,7 +173,7 @@ export const createTranslateDocumentHandler = (options)=>async (req)=>{
164
173
  }
165
174
  return Response.json({
166
175
  available: true,
167
- values: missingSourceValues.map((entry, index)=>({
176
+ values: translationSourceValues.map((entry, index)=>({
168
177
  fieldType: entry.fieldType,
169
178
  path: entry.path,
170
179
  value: translatedValues[index]
package/dist/index.d.ts CHANGED
@@ -4,9 +4,14 @@ import { type MaxTokenUsageOptions } from "./ai/tokenUsage.js";
4
4
  import { type CollectionPermissionMap } from "./payload/collectionPermissions.js";
5
5
  export type { AIModelConfig, AIProviderConfig, AIProviderModelOption } from "./ai/providerOptions.js";
6
6
  export type { MaxTokenUsageOptions } from "./ai/tokenUsage.js";
7
+ export type CollectionTypeAIOptions = {
8
+ aiInput?: boolean;
9
+ generateFields?: boolean;
10
+ };
7
11
  export type PayloadAIPluginOptions = {
8
12
  aiInput?: boolean;
9
13
  allowUserApiKeys?: boolean;
14
+ authCollections?: CollectionTypeAIOptions;
10
15
  collections?: CollectionPermissionMap;
11
16
  disabled?: boolean;
12
17
  generateFields?: boolean;
@@ -22,5 +27,6 @@ export type PayloadAIPluginOptions = {
22
27
  promptCaching?: boolean;
23
28
  providers?: AIProviderConfig[];
24
29
  translate?: boolean;
30
+ uploadCollections?: CollectionTypeAIOptions;
25
31
  };
26
32
  export declare const payloadAiPlugin: (pluginOptions: PayloadAIPluginOptions) => (config: Config) => Config;
package/dist/index.js CHANGED
@@ -192,6 +192,7 @@ const createAITokenUsageCollection = ()=>({
192
192
  ],
193
193
  timestamps: true
194
194
  });
195
+ const hasAuthenticatedUser = (_data, _siblingData, { user })=>Boolean(user);
195
196
  const addAccountFields = ({ allowUserApiKeys, config })=>{
196
197
  const adminUserSlug = config.admin?.user;
197
198
  if (!adminUserSlug || !config.collections) return;
@@ -200,6 +201,9 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
200
201
  userCollection.fields.push({
201
202
  name: "aiProvider",
202
203
  type: "select",
204
+ admin: {
205
+ condition: hasAuthenticatedUser
206
+ },
203
207
  defaultValue: "openai",
204
208
  label: "AI Provider",
205
209
  options: aiProviders
@@ -209,6 +213,7 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
209
213
  name: "aiApiKey",
210
214
  type: "text",
211
215
  admin: {
216
+ condition: hasAuthenticatedUser,
212
217
  components: {
213
218
  Field: "@mvriu5/payload-ai/client#AIApiKeyField"
214
219
  },
@@ -222,6 +227,7 @@ const aiField = {
222
227
  name: "payloadAi",
223
228
  type: "ui",
224
229
  admin: {
230
+ condition: hasAuthenticatedUser,
225
231
  components: {
226
232
  Field: "@mvriu5/payload-ai/client#AIInput"
227
233
  }
@@ -236,13 +242,15 @@ const getEntityLabel = (label, fallback)=>{
236
242
  }
237
243
  return fallback;
238
244
  };
239
- const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTranslation, config })=>{
245
+ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTranslation, authCollections, config, uploadCollections })=>{
240
246
  const pageContexts = new Map();
241
247
  const translationPageContexts = new Map();
242
248
  for (const collection of config.collections || []){
243
249
  if (isInternalCollection(collection.slug)) continue;
244
250
  if (collection.slug === "payload-ai-auditlog") continue;
245
- if (addGenerateFields) {
251
+ const allowAIInput = addAIInput && (!collection.auth || authCollections?.aiInput === true) && (!collection.upload || uploadCollections?.aiInput === true);
252
+ const allowGenerateFields = addGenerateFields && (!collection.auth || authCollections?.generateFields === true) && (!collection.upload || uploadCollections?.generateFields === true);
253
+ if (allowGenerateFields) {
246
254
  const pageContext = addTextGenerationFields({
247
255
  fields: collection.fields || [],
248
256
  label: getEntityLabel(collection.labels?.singular, collection.slug),
@@ -251,7 +259,7 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTr
251
259
  });
252
260
  pageContexts.set(`collection:${collection.slug}`, pageContext);
253
261
  }
254
- if (addAIInput) collection.fields = [
262
+ if (allowAIInput) collection.fields = [
255
263
  aiField,
256
264
  ...collection.fields || []
257
265
  ];
@@ -339,7 +347,9 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
339
347
  addGenerateFields: generateFields,
340
348
  addAIInput: pluginOptions.aiInput !== false,
341
349
  addTranslation: pluginOptions.translate !== false,
342
- config
350
+ authCollections: pluginOptions.authCollections,
351
+ config,
352
+ uploadCollections: pluginOptions.uploadCollections
343
353
  });
344
354
  const mentionCollectionSlugs = config.collections.flatMap((collection)=>{
345
355
  if (isInternalCollection(collection.slug)) return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mvriu5/payload-ai",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "AI assistant plugin for Payload CMS with provider selection, CMS mentions, and signed action proposals.",
5
5
  "keywords": [
6
6
  "payload",
@@ -66,10 +66,10 @@
66
66
  "knip": "knip"
67
67
  },
68
68
  "devDependencies": {
69
- "@ai-sdk/anthropic": "^3.0.81",
69
+ "@ai-sdk/anthropic": "^4.0.21",
70
70
  "@ai-sdk/google": "^3.0.80",
71
71
  "@ai-sdk/mistral": "^4.0.13",
72
- "@ai-sdk/openai": "^3.0.67",
72
+ "@ai-sdk/openai": "^4.0.20",
73
73
  "@floating-ui/react": "0.27.20",
74
74
  "@openrouter/ai-sdk-provider": "^3.0.0",
75
75
  "@payloadcms/db-postgres": "3.86.0",
@@ -85,7 +85,7 @@
85
85
  "cross-env": "^10.1.0",
86
86
  "eslint": "^10.5.0",
87
87
  "graphql": "^16.8.1",
88
- "jsdom": "^29.1.1",
88
+ "jsdom": "^30.0.0",
89
89
  "knip": "^6.18.0",
90
90
  "next": "16.2.11",
91
91
  "payload": "3.86.0",
@@ -98,10 +98,10 @@
98
98
  "vitest": "4.1.10"
99
99
  },
100
100
  "peerDependencies": {
101
- "@ai-sdk/anthropic": "^3.0.81",
101
+ "@ai-sdk/anthropic": "^4.0.21",
102
102
  "@ai-sdk/google": "^3.0.80",
103
103
  "@ai-sdk/mistral": "^4.0.13",
104
- "@ai-sdk/openai": "^3.0.67",
104
+ "@ai-sdk/openai": "^4.0.20",
105
105
  "@openrouter/ai-sdk-provider": "^3.0.0",
106
106
  "@payloadcms/ui": "^3.84.1",
107
107
  "payload": "^3.84.1"