@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.
- package/README.md +277 -2
- package/dist/admin/AiAssistantControl.d.ts +11 -0
- package/dist/admin/AiAssistantControl.js +31 -0
- package/dist/admin/AiAssistantControl.js.map +1 -0
- package/dist/admin/AiAssistantPanel.d.ts +10 -0
- package/dist/admin/AiAssistantPanel.js +208 -0
- package/dist/admin/AiAssistantPanel.js.map +1 -0
- package/dist/admin/AiAssistantPanel.module.scss +689 -0
- package/dist/admin/AiSourceSwitch.d.ts +13 -0
- package/dist/admin/AiSourceSwitch.js +14 -0
- package/dist/admin/AiSourceSwitch.js.map +1 -0
- package/dist/admin/BudgetField.d.ts +8 -0
- package/dist/admin/BudgetField.js +145 -0
- package/dist/admin/BudgetField.js.map +1 -0
- package/dist/admin/BudgetField.module.scss +47 -0
- package/dist/admin/Conversation.d.ts +12 -0
- package/dist/admin/Conversation.js +469 -0
- package/dist/admin/Conversation.js.map +1 -0
- package/dist/admin/Markdown.d.ts +9 -0
- package/dist/admin/Markdown.js +63 -0
- package/dist/admin/Markdown.js.map +1 -0
- package/dist/admin/PageContextBridge.d.ts +5 -0
- package/dist/admin/PageContextBridge.js +72 -0
- package/dist/admin/PageContextBridge.js.map +1 -0
- package/dist/admin/Pill.d.ts +11 -0
- package/dist/admin/Pill.js +27 -0
- package/dist/admin/Pill.js.map +1 -0
- package/dist/admin/PlanCard.d.ts +14 -0
- package/dist/admin/PlanCard.js +465 -0
- package/dist/admin/PlanCard.js.map +1 -0
- package/dist/admin/SettingsAlerts.d.ts +7 -0
- package/dist/admin/SettingsAlerts.js +37 -0
- package/dist/admin/SettingsAlerts.js.map +1 -0
- package/dist/admin/UsageSummary.d.ts +11 -0
- package/dist/admin/UsageSummary.js +129 -0
- package/dist/admin/UsageSummary.js.map +1 -0
- package/dist/admin/attachmentUpload.d.ts +25 -0
- package/dist/admin/attachmentUpload.js +76 -0
- package/dist/admin/attachmentUpload.js.map +1 -0
- package/dist/admin/openDocument.d.ts +13 -0
- package/dist/admin/openDocument.js +34 -0
- package/dist/admin/openDocument.js.map +1 -0
- package/dist/admin/percent.d.ts +10 -0
- package/dist/admin/percent.js +21 -0
- package/dist/admin/percent.js.map +1 -0
- package/dist/admin/threads.d.ts +21 -0
- package/dist/admin/threads.js +92 -0
- package/dist/admin/threads.js.map +1 -0
- package/dist/admin/toolSteps.d.ts +12 -0
- package/dist/admin/toolSteps.js +57 -0
- package/dist/admin/toolSteps.js.map +1 -0
- package/dist/admin/transport.d.ts +24 -0
- package/dist/admin/transport.js +75 -0
- package/dist/admin/transport.js.map +1 -0
- package/dist/admin/useAttachments.d.ts +32 -0
- package/dist/admin/useAttachments.js +91 -0
- package/dist/admin/useAttachments.js.map +1 -0
- package/dist/admin/useEntityLabel.d.ts +3 -0
- package/dist/admin/useEntityLabel.js +23 -0
- package/dist/admin/useEntityLabel.js.map +1 -0
- package/dist/attachments/collection.d.ts +15 -0
- package/dist/attachments/collection.js +90 -0
- package/dist/attachments/collection.js.map +1 -0
- package/dist/attachments/conversation.d.ts +25 -0
- package/dist/attachments/conversation.js +105 -0
- package/dist/attachments/conversation.js.map +1 -0
- package/dist/attachments/rules.d.ts +9 -0
- package/dist/attachments/rules.js +17 -0
- package/dist/attachments/rules.js.map +1 -0
- package/dist/attachments/store.d.ts +27 -0
- package/dist/attachments/store.js +187 -0
- package/dist/attachments/store.js.map +1 -0
- package/dist/billing.d.ts +60 -0
- package/dist/billing.js +116 -0
- package/dist/billing.js.map +1 -0
- package/dist/chat/history.d.ts +37 -0
- package/dist/chat/history.js +165 -0
- package/dist/chat/history.js.map +1 -0
- package/dist/chat/messages.d.ts +5 -0
- package/dist/chat/messages.js +3 -0
- package/dist/chat/messages.js.map +1 -0
- package/dist/chat/threads.d.ts +33 -0
- package/dist/chat/threads.js +77 -0
- package/dist/chat/threads.js.map +1 -0
- package/dist/chat/tools.d.ts +76 -0
- package/dist/chat/tools.js +596 -0
- package/dist/chat/tools.js.map +1 -0
- package/dist/chat/usage.d.ts +66 -0
- package/dist/chat/usage.js +219 -0
- package/dist/chat/usage.js.map +1 -0
- package/dist/collectionAccess.d.ts +6 -0
- package/dist/collectionAccess.js +32 -0
- package/dist/collectionAccess.js.map +1 -0
- package/dist/constants.d.ts +64 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/endpoints/access.d.ts +4 -0
- package/dist/endpoints/access.js +28 -0
- package/dist/endpoints/access.js.map +1 -0
- package/dist/endpoints/allowance.d.ts +22 -0
- package/dist/endpoints/allowance.js +37 -0
- package/dist/endpoints/allowance.js.map +1 -0
- package/dist/endpoints/applyEndpoint.d.ts +12 -0
- package/dist/endpoints/applyEndpoint.js +170 -0
- package/dist/endpoints/applyEndpoint.js.map +1 -0
- package/dist/endpoints/budget.d.ts +30 -0
- package/dist/endpoints/budget.js +153 -0
- package/dist/endpoints/budget.js.map +1 -0
- package/dist/endpoints/chatEndpoint.d.ts +12 -0
- package/dist/endpoints/chatEndpoint.js +295 -0
- package/dist/endpoints/chatEndpoint.js.map +1 -0
- package/dist/endpoints/form.d.ts +23 -0
- package/dist/endpoints/form.js +95 -0
- package/dist/endpoints/form.js.map +1 -0
- package/dist/endpoints/usageEndpoint.d.ts +13 -0
- package/dist/endpoints/usageEndpoint.js +70 -0
- package/dist/endpoints/usageEndpoint.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/lexical/editorConfig.d.ts +4 -0
- package/dist/lexical/editorConfig.js +19 -0
- package/dist/lexical/editorConfig.js.map +1 -0
- package/dist/lexical/lexicalToMarkdown.d.ts +10 -0
- package/dist/lexical/lexicalToMarkdown.js +10 -0
- package/dist/lexical/lexicalToMarkdown.js.map +1 -0
- package/dist/lexical/markdownToLexical.d.ts +7 -0
- package/dist/lexical/markdownToLexical.js +10 -0
- package/dist/lexical/markdownToLexical.js.map +1 -0
- package/dist/plan/allowed.d.ts +7 -0
- package/dist/plan/allowed.js +19 -0
- package/dist/plan/allowed.js.map +1 -0
- package/dist/plan/applyPlan.d.ts +16 -0
- package/dist/plan/applyPlan.js +353 -0
- package/dist/plan/applyPlan.js.map +1 -0
- package/dist/plan/catalogue.d.ts +37 -0
- package/dist/plan/catalogue.js +157 -0
- package/dist/plan/catalogue.js.map +1 -0
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +87 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/parsePlan.d.ts +27 -0
- package/dist/plan/parsePlan.js +322 -0
- package/dist/plan/parsePlan.js.map +1 -0
- package/dist/plan/prompt.d.ts +1 -0
- package/dist/plan/prompt.js +45 -0
- package/dist/plan/prompt.js.map +1 -0
- package/dist/plan/stored.d.ts +53 -0
- package/dist/plan/stored.js +3 -0
- package/dist/plan/stored.js.map +1 -0
- package/dist/plan/tokens.d.ts +8 -0
- package/dist/plan/tokens.js +79 -0
- package/dist/plan/tokens.js.map +1 -0
- package/dist/plan/view.d.ts +4 -0
- package/dist/plan/view.js +43 -0
- package/dist/plan/view.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +464 -0
- package/dist/plugin.js.map +1 -0
- package/dist/read/outline.d.ts +7 -0
- package/dist/read/outline.js +70 -0
- package/dist/read/outline.js.map +1 -0
- package/dist/read/readTarget.d.ts +4 -0
- package/dist/read/readTarget.js +13 -0
- package/dist/read/readTarget.js.map +1 -0
- package/dist/read/readable.d.ts +8 -0
- package/dist/read/readable.js +131 -0
- package/dist/read/readable.js.map +1 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +14 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema/fields.d.ts +113 -0
- package/dist/schema/fields.js +1036 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/listingFields.d.ts +9 -0
- package/dist/schema/listingFields.js +95 -0
- package/dist/schema/listingFields.js.map +1 -0
- package/dist/schema/slugify.d.ts +2 -0
- package/dist/schema/slugify.js +5 -0
- package/dist/schema/slugify.js.map +1 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +54 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +198 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/uploads/routeAsset.d.ts +27 -0
- package/dist/uploads/routeAsset.js +127 -0
- package/dist/uploads/routeAsset.js.map +1 -0
- package/package.json +122 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/attachments/conversation.ts"],"sourcesContent":["import type { FileUIPart } from 'ai'\nimport type { PayloadRequest, SanitizedConfig } from 'payload'\n\nimport type { ThreadFile } from '../chat/threads.js'\nimport type { IncomingFile } from '../plan/parsePlan.js'\nimport type { AttachmentRecord } from './store.js'\n\nimport { maxFiles } from '../constants.js'\nimport { uploadTargets } from '../plan/catalogue.js'\nimport { chooseUploadCollection, kindFromMime } from '../uploads/routeAsset.js'\nimport { readAttachment } from './store.js'\n\nexport type AttachmentAddition =\n { added: ThreadFile[]; files: ThreadFile[]; ok: true } | { message: string; ok: false }\n\n/**\n * Adds a message's new attachments to the conversation's list. `found` is what the attachments collection returned\n * for the requested ids, with the editor's access, so an id that is missing is someone else's or no longer exists.\n */\nexport function addAttachments(\n known: readonly ThreadFile[],\n requested: readonly string[],\n found: readonly AttachmentRecord[],\n): AttachmentAddition {\n const knownIds = new Set(known.flatMap((file) => (file.id ? [file.id] : [])))\n const newIds = [...new Set(requested)].filter((id) => !knownIds.has(id))\n if (known.length + newIds.length > maxFiles) {\n return {\n ok: false,\n message: `A conversation can have up to ${maxFiles} files. Start a new conversation for more.`,\n }\n }\n const byId = new Map(found.map((record) => [record.id, record]))\n const added: ThreadFile[] = []\n for (const id of newIds) {\n const record = byId.get(id)\n if (!record) {\n return { ok: false, message: 'An attached file could not be found. Attach it again.' }\n }\n added.push({ id, name: record.name, size: record.size, type: record.type })\n }\n return { ok: true, files: [...known, ...added], added }\n}\n\n/**\n * The conversation's readable attachments, with where a plan would save each one. Only upload collections the\n * assistant may change are considered. A file none of them accepts can still be read, but not saved.\n */\nexport function conversationFiles(\n config: SanitizedConfig,\n files: readonly ThreadFile[],\n): IncomingFile[] {\n const targets = uploadTargets(config).map((target) => {\n const collection = config.collections.find((item) => item.slug === target.slug)\n const drafts = Boolean(\n collection?.versions && typeof collection.versions === 'object' && collection.versions.drafts,\n )\n return { ...target, drafts }\n })\n\n return files.flatMap((file, index) => {\n if (!file.id) {\n return []\n }\n const choice = chooseUploadCollection(file, targets)\n const target = 'error' in choice ? undefined : targets.find((item) => item.slug === choice.slug)\n return [\n {\n index,\n attachmentId: file.id,\n name: file.name,\n size: file.size,\n type: 'error' in choice ? file.type : choice.mime,\n collection: target?.slug ?? '',\n kind: 'error' in choice ? kindFromMime(file.type) : choice.kind,\n savesImmediately: !target?.drafts,\n },\n ]\n })\n}\n\n/** Images and PDFs, read from storage so the model can see them. A file that cannot be read is described by name only. */\nexport async function modelFileParts(\n req: PayloadRequest,\n files: readonly IncomingFile[],\n): Promise<Map<number, FileUIPart>> {\n const readable = files.filter((file) => file.kind === 'image' || file.type === 'application/pdf')\n const parts = new Map<number, FileUIPart>()\n await Promise.all(\n readable.map(async (file) => {\n try {\n // The type the bytes arrived as, in case storage converted the format.\n const { data, type } = await readAttachment(req, file.attachmentId)\n parts.set(file.index, {\n type: 'file',\n mediaType: type,\n filename: file.name,\n url: `data:${type};base64,${data.toString('base64')}`,\n })\n } catch (error) {\n req.payload.logger.warn(\n { err: error, attachment: file.attachmentId },\n 'Humaan AI could not read an attachment',\n )\n }\n }),\n )\n return parts\n}\n"],"names":["maxFiles","uploadTargets","chooseUploadCollection","kindFromMime","readAttachment","addAttachments","known","requested","found","knownIds","Set","flatMap","file","id","newIds","filter","has","length","ok","message","byId","Map","map","record","added","get","push","name","size","type","files","conversationFiles","config","targets","target","collection","collections","find","item","slug","drafts","Boolean","versions","index","choice","undefined","attachmentId","mime","kind","savesImmediately","modelFileParts","req","readable","parts","Promise","all","data","set","mediaType","filename","url","toString","error","payload","logger","warn","err","attachment"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a file cannot be attached, or undefined when it can. The panel checks before uploading,
|
|
3
|
+
* and the collection checks again on the server.
|
|
4
|
+
*/
|
|
5
|
+
export declare function attachmentProblem({ name, type, size, }: {
|
|
6
|
+
name: string;
|
|
7
|
+
size: number;
|
|
8
|
+
type: string;
|
|
9
|
+
}): string | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { maxAssetBytes, maxImageBytes } from '../constants.js';
|
|
2
|
+
import { isBlockedFilename } from '../uploads/routeAsset.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why a file cannot be attached, or undefined when it can. The panel checks before uploading,
|
|
5
|
+
* and the collection checks again on the server.
|
|
6
|
+
*/ export function attachmentProblem({ name, type, size }) {
|
|
7
|
+
if (isBlockedFilename(name)) {
|
|
8
|
+
return `"${name}" cannot be attached.`;
|
|
9
|
+
}
|
|
10
|
+
const limit = type.startsWith('image/') ? maxImageBytes : maxAssetBytes;
|
|
11
|
+
if (size > limit) {
|
|
12
|
+
return `"${name}" is larger than ${limit / 1024 / 1024} MB.`;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/attachments/rules.ts"],"sourcesContent":["import { maxAssetBytes, maxImageBytes } from '../constants.js'\nimport { isBlockedFilename } from '../uploads/routeAsset.js'\n\n/**\n * Why a file cannot be attached, or undefined when it can. The panel checks before uploading,\n * and the collection checks again on the server.\n */\nexport function attachmentProblem({\n name,\n type,\n size,\n}: {\n name: string\n size: number\n type: string\n}): string | undefined {\n if (isBlockedFilename(name)) {\n return `\"${name}\" cannot be attached.`\n }\n const limit = type.startsWith('image/') ? maxImageBytes : maxAssetBytes\n if (size > limit) {\n return `\"${name}\" is larger than ${limit / 1024 / 1024} MB.`\n }\n return undefined\n}\n"],"names":["maxAssetBytes","maxImageBytes","isBlockedFilename","attachmentProblem","name","type","size","limit","startsWith","undefined"],"mappings":"AAAA,SAASA,aAAa,EAAEC,aAAa,QAAQ,kBAAiB;AAC9D,SAASC,iBAAiB,QAAQ,2BAA0B;AAE5D;;;CAGC,GACD,OAAO,SAASC,kBAAkB,EAChCC,IAAI,EACJC,IAAI,EACJC,IAAI,EAKL;IACC,IAAIJ,kBAAkBE,OAAO;QAC3B,OAAO,CAAC,CAAC,EAAEA,KAAK,qBAAqB,CAAC;IACxC;IACA,MAAMG,QAAQF,KAAKG,UAAU,CAAC,YAAYP,gBAAgBD;IAC1D,IAAIM,OAAOC,OAAO;QAChB,OAAO,CAAC,CAAC,EAAEH,KAAK,iBAAiB,EAAEG,QAAQ,OAAO,KAAK,IAAI,CAAC;IAC9D;IACA,OAAOE;AACT"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
/** An attachment as a conversation records it. The file itself stays in the project's storage. */
|
|
3
|
+
export interface AttachmentRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
size: number;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function attachmentsConfigured(req: PayloadRequest): boolean;
|
|
10
|
+
/** The editor's own attachments with these ids, in the order asked for. Read access limits it to their own. */
|
|
11
|
+
export declare function findAttachments(req: PayloadRequest, ids: readonly string[]): Promise<AttachmentRecord[]>;
|
|
12
|
+
/** An attachment's bytes and the type they arrived as. */
|
|
13
|
+
export interface AttachmentFile {
|
|
14
|
+
data: Buffer;
|
|
15
|
+
type: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads an attachment's bytes through the collection's storage adapter, the way Payload's file route does:
|
|
19
|
+
* each adapter handler in turn, then the local upload directory. Works for any adapter, public or private.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readAttachment(req: PayloadRequest, id: string): Promise<AttachmentFile>;
|
|
22
|
+
/**
|
|
23
|
+
* Deletes attachments past their retention, a few at a time, from Payload and, through the adapter, from storage.
|
|
24
|
+
* An administrative clean-up, so it runs without a user.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sweepExpiredAttachments(req: PayloadRequest): Promise<number>;
|
|
27
|
+
export declare function recordOf(doc: unknown): AttachmentRecord;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { aiAssistantAttachmentSlug, attachmentRetentionDays, attachmentSweepBatch } from '../constants.js';
|
|
4
|
+
import { pluginRuntime } from '../runtime.js';
|
|
5
|
+
const slug = aiAssistantAttachmentSlug;
|
|
6
|
+
export function attachmentsConfigured(req) {
|
|
7
|
+
return Boolean(req.payload.collections[slug]);
|
|
8
|
+
}
|
|
9
|
+
/** The editor's own attachments with these ids, in the order asked for. Read access limits it to their own. */ export async function findAttachments(req, ids) {
|
|
10
|
+
if (ids.length === 0 || !attachmentsConfigured(req)) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const found = await req.payload.find({
|
|
14
|
+
collection: slug,
|
|
15
|
+
where: {
|
|
16
|
+
id: {
|
|
17
|
+
in: [
|
|
18
|
+
...ids
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
limit: ids.length,
|
|
23
|
+
depth: 0,
|
|
24
|
+
pagination: false,
|
|
25
|
+
overrideAccess: false,
|
|
26
|
+
req,
|
|
27
|
+
user: req.user
|
|
28
|
+
});
|
|
29
|
+
const byId = new Map(found.docs.map((doc)=>[
|
|
30
|
+
String(doc.id),
|
|
31
|
+
recordOf(doc)
|
|
32
|
+
]));
|
|
33
|
+
return ids.flatMap((id)=>{
|
|
34
|
+
const record = byId.get(id);
|
|
35
|
+
return record ? [
|
|
36
|
+
record
|
|
37
|
+
] : [];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads an attachment's bytes through the collection's storage adapter, the way Payload's file route does:
|
|
42
|
+
* each adapter handler in turn, then the local upload directory. Works for any adapter, public or private.
|
|
43
|
+
*/ export async function readAttachment(req, id) {
|
|
44
|
+
const collection = req.payload.collections[slug]?.config;
|
|
45
|
+
if (!collection) {
|
|
46
|
+
throw new Error('Attachments are not set up.');
|
|
47
|
+
}
|
|
48
|
+
const doc = await req.payload.findByID({
|
|
49
|
+
collection: slug,
|
|
50
|
+
id,
|
|
51
|
+
depth: 0,
|
|
52
|
+
overrideAccess: false,
|
|
53
|
+
req,
|
|
54
|
+
user: req.user
|
|
55
|
+
});
|
|
56
|
+
const filename = typeof doc.filename === 'string' ? doc.filename : '';
|
|
57
|
+
if (!filename) {
|
|
58
|
+
throw new Error('The attachment has no file.');
|
|
59
|
+
}
|
|
60
|
+
const type = typeof doc.mimeType === 'string' && doc.mimeType ? doc.mimeType : 'application/octet-stream';
|
|
61
|
+
const prefix = typeof doc.prefix === 'string' ? doc.prefix : undefined;
|
|
62
|
+
const handlerReq = withReadParams(req, collection.slug, filename, pluginRuntime(req.payload.config).attachmentReadParams);
|
|
63
|
+
for (const handler of uploadHandlers(collection)){
|
|
64
|
+
const response = await handler(handlerReq, {
|
|
65
|
+
doc: doc,
|
|
66
|
+
headers: new Headers(),
|
|
67
|
+
params: {
|
|
68
|
+
collection: collection.slug,
|
|
69
|
+
filename,
|
|
70
|
+
...prefix ? {
|
|
71
|
+
prefix
|
|
72
|
+
} : {}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (response instanceof Response) {
|
|
76
|
+
return fileOf(response, type);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
data: await localFile(collection, filename),
|
|
81
|
+
type
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Deletes attachments past their retention, a few at a time, from Payload and, through the adapter, from storage.
|
|
86
|
+
* An administrative clean-up, so it runs without a user.
|
|
87
|
+
*/ export async function sweepExpiredAttachments(req) {
|
|
88
|
+
if (!attachmentsConfigured(req)) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const cutoff = new Date(Date.now() - attachmentRetentionDays * 86_400_000).toISOString();
|
|
92
|
+
const expired = await req.payload.find({
|
|
93
|
+
collection: slug,
|
|
94
|
+
where: {
|
|
95
|
+
createdAt: {
|
|
96
|
+
less_than: cutoff
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
limit: attachmentSweepBatch,
|
|
100
|
+
depth: 0,
|
|
101
|
+
pagination: false,
|
|
102
|
+
req
|
|
103
|
+
});
|
|
104
|
+
let deleted = 0;
|
|
105
|
+
for (const doc of expired.docs){
|
|
106
|
+
try {
|
|
107
|
+
await req.payload.delete({
|
|
108
|
+
collection: slug,
|
|
109
|
+
id: String(doc.id),
|
|
110
|
+
req
|
|
111
|
+
});
|
|
112
|
+
deleted += 1;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
req.payload.logger.error({
|
|
115
|
+
err: error
|
|
116
|
+
}, 'Humaan AI could not delete an expired attachment');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return deleted;
|
|
120
|
+
}
|
|
121
|
+
export function recordOf(doc) {
|
|
122
|
+
const value = doc;
|
|
123
|
+
return {
|
|
124
|
+
id: String(value.id),
|
|
125
|
+
name: typeof value.filename === 'string' ? value.filename : 'file',
|
|
126
|
+
size: typeof value.filesize === 'number' ? value.filesize : 0,
|
|
127
|
+
type: typeof value.mimeType === 'string' ? value.mimeType : 'application/octet-stream'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Adapter handlers read query parameters from the request, as on Payload's file route (`?tr=…` with ImageKit).
|
|
132
|
+
* This presents the request as a file-route request carrying the configured parameters; everything else is unchanged.
|
|
133
|
+
*/ function withReadParams(req, collectionSlug, filename, params) {
|
|
134
|
+
if (!params || Object.keys(params).length === 0) {
|
|
135
|
+
return req;
|
|
136
|
+
}
|
|
137
|
+
const url = new URL(`/api/${collectionSlug}/file/${encodeURIComponent(filename)}`, req.url ?? 'http://localhost');
|
|
138
|
+
for (const [key, value] of Object.entries(params)){
|
|
139
|
+
url.searchParams.set(key, value);
|
|
140
|
+
}
|
|
141
|
+
return new Proxy(req, {
|
|
142
|
+
get (target, property) {
|
|
143
|
+
if (property === 'url') {
|
|
144
|
+
return url.toString();
|
|
145
|
+
}
|
|
146
|
+
if (property === 'searchParams') {
|
|
147
|
+
return url.searchParams;
|
|
148
|
+
}
|
|
149
|
+
const value = Reflect.get(target, property, target);
|
|
150
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function uploadHandlers(collection) {
|
|
155
|
+
const upload = collection.upload;
|
|
156
|
+
return upload && typeof upload === 'object' && Array.isArray(upload.handlers) ? upload.handlers : [];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Private storage answers with a redirect to a short-lived signed link, which is followed here. Image CDNs can
|
|
160
|
+
* convert formats to suit the requester, so the stored type is asked for, to get the original file back.
|
|
161
|
+
*/ async function fileOf(response, type) {
|
|
162
|
+
const location = response.headers.get('location');
|
|
163
|
+
const final = response.status >= 300 && response.status < 400 && location ? await fetch(location, {
|
|
164
|
+
headers: {
|
|
165
|
+
Accept: type
|
|
166
|
+
}
|
|
167
|
+
}) : response;
|
|
168
|
+
if (!final.ok) {
|
|
169
|
+
throw new Error(`The attachment could not be read (${final.status}).`);
|
|
170
|
+
}
|
|
171
|
+
const received = final.headers.get('content-type')?.split(';')[0]?.trim();
|
|
172
|
+
return {
|
|
173
|
+
data: Buffer.from(await final.arrayBuffer()),
|
|
174
|
+
type: received || type
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function localFile(collection, filename) {
|
|
178
|
+
const upload = collection.upload;
|
|
179
|
+
const directory = path.resolve(upload && typeof upload === 'object' && upload.staticDir ? upload.staticDir : collection.slug);
|
|
180
|
+
const file = path.resolve(directory, filename);
|
|
181
|
+
if (!file.startsWith(directory + path.sep)) {
|
|
182
|
+
throw new Error('Invalid attachment filename.');
|
|
183
|
+
}
|
|
184
|
+
return fs.readFile(file);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/attachments/store.ts"],"sourcesContent":["import type { CollectionSlug, PayloadRequest, SanitizedCollectionConfig } from 'payload'\n\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport {\n aiAssistantAttachmentSlug,\n attachmentRetentionDays,\n attachmentSweepBatch,\n} from '../constants.js'\nimport { pluginRuntime } from '../runtime.js'\n\n/** An attachment as a conversation records it. The file itself stays in the project's storage. */\nexport interface AttachmentRecord {\n id: string\n name: string\n size: number\n type: string\n}\n\nconst slug = aiAssistantAttachmentSlug as CollectionSlug\n\nexport function attachmentsConfigured(req: PayloadRequest): boolean {\n return Boolean(req.payload.collections[slug])\n}\n\n/** The editor's own attachments with these ids, in the order asked for. Read access limits it to their own. */\nexport async function findAttachments(\n req: PayloadRequest,\n ids: readonly string[],\n): Promise<AttachmentRecord[]> {\n if (ids.length === 0 || !attachmentsConfigured(req)) {\n return []\n }\n const found = await req.payload.find({\n collection: slug,\n where: { id: { in: [...ids] } },\n limit: ids.length,\n depth: 0,\n pagination: false,\n overrideAccess: false,\n req,\n user: req.user,\n })\n const byId = new Map(\n found.docs.map((doc) => [String((doc as { id: unknown }).id), recordOf(doc)]),\n )\n return ids.flatMap((id) => {\n const record = byId.get(id)\n return record ? [record] : []\n })\n}\n\n/** An attachment's bytes and the type they arrived as. */\nexport interface AttachmentFile {\n data: Buffer\n type: string\n}\n\n/**\n * Reads an attachment's bytes through the collection's storage adapter, the way Payload's file route does:\n * each adapter handler in turn, then the local upload directory. Works for any adapter, public or private.\n */\nexport async function readAttachment(req: PayloadRequest, id: string): Promise<AttachmentFile> {\n const collection = req.payload.collections[slug]?.config\n if (!collection) {\n throw new Error('Attachments are not set up.')\n }\n const doc = (await req.payload.findByID({\n collection: slug,\n id,\n depth: 0,\n overrideAccess: false,\n req,\n user: req.user,\n })) as unknown as Record<string, unknown>\n const filename = typeof doc.filename === 'string' ? doc.filename : ''\n if (!filename) {\n throw new Error('The attachment has no file.')\n }\n const type =\n typeof doc.mimeType === 'string' && doc.mimeType ? doc.mimeType : 'application/octet-stream'\n const prefix = typeof doc.prefix === 'string' ? doc.prefix : undefined\n\n const handlerReq = withReadParams(\n req,\n collection.slug,\n filename,\n pluginRuntime(req.payload.config).attachmentReadParams,\n )\n for (const handler of uploadHandlers(collection)) {\n const response = await handler(handlerReq, {\n doc: doc as never,\n headers: new Headers(),\n params: { collection: collection.slug, filename, ...(prefix ? { prefix } : {}) },\n })\n if (response instanceof Response) {\n return fileOf(response, type)\n }\n }\n return { data: await localFile(collection, filename), type }\n}\n\n/**\n * Deletes attachments past their retention, a few at a time, from Payload and, through the adapter, from storage.\n * An administrative clean-up, so it runs without a user.\n */\nexport async function sweepExpiredAttachments(req: PayloadRequest): Promise<number> {\n if (!attachmentsConfigured(req)) {\n return 0\n }\n const cutoff = new Date(Date.now() - attachmentRetentionDays * 86_400_000).toISOString()\n const expired = await req.payload.find({\n collection: slug,\n where: { createdAt: { less_than: cutoff } },\n limit: attachmentSweepBatch,\n depth: 0,\n pagination: false,\n req,\n })\n let deleted = 0\n for (const doc of expired.docs) {\n try {\n await req.payload.delete({ collection: slug, id: String((doc as { id: unknown }).id), req })\n deleted += 1\n } catch (error) {\n req.payload.logger.error({ err: error }, 'Humaan AI could not delete an expired attachment')\n }\n }\n return deleted\n}\n\nexport function recordOf(doc: unknown): AttachmentRecord {\n const value = doc as Record<string, unknown>\n return {\n id: String(value.id),\n name: typeof value.filename === 'string' ? value.filename : 'file',\n size: typeof value.filesize === 'number' ? value.filesize : 0,\n type: typeof value.mimeType === 'string' ? value.mimeType : 'application/octet-stream',\n }\n}\n\n/**\n * Adapter handlers read query parameters from the request, as on Payload's file route (`?tr=…` with ImageKit).\n * This presents the request as a file-route request carrying the configured parameters; everything else is unchanged.\n */\nfunction withReadParams(\n req: PayloadRequest,\n collectionSlug: string,\n filename: string,\n params: Record<string, string> | undefined,\n): PayloadRequest {\n if (!params || Object.keys(params).length === 0) {\n return req\n }\n const url = new URL(\n `/api/${collectionSlug}/file/${encodeURIComponent(filename)}`,\n req.url ?? 'http://localhost',\n )\n for (const [key, value] of Object.entries(params)) {\n url.searchParams.set(key, value)\n }\n return new Proxy(req, {\n get(target, property) {\n if (property === 'url') {\n return url.toString()\n }\n if (property === 'searchParams') {\n return url.searchParams\n }\n const value = Reflect.get(target, property, target) as unknown\n return typeof value === 'function'\n ? (value as (...args: unknown[]) => unknown).bind(target)\n : value\n },\n })\n}\n\ntype UploadHandler = NonNullable<\n Extract<SanitizedCollectionConfig['upload'], object>['handlers']\n>[number]\n\nfunction uploadHandlers(collection: SanitizedCollectionConfig): UploadHandler[] {\n const upload = collection.upload\n return upload && typeof upload === 'object' && Array.isArray(upload.handlers)\n ? upload.handlers\n : []\n}\n\n/**\n * Private storage answers with a redirect to a short-lived signed link, which is followed here. Image CDNs can\n * convert formats to suit the requester, so the stored type is asked for, to get the original file back.\n */\nasync function fileOf(response: Response, type: string): Promise<AttachmentFile> {\n const location = response.headers.get('location')\n const final =\n response.status >= 300 && response.status < 400 && location\n ? await fetch(location, { headers: { Accept: type } })\n : response\n if (!final.ok) {\n throw new Error(`The attachment could not be read (${final.status}).`)\n }\n const received = final.headers.get('content-type')?.split(';')[0]?.trim()\n return { data: Buffer.from(await final.arrayBuffer()), type: received || type }\n}\n\nasync function localFile(collection: SanitizedCollectionConfig, filename: string): Promise<Buffer> {\n const upload = collection.upload\n const directory = path.resolve(\n upload && typeof upload === 'object' && upload.staticDir ? upload.staticDir : collection.slug,\n )\n const file = path.resolve(directory, filename)\n if (!file.startsWith(directory + path.sep)) {\n throw new Error('Invalid attachment filename.')\n }\n return fs.readFile(file)\n}\n"],"names":["fs","path","aiAssistantAttachmentSlug","attachmentRetentionDays","attachmentSweepBatch","pluginRuntime","slug","attachmentsConfigured","req","Boolean","payload","collections","findAttachments","ids","length","found","find","collection","where","id","in","limit","depth","pagination","overrideAccess","user","byId","Map","docs","map","doc","String","recordOf","flatMap","record","get","readAttachment","config","Error","findByID","filename","type","mimeType","prefix","undefined","handlerReq","withReadParams","attachmentReadParams","handler","uploadHandlers","response","headers","Headers","params","Response","fileOf","data","localFile","sweepExpiredAttachments","cutoff","Date","now","toISOString","expired","createdAt","less_than","deleted","delete","error","logger","err","value","name","size","filesize","collectionSlug","Object","keys","url","URL","encodeURIComponent","key","entries","searchParams","set","Proxy","target","property","toString","Reflect","bind","upload","Array","isArray","handlers","location","final","status","fetch","Accept","ok","received","split","trim","Buffer","from","arrayBuffer","directory","resolve","staticDir","file","startsWith","sep","readFile"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { JSONValue } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* Who pays for the model. `vercel` bills the Vercel project, capped by its AI Gateway budget. `own-key` sends the
|
|
4
|
+
* site's own provider credentials with each request, so the provider bills the account that owns them.
|
|
5
|
+
*/
|
|
6
|
+
export type Billing = 'own-key' | 'vercel';
|
|
7
|
+
/**
|
|
8
|
+
* One set of credentials for a provider, in the gateway's request-scoped BYOK shape, such as `{ apiKey }`.
|
|
9
|
+
* Values usually come from environment variables, so a value may be undefined until the variable is set.
|
|
10
|
+
*/
|
|
11
|
+
export type ProviderCredentialInput = {
|
|
12
|
+
[field: string]: JSONValue | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The `byok` plugin option: credentials by gateway provider slug, one or a list to try in order.
|
|
16
|
+
* The gateway checks each provider's fields; the plugin only checks that every value is set.
|
|
17
|
+
*/
|
|
18
|
+
export type ByokOption = Record<string, ProviderCredentialInput | ProviderCredentialInput[]>;
|
|
19
|
+
/** A complete set of credentials, ready to send. */
|
|
20
|
+
export type ProviderCredential = {
|
|
21
|
+
[field: string]: JSONValue;
|
|
22
|
+
};
|
|
23
|
+
export type ProviderCredentials = Record<string, ProviderCredential[]>;
|
|
24
|
+
/** The providers the own key covers, without the credentials. Safe to keep on the config for admin views. */
|
|
25
|
+
export interface OwnKeySummary {
|
|
26
|
+
/** The providers' names for people, such as `Amazon Bedrock and Anthropic`. */
|
|
27
|
+
label: string;
|
|
28
|
+
/** Provider slugs, in the order given. The gateway tries these providers first. */
|
|
29
|
+
providers: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface OwnKey extends OwnKeySummary {
|
|
32
|
+
credentials: ProviderCredentials;
|
|
33
|
+
}
|
|
34
|
+
/** What `byok` resolves to: the credentials that are complete, and the providers left out because a value is missing. */
|
|
35
|
+
export interface ResolvedByok {
|
|
36
|
+
/** Names of providers whose credentials are incomplete, usually because an environment variable is not set. */
|
|
37
|
+
incomplete: string[];
|
|
38
|
+
key?: OwnKey;
|
|
39
|
+
}
|
|
40
|
+
/** What pays for this request. Own key only when it is chosen and some credentials are complete. */
|
|
41
|
+
export type ActiveBilling = {
|
|
42
|
+
billing: 'own-key';
|
|
43
|
+
key: OwnKey;
|
|
44
|
+
} | {
|
|
45
|
+
billing: 'vercel';
|
|
46
|
+
};
|
|
47
|
+
/** The reason the `byok` option is malformed, or undefined when it is fine. Missing values are not a fault. */
|
|
48
|
+
export declare function byokProblem(byok: ByokOption | undefined): string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The credentials whose values are all set. A credential with any value undefined or empty is left out, so a site
|
|
51
|
+
* only gets the own key once its environment variables are populated. A provider with none left is listed as incomplete.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveByok(byok: ByokOption | undefined): ResolvedByok;
|
|
54
|
+
/** The billing that applies, whatever is saved. A saved `own-key` falls back to Vercel while no credentials are complete. */
|
|
55
|
+
export declare function activeBilling(chosen: Billing, key: OwnKey | undefined): ActiveBilling;
|
|
56
|
+
/**
|
|
57
|
+
* Removes the credentials from text that might echo them, such as a provider's error message. Every string value
|
|
58
|
+
* of eight characters or more is removed: shorter ones, such as a resource name, are not secret and would mangle the text.
|
|
59
|
+
*/
|
|
60
|
+
export declare function redactKey(text: string, key: OwnKey | undefined): string;
|
package/dist/billing.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Names for the providers people are most likely to bring. Any other slug is shown as it is. */ const providerNames = {
|
|
2
|
+
anthropic: 'Anthropic',
|
|
3
|
+
openai: 'OpenAI',
|
|
4
|
+
azure: 'Azure',
|
|
5
|
+
vertex: 'Google Vertex AI',
|
|
6
|
+
google: 'Google',
|
|
7
|
+
bedrock: 'Amazon Bedrock',
|
|
8
|
+
mistral: 'Mistral',
|
|
9
|
+
groq: 'Groq',
|
|
10
|
+
xai: 'xAI'
|
|
11
|
+
};
|
|
12
|
+
/** The reason the `byok` option is malformed, or undefined when it is fine. Missing values are not a fault. */ export function byokProblem(byok) {
|
|
13
|
+
for (const [provider, entry] of Object.entries(byok ?? {})){
|
|
14
|
+
if (!/^[a-z0-9-]+$/.test(provider)) {
|
|
15
|
+
return `unknown provider "${provider}". Use the gateway's slug, such as "anthropic".`;
|
|
16
|
+
}
|
|
17
|
+
const list = Array.isArray(entry) ? entry : [
|
|
18
|
+
entry
|
|
19
|
+
];
|
|
20
|
+
if (list.length === 0 || list.some((item)=>!isRecord(item) || Object.keys(item).length === 0)) {
|
|
21
|
+
return `"${provider}" needs at least one credential object.`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The credentials whose values are all set. A credential with any value undefined or empty is left out, so a site
|
|
28
|
+
* only gets the own key once its environment variables are populated. A provider with none left is listed as incomplete.
|
|
29
|
+
*/ export function resolveByok(byok) {
|
|
30
|
+
const credentials = {};
|
|
31
|
+
const incomplete = [];
|
|
32
|
+
for (const [provider, entry] of Object.entries(byok ?? {})){
|
|
33
|
+
const complete = (Array.isArray(entry) ? entry : [
|
|
34
|
+
entry
|
|
35
|
+
]).filter(isComplete);
|
|
36
|
+
if (complete.length > 0) {
|
|
37
|
+
credentials[provider] = complete;
|
|
38
|
+
} else {
|
|
39
|
+
incomplete.push(nameOf(provider));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const providers = Object.keys(credentials);
|
|
43
|
+
if (providers.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
incomplete
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
key: {
|
|
50
|
+
credentials,
|
|
51
|
+
providers,
|
|
52
|
+
label: listOf(providers.map(nameOf))
|
|
53
|
+
},
|
|
54
|
+
incomplete
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The billing that applies, whatever is saved. A saved `own-key` falls back to Vercel while no credentials are complete. */ export function activeBilling(chosen, key) {
|
|
58
|
+
return chosen === 'own-key' && key ? {
|
|
59
|
+
billing: 'own-key',
|
|
60
|
+
key
|
|
61
|
+
} : {
|
|
62
|
+
billing: 'vercel'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Removes the credentials from text that might echo them, such as a provider's error message. Every string value
|
|
67
|
+
* of eight characters or more is removed: shorter ones, such as a resource name, are not secret and would mangle the text.
|
|
68
|
+
*/ export function redactKey(text, key) {
|
|
69
|
+
if (!key) {
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
return secretsOf(key.credentials).sort((a, b)=>b.length - a.length).reduce((redacted, secret)=>redacted.split(secret).join('[redacted]'), text);
|
|
73
|
+
}
|
|
74
|
+
function isComplete(credential) {
|
|
75
|
+
return Object.values(credential).every(isSet);
|
|
76
|
+
}
|
|
77
|
+
/** Set all the way down, so a nested value such as Vertex's `googleCredentials.privateKey` counts too. */ function isSet(value) {
|
|
78
|
+
if (value === undefined || value === null) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
return value.trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.every(isSet);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'object') {
|
|
88
|
+
return Object.values(value).every(isSet);
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function secretsOf(value) {
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
return value.length >= 8 ? [
|
|
95
|
+
value
|
|
96
|
+
] : [];
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return value.flatMap(secretsOf);
|
|
100
|
+
}
|
|
101
|
+
if (isRecord(value)) {
|
|
102
|
+
return Object.values(value).flatMap(secretsOf);
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
function nameOf(provider) {
|
|
107
|
+
return providerNames[provider] ?? provider;
|
|
108
|
+
}
|
|
109
|
+
function listOf(names) {
|
|
110
|
+
return names.length <= 1 ? names[0] ?? '' : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
|
|
111
|
+
}
|
|
112
|
+
function isRecord(value) {
|
|
113
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=billing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/billing.ts"],"sourcesContent":["import type { JSONValue } from 'ai'\n\n/**\n * Who pays for the model. `vercel` bills the Vercel project, capped by its AI Gateway budget. `own-key` sends the\n * site's own provider credentials with each request, so the provider bills the account that owns them.\n */\nexport type Billing = 'own-key' | 'vercel'\n\n/**\n * One set of credentials for a provider, in the gateway's request-scoped BYOK shape, such as `{ apiKey }`.\n * Values usually come from environment variables, so a value may be undefined until the variable is set.\n */\nexport type ProviderCredentialInput = { [field: string]: JSONValue | undefined }\n\n/**\n * The `byok` plugin option: credentials by gateway provider slug, one or a list to try in order.\n * The gateway checks each provider's fields; the plugin only checks that every value is set.\n */\nexport type ByokOption = Record<string, ProviderCredentialInput | ProviderCredentialInput[]>\n\n/** A complete set of credentials, ready to send. */\nexport type ProviderCredential = { [field: string]: JSONValue }\n\nexport type ProviderCredentials = Record<string, ProviderCredential[]>\n\n/** The providers the own key covers, without the credentials. Safe to keep on the config for admin views. */\nexport interface OwnKeySummary {\n /** The providers' names for people, such as `Amazon Bedrock and Anthropic`. */\n label: string\n /** Provider slugs, in the order given. The gateway tries these providers first. */\n providers: string[]\n}\n\nexport interface OwnKey extends OwnKeySummary {\n credentials: ProviderCredentials\n}\n\n/** What `byok` resolves to: the credentials that are complete, and the providers left out because a value is missing. */\nexport interface ResolvedByok {\n /** Names of providers whose credentials are incomplete, usually because an environment variable is not set. */\n incomplete: string[]\n key?: OwnKey\n}\n\n/** What pays for this request. Own key only when it is chosen and some credentials are complete. */\nexport type ActiveBilling = { billing: 'own-key'; key: OwnKey } | { billing: 'vercel' }\n\n/** Names for the providers people are most likely to bring. Any other slug is shown as it is. */\nconst providerNames: Record<string, string> = {\n anthropic: 'Anthropic',\n openai: 'OpenAI',\n azure: 'Azure',\n vertex: 'Google Vertex AI',\n google: 'Google',\n bedrock: 'Amazon Bedrock',\n mistral: 'Mistral',\n groq: 'Groq',\n xai: 'xAI',\n}\n\n/** The reason the `byok` option is malformed, or undefined when it is fine. Missing values are not a fault. */\nexport function byokProblem(byok: ByokOption | undefined): string | undefined {\n for (const [provider, entry] of Object.entries(byok ?? {})) {\n if (!/^[a-z0-9-]+$/.test(provider)) {\n return `unknown provider \"${provider}\". Use the gateway's slug, such as \"anthropic\".`\n }\n const list = Array.isArray(entry) ? entry : [entry]\n if (\n list.length === 0 ||\n list.some((item) => !isRecord(item) || Object.keys(item).length === 0)\n ) {\n return `\"${provider}\" needs at least one credential object.`\n }\n }\n return undefined\n}\n\n/**\n * The credentials whose values are all set. A credential with any value undefined or empty is left out, so a site\n * only gets the own key once its environment variables are populated. A provider with none left is listed as incomplete.\n */\nexport function resolveByok(byok: ByokOption | undefined): ResolvedByok {\n const credentials: ProviderCredentials = {}\n const incomplete: string[] = []\n for (const [provider, entry] of Object.entries(byok ?? {})) {\n const complete = (Array.isArray(entry) ? entry : [entry]).filter(isComplete)\n if (complete.length > 0) {\n credentials[provider] = complete\n } else {\n incomplete.push(nameOf(provider))\n }\n }\n const providers = Object.keys(credentials)\n if (providers.length === 0) {\n return { incomplete }\n }\n return { key: { credentials, providers, label: listOf(providers.map(nameOf)) }, incomplete }\n}\n\n/** The billing that applies, whatever is saved. A saved `own-key` falls back to Vercel while no credentials are complete. */\nexport function activeBilling(chosen: Billing, key: OwnKey | undefined): ActiveBilling {\n return chosen === 'own-key' && key ? { billing: 'own-key', key } : { billing: 'vercel' }\n}\n\n/**\n * Removes the credentials from text that might echo them, such as a provider's error message. Every string value\n * of eight characters or more is removed: shorter ones, such as a resource name, are not secret and would mangle the text.\n */\nexport function redactKey(text: string, key: OwnKey | undefined): string {\n if (!key) {\n return text\n }\n return secretsOf(key.credentials)\n .sort((a, b) => b.length - a.length)\n .reduce((redacted, secret) => redacted.split(secret).join('[redacted]'), text)\n}\n\nfunction isComplete(credential: ProviderCredentialInput): credential is ProviderCredential {\n return Object.values(credential).every(isSet)\n}\n\n/** Set all the way down, so a nested value such as Vertex's `googleCredentials.privateKey` counts too. */\nfunction isSet(value: JSONValue | undefined): boolean {\n if (value === undefined || value === null) {\n return false\n }\n if (typeof value === 'string') {\n return value.trim().length > 0\n }\n if (Array.isArray(value)) {\n return value.every(isSet)\n }\n if (typeof value === 'object') {\n return Object.values(value).every(isSet)\n }\n return true\n}\n\nfunction secretsOf(value: unknown): string[] {\n if (typeof value === 'string') {\n return value.length >= 8 ? [value] : []\n }\n if (Array.isArray(value)) {\n return value.flatMap(secretsOf)\n }\n if (isRecord(value)) {\n return Object.values(value).flatMap(secretsOf)\n }\n return []\n}\n\nfunction nameOf(provider: string): string {\n return providerNames[provider] ?? provider\n}\n\nfunction listOf(names: string[]): string {\n return names.length <= 1\n ? (names[0] ?? '')\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n}\n"],"names":["providerNames","anthropic","openai","azure","vertex","google","bedrock","mistral","groq","xai","byokProblem","byok","provider","entry","Object","entries","test","list","Array","isArray","length","some","item","isRecord","keys","undefined","resolveByok","credentials","incomplete","complete","filter","isComplete","push","nameOf","providers","key","label","listOf","map","activeBilling","chosen","billing","redactKey","text","secretsOf","sort","a","b","reduce","redacted","secret","split","join","credential","values","every","isSet","value","trim","flatMap","names","slice","Boolean"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FileUIPart } from 'ai';
|
|
2
|
+
import type { IncomingFile } from '../plan/parsePlan.js';
|
|
3
|
+
import type { AssistantChange, PlanStatus } from '../types.js';
|
|
4
|
+
import type { AssistantUIMessage } from './messages.js';
|
|
5
|
+
/**
|
|
6
|
+
* Read results from older turns are the bulk of a long conversation. Plans stay, because later turns refer to them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pruneToolResults(messages: AssistantUIMessage[], keepTurns: number): AssistantUIMessage[];
|
|
9
|
+
/**
|
|
10
|
+
* The JSON field stores an undefined property as null. The message schema allows a missing property but not null,
|
|
11
|
+
* so a stored thread would fail validation on the next turn. Values inside tool inputs and outputs are left alone.
|
|
12
|
+
* Empty assistant replies are dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function restoreStoredMessages(messages: unknown[]): AssistantUIMessage[];
|
|
15
|
+
export interface TurnContext {
|
|
16
|
+
/** Images and PDFs the model can look at, by conversation index. */
|
|
17
|
+
fileParts: ReadonlyMap<number, FileUIPart>;
|
|
18
|
+
/** Files this request uploaded, by conversation index. An earlier file missing here is no longer available. */
|
|
19
|
+
files: readonly IncomingFile[];
|
|
20
|
+
/** The open document's current values, added to the newest message only. */
|
|
21
|
+
openDocument?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Adds what the model needs to know about each user message: the open document and its attachments.
|
|
25
|
+
* This is added for the model run only. The stored thread keeps what the editor typed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withTurnContext(messages: AssistantUIMessage[], context: TurnContext): AssistantUIMessage[];
|
|
28
|
+
/** Records what the editor did with a plan, so the card and the model both see it. */
|
|
29
|
+
export declare function markPlan(messages: AssistantUIMessage[], planId: string, update: {
|
|
30
|
+
changes?: AssistantChange[];
|
|
31
|
+
status: PlanStatus;
|
|
32
|
+
}): {
|
|
33
|
+
changed: boolean;
|
|
34
|
+
messages: AssistantUIMessage[];
|
|
35
|
+
};
|
|
36
|
+
export declare function messageText(message: AssistantUIMessage): string;
|
|
37
|
+
export declare function threadTitle(text: string): string;
|