@humaan/payload-plugin-ai-assistant 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +277 -2
  2. package/dist/admin/AiAssistantControl.d.ts +11 -0
  3. package/dist/admin/AiAssistantControl.js +31 -0
  4. package/dist/admin/AiAssistantControl.js.map +1 -0
  5. package/dist/admin/AiAssistantPanel.d.ts +10 -0
  6. package/dist/admin/AiAssistantPanel.js +208 -0
  7. package/dist/admin/AiAssistantPanel.js.map +1 -0
  8. package/dist/admin/AiAssistantPanel.module.scss +689 -0
  9. package/dist/admin/AiSourceSwitch.d.ts +13 -0
  10. package/dist/admin/AiSourceSwitch.js +14 -0
  11. package/dist/admin/AiSourceSwitch.js.map +1 -0
  12. package/dist/admin/BudgetField.d.ts +8 -0
  13. package/dist/admin/BudgetField.js +145 -0
  14. package/dist/admin/BudgetField.js.map +1 -0
  15. package/dist/admin/BudgetField.module.scss +47 -0
  16. package/dist/admin/Conversation.d.ts +12 -0
  17. package/dist/admin/Conversation.js +469 -0
  18. package/dist/admin/Conversation.js.map +1 -0
  19. package/dist/admin/Markdown.d.ts +9 -0
  20. package/dist/admin/Markdown.js +63 -0
  21. package/dist/admin/Markdown.js.map +1 -0
  22. package/dist/admin/PageContextBridge.d.ts +5 -0
  23. package/dist/admin/PageContextBridge.js +72 -0
  24. package/dist/admin/PageContextBridge.js.map +1 -0
  25. package/dist/admin/Pill.d.ts +11 -0
  26. package/dist/admin/Pill.js +27 -0
  27. package/dist/admin/Pill.js.map +1 -0
  28. package/dist/admin/PlanCard.d.ts +14 -0
  29. package/dist/admin/PlanCard.js +465 -0
  30. package/dist/admin/PlanCard.js.map +1 -0
  31. package/dist/admin/SettingsAlerts.d.ts +7 -0
  32. package/dist/admin/SettingsAlerts.js +37 -0
  33. package/dist/admin/SettingsAlerts.js.map +1 -0
  34. package/dist/admin/UsageSummary.d.ts +11 -0
  35. package/dist/admin/UsageSummary.js +129 -0
  36. package/dist/admin/UsageSummary.js.map +1 -0
  37. package/dist/admin/attachmentUpload.d.ts +25 -0
  38. package/dist/admin/attachmentUpload.js +76 -0
  39. package/dist/admin/attachmentUpload.js.map +1 -0
  40. package/dist/admin/openDocument.d.ts +13 -0
  41. package/dist/admin/openDocument.js +34 -0
  42. package/dist/admin/openDocument.js.map +1 -0
  43. package/dist/admin/percent.d.ts +10 -0
  44. package/dist/admin/percent.js +21 -0
  45. package/dist/admin/percent.js.map +1 -0
  46. package/dist/admin/threads.d.ts +21 -0
  47. package/dist/admin/threads.js +92 -0
  48. package/dist/admin/threads.js.map +1 -0
  49. package/dist/admin/toolSteps.d.ts +12 -0
  50. package/dist/admin/toolSteps.js +57 -0
  51. package/dist/admin/toolSteps.js.map +1 -0
  52. package/dist/admin/transport.d.ts +24 -0
  53. package/dist/admin/transport.js +75 -0
  54. package/dist/admin/transport.js.map +1 -0
  55. package/dist/admin/useAttachments.d.ts +32 -0
  56. package/dist/admin/useAttachments.js +91 -0
  57. package/dist/admin/useAttachments.js.map +1 -0
  58. package/dist/admin/useEntityLabel.d.ts +3 -0
  59. package/dist/admin/useEntityLabel.js +23 -0
  60. package/dist/admin/useEntityLabel.js.map +1 -0
  61. package/dist/attachments/collection.d.ts +15 -0
  62. package/dist/attachments/collection.js +90 -0
  63. package/dist/attachments/collection.js.map +1 -0
  64. package/dist/attachments/conversation.d.ts +25 -0
  65. package/dist/attachments/conversation.js +105 -0
  66. package/dist/attachments/conversation.js.map +1 -0
  67. package/dist/attachments/rules.d.ts +9 -0
  68. package/dist/attachments/rules.js +17 -0
  69. package/dist/attachments/rules.js.map +1 -0
  70. package/dist/attachments/store.d.ts +27 -0
  71. package/dist/attachments/store.js +187 -0
  72. package/dist/attachments/store.js.map +1 -0
  73. package/dist/billing.d.ts +60 -0
  74. package/dist/billing.js +116 -0
  75. package/dist/billing.js.map +1 -0
  76. package/dist/chat/history.d.ts +37 -0
  77. package/dist/chat/history.js +165 -0
  78. package/dist/chat/history.js.map +1 -0
  79. package/dist/chat/messages.d.ts +5 -0
  80. package/dist/chat/messages.js +3 -0
  81. package/dist/chat/messages.js.map +1 -0
  82. package/dist/chat/threads.d.ts +33 -0
  83. package/dist/chat/threads.js +77 -0
  84. package/dist/chat/threads.js.map +1 -0
  85. package/dist/chat/tools.d.ts +76 -0
  86. package/dist/chat/tools.js +596 -0
  87. package/dist/chat/tools.js.map +1 -0
  88. package/dist/chat/usage.d.ts +66 -0
  89. package/dist/chat/usage.js +219 -0
  90. package/dist/chat/usage.js.map +1 -0
  91. package/dist/collectionAccess.d.ts +6 -0
  92. package/dist/collectionAccess.js +32 -0
  93. package/dist/collectionAccess.js.map +1 -0
  94. package/dist/constants.d.ts +64 -0
  95. package/dist/constants.js +51 -0
  96. package/dist/constants.js.map +1 -0
  97. package/dist/endpoints/access.d.ts +4 -0
  98. package/dist/endpoints/access.js +28 -0
  99. package/dist/endpoints/access.js.map +1 -0
  100. package/dist/endpoints/allowance.d.ts +22 -0
  101. package/dist/endpoints/allowance.js +37 -0
  102. package/dist/endpoints/allowance.js.map +1 -0
  103. package/dist/endpoints/applyEndpoint.d.ts +12 -0
  104. package/dist/endpoints/applyEndpoint.js +170 -0
  105. package/dist/endpoints/applyEndpoint.js.map +1 -0
  106. package/dist/endpoints/budget.d.ts +30 -0
  107. package/dist/endpoints/budget.js +153 -0
  108. package/dist/endpoints/budget.js.map +1 -0
  109. package/dist/endpoints/chatEndpoint.d.ts +12 -0
  110. package/dist/endpoints/chatEndpoint.js +295 -0
  111. package/dist/endpoints/chatEndpoint.js.map +1 -0
  112. package/dist/endpoints/form.d.ts +23 -0
  113. package/dist/endpoints/form.js +95 -0
  114. package/dist/endpoints/form.js.map +1 -0
  115. package/dist/endpoints/usageEndpoint.d.ts +13 -0
  116. package/dist/endpoints/usageEndpoint.js +70 -0
  117. package/dist/endpoints/usageEndpoint.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.js +5 -0
  120. package/dist/index.js.map +1 -0
  121. package/dist/lexical/editorConfig.d.ts +4 -0
  122. package/dist/lexical/editorConfig.js +19 -0
  123. package/dist/lexical/editorConfig.js.map +1 -0
  124. package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
  125. package/dist/lexical/lexicalToMarkdown.js +10 -0
  126. package/dist/lexical/lexicalToMarkdown.js.map +1 -0
  127. package/dist/lexical/markdownToLexical.d.ts +7 -0
  128. package/dist/lexical/markdownToLexical.js +10 -0
  129. package/dist/lexical/markdownToLexical.js.map +1 -0
  130. package/dist/plan/allowed.d.ts +7 -0
  131. package/dist/plan/allowed.js +19 -0
  132. package/dist/plan/allowed.js.map +1 -0
  133. package/dist/plan/applyPlan.d.ts +16 -0
  134. package/dist/plan/applyPlan.js +353 -0
  135. package/dist/plan/applyPlan.js.map +1 -0
  136. package/dist/plan/catalogue.d.ts +37 -0
  137. package/dist/plan/catalogue.js +157 -0
  138. package/dist/plan/catalogue.js.map +1 -0
  139. package/dist/plan/diff.d.ts +13 -0
  140. package/dist/plan/diff.js +87 -0
  141. package/dist/plan/diff.js.map +1 -0
  142. package/dist/plan/parsePlan.d.ts +27 -0
  143. package/dist/plan/parsePlan.js +322 -0
  144. package/dist/plan/parsePlan.js.map +1 -0
  145. package/dist/plan/prompt.d.ts +1 -0
  146. package/dist/plan/prompt.js +45 -0
  147. package/dist/plan/prompt.js.map +1 -0
  148. package/dist/plan/stored.d.ts +53 -0
  149. package/dist/plan/stored.js +3 -0
  150. package/dist/plan/stored.js.map +1 -0
  151. package/dist/plan/tokens.d.ts +8 -0
  152. package/dist/plan/tokens.js +79 -0
  153. package/dist/plan/tokens.js.map +1 -0
  154. package/dist/plan/view.d.ts +4 -0
  155. package/dist/plan/view.js +43 -0
  156. package/dist/plan/view.js.map +1 -0
  157. package/dist/plugin.d.ts +3 -0
  158. package/dist/plugin.js +464 -0
  159. package/dist/plugin.js.map +1 -0
  160. package/dist/read/outline.d.ts +7 -0
  161. package/dist/read/outline.js +70 -0
  162. package/dist/read/outline.js.map +1 -0
  163. package/dist/read/readTarget.d.ts +4 -0
  164. package/dist/read/readTarget.js +13 -0
  165. package/dist/read/readTarget.js.map +1 -0
  166. package/dist/read/readable.d.ts +8 -0
  167. package/dist/read/readable.js +131 -0
  168. package/dist/read/readable.js.map +1 -0
  169. package/dist/runtime.d.ts +15 -0
  170. package/dist/runtime.js +14 -0
  171. package/dist/runtime.js.map +1 -0
  172. package/dist/schema/fields.d.ts +113 -0
  173. package/dist/schema/fields.js +1036 -0
  174. package/dist/schema/fields.js.map +1 -0
  175. package/dist/schema/listingFields.d.ts +9 -0
  176. package/dist/schema/listingFields.js +95 -0
  177. package/dist/schema/listingFields.js.map +1 -0
  178. package/dist/schema/slugify.d.ts +2 -0
  179. package/dist/schema/slugify.js +5 -0
  180. package/dist/schema/slugify.js.map +1 -0
  181. package/dist/settings.d.ts +10 -0
  182. package/dist/settings.js +54 -0
  183. package/dist/settings.js.map +1 -0
  184. package/dist/types.d.ts +198 -0
  185. package/dist/types.js +6 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/uploads/routeAsset.d.ts +27 -0
  188. package/dist/uploads/routeAsset.js +127 -0
  189. package/dist/uploads/routeAsset.js.map +1 -0
  190. package/package.json +122 -3
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/chat/tools.ts"],"sourcesContent":["import type { JSONValue } from 'ai'\nimport type { CollectionSlug, GlobalSlug, PayloadRequest, Where } from 'payload'\n\nimport { jsonSchema, tool } from 'ai'\nimport { z } from 'zod'\n\nimport type { SearchFilter } from '../plan/catalogue.js'\nimport type { IncomingFile } from '../plan/parsePlan.js'\nimport type { StoredFile, StoredOperation, StoredPlan } from '../plan/stored.js'\nimport type {\n AiAssistantPluginOptions,\n FieldChangeView,\n MediaView,\n PageContext,\n SubmitPlanOutput,\n} from '../types.js'\n\nimport { aiAssistantPlanSlug, maxReadCharacters } from '../constants.js'\nimport { notAllowed } from '../plan/allowed.js'\nimport {\n canChangeTarget,\n collectionFields,\n describeTarget,\n globalFields,\n planTargets,\n searchWhere,\n titleFieldOf,\n} from '../plan/catalogue.js'\nimport { fieldChanges } from '../plan/diff.js'\nimport { parsePlan } from '../plan/parsePlan.js'\nimport { valueAtPath } from '../plan/tokens.js'\nimport { planView } from '../plan/view.js'\nimport { outline, valueAtReadablePath } from '../read/outline.js'\nimport { readableTarget } from '../read/readTarget.js'\nimport { compiledFieldAtPath, followedSlug, unknownFieldPaths } from '../schema/fields.js'\nimport { missingListingCreatePaths } from '../schema/listingFields.js'\nimport { userIdOf } from '../settings.js'\n\nexport interface AssistantToolsOptions {\n /** The conversation a submitted plan belongs to, so applying it can update the plan card. */\n chatId: string\n collectionRules?: AiAssistantPluginOptions['collectionRules']\n /** Files attached to the conversation that this request uploaded again. */\n files: IncomingFile[]\n /** Called once a plan is saved, so the model run can stop. */\n onPlan: () => void\n /** The document open in the editor, read from its form, so reads include unsaved changes. */\n openDocument?: OpenDocument\n req: PayloadRequest\n}\n\nexport interface OpenDocument {\n context: PageContext\n /** The form values as the assistant reads them. */\n document: unknown\n}\n\n// Exported so the declaration build can name the tool types without depending on @ai-sdk/provider.\nexport type PlanInput = Record<string, JSONValue>\n\n/**\n * The full plan shape, so the model gets it right first time. parsePlan still checks every value,\n * since a schema only describes what the model should send.\n */\nconst planInput = jsonSchema<PlanInput>({\n type: 'object',\n additionalProperties: false,\n required: ['summary', 'blocked', 'operations'],\n properties: {\n summary: { type: 'string', description: 'What the plan changes, in one or two sentences.' },\n blocked: {\n type: 'boolean',\n description: 'True when the editor must choose first. Then operations is empty.',\n },\n questions: { type: 'array', items: { type: 'string' } },\n candidates: {\n type: 'array',\n items: {\n type: 'object',\n required: ['kind', 'slug', 'label'],\n properties: {\n kind: { type: 'string', enum: ['collection', 'global'] },\n slug: { type: 'string' },\n id: { type: 'string' },\n label: { type: 'string' },\n },\n },\n },\n operations: {\n type: 'array',\n items: {\n type: 'object',\n required: ['action', 'kind', 'slug', 'label', 'summary'],\n properties: {\n action: { type: 'string', enum: ['create', 'update'] },\n kind: { type: 'string', enum: ['collection', 'global'] },\n slug: { type: 'string', description: 'Collection or global slug.' },\n id: { type: 'string', description: 'Required to update a collection document.' },\n label: { type: 'string', description: \"The document's title, for the plan card.\" },\n summary: {\n type: 'string',\n description: 'What this operation changes, and whether it saves as a draft.',\n },\n set: { type: 'object', description: 'Field values, nested as in the field guide.' },\n append: {\n type: 'array',\n description: 'Blocks to add to the end of a blocks field.',\n items: {\n type: 'object',\n required: ['path', 'blocks'],\n properties: {\n path: {\n type: 'string',\n description: 'Dotted path to the blocks field, such as content.content.',\n },\n blocks: { type: 'array', items: { type: 'object' } },\n },\n },\n },\n },\n },\n },\n files: {\n type: 'array',\n description: 'Attachments this plan saves. Only listed files are saved.',\n items: {\n type: 'object',\n required: ['index'],\n properties: {\n index: { type: 'integer' },\n alt: { type: 'string', description: 'Required for images.' },\n title: { type: 'string' },\n note: { type: 'string' },\n },\n },\n },\n },\n})\n\n/**\n * Every tool reads with the editor's access. Only submitPlan writes, and it writes a plan, not the documents.\n */\nexport function assistantTools({\n req,\n chatId,\n collectionRules,\n files,\n onPlan,\n openDocument,\n}: AssistantToolsOptions) {\n const targets = planTargets(req.payload.config)\n\n return {\n listTargets: tool({\n description:\n 'List the collections and globals. canChange says whether a plan may change it; the others can still be read.',\n inputSchema: z.object({}),\n execute: () => ({\n targets: targets.map((target) => ({\n slug: target.slug,\n kind: target.kind,\n label: target.label,\n drafts: target.drafts,\n upload: target.upload,\n canChange: canChangeTarget(req.payload.config, target.kind, target.slug),\n })),\n }),\n }),\n describeTarget: tool({\n description:\n 'Return the field guide for one collection or global. Call this before writing its fields. Blocks are listed by name; pass blocks to get the fields of the ones you will write.',\n inputSchema: z.object({\n kind: z.enum(['collection', 'global']),\n slug: z.string(),\n blocks: z.array(z.string()).max(12).optional().describe('Block slugs to describe in full.'),\n }),\n execute: ({ kind, slug, blocks }) =>\n describeTarget(req.payload.config, kind, slug, collectionRules, blocks),\n }),\n searchDocuments: tool({\n description:\n \"Search one collection by its title or name, and filter by any field from its field guide, such as a category or relationship id. An empty query lists recent documents. If more than one result could be the editor's target, do not pick one.\",\n inputSchema: z.object({\n collection: z.string(),\n query: z.string(),\n filters: z\n .array(\n z.object({\n field: z\n .string()\n .regex(/^[\\w.]+$/)\n .describe('Field path from the field guide, such as category.'),\n operator: z.enum(['equals', 'not_equals', 'in', 'like', 'exists']),\n value: z.union([\n z.string(),\n z.number(),\n z.boolean(),\n z.array(z.union([z.string(), z.number()])),\n ]),\n }),\n )\n .max(5)\n .optional(),\n limit: z.number().int().min(1).max(25).optional(),\n }),\n execute: (input) =>\n searchDocuments(req, input.collection, input.query, input.filters, input.limit),\n }),\n readDocument: tool({\n description:\n 'Read one document, or a global, with rich text as markdown. A long document comes back as an outline; pass a path, such as content.content.3, to read one part in full.',\n inputSchema: z.object({\n kind: z.enum(['collection', 'global']),\n slug: z.string(),\n id: z.string().optional(),\n path: z\n .string()\n .optional()\n .describe('Dotted path to one part of the document. List items are numbered from 0.'),\n }),\n execute: (input) => readForModel(req, input, openDocument),\n }),\n submitPlan: tool({\n description:\n 'Submit a plan for the editor to review. This does not save the changes. If this returns an error, correct the plan and call it again.',\n inputSchema: planInput,\n execute: async (input): Promise<SubmitPlanOutput> => {\n const parsed = parsePlan(input, targets, files)\n if (!parsed.ok) {\n return { ok: false, error: parsed.errors.join(' ') }\n }\n const problem =\n notAllowed(parsed.plan, req.payload.config) ??\n unknownFields(req, parsed.plan) ??\n missingListingFields(req, parsed.plan) ??\n (await missingDocuments(req, parsed.plan))\n if (problem) {\n return { ok: false, error: problem }\n }\n\n const status = parsed.plan.blocked ? 'blocked' : 'proposed'\n // Plans are written only here and by the apply endpoints, so a plan always comes from the model and its checks.\n const saved = await req.payload.create({\n collection: aiAssistantPlanSlug as CollectionSlug,\n data: {\n owner: userIdOf(req.user),\n thread: chatId,\n status,\n summary: parsed.plan.summary,\n plan: parsed.plan,\n } as never,\n req,\n })\n onPlan()\n const adminRoute = req.payload.config.routes?.admin ?? '/admin'\n const changes = await planFieldChanges(req, parsed.plan)\n return {\n ok: true,\n status,\n plan: planView(String((saved as { id: unknown }).id), parsed.plan, adminRoute, changes),\n }\n },\n // The editor sees each value beside the current one. The model wrote those values, so it gets the plan without them.\n toModelOutput: ({ output }) => ({ type: 'json', value: withoutFieldChanges(output) }),\n }),\n }\n}\n\nexport type AssistantTools = ReturnType<typeof assistantTools>\n\nasync function planFieldChanges(req: PayloadRequest, plan: StoredPlan) {\n return Promise.all(\n plan.operations.map(async (operation) => {\n const read =\n operation.action === 'update'\n ? await readDocument(req, operation.kind, operation.slug, operation.id)\n : undefined\n const current = read && 'document' in read ? read.document : undefined\n const changes = fieldChanges({\n operation,\n current,\n files: plan.files,\n operations: plan.operations,\n })\n const slug = slugChange(req, operation, current)\n return Promise.all(\n [...changes, ...(slug ? [slug] : [])].map((change) =>\n withMedia(req, operation, change, current, plan.files),\n ),\n )\n }),\n )\n}\n\n/** A rename moves the URL when the slug follows the title, so the card shows it before Proceed. */\nfunction slugChange(\n req: PayloadRequest,\n operation: StoredOperation,\n current: unknown,\n): FieldChangeView | undefined {\n if (operation.action !== 'update' || current === undefined) {\n return undefined\n }\n const fields =\n (operation.kind === 'collection'\n ? collectionFields(req.payload.config, operation.slug)\n : globalFields(req.payload.config, operation.slug)) ?? []\n const slug = followedSlug(fields, current, valueAtPath(operation.set, 'title'))\n if (!slug) {\n return undefined\n }\n const before = valueAtPath(current, 'slug')\n return {\n path: 'slug',\n ...(typeof before === 'string' && before ? { before } : {}),\n after: `${slug} (follows the new title; the URL changes)`,\n }\n}\n\n/** Upload fields show the file on each side, read with the editor's access. */\nasync function withMedia(\n req: PayloadRequest,\n operation: StoredOperation,\n change: FieldChangeView,\n current: unknown,\n files: readonly StoredFile[],\n): Promise<FieldChangeView> {\n if (change.added) {\n return change\n }\n const fields =\n (operation.kind === 'collection'\n ? collectionFields(req.payload.config, operation.slug)\n : globalFields(req.payload.config, operation.slug)) ?? []\n const field = compiledFieldAtPath(fields, change.path)\n if (field?.kind !== 'upload' || field.hasMany) {\n return change\n }\n const [before, after] = await Promise.all([\n mediaView(\n req,\n field.relationTo,\n current === undefined ? undefined : valueAtPath(current, change.path),\n files,\n ),\n mediaView(req, field.relationTo, valueAtPath(operation.set, change.path), files),\n ])\n if (!before && !after) {\n return change\n }\n return { ...change, media: { ...(before ? { before } : {}), ...(after ? { after } : {}) } }\n}\n\nasync function mediaView(\n req: PayloadRequest,\n relationTo: readonly string[],\n value: unknown,\n files: readonly StoredFile[],\n): Promise<MediaView | undefined> {\n if (value && typeof value === 'object' && '$file' in value) {\n const file = files.find((item) => item.index === (value as { $file: unknown }).$file)\n return file ? { label: file.title || file.name, pending: true } : undefined\n }\n const reference =\n value && typeof value === 'object' && 'relationTo' in value && 'value' in value\n ? {\n collection: String((value as { relationTo: unknown }).relationTo),\n id: (value as { value: unknown }).value,\n }\n : { collection: relationTo[0], id: value }\n if (\n !reference.collection ||\n (typeof reference.id !== 'string' && typeof reference.id !== 'number')\n ) {\n return undefined\n }\n try {\n const doc = (await req.payload.findByID({\n collection: reference.collection as CollectionSlug,\n id: reference.id,\n depth: 0,\n overrideAccess: false,\n req,\n user: req.user,\n })) as unknown as Record<string, unknown>\n const text = (key: string) =>\n typeof doc[key] === 'string' && doc[key] ? (doc[key] as string) : undefined\n const isImage = text('mimeType')?.startsWith('image/')\n return {\n label: text('alt') ?? text('title') ?? text('filename') ?? String(reference.id),\n ...(isImage && (text('thumbnailURL') ?? text('url'))\n ? { thumbnail: text('thumbnailURL') ?? text('url') }\n : {}),\n }\n } catch {\n return undefined\n }\n}\n\nfunction withoutFieldChanges(output: SubmitPlanOutput): JSONValue {\n if (!output.ok) {\n return output as unknown as JSONValue\n }\n const operations = output.plan.operations.map(\n ({ fieldChanges: _shown, ...operation }) => operation,\n )\n return { ...output, plan: { ...output.plan, operations } } as unknown as JSONValue\n}\n\nasync function searchDocuments(\n req: PayloadRequest,\n collection: string,\n query: string,\n filters: readonly SearchFilter[] = [],\n limit = 8,\n): Promise<unknown> {\n const target = planTargets(req.payload.config).find(\n (item) => item.kind === 'collection' && item.slug === collection,\n )\n if (!target) {\n return { error: `Unknown collection \"${collection}\".` }\n }\n const titleKey = titleFieldOf(req.payload.config, collection)\n try {\n const found = await req.payload.find({\n collection: collection as CollectionSlug,\n where: searchWhere(req.payload.config, collection, query, filters) as Where,\n sort: '-updatedAt',\n limit,\n depth: 0,\n draft: true,\n overrideAccess: false,\n req,\n user: req.user,\n select: { [titleKey]: true } as never,\n })\n return {\n count: found.docs.length,\n multiple: found.docs.length > 1,\n documents: found.docs.map((doc) => {\n const record = doc as unknown as Record<string, unknown>\n const label = typeof record[titleKey] === 'string' ? record[titleKey] : String(record.id)\n return { id: String(record.id), label }\n }),\n }\n } catch (error) {\n return { error: error instanceof Error ? error.message : 'The search failed.' }\n }\n}\n\n/**\n * What readDocument returns to the model. The open document is read from its form, so it includes unsaved changes.\n * Long documents come back as an outline, and one part can be read in full by path.\n */\nasync function readForModel(\n req: PayloadRequest,\n input: { id?: string; kind: 'collection' | 'global'; path?: string; slug: string },\n open: OpenDocument | undefined,\n): Promise<unknown> {\n const isOpen =\n open?.context.kind === input.kind &&\n open.context.slug === input.slug &&\n (input.kind === 'global' || open.context.id === input.id)\n const read =\n isOpen && open\n ? { label: open.context.label, document: open.document }\n : await readDocument(req, input.kind, input.slug, input.id)\n if ('error' in read) {\n return read\n }\n\n const path = input.path?.trim()\n if (path) {\n const value = valueAtReadablePath(read.document, path)\n if (value === undefined) {\n return { error: `Nothing at ${path}. Read the document without a path to see its outline.` }\n }\n const json = JSON.stringify(value) ?? ''\n return json.length > maxReadCharacters\n ? {\n label: read.label,\n path,\n outline: outline(value),\n note: 'This part is long, so this is an outline. Read a deeper path for full values.',\n }\n : { label: read.label, path, value }\n }\n if ((JSON.stringify(read.document) ?? '').length <= maxReadCharacters) {\n return read\n }\n return {\n label: read.label,\n outline: outline(read.document),\n note: 'This document is long, so this is an outline. Call readDocument again with a path for full values.',\n }\n}\n\n/** Reads with the editor's access, so the assistant sees only what the editor can see. */\nexport async function readDocument(\n req: PayloadRequest,\n kind: 'collection' | 'global',\n slug: string,\n id: string | undefined,\n): Promise<{ document: unknown; label: string } | { error: string }> {\n const target = planTargets(req.payload.config).find(\n (item) => item.kind === kind && item.slug === slug,\n )\n if (!target) {\n return { error: `Unknown ${kind} \"${slug}\".` }\n }\n try {\n if (kind === 'global') {\n const doc = await req.payload.findGlobal({\n slug: slug as GlobalSlug,\n depth: 0,\n draft: true,\n overrideAccess: false,\n req,\n user: req.user,\n })\n return {\n label: target.label,\n document: await readableTarget(req.payload.config, kind, slug, doc),\n }\n }\n if (!id) {\n return { error: 'An id is required to read a collection document.' }\n }\n const doc = await req.payload.findByID({\n collection: slug as CollectionSlug,\n id,\n depth: 0,\n draft: true,\n overrideAccess: false,\n req,\n user: req.user,\n })\n const title = (doc as unknown as Record<string, unknown>)[\n titleFieldOf(req.payload.config, slug)\n ]\n return {\n label: typeof title === 'string' && title ? title : id,\n document: await readableTarget(req.payload.config, kind, slug, doc),\n }\n } catch (error) {\n return { error: error instanceof Error ? error.message : 'The document could not be read.' }\n }\n}\n\nfunction unknownFields(req: PayloadRequest, plan: StoredPlan): string | undefined {\n for (const operation of plan.operations) {\n const fields =\n operation.kind === 'collection'\n ? collectionFields(req.payload.config, operation.slug)\n : globalFields(req.payload.config, operation.slug)\n if (!fields) {\n continue\n }\n const unknown = unknownFieldPaths(fields, operation.set)\n if (unknown.length > 0) {\n return `${operation.label} (${operation.slug}) has no field ${unknown.join(', ')}. Use the names in the field guide, nested as objects.`\n }\n }\n return undefined\n}\n\nfunction missingListingFields(req: PayloadRequest, plan: StoredPlan): string | undefined {\n for (const operation of plan.operations) {\n if (operation.action !== 'create') {\n continue\n }\n const fields =\n operation.kind === 'collection'\n ? collectionFields(req.payload.config, operation.slug)\n : globalFields(req.payload.config, operation.slug)\n if (!fields) {\n continue\n }\n const missing = missingListingCreatePaths(fields, operation.set)\n if (missing.length === 0) {\n continue\n }\n return `When creating ${operation.label} (${operation.slug}), fill: ${missing.join(', ')}.`\n }\n return undefined\n}\n\nasync function missingDocuments(\n req: PayloadRequest,\n plan: StoredPlan,\n): Promise<string | undefined> {\n const checks = [\n ...plan.operations\n .filter(\n (operation) =>\n operation.action === 'update' && operation.kind === 'collection' && operation.id,\n )\n .map((operation) => ({\n slug: operation.slug,\n id: operation.id as string,\n message: `No ${operation.slug} document has id ${operation.id}. Search again and use an id from the result.`,\n })),\n ...plan.candidates\n .filter((candidate) => candidate.kind === 'collection' && candidate.id)\n .map((candidate) => ({\n slug: candidate.slug,\n id: candidate.id as string,\n message: `Candidate ${candidate.label} was not found in ${candidate.slug}.`,\n })),\n ]\n for (const check of checks) {\n try {\n await req.payload.findByID({\n collection: check.slug as CollectionSlug,\n id: check.id,\n depth: 0,\n draft: true,\n overrideAccess: false,\n req,\n user: req.user,\n })\n } catch {\n return check.message\n }\n }\n return undefined\n}\n"],"names":["jsonSchema","tool","z","aiAssistantPlanSlug","maxReadCharacters","notAllowed","canChangeTarget","collectionFields","describeTarget","globalFields","planTargets","searchWhere","titleFieldOf","fieldChanges","parsePlan","valueAtPath","planView","outline","valueAtReadablePath","readableTarget","compiledFieldAtPath","followedSlug","unknownFieldPaths","missingListingCreatePaths","userIdOf","planInput","type","additionalProperties","required","properties","summary","description","blocked","questions","items","candidates","kind","enum","slug","id","label","operations","action","set","append","path","blocks","files","index","alt","title","note","assistantTools","req","chatId","collectionRules","onPlan","openDocument","targets","payload","config","listTargets","inputSchema","object","execute","map","target","drafts","upload","canChange","string","array","max","optional","describe","searchDocuments","collection","query","filters","field","regex","operator","value","union","number","boolean","limit","int","min","input","readDocument","readForModel","submitPlan","parsed","ok","error","errors","join","problem","plan","unknownFields","missingListingFields","missingDocuments","status","saved","create","data","owner","user","thread","adminRoute","routes","admin","changes","planFieldChanges","String","toModelOutput","output","withoutFieldChanges","Promise","all","operation","read","undefined","current","document","slugChange","change","withMedia","fields","before","after","added","hasMany","mediaView","relationTo","media","file","find","item","$file","name","pending","reference","doc","findByID","depth","overrideAccess","text","key","isImage","startsWith","thumbnail","_shown","titleKey","found","where","sort","draft","select","count","docs","length","multiple","documents","record","Error","message","open","isOpen","context","trim","json","JSON","stringify","findGlobal","unknown","missing","checks","filter","candidate","check"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import type { GatewayProvider } from '@ai-sdk/gateway';
2
+ import type { PayloadRequest } from 'payload';
3
+ import type { Billing } from '../billing.js';
4
+ type GenerationLookup = Pick<GatewayProvider, 'getGenerationInfo'>;
5
+ /** Where one step's cost went, in US dollars as the gateway prices it. */
6
+ export interface StepCost {
7
+ /** Meant for the own key, but the gateway retried on Vercel's credentials because the key failed. */
8
+ fellBack: boolean;
9
+ /** Billed by the provider to the account that owns the site's own key. */
10
+ ownKeyUsd: number;
11
+ /** Billed to the Vercel team's AI Gateway credit, and counted in the project's budget. */
12
+ vercelUsd: number;
13
+ }
14
+ /** A day's or a month's usage, summed. */
15
+ export interface UsageTotals {
16
+ fallbackSteps: number;
17
+ ownKeyUsd: number;
18
+ replies: number;
19
+ tokens: number;
20
+ vercelUsd: number;
21
+ }
22
+ /** What one model step reports about itself, as far as spend is concerned. */
23
+ export interface StepSpend {
24
+ providerMetadata?: Record<string, Record<string, unknown> | undefined>;
25
+ usage?: {
26
+ totalTokens?: number;
27
+ };
28
+ }
29
+ /** The calendar day in the site's time zone, so the cap resets at local midnight. */
30
+ export declare function dayIn(timeZone: string, date?: Date): string;
31
+ /** The editor's spend today on Vercel's credit, in US dollars, as the gateway billed it. What the daily cap counts. */
32
+ export declare function spentToday(req: PayloadRequest, timeZone: string): Promise<number>;
33
+ /**
34
+ * Adds a reply's cost to the editor's day, split between Vercel's credit and the site's own key.
35
+ * Spend that cannot be priced is still counted by tokens.
36
+ */
37
+ export declare function recordSpend(req: PayloadRequest, timeZone: string, gateway: GenerationLookup, steps: readonly StepSpend[], billing: Billing): Promise<void>;
38
+ /**
39
+ * Where a step's cost went. On Vercel billing, the gateway's reported cost, or its generation when it reports none.
40
+ *
41
+ * On the own key, the gateway's routing metadata says which credentials served the step. Served on the site's own
42
+ * credentials: the provider charges the list price, the gateway's market cost, and Vercel charges nothing. Served on
43
+ * Vercel's: the reported cost is billed to Vercel, and it is a fallback, because the own key was meant to pay.
44
+ * The generation lookup is not used here: with the deployment's OIDC token the gateway refuses it.
45
+ */
46
+ export declare function stepCost(step: StepSpend, gateway: GenerationLookup, billing: Billing): Promise<StepCost>;
47
+ /**
48
+ * The credentials that served a step: the `credentialType` of the provider attempt that succeeded, in the gateway's
49
+ * routing metadata. A failed attempt on the site's own key followed by a successful one on Vercel's reads `system`.
50
+ */
51
+ export declare function servedWith(routing: unknown): 'byok' | 'system' | undefined;
52
+ /** Everyone's usage across a range of days, inclusive, as `YYYY-MM-DD` in the site's time zone. For admin views. */
53
+ export declare function usageTotals(req: PayloadRequest, from: string, to: string): Promise<UsageTotals>;
54
+ /** The first day of the month that `day` falls in, such as `2026-10-01` for `2026-10-26`. */
55
+ export declare function monthStart(day: string): string;
56
+ export declare function amount(value: unknown): number | undefined;
57
+ /** Whether the editor has reached the daily cap, in US dollars as the gateway bills. No cap when it is undefined. */
58
+ export declare function spendCheck({ spentUsd, capUsd, }: {
59
+ capUsd: number | undefined;
60
+ spentUsd: number;
61
+ }): {
62
+ blocked: boolean;
63
+ capUsd: number | undefined;
64
+ spentUsd: number;
65
+ };
66
+ export {};
@@ -0,0 +1,219 @@
1
+ import { aiAssistantUsageSlug } from '../constants.js';
2
+ import { userIdOf } from '../settings.js';
3
+ /** The calendar day in the site's time zone, so the cap resets at local midnight. */ export function dayIn(timeZone, date = new Date()) {
4
+ // en-CA formats as YYYY-MM-DD.
5
+ return new Intl.DateTimeFormat('en-CA', {
6
+ timeZone,
7
+ year: 'numeric',
8
+ month: '2-digit',
9
+ day: '2-digit'
10
+ }).format(date);
11
+ }
12
+ /** The editor's spend today on Vercel's credit, in US dollars, as the gateway billed it. What the daily cap counts. */ export async function spentToday(req, timeZone) {
13
+ const found = await findDay(req, timeZone);
14
+ return found ? found.vercelUsd : 0;
15
+ }
16
+ /**
17
+ * Adds a reply's cost to the editor's day, split between Vercel's credit and the site's own key.
18
+ * Spend that cannot be priced is still counted by tokens.
19
+ */ export async function recordSpend(req, timeZone, gateway, steps, billing) {
20
+ if (steps.length === 0) {
21
+ return;
22
+ }
23
+ const costs = await Promise.all(steps.map((step)=>stepCost(step, gateway, billing)));
24
+ const vercelUsd = costs.reduce((total, cost)=>total + cost.vercelUsd, 0);
25
+ const ownKeyUsd = costs.reduce((total, cost)=>total + cost.ownKeyUsd, 0);
26
+ const fallbackSteps = costs.filter((cost)=>cost.fellBack).length;
27
+ const tokens = steps.reduce((total, step)=>total + (step.usage?.totalTokens ?? 0), 0);
28
+ const existing = await findDay(req, timeZone);
29
+ // Usage is written only here, after the owner's own request, so it is an administrative write.
30
+ if (existing) {
31
+ await req.payload.update({
32
+ collection: aiAssistantUsageSlug,
33
+ id: existing.id,
34
+ data: {
35
+ costUsd: existing.vercelUsd + vercelUsd,
36
+ ownKeyCostUsd: existing.ownKeyUsd + ownKeyUsd,
37
+ fallbackSteps: existing.fallbackSteps + fallbackSteps,
38
+ tokens: existing.tokens + tokens,
39
+ replies: existing.replies + 1
40
+ },
41
+ req
42
+ });
43
+ return;
44
+ }
45
+ await req.payload.create({
46
+ collection: aiAssistantUsageSlug,
47
+ data: {
48
+ owner: userIdOf(req.user),
49
+ day: dayIn(timeZone),
50
+ costUsd: vercelUsd,
51
+ ownKeyCostUsd: ownKeyUsd,
52
+ fallbackSteps,
53
+ tokens,
54
+ replies: 1
55
+ },
56
+ req
57
+ });
58
+ }
59
+ /**
60
+ * Where a step's cost went. On Vercel billing, the gateway's reported cost, or its generation when it reports none.
61
+ *
62
+ * On the own key, the gateway's routing metadata says which credentials served the step. Served on the site's own
63
+ * credentials: the provider charges the list price, the gateway's market cost, and Vercel charges nothing. Served on
64
+ * Vercel's: the reported cost is billed to Vercel, and it is a fallback, because the own key was meant to pay.
65
+ * The generation lookup is not used here: with the deployment's OIDC token the gateway refuses it.
66
+ */ export async function stepCost(step, gateway, billing) {
67
+ const metadata = step.providerMetadata?.gateway;
68
+ const reported = amount(metadata?.cost);
69
+ const vercelOnly = (vercelUsd)=>({
70
+ vercelUsd,
71
+ ownKeyUsd: 0,
72
+ fellBack: false
73
+ });
74
+ if (billing === 'own-key') {
75
+ const credentials = servedWith(metadata?.routing);
76
+ if (credentials === 'byok') {
77
+ return {
78
+ vercelUsd: 0,
79
+ ownKeyUsd: amount(metadata?.marketCost) ?? amount(metadata?.inferenceCost) ?? reported ?? 0,
80
+ fellBack: false
81
+ };
82
+ }
83
+ if (credentials === 'system') {
84
+ return {
85
+ vercelUsd: reported ?? 0,
86
+ ownKeyUsd: 0,
87
+ fellBack: true
88
+ };
89
+ }
90
+ // No routing to go by: counted against Vercel, the safer assumption, but not flagged.
91
+ return vercelOnly(reported ?? 0);
92
+ }
93
+ if (reported !== undefined) {
94
+ return vercelOnly(reported);
95
+ }
96
+ const id = typeof metadata?.generationId === 'string' ? metadata.generationId : undefined;
97
+ if (!id) {
98
+ return vercelOnly(0);
99
+ }
100
+ try {
101
+ return vercelOnly((await gateway.getGenerationInfo({
102
+ id
103
+ })).totalCost);
104
+ } catch {
105
+ return vercelOnly(0);
106
+ }
107
+ }
108
+ /**
109
+ * The credentials that served a step: the `credentialType` of the provider attempt that succeeded, in the gateway's
110
+ * routing metadata. A failed attempt on the site's own key followed by a successful one on Vercel's reads `system`.
111
+ */ export function servedWith(routing) {
112
+ const modelAttempts = isRecord(routing) && Array.isArray(routing.modelAttempts) ? routing.modelAttempts : [];
113
+ for (const model of modelAttempts){
114
+ if (!isRecord(model) || model.success !== true || !Array.isArray(model.providerAttempts)) {
115
+ continue;
116
+ }
117
+ const served = model.providerAttempts.find((attempt)=>isRecord(attempt) && attempt.success === true);
118
+ const credentialType = isRecord(served) ? served.credentialType : undefined;
119
+ if (credentialType === 'byok' || credentialType === 'system') {
120
+ return credentialType;
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ function isRecord(value) {
126
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
127
+ }
128
+ /** Everyone's usage across a range of days, inclusive, as `YYYY-MM-DD` in the site's time zone. For admin views. */ export async function usageTotals(req, from, to) {
129
+ // An administrative read: admins see everyone's usage, and only shares and totals leave the server.
130
+ const found = await req.payload.find({
131
+ collection: aiAssistantUsageSlug,
132
+ where: {
133
+ and: [
134
+ {
135
+ day: {
136
+ greater_than_equal: from
137
+ }
138
+ },
139
+ {
140
+ day: {
141
+ less_than_equal: to
142
+ }
143
+ }
144
+ ]
145
+ },
146
+ pagination: false,
147
+ depth: 0,
148
+ req
149
+ });
150
+ const totals = {
151
+ vercelUsd: 0,
152
+ ownKeyUsd: 0,
153
+ tokens: 0,
154
+ replies: 0,
155
+ fallbackSteps: 0
156
+ };
157
+ for (const doc of found.docs){
158
+ totals.vercelUsd += amount(doc.costUsd) ?? 0;
159
+ totals.ownKeyUsd += amount(doc.ownKeyCostUsd) ?? 0;
160
+ totals.tokens += amount(doc.tokens) ?? 0;
161
+ totals.replies += amount(doc.replies) ?? 0;
162
+ totals.fallbackSteps += amount(doc.fallbackSteps) ?? 0;
163
+ }
164
+ return totals;
165
+ }
166
+ /** The first day of the month that `day` falls in, such as `2026-10-01` for `2026-10-26`. */ export function monthStart(day) {
167
+ return `${day.slice(0, 7)}-01`;
168
+ }
169
+ export function amount(value) {
170
+ const number = typeof value === 'string' ? Number(value) : value;
171
+ return typeof number === 'number' && Number.isFinite(number) && number >= 0 ? number : undefined;
172
+ }
173
+ async function findDay(req, timeZone) {
174
+ const owner = userIdOf(req.user);
175
+ if (!owner) {
176
+ return undefined;
177
+ }
178
+ const found = await req.payload.find({
179
+ collection: aiAssistantUsageSlug,
180
+ where: {
181
+ and: [
182
+ {
183
+ owner: {
184
+ equals: owner
185
+ }
186
+ },
187
+ {
188
+ day: {
189
+ equals: dayIn(timeZone)
190
+ }
191
+ }
192
+ ]
193
+ },
194
+ limit: 1,
195
+ depth: 0,
196
+ req
197
+ });
198
+ const doc = found.docs[0];
199
+ if (!doc) {
200
+ return undefined;
201
+ }
202
+ return {
203
+ id: String(doc.id),
204
+ vercelUsd: amount(doc.costUsd) ?? 0,
205
+ ownKeyUsd: amount(doc.ownKeyCostUsd) ?? 0,
206
+ fallbackSteps: amount(doc.fallbackSteps) ?? 0,
207
+ tokens: amount(doc.tokens) ?? 0,
208
+ replies: amount(doc.replies) ?? 0
209
+ };
210
+ }
211
+ /** Whether the editor has reached the daily cap, in US dollars as the gateway bills. No cap when it is undefined. */ export function spendCheck({ spentUsd, capUsd }) {
212
+ return {
213
+ blocked: capUsd !== undefined && spentUsd >= capUsd,
214
+ spentUsd,
215
+ capUsd
216
+ };
217
+ }
218
+
219
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/chat/usage.ts"],"sourcesContent":["import type { GatewayProvider } from '@ai-sdk/gateway'\nimport type { CollectionSlug, PayloadRequest } from 'payload'\n\nimport type { Billing } from '../billing.js'\n\nimport { aiAssistantUsageSlug } from '../constants.js'\nimport { userIdOf } from '../settings.js'\n\ntype GenerationLookup = Pick<GatewayProvider, 'getGenerationInfo'>\n\n/** Where one step's cost went, in US dollars as the gateway prices it. */\nexport interface StepCost {\n /** Meant for the own key, but the gateway retried on Vercel's credentials because the key failed. */\n fellBack: boolean\n /** Billed by the provider to the account that owns the site's own key. */\n ownKeyUsd: number\n /** Billed to the Vercel team's AI Gateway credit, and counted in the project's budget. */\n vercelUsd: number\n}\n\n/** A day's or a month's usage, summed. */\nexport interface UsageTotals {\n fallbackSteps: number\n ownKeyUsd: number\n replies: number\n tokens: number\n vercelUsd: number\n}\n\n/** What one model step reports about itself, as far as spend is concerned. */\nexport interface StepSpend {\n providerMetadata?: Record<string, Record<string, unknown> | undefined>\n usage?: { totalTokens?: number }\n}\n\n/** The calendar day in the site's time zone, so the cap resets at local midnight. */\nexport function dayIn(timeZone: string, date = new Date()): string {\n // en-CA formats as YYYY-MM-DD.\n return new Intl.DateTimeFormat('en-CA', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).format(date)\n}\n\n/** The editor's spend today on Vercel's credit, in US dollars, as the gateway billed it. What the daily cap counts. */\nexport async function spentToday(req: PayloadRequest, timeZone: string): Promise<number> {\n const found = await findDay(req, timeZone)\n return found ? found.vercelUsd : 0\n}\n\n/**\n * Adds a reply's cost to the editor's day, split between Vercel's credit and the site's own key.\n * Spend that cannot be priced is still counted by tokens.\n */\nexport async function recordSpend(\n req: PayloadRequest,\n timeZone: string,\n gateway: GenerationLookup,\n steps: readonly StepSpend[],\n billing: Billing,\n): Promise<void> {\n if (steps.length === 0) {\n return\n }\n const costs = await Promise.all(steps.map((step) => stepCost(step, gateway, billing)))\n const vercelUsd = costs.reduce((total, cost) => total + cost.vercelUsd, 0)\n const ownKeyUsd = costs.reduce((total, cost) => total + cost.ownKeyUsd, 0)\n const fallbackSteps = costs.filter((cost) => cost.fellBack).length\n const tokens = steps.reduce((total, step) => total + (step.usage?.totalTokens ?? 0), 0)\n\n const existing = await findDay(req, timeZone)\n // Usage is written only here, after the owner's own request, so it is an administrative write.\n if (existing) {\n await req.payload.update({\n collection: aiAssistantUsageSlug as CollectionSlug,\n id: existing.id,\n data: {\n costUsd: existing.vercelUsd + vercelUsd,\n ownKeyCostUsd: existing.ownKeyUsd + ownKeyUsd,\n fallbackSteps: existing.fallbackSteps + fallbackSteps,\n tokens: existing.tokens + tokens,\n replies: existing.replies + 1,\n } as never,\n req,\n })\n return\n }\n await req.payload.create({\n collection: aiAssistantUsageSlug as CollectionSlug,\n data: {\n owner: userIdOf(req.user),\n day: dayIn(timeZone),\n costUsd: vercelUsd,\n ownKeyCostUsd: ownKeyUsd,\n fallbackSteps,\n tokens,\n replies: 1,\n } as never,\n req,\n })\n}\n\n/**\n * Where a step's cost went. On Vercel billing, the gateway's reported cost, or its generation when it reports none.\n *\n * On the own key, the gateway's routing metadata says which credentials served the step. Served on the site's own\n * credentials: the provider charges the list price, the gateway's market cost, and Vercel charges nothing. Served on\n * Vercel's: the reported cost is billed to Vercel, and it is a fallback, because the own key was meant to pay.\n * The generation lookup is not used here: with the deployment's OIDC token the gateway refuses it.\n */\nexport async function stepCost(\n step: StepSpend,\n gateway: GenerationLookup,\n billing: Billing,\n): Promise<StepCost> {\n const metadata = step.providerMetadata?.gateway\n const reported = amount(metadata?.cost)\n const vercelOnly = (vercelUsd: number): StepCost => ({ vercelUsd, ownKeyUsd: 0, fellBack: false })\n\n if (billing === 'own-key') {\n const credentials = servedWith(metadata?.routing)\n if (credentials === 'byok') {\n return {\n vercelUsd: 0,\n ownKeyUsd: amount(metadata?.marketCost) ?? amount(metadata?.inferenceCost) ?? reported ?? 0,\n fellBack: false,\n }\n }\n if (credentials === 'system') {\n return { vercelUsd: reported ?? 0, ownKeyUsd: 0, fellBack: true }\n }\n // No routing to go by: counted against Vercel, the safer assumption, but not flagged.\n return vercelOnly(reported ?? 0)\n }\n\n if (reported !== undefined) {\n return vercelOnly(reported)\n }\n const id = typeof metadata?.generationId === 'string' ? metadata.generationId : undefined\n if (!id) {\n return vercelOnly(0)\n }\n try {\n return vercelOnly((await gateway.getGenerationInfo({ id })).totalCost)\n } catch {\n return vercelOnly(0)\n }\n}\n\n/**\n * The credentials that served a step: the `credentialType` of the provider attempt that succeeded, in the gateway's\n * routing metadata. A failed attempt on the site's own key followed by a successful one on Vercel's reads `system`.\n */\nexport function servedWith(routing: unknown): 'byok' | 'system' | undefined {\n const modelAttempts =\n isRecord(routing) && Array.isArray(routing.modelAttempts) ? routing.modelAttempts : []\n for (const model of modelAttempts) {\n if (!isRecord(model) || model.success !== true || !Array.isArray(model.providerAttempts)) {\n continue\n }\n const served = model.providerAttempts.find(\n (attempt) => isRecord(attempt) && attempt.success === true,\n )\n const credentialType = isRecord(served) ? served.credentialType : undefined\n if (credentialType === 'byok' || credentialType === 'system') {\n return credentialType\n }\n }\n return undefined\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n\n/** Everyone's usage across a range of days, inclusive, as `YYYY-MM-DD` in the site's time zone. For admin views. */\nexport async function usageTotals(\n req: PayloadRequest,\n from: string,\n to: string,\n): Promise<UsageTotals> {\n // An administrative read: admins see everyone's usage, and only shares and totals leave the server.\n const found = await req.payload.find({\n collection: aiAssistantUsageSlug as CollectionSlug,\n where: { and: [{ day: { greater_than_equal: from } }, { day: { less_than_equal: to } }] },\n pagination: false,\n depth: 0,\n req,\n })\n const totals: UsageTotals = {\n vercelUsd: 0,\n ownKeyUsd: 0,\n tokens: 0,\n replies: 0,\n fallbackSteps: 0,\n }\n for (const doc of found.docs as unknown as Record<string, unknown>[]) {\n totals.vercelUsd += amount(doc.costUsd) ?? 0\n totals.ownKeyUsd += amount(doc.ownKeyCostUsd) ?? 0\n totals.tokens += amount(doc.tokens) ?? 0\n totals.replies += amount(doc.replies) ?? 0\n totals.fallbackSteps += amount(doc.fallbackSteps) ?? 0\n }\n return totals\n}\n\n/** The first day of the month that `day` falls in, such as `2026-10-01` for `2026-10-26`. */\nexport function monthStart(day: string): string {\n return `${day.slice(0, 7)}-01`\n}\n\nexport function amount(value: unknown): number | undefined {\n const number = typeof value === 'string' ? Number(value) : value\n return typeof number === 'number' && Number.isFinite(number) && number >= 0 ? number : undefined\n}\n\nasync function findDay(\n req: PayloadRequest,\n timeZone: string,\n): Promise<\n | {\n fallbackSteps: number\n id: string\n ownKeyUsd: number\n replies: number\n tokens: number\n vercelUsd: number\n }\n | undefined\n> {\n const owner = userIdOf(req.user)\n if (!owner) {\n return undefined\n }\n const found = await req.payload.find({\n collection: aiAssistantUsageSlug as CollectionSlug,\n where: { and: [{ owner: { equals: owner } }, { day: { equals: dayIn(timeZone) } }] },\n limit: 1,\n depth: 0,\n req,\n })\n const doc = found.docs[0] as unknown as Record<string, unknown> | undefined\n if (!doc) {\n return undefined\n }\n return {\n id: String(doc.id),\n vercelUsd: amount(doc.costUsd) ?? 0,\n ownKeyUsd: amount(doc.ownKeyCostUsd) ?? 0,\n fallbackSteps: amount(doc.fallbackSteps) ?? 0,\n tokens: amount(doc.tokens) ?? 0,\n replies: amount(doc.replies) ?? 0,\n }\n}\n\n/** Whether the editor has reached the daily cap, in US dollars as the gateway bills. No cap when it is undefined. */\nexport function spendCheck({\n spentUsd,\n capUsd,\n}: {\n capUsd: number | undefined\n spentUsd: number\n}): {\n blocked: boolean\n capUsd: number | undefined\n spentUsd: number\n} {\n return { blocked: capUsd !== undefined && spentUsd >= capUsd, spentUsd, capUsd }\n}\n"],"names":["aiAssistantUsageSlug","userIdOf","dayIn","timeZone","date","Date","Intl","DateTimeFormat","year","month","day","format","spentToday","req","found","findDay","vercelUsd","recordSpend","gateway","steps","billing","length","costs","Promise","all","map","step","stepCost","reduce","total","cost","ownKeyUsd","fallbackSteps","filter","fellBack","tokens","usage","totalTokens","existing","payload","update","collection","id","data","costUsd","ownKeyCostUsd","replies","create","owner","user","metadata","providerMetadata","reported","amount","vercelOnly","credentials","servedWith","routing","marketCost","inferenceCost","undefined","generationId","getGenerationInfo","totalCost","modelAttempts","isRecord","Array","isArray","model","success","providerAttempts","served","find","attempt","credentialType","value","Boolean","usageTotals","from","to","where","and","greater_than_equal","less_than_equal","pagination","depth","totals","doc","docs","monthStart","slice","number","Number","isFinite","equals","limit","String","spendCheck","spentUsd","capUsd","blocked"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Access } from 'payload';
2
+ import type { AssistantSettings } from './types.js';
3
+ /** Can this user run the assistant at all. */
4
+ export declare function assistantAccess(fallback: AssistantSettings): Access;
5
+ /** Plans, conversations, usage, and attachments are private to the editor who made them. */
6
+ export declare function ownerAccess(fallback: AssistantSettings): Access;
@@ -0,0 +1,32 @@
1
+ import { aiAssistantGlobalSlug } from './constants.js';
2
+ import { canUseAssistant, parseSettings, userIdOf } from './settings.js';
3
+ /** Can this user run the assistant at all. */ export function assistantAccess(fallback) {
4
+ return async ({ req })=>{
5
+ if (!req.user) {
6
+ return false;
7
+ }
8
+ const stored = await req.payload.findGlobal({
9
+ slug: aiAssistantGlobalSlug,
10
+ depth: 0,
11
+ overrideAccess: true,
12
+ req
13
+ });
14
+ return canUseAssistant(req.user, parseSettings(stored, fallback));
15
+ };
16
+ }
17
+ /** Plans, conversations, usage, and attachments are private to the editor who made them. */ export function ownerAccess(fallback) {
18
+ const allowed = assistantAccess(fallback);
19
+ return async (args)=>{
20
+ if (!await allowed(args)) {
21
+ return false;
22
+ }
23
+ const owner = userIdOf(args.req.user);
24
+ return owner ? {
25
+ owner: {
26
+ equals: owner
27
+ }
28
+ } : false;
29
+ };
30
+ }
31
+
32
+ //# sourceMappingURL=collectionAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/collectionAccess.ts"],"sourcesContent":["import type { Access, GlobalSlug } from 'payload'\n\nimport type { AssistantSettings } from './types.js'\n\nimport { aiAssistantGlobalSlug } from './constants.js'\nimport { canUseAssistant, parseSettings, userIdOf } from './settings.js'\n\n/** Can this user run the assistant at all. */\nexport function assistantAccess(fallback: AssistantSettings): Access {\n return async ({ req }) => {\n if (!req.user) {\n return false\n }\n const stored = await req.payload.findGlobal({\n slug: aiAssistantGlobalSlug as GlobalSlug,\n depth: 0,\n overrideAccess: true,\n req,\n })\n return canUseAssistant(req.user, parseSettings(stored, fallback))\n }\n}\n\n/** Plans, conversations, usage, and attachments are private to the editor who made them. */\nexport function ownerAccess(fallback: AssistantSettings): Access {\n const allowed = assistantAccess(fallback)\n return async (args) => {\n if (!(await allowed(args))) {\n return false\n }\n const owner = userIdOf(args.req.user)\n return owner ? { owner: { equals: owner } } : false\n }\n}\n"],"names":["aiAssistantGlobalSlug","canUseAssistant","parseSettings","userIdOf","assistantAccess","fallback","req","user","stored","payload","findGlobal","slug","depth","overrideAccess","ownerAccess","allowed","args","owner","equals"],"mappings":"AAIA,SAASA,qBAAqB,QAAQ,iBAAgB;AACtD,SAASC,eAAe,EAAEC,aAAa,EAAEC,QAAQ,QAAQ,gBAAe;AAExE,4CAA4C,GAC5C,OAAO,SAASC,gBAAgBC,QAA2B;IACzD,OAAO,OAAO,EAAEC,GAAG,EAAE;QACnB,IAAI,CAACA,IAAIC,IAAI,EAAE;YACb,OAAO;QACT;QACA,MAAMC,SAAS,MAAMF,IAAIG,OAAO,CAACC,UAAU,CAAC;YAC1CC,MAAMX;YACNY,OAAO;YACPC,gBAAgB;YAChBP;QACF;QACA,OAAOL,gBAAgBK,IAAIC,IAAI,EAAEL,cAAcM,QAAQH;IACzD;AACF;AAEA,0FAA0F,GAC1F,OAAO,SAASS,YAAYT,QAA2B;IACrD,MAAMU,UAAUX,gBAAgBC;IAChC,OAAO,OAAOW;QACZ,IAAI,CAAE,MAAMD,QAAQC,OAAQ;YAC1B,OAAO;QACT;QACA,MAAMC,QAAQd,SAASa,KAAKV,GAAG,CAACC,IAAI;QACpC,OAAOU,QAAQ;YAAEA,OAAO;gBAAEC,QAAQD;YAAM;QAAE,IAAI;IAChD;AACF"}
@@ -0,0 +1,64 @@
1
+ /** Global that stores who can run the assistant and the spend limits. Not versioned. */
2
+ export declare const aiAssistantGlobalSlug = "ai-assistant";
3
+ /** Hidden collection that stores a confirmed plan until it is applied or cancelled. */
4
+ export declare const aiAssistantPlanSlug = "ai-assistant-plans";
5
+ /** Hidden collection that stores each editor's conversations with the assistant. */
6
+ export declare const aiAssistantThreadSlug = "ai-assistant-threads";
7
+ /** Hidden collection that totals each editor's gateway spend per day, for the daily cap. */
8
+ export declare const aiAssistantUsageSlug = "ai-assistant-usage";
9
+ /**
10
+ * Hidden upload collection for files attached in a conversation. The project registers it, so its storage adapter
11
+ * can store the files and, with client uploads, take them straight from the browser.
12
+ */
13
+ export declare const aiAssistantAttachmentSlug = "ai-assistant-attachments";
14
+ /** Attachments are deleted this long after upload. */
15
+ export declare const attachmentRetentionDays = 30;
16
+ /** Expired attachments deleted per sweep, so a sweep never slows a request much. */
17
+ export declare const attachmentSweepBatch = 10;
18
+ /** Where the plugin keeps runtime settings on `config.custom`, for code that only has the request. */
19
+ export declare const pluginCustomKey = "aiAssistantPlugin";
20
+ /** Control in the admin header, beside the account link. Named export on the `./client` entry. */
21
+ export declare const assistantControlPath = "@humaan/payload-plugin-ai-assistant/client#AiAssistantControl";
22
+ /** Warnings at the top of the settings global. Named export on the `./settings-alerts` entry. */
23
+ export declare const settingsAlertsPath = "@humaan/payload-plugin-ai-assistant/settings-alerts#SettingsAlerts";
24
+ /** The project's AI Gateway budget on the settings global. Named export on the `./budget-field` entry. */
25
+ export declare const budgetFieldPath = "@humaan/payload-plugin-ai-assistant/budget-field#BudgetField";
26
+ /** Renders nothing. Tells the assistant which document the editor has open. Named export on the `./page-context` entry. */
27
+ export declare const pageContextBridgePath = "@humaan/payload-plugin-ai-assistant/page-context#PageContextBridge";
28
+ /**
29
+ * Environment variable holding a Vercel access token that reads the project's AI Gateway budget. Kept out of the
30
+ * database: it should belong to a Viewer-role account on the team, so it can read budgets but change nothing.
31
+ */
32
+ export declare const budgetTokenEnvVar = "VERCEL_BUDGET_READ_TOKEN";
33
+ export declare const chatEndpointPath = "/ai-assistant/chat";
34
+ export declare const applyEndpointPath = "/ai-assistant/apply";
35
+ export declare const cancelEndpointPath = "/ai-assistant/cancel";
36
+ export declare const usageEndpointPath = "/ai-assistant/usage";
37
+ /**
38
+ * Share of the project's AI Gateway budget used, from 0 to 1, at which admins see it flagged as low. Fixed rather
39
+ * than a setting: the budget bar already shows the share used, and Vercel's spend alerts tell the team that sets
40
+ * the budget, who are the ones who can raise it.
41
+ */
42
+ export declare const lowBudgetUsedShare = 0.8;
43
+ /** Share of the project's AI Gateway budget, as a percentage, each editor can spend per day. 0 turns the cap off. Overridable in the global. */
44
+ export declare const defaultDailySpendCapPercent = 5;
45
+ /** Vision model with tool use. Overridable with the plugin's `model` option. */
46
+ export declare const defaultModelId = "anthropic/claude-sonnet-5.5";
47
+ /** Collections and globals the assistant never reads or writes, whatever the plugin options say. */
48
+ export declare const deniedSlugs: readonly ["ai-assistant", "ai-assistant-plans", "ai-assistant-threads", "ai-assistant-usage", "ai-assistant-attachments", "users"];
49
+ export declare const maxFiles = 8;
50
+ export declare const maxImageBytes: number;
51
+ export declare const maxAssetBytes: number;
52
+ export declare const maxSourceCharacters = 100000;
53
+ export declare const maxModelSteps = 24;
54
+ export declare const maxThreadTitleCharacters = 80;
55
+ /** Rich text longer than this is cut when the assistant reads a document. */
56
+ export declare const maxReadRichTextCharacters = 8000;
57
+ /** An outline of the open document is sent with each message, cut to this length. */
58
+ export declare const maxContextCharacters = 12000;
59
+ /** A document longer than this comes back from readDocument as an outline, and the model reads parts of it by path. */
60
+ export declare const maxReadCharacters = 40000;
61
+ /** Form values for a large page can run long before they are cut. */
62
+ export declare const maxContextUploadCharacters = 2000000;
63
+ /** Read results from turns older than this are dropped from the model history. Plans are kept. */
64
+ export declare const keepToolResultsForTurns = 2;
@@ -0,0 +1,51 @@
1
+ /** Global that stores who can run the assistant and the spend limits. Not versioned. */ export const aiAssistantGlobalSlug = 'ai-assistant';
2
+ /** Hidden collection that stores a confirmed plan until it is applied or cancelled. */ export const aiAssistantPlanSlug = 'ai-assistant-plans';
3
+ /** Hidden collection that stores each editor's conversations with the assistant. */ export const aiAssistantThreadSlug = 'ai-assistant-threads';
4
+ /** Hidden collection that totals each editor's gateway spend per day, for the daily cap. */ export const aiAssistantUsageSlug = 'ai-assistant-usage';
5
+ /**
6
+ * Hidden upload collection for files attached in a conversation. The project registers it, so its storage adapter
7
+ * can store the files and, with client uploads, take them straight from the browser.
8
+ */ export const aiAssistantAttachmentSlug = 'ai-assistant-attachments';
9
+ /** Attachments are deleted this long after upload. */ export const attachmentRetentionDays = 30;
10
+ /** Expired attachments deleted per sweep, so a sweep never slows a request much. */ export const attachmentSweepBatch = 10;
11
+ /** Where the plugin keeps runtime settings on `config.custom`, for code that only has the request. */ export const pluginCustomKey = 'aiAssistantPlugin';
12
+ /** Control in the admin header, beside the account link. Named export on the `./client` entry. */ export const assistantControlPath = '@humaan/payload-plugin-ai-assistant/client#AiAssistantControl';
13
+ /** Warnings at the top of the settings global. Named export on the `./settings-alerts` entry. */ export const settingsAlertsPath = '@humaan/payload-plugin-ai-assistant/settings-alerts#SettingsAlerts';
14
+ /** The project's AI Gateway budget on the settings global. Named export on the `./budget-field` entry. */ export const budgetFieldPath = '@humaan/payload-plugin-ai-assistant/budget-field#BudgetField';
15
+ /** Renders nothing. Tells the assistant which document the editor has open. Named export on the `./page-context` entry. */ export const pageContextBridgePath = '@humaan/payload-plugin-ai-assistant/page-context#PageContextBridge';
16
+ /**
17
+ * Environment variable holding a Vercel access token that reads the project's AI Gateway budget. Kept out of the
18
+ * database: it should belong to a Viewer-role account on the team, so it can read budgets but change nothing.
19
+ */ export const budgetTokenEnvVar = 'VERCEL_BUDGET_READ_TOKEN';
20
+ export const chatEndpointPath = '/ai-assistant/chat';
21
+ export const applyEndpointPath = '/ai-assistant/apply';
22
+ export const cancelEndpointPath = '/ai-assistant/cancel';
23
+ export const usageEndpointPath = '/ai-assistant/usage';
24
+ /**
25
+ * Share of the project's AI Gateway budget used, from 0 to 1, at which admins see it flagged as low. Fixed rather
26
+ * than a setting: the budget bar already shows the share used, and Vercel's spend alerts tell the team that sets
27
+ * the budget, who are the ones who can raise it.
28
+ */ export const lowBudgetUsedShare = 0.8;
29
+ /** Share of the project's AI Gateway budget, as a percentage, each editor can spend per day. 0 turns the cap off. Overridable in the global. */ export const defaultDailySpendCapPercent = 5;
30
+ /** Vision model with tool use. Overridable with the plugin's `model` option. */ export const defaultModelId = 'anthropic/claude-sonnet-5.5';
31
+ /** Collections and globals the assistant never reads or writes, whatever the plugin options say. */ export const deniedSlugs = [
32
+ aiAssistantGlobalSlug,
33
+ aiAssistantPlanSlug,
34
+ aiAssistantThreadSlug,
35
+ aiAssistantUsageSlug,
36
+ aiAssistantAttachmentSlug,
37
+ 'users'
38
+ ];
39
+ export const maxFiles = 8;
40
+ export const maxImageBytes = 6 * 1024 * 1024;
41
+ export const maxAssetBytes = 20 * 1024 * 1024;
42
+ export const maxSourceCharacters = 100_000;
43
+ export const maxModelSteps = 24;
44
+ export const maxThreadTitleCharacters = 80;
45
+ /** Rich text longer than this is cut when the assistant reads a document. */ export const maxReadRichTextCharacters = 8_000;
46
+ /** An outline of the open document is sent with each message, cut to this length. */ export const maxContextCharacters = 12_000;
47
+ /** A document longer than this comes back from readDocument as an outline, and the model reads parts of it by path. */ export const maxReadCharacters = 40_000;
48
+ /** Form values for a large page can run long before they are cut. */ export const maxContextUploadCharacters = 2_000_000;
49
+ /** Read results from turns older than this are dropped from the model history. Plans are kept. */ export const keepToolResultsForTurns = 2;
50
+
51
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/** Global that stores who can run the assistant and the spend limits. Not versioned. */\nexport const aiAssistantGlobalSlug = 'ai-assistant'\n\n/** Hidden collection that stores a confirmed plan until it is applied or cancelled. */\nexport const aiAssistantPlanSlug = 'ai-assistant-plans'\n\n/** Hidden collection that stores each editor's conversations with the assistant. */\nexport const aiAssistantThreadSlug = 'ai-assistant-threads'\n\n/** Hidden collection that totals each editor's gateway spend per day, for the daily cap. */\nexport const aiAssistantUsageSlug = 'ai-assistant-usage'\n\n/**\n * Hidden upload collection for files attached in a conversation. The project registers it, so its storage adapter\n * can store the files and, with client uploads, take them straight from the browser.\n */\nexport const aiAssistantAttachmentSlug = 'ai-assistant-attachments'\n\n/** Attachments are deleted this long after upload. */\nexport const attachmentRetentionDays = 30\n\n/** Expired attachments deleted per sweep, so a sweep never slows a request much. */\nexport const attachmentSweepBatch = 10\n\n/** Where the plugin keeps runtime settings on `config.custom`, for code that only has the request. */\nexport const pluginCustomKey = 'aiAssistantPlugin'\n\n/** Control in the admin header, beside the account link. Named export on the `./client` entry. */\nexport const assistantControlPath = '@humaan/payload-plugin-ai-assistant/client#AiAssistantControl'\n\n/** Warnings at the top of the settings global. Named export on the `./settings-alerts` entry. */\nexport const settingsAlertsPath =\n '@humaan/payload-plugin-ai-assistant/settings-alerts#SettingsAlerts'\n\n/** The project's AI Gateway budget on the settings global. Named export on the `./budget-field` entry. */\nexport const budgetFieldPath = '@humaan/payload-plugin-ai-assistant/budget-field#BudgetField'\n\n/** Renders nothing. Tells the assistant which document the editor has open. Named export on the `./page-context` entry. */\nexport const pageContextBridgePath =\n '@humaan/payload-plugin-ai-assistant/page-context#PageContextBridge'\n\n/**\n * Environment variable holding a Vercel access token that reads the project's AI Gateway budget. Kept out of the\n * database: it should belong to a Viewer-role account on the team, so it can read budgets but change nothing.\n */\nexport const budgetTokenEnvVar = 'VERCEL_BUDGET_READ_TOKEN'\n\nexport const chatEndpointPath = '/ai-assistant/chat'\nexport const applyEndpointPath = '/ai-assistant/apply'\nexport const cancelEndpointPath = '/ai-assistant/cancel'\nexport const usageEndpointPath = '/ai-assistant/usage'\n\n/**\n * Share of the project's AI Gateway budget used, from 0 to 1, at which admins see it flagged as low. Fixed rather\n * than a setting: the budget bar already shows the share used, and Vercel's spend alerts tell the team that sets\n * the budget, who are the ones who can raise it.\n */\nexport const lowBudgetUsedShare = 0.8\n\n/** Share of the project's AI Gateway budget, as a percentage, each editor can spend per day. 0 turns the cap off. Overridable in the global. */\nexport const defaultDailySpendCapPercent = 5\n\n/** Vision model with tool use. Overridable with the plugin's `model` option. */\nexport const defaultModelId = 'anthropic/claude-sonnet-5.5'\n\n/** Collections and globals the assistant never reads or writes, whatever the plugin options say. */\nexport const deniedSlugs = [\n aiAssistantGlobalSlug,\n aiAssistantPlanSlug,\n aiAssistantThreadSlug,\n aiAssistantUsageSlug,\n aiAssistantAttachmentSlug,\n 'users',\n] as const\n\nexport const maxFiles = 8\nexport const maxImageBytes = 6 * 1024 * 1024\nexport const maxAssetBytes = 20 * 1024 * 1024\nexport const maxSourceCharacters = 100_000\nexport const maxModelSteps = 24\nexport const maxThreadTitleCharacters = 80\n/** Rich text longer than this is cut when the assistant reads a document. */\nexport const maxReadRichTextCharacters = 8_000\n/** An outline of the open document is sent with each message, cut to this length. */\nexport const maxContextCharacters = 12_000\n/** A document longer than this comes back from readDocument as an outline, and the model reads parts of it by path. */\nexport const maxReadCharacters = 40_000\n/** Form values for a large page can run long before they are cut. */\nexport const maxContextUploadCharacters = 2_000_000\n/** Read results from turns older than this are dropped from the model history. Plans are kept. */\nexport const keepToolResultsForTurns = 2\n"],"names":["aiAssistantGlobalSlug","aiAssistantPlanSlug","aiAssistantThreadSlug","aiAssistantUsageSlug","aiAssistantAttachmentSlug","attachmentRetentionDays","attachmentSweepBatch","pluginCustomKey","assistantControlPath","settingsAlertsPath","budgetFieldPath","pageContextBridgePath","budgetTokenEnvVar","chatEndpointPath","applyEndpointPath","cancelEndpointPath","usageEndpointPath","lowBudgetUsedShare","defaultDailySpendCapPercent","defaultModelId","deniedSlugs","maxFiles","maxImageBytes","maxAssetBytes","maxSourceCharacters","maxModelSteps","maxThreadTitleCharacters","maxReadRichTextCharacters","maxContextCharacters","maxReadCharacters","maxContextUploadCharacters","keepToolResultsForTurns"],"mappings":"AAAA,sFAAsF,GACtF,OAAO,MAAMA,wBAAwB,eAAc;AAEnD,qFAAqF,GACrF,OAAO,MAAMC,sBAAsB,qBAAoB;AAEvD,kFAAkF,GAClF,OAAO,MAAMC,wBAAwB,uBAAsB;AAE3D,0FAA0F,GAC1F,OAAO,MAAMC,uBAAuB,qBAAoB;AAExD;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,2BAA0B;AAEnE,oDAAoD,GACpD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC,kFAAkF,GAClF,OAAO,MAAMC,uBAAuB,GAAE;AAEtC,oGAAoG,GACpG,OAAO,MAAMC,kBAAkB,oBAAmB;AAElD,gGAAgG,GAChG,OAAO,MAAMC,uBAAuB,gEAA+D;AAEnG,+FAA+F,GAC/F,OAAO,MAAMC,qBACX,qEAAoE;AAEtE,wGAAwG,GACxG,OAAO,MAAMC,kBAAkB,+DAA8D;AAE7F,yHAAyH,GACzH,OAAO,MAAMC,wBACX,qEAAoE;AAEtE;;;CAGC,GACD,OAAO,MAAMC,oBAAoB,2BAA0B;AAE3D,OAAO,MAAMC,mBAAmB,qBAAoB;AACpD,OAAO,MAAMC,oBAAoB,sBAAqB;AACtD,OAAO,MAAMC,qBAAqB,uBAAsB;AACxD,OAAO,MAAMC,oBAAoB,sBAAqB;AAEtD;;;;CAIC,GACD,OAAO,MAAMC,qBAAqB,IAAG;AAErC,8IAA8I,GAC9I,OAAO,MAAMC,8BAA8B,EAAC;AAE5C,8EAA8E,GAC9E,OAAO,MAAMC,iBAAiB,8BAA6B;AAE3D,kGAAkG,GAClG,OAAO,MAAMC,cAAc;IACzBpB;IACAC;IACAC;IACAC;IACAC;IACA;CACD,CAAS;AAEV,OAAO,MAAMiB,WAAW,EAAC;AACzB,OAAO,MAAMC,gBAAgB,IAAI,OAAO,KAAI;AAC5C,OAAO,MAAMC,gBAAgB,KAAK,OAAO,KAAI;AAC7C,OAAO,MAAMC,sBAAsB,QAAO;AAC1C,OAAO,MAAMC,gBAAgB,GAAE;AAC/B,OAAO,MAAMC,2BAA2B,GAAE;AAC1C,2EAA2E,GAC3E,OAAO,MAAMC,4BAA4B,MAAK;AAC9C,mFAAmF,GACnF,OAAO,MAAMC,uBAAuB,OAAM;AAC1C,qHAAqH,GACrH,OAAO,MAAMC,oBAAoB,OAAM;AACvC,mEAAmE,GACnE,OAAO,MAAMC,6BAA6B,UAAS;AACnD,gGAAgG,GAChG,OAAO,MAAMC,0BAA0B,EAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { AssistantSettings } from '../types.js';
3
+ /** The stored settings when the user may run the assistant, otherwise the response to send. */
4
+ export declare function allowAssistant(req: PayloadRequest, fallback: AssistantSettings): Promise<AssistantSettings | Response>;