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