@directus/api 33.3.0 → 34.0.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/dist/ai/chat/lib/create-ui-stream.js +2 -1
- package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
- package/dist/ai/chat/lib/transform-file-parts.js +36 -0
- package/dist/ai/files/adapters/anthropic.d.ts +3 -0
- package/dist/ai/files/adapters/anthropic.js +25 -0
- package/dist/ai/files/adapters/google.d.ts +3 -0
- package/dist/ai/files/adapters/google.js +58 -0
- package/dist/ai/files/adapters/index.d.ts +3 -0
- package/dist/ai/files/adapters/index.js +3 -0
- package/dist/ai/files/adapters/openai.d.ts +3 -0
- package/dist/ai/files/adapters/openai.js +22 -0
- package/dist/ai/files/controllers/upload.d.ts +2 -0
- package/dist/ai/files/controllers/upload.js +101 -0
- package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
- package/dist/ai/files/lib/fetch-provider.js +23 -0
- package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
- package/dist/ai/files/lib/upload-to-provider.js +26 -0
- package/dist/ai/files/router.d.ts +1 -0
- package/dist/ai/files/router.js +5 -0
- package/dist/ai/files/types.d.ts +5 -0
- package/dist/ai/files/types.js +1 -0
- package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
- package/dist/ai/providers/anthropic-file-support.js +94 -0
- package/dist/ai/providers/registry.js +3 -6
- package/dist/ai/tools/flows/index.d.ts +16 -16
- package/dist/ai/tools/schema.d.ts +8 -8
- package/dist/ai/tools/schema.js +2 -2
- package/dist/app.js +10 -1
- package/dist/controllers/deployment-webhooks.d.ts +2 -0
- package/dist/controllers/deployment-webhooks.js +95 -0
- package/dist/controllers/deployment.js +61 -165
- package/dist/controllers/files.js +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
- package/dist/database/helpers/date/dialects/oracle.js +2 -0
- package/dist/database/helpers/date/dialects/sqlite.js +2 -0
- package/dist/database/helpers/date/types.d.ts +1 -1
- package/dist/database/helpers/date/types.js +3 -1
- package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/mssql.js +21 -0
- package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
- package/dist/database/helpers/fn/dialects/mysql.js +30 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/oracle.js +21 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
- package/dist/database/helpers/fn/dialects/postgres.js +40 -0
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
- package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
- package/dist/database/helpers/fn/json/parse-function.js +66 -0
- package/dist/database/helpers/fn/types.d.ts +8 -0
- package/dist/database/helpers/fn/types.js +19 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +11 -0
- package/dist/database/helpers/schema/types.d.ts +1 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
- package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +17 -7
- package/dist/database/run-ast/lib/parse-current-level.js +8 -1
- package/dist/database/run-ast/run-ast.js +11 -1
- package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
- package/dist/database/run-ast/utils/get-column.js +13 -2
- package/dist/deployment/deployment.d.ts +25 -2
- package/dist/deployment/drivers/netlify.d.ts +6 -2
- package/dist/deployment/drivers/netlify.js +114 -12
- package/dist/deployment/drivers/vercel.d.ts +5 -2
- package/dist/deployment/drivers/vercel.js +84 -5
- package/dist/deployment.d.ts +5 -0
- package/dist/deployment.js +34 -0
- package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
- package/dist/request/is-denied-ip.js +24 -23
- package/dist/services/authentication.js +27 -22
- package/dist/services/collections.js +1 -0
- package/dist/services/deployment-projects.d.ts +31 -2
- package/dist/services/deployment-projects.js +109 -5
- package/dist/services/deployment-runs.d.ts +19 -1
- package/dist/services/deployment-runs.js +86 -0
- package/dist/services/deployment.d.ts +44 -3
- package/dist/services/deployment.js +263 -15
- package/dist/services/files/utils/get-metadata.js +6 -6
- package/dist/services/files.d.ts +3 -1
- package/dist/services/files.js +26 -3
- package/dist/services/graphql/resolvers/query.js +23 -6
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +3 -0
- package/dist/services/users.js +6 -1
- package/dist/utils/get-field-relational-depth.d.ts +13 -0
- package/dist/utils/get-field-relational-depth.js +22 -0
- package/dist/utils/parse-value.d.ts +4 -0
- package/dist/utils/parse-value.js +11 -0
- package/dist/utils/sanitize-query.js +3 -2
- package/dist/utils/split-fields.d.ts +4 -0
- package/dist/utils/split-fields.js +32 -0
- package/dist/utils/validate-query.js +2 -1
- package/package.json +28 -28
|
@@ -3,6 +3,7 @@ import { convertToModelMessages, stepCountIs, streamText, } from 'ai';
|
|
|
3
3
|
import { buildProviderConfigs, createAIProviderRegistry, getProviderOptions, } from '../../providers/index.js';
|
|
4
4
|
import { SYSTEM_PROMPT } from '../constants/system-prompt.js';
|
|
5
5
|
import { formatContextForSystemPrompt } from '../utils/format-context.js';
|
|
6
|
+
import { transformFilePartsForProvider } from './transform-file-parts.js';
|
|
6
7
|
export const createUiStream = async (messages, { provider, model, tools, aiSettings, systemPrompt, context, onUsage }) => {
|
|
7
8
|
const configs = buildProviderConfigs(aiSettings);
|
|
8
9
|
const providerConfig = configs.find((c) => c.type === provider);
|
|
@@ -18,7 +19,7 @@ export const createUiStream = async (messages, { provider, model, tools, aiSetti
|
|
|
18
19
|
const stream = streamText({
|
|
19
20
|
system: baseSystemPrompt,
|
|
20
21
|
model: registry.languageModel(`${provider}:${model}`),
|
|
21
|
-
messages: await convertToModelMessages(messages),
|
|
22
|
+
messages: await convertToModelMessages(transformFilePartsForProvider(messages)),
|
|
22
23
|
stopWhen: [stepCountIs(10)],
|
|
23
24
|
providerOptions,
|
|
24
25
|
tools,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UIMessage } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* Transforms UIMessage file parts to use provider file_id instead of display URL.
|
|
4
|
+
*
|
|
5
|
+
* The frontend sends files with:
|
|
6
|
+
* - url: display URL for UI rendering (blob: or /assets/ URL)
|
|
7
|
+
* - providerMetadata.directus.fileId: the actual provider file ID
|
|
8
|
+
*
|
|
9
|
+
* This function replaces the url with the fileId so the AI SDK can use it
|
|
10
|
+
* with the provider's native file handling.
|
|
11
|
+
*/
|
|
12
|
+
export declare function transformFilePartsForProvider(messages: UIMessage[]): UIMessage[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useLogger } from '../../../logger/index.js';
|
|
2
|
+
function isFileUIPart(part) {
|
|
3
|
+
return typeof part === 'object' && part !== null && part.type === 'file';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Transforms UIMessage file parts to use provider file_id instead of display URL.
|
|
7
|
+
*
|
|
8
|
+
* The frontend sends files with:
|
|
9
|
+
* - url: display URL for UI rendering (blob: or /assets/ URL)
|
|
10
|
+
* - providerMetadata.directus.fileId: the actual provider file ID
|
|
11
|
+
*
|
|
12
|
+
* This function replaces the url with the fileId so the AI SDK can use it
|
|
13
|
+
* with the provider's native file handling.
|
|
14
|
+
*/
|
|
15
|
+
export function transformFilePartsForProvider(messages) {
|
|
16
|
+
const logger = useLogger();
|
|
17
|
+
return messages.map((msg) => {
|
|
18
|
+
if (!Array.isArray(msg.parts)) {
|
|
19
|
+
return msg;
|
|
20
|
+
}
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (const part of msg.parts) {
|
|
23
|
+
if (!isFileUIPart(part)) {
|
|
24
|
+
parts.push(part);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const fileId = part.providerMetadata?.directus?.fileId;
|
|
28
|
+
if (!fileId) {
|
|
29
|
+
logger.warn('File part missing providerMetadata.directus.fileId, filtering out');
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
parts.push({ ...part, url: fileId });
|
|
33
|
+
}
|
|
34
|
+
return { ...msg, parts };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fetchProvider } from '../lib/fetch-provider.js';
|
|
2
|
+
export async function uploadToAnthropic(file, apiKey) {
|
|
3
|
+
const formData = new FormData();
|
|
4
|
+
formData.append('file', new Blob([new Uint8Array(file.data)], { type: file.mimeType }), file.filename);
|
|
5
|
+
const result = (await fetchProvider('https://api.anthropic.com/v1/files', {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
'x-api-key': apiKey,
|
|
9
|
+
'anthropic-version': '2023-06-01',
|
|
10
|
+
'anthropic-beta': 'files-api-2025-04-14',
|
|
11
|
+
},
|
|
12
|
+
body: formData,
|
|
13
|
+
}, 'Anthropic'));
|
|
14
|
+
if (!result.id) {
|
|
15
|
+
throw new Error('Anthropic upload returned unexpected response');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
provider: 'anthropic',
|
|
19
|
+
fileId: result.id,
|
|
20
|
+
filename: result.filename ?? file.filename,
|
|
21
|
+
mimeType: result.mime_type ?? file.mimeType,
|
|
22
|
+
sizeBytes: result.size_bytes ?? file.data.length,
|
|
23
|
+
expiresAt: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { fetchProvider } from '../lib/fetch-provider.js';
|
|
2
|
+
export async function uploadToGoogle(file, apiKey) {
|
|
3
|
+
const baseUrl = 'https://generativelanguage.googleapis.com/upload/v1beta/files';
|
|
4
|
+
// Use a manual fetch here since we need the response headers, not JSON
|
|
5
|
+
const UPLOAD_TIMEOUT = 120_000;
|
|
6
|
+
let startResponse;
|
|
7
|
+
try {
|
|
8
|
+
startResponse = await fetch(baseUrl, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'x-goog-api-key': apiKey,
|
|
12
|
+
'X-Goog-Upload-Protocol': 'resumable',
|
|
13
|
+
'X-Goog-Upload-Command': 'start',
|
|
14
|
+
'X-Goog-Upload-Header-Content-Length': String(file.data.length),
|
|
15
|
+
'X-Goog-Upload-Header-Content-Type': file.mimeType,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ file: { display_name: file.filename } }),
|
|
19
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
24
|
+
throw new Error(`Google upload timed out after ${UPLOAD_TIMEOUT / 1000}s`);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (!startResponse.ok) {
|
|
29
|
+
const text = await startResponse.text().catch(() => `HTTP ${startResponse.status}`);
|
|
30
|
+
throw new Error(`Google upload init failed: ${text}`);
|
|
31
|
+
}
|
|
32
|
+
const uploadUrl = startResponse.headers.get('X-Goog-Upload-URL');
|
|
33
|
+
if (!uploadUrl) {
|
|
34
|
+
throw new Error('Google upload init did not return upload URL');
|
|
35
|
+
}
|
|
36
|
+
const result = (await fetchProvider(uploadUrl, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
'X-Goog-Upload-Command': 'upload, finalize',
|
|
40
|
+
'X-Goog-Upload-Offset': '0',
|
|
41
|
+
'Content-Type': file.mimeType,
|
|
42
|
+
},
|
|
43
|
+
body: new Uint8Array(file.data),
|
|
44
|
+
}, 'Google'));
|
|
45
|
+
const fileData = result.file;
|
|
46
|
+
if (!fileData?.uri) {
|
|
47
|
+
throw new Error('Google upload returned unexpected response');
|
|
48
|
+
}
|
|
49
|
+
const parsedSize = parseInt(fileData.sizeBytes ?? '', 10);
|
|
50
|
+
return {
|
|
51
|
+
provider: 'google',
|
|
52
|
+
fileId: fileData.uri,
|
|
53
|
+
filename: fileData.displayName ?? file.filename,
|
|
54
|
+
mimeType: fileData.mimeType ?? file.mimeType,
|
|
55
|
+
sizeBytes: Number.isNaN(parsedSize) ? file.data.length : parsedSize,
|
|
56
|
+
expiresAt: fileData.expirationTime ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fetchProvider } from '../lib/fetch-provider.js';
|
|
2
|
+
export async function uploadToOpenAI(file, apiKey) {
|
|
3
|
+
const formData = new FormData();
|
|
4
|
+
formData.append('file', new Blob([new Uint8Array(file.data)], { type: file.mimeType }), file.filename);
|
|
5
|
+
formData.append('purpose', 'user_data');
|
|
6
|
+
const result = (await fetchProvider('https://api.openai.com/v1/files', {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
9
|
+
body: formData,
|
|
10
|
+
}, 'OpenAI'));
|
|
11
|
+
if (!result.id) {
|
|
12
|
+
throw new Error('OpenAI upload returned unexpected response');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
provider: 'openai',
|
|
16
|
+
fileId: result.id,
|
|
17
|
+
filename: file.filename,
|
|
18
|
+
mimeType: file.mimeType,
|
|
19
|
+
sizeBytes: result.bytes ?? file.data.length,
|
|
20
|
+
expiresAt: null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AI_ALLOWED_MIME_TYPES } from '@directus/ai';
|
|
2
|
+
import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
|
|
3
|
+
import Busboy from 'busboy';
|
|
4
|
+
import { useLogger } from '../../../logger/index.js';
|
|
5
|
+
import { uploadToProvider } from '../lib/upload-to-provider.js';
|
|
6
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
7
|
+
const ALLOWED_MIME_TYPES = new Set(AI_ALLOWED_MIME_TYPES);
|
|
8
|
+
const SUPPORTED_PROVIDERS = new Set(['openai', 'anthropic', 'google']);
|
|
9
|
+
function isSupportedProvider(provider) {
|
|
10
|
+
return SUPPORTED_PROVIDERS.has(provider);
|
|
11
|
+
}
|
|
12
|
+
async function parseMultipart(headers, stream) {
|
|
13
|
+
const contentType = headers['content-type'];
|
|
14
|
+
if (!contentType || !contentType.toLowerCase().startsWith('multipart/')) {
|
|
15
|
+
throw new InvalidPayloadError({ reason: 'Expected multipart/form-data content type' });
|
|
16
|
+
}
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
let file;
|
|
19
|
+
let provider;
|
|
20
|
+
let settled = false;
|
|
21
|
+
const safeReject = (error) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
reject(error);
|
|
26
|
+
};
|
|
27
|
+
const bb = Busboy({
|
|
28
|
+
headers: headers,
|
|
29
|
+
limits: { fileSize: MAX_FILE_SIZE, files: 1 },
|
|
30
|
+
});
|
|
31
|
+
bb.on('file', (_name, fileStream, info) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let exceeded = false;
|
|
34
|
+
fileStream.on('data', (chunk) => chunks.push(chunk));
|
|
35
|
+
fileStream.on('limit', () => {
|
|
36
|
+
exceeded = true;
|
|
37
|
+
fileStream.destroy();
|
|
38
|
+
safeReject(new InvalidPayloadError({ reason: `File exceeds maximum size of ${MAX_FILE_SIZE / (1024 * 1024)}MB` }));
|
|
39
|
+
});
|
|
40
|
+
fileStream.on('close', () => {
|
|
41
|
+
if (exceeded)
|
|
42
|
+
return;
|
|
43
|
+
file = {
|
|
44
|
+
filename: info.filename || 'file',
|
|
45
|
+
mimeType: info.mimeType,
|
|
46
|
+
data: Buffer.concat(chunks),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
fileStream.on('error', safeReject);
|
|
50
|
+
});
|
|
51
|
+
bb.on('field', (name, value) => {
|
|
52
|
+
if (name === 'provider')
|
|
53
|
+
provider = value;
|
|
54
|
+
});
|
|
55
|
+
bb.on('close', () => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
resolve({ file, provider });
|
|
60
|
+
});
|
|
61
|
+
bb.on('error', safeReject);
|
|
62
|
+
stream.pipe(bb);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export const aiFileUploadHandler = async (req, res, next) => {
|
|
66
|
+
const logger = useLogger();
|
|
67
|
+
try {
|
|
68
|
+
if (!req.accountability?.app) {
|
|
69
|
+
throw new ForbiddenError();
|
|
70
|
+
}
|
|
71
|
+
const aiSettings = res.locals['ai']?.settings;
|
|
72
|
+
if (!aiSettings) {
|
|
73
|
+
throw new InvalidPayloadError({ reason: 'AI settings not loaded' });
|
|
74
|
+
}
|
|
75
|
+
const { file, provider } = await parseMultipart(req.headers, req);
|
|
76
|
+
if (!file) {
|
|
77
|
+
throw new InvalidPayloadError({ reason: 'No file provided' });
|
|
78
|
+
}
|
|
79
|
+
if (!provider) {
|
|
80
|
+
throw new InvalidPayloadError({ reason: 'No provider specified' });
|
|
81
|
+
}
|
|
82
|
+
if (!isSupportedProvider(provider)) {
|
|
83
|
+
throw new InvalidPayloadError({
|
|
84
|
+
reason: provider === 'openai-compatible'
|
|
85
|
+
? 'File uploads not supported for openai-compatible provider'
|
|
86
|
+
: `Unsupported provider: ${provider}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!ALLOWED_MIME_TYPES.has(file.mimeType)) {
|
|
90
|
+
throw new InvalidPayloadError({ reason: `Unsupported file type: ${file.mimeType}` });
|
|
91
|
+
}
|
|
92
|
+
const result = await uploadToProvider(file, provider, aiSettings);
|
|
93
|
+
res.json(result);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error && !(error instanceof ForbiddenError) && !(error instanceof InvalidPayloadError)) {
|
|
97
|
+
logger.error(error, 'AI file upload failed');
|
|
98
|
+
}
|
|
99
|
+
next(error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fetchProvider(url: string, options: RequestInit, providerName: string): Promise<unknown>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const UPLOAD_TIMEOUT = 120_000;
|
|
2
|
+
export async function fetchProvider(url, options, providerName) {
|
|
3
|
+
let response;
|
|
4
|
+
try {
|
|
5
|
+
response = await fetch(url, { ...options, signal: AbortSignal.timeout(UPLOAD_TIMEOUT) });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
9
|
+
throw new Error(`${providerName} upload timed out after ${UPLOAD_TIMEOUT / 1000}s`);
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const text = await response.text().catch(() => `HTTP ${response.status}`);
|
|
15
|
+
throw new Error(`${providerName} upload failed: ${text}`);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
return await response.json();
|
|
19
|
+
}
|
|
20
|
+
catch (cause) {
|
|
21
|
+
throw new Error(`${providerName} upload succeeded but returned invalid response`, { cause });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderFileRef, StandardProviderType } from '@directus/ai';
|
|
2
|
+
import type { AISettings } from '../../providers/types.js';
|
|
3
|
+
import type { UploadedFile } from '../types.js';
|
|
4
|
+
export declare function uploadToProvider(file: UploadedFile, provider: StandardProviderType, settings: AISettings): Promise<ProviderFileRef>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
2
|
+
import { uploadToAnthropic, uploadToGoogle, uploadToOpenAI } from '../adapters/index.js';
|
|
3
|
+
export async function uploadToProvider(file, provider, settings) {
|
|
4
|
+
switch (provider) {
|
|
5
|
+
case 'openai': {
|
|
6
|
+
if (!settings.openaiApiKey) {
|
|
7
|
+
throw new InvalidPayloadError({ reason: 'OpenAI API key not configured' });
|
|
8
|
+
}
|
|
9
|
+
return uploadToOpenAI(file, settings.openaiApiKey);
|
|
10
|
+
}
|
|
11
|
+
case 'anthropic': {
|
|
12
|
+
if (!settings.anthropicApiKey) {
|
|
13
|
+
throw new InvalidPayloadError({ reason: 'Anthropic API key not configured' });
|
|
14
|
+
}
|
|
15
|
+
return uploadToAnthropic(file, settings.anthropicApiKey);
|
|
16
|
+
}
|
|
17
|
+
case 'google': {
|
|
18
|
+
if (!settings.googleApiKey) {
|
|
19
|
+
throw new InvalidPayloadError({ reason: 'Google API key not configured' });
|
|
20
|
+
}
|
|
21
|
+
return uploadToGoogle(file, settings.googleApiKey);
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
throw new InvalidPayloadError({ reason: `Provider ${provider} does not support file uploads` });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const aiFilesRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import asyncHandler from '../../utils/async-handler.js';
|
|
3
|
+
import { loadSettings } from '../chat/middleware/load-settings.js';
|
|
4
|
+
import { aiFileUploadHandler } from './controllers/upload.js';
|
|
5
|
+
export const aiFilesRouter = Router().post('/', asyncHandler(loadSettings), asyncHandler(aiFileUploadHandler));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an Anthropic provider with file_id support.
|
|
3
|
+
*
|
|
4
|
+
* The AI SDK's @ai-sdk/anthropic provider doesn't support Anthropic's Files API file_id.
|
|
5
|
+
* This wrapper intercepts the HTTP request and transforms base64 sources that contain
|
|
6
|
+
* a file_id marker into the native Anthropic file source format.
|
|
7
|
+
*
|
|
8
|
+
* When the AI SDK converts a FileUIPart with url=file_id, it creates a base64 source
|
|
9
|
+
* with the file_id as the data (since it's not a valid URL or base64). We detect this
|
|
10
|
+
* pattern and transform it to use the native file source type.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createAnthropicWithFileSupport(apiKey: string): import("@ai-sdk/anthropic").AnthropicProvider;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { useLogger } from '../../logger/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an Anthropic provider with file_id support.
|
|
5
|
+
*
|
|
6
|
+
* The AI SDK's @ai-sdk/anthropic provider doesn't support Anthropic's Files API file_id.
|
|
7
|
+
* This wrapper intercepts the HTTP request and transforms base64 sources that contain
|
|
8
|
+
* a file_id marker into the native Anthropic file source format.
|
|
9
|
+
*
|
|
10
|
+
* When the AI SDK converts a FileUIPart with url=file_id, it creates a base64 source
|
|
11
|
+
* with the file_id as the data (since it's not a valid URL or base64). We detect this
|
|
12
|
+
* pattern and transform it to use the native file source type.
|
|
13
|
+
*/
|
|
14
|
+
export function createAnthropicWithFileSupport(apiKey) {
|
|
15
|
+
return createAnthropic({
|
|
16
|
+
apiKey,
|
|
17
|
+
fetch: async (url, options) => {
|
|
18
|
+
if (!options?.body || typeof options.body !== 'string') {
|
|
19
|
+
return fetch(url, options);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const body = JSON.parse(options.body);
|
|
23
|
+
if (!body.messages) {
|
|
24
|
+
return fetch(url, options);
|
|
25
|
+
}
|
|
26
|
+
const { messages, hasFileIds } = transformMessagesForFileId(body.messages);
|
|
27
|
+
body.messages = messages;
|
|
28
|
+
const headersObj = {};
|
|
29
|
+
if (options.headers instanceof Headers) {
|
|
30
|
+
options.headers.forEach((value, key) => {
|
|
31
|
+
headersObj[key] = value;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
Object.assign(headersObj, options.headers);
|
|
36
|
+
}
|
|
37
|
+
if (hasFileIds) {
|
|
38
|
+
const existing = headersObj['anthropic-beta'];
|
|
39
|
+
const betaFlag = 'files-api-2025-04-14';
|
|
40
|
+
if (!existing?.includes(betaFlag)) {
|
|
41
|
+
headersObj['anthropic-beta'] = existing ? `${existing},${betaFlag}` : betaFlag;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fetch(url, {
|
|
45
|
+
...options,
|
|
46
|
+
headers: headersObj,
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const logger = useLogger();
|
|
52
|
+
logger.error('Anthropic file support: could not parse request body');
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Transforms messages to use file_id source type where applicable.
|
|
60
|
+
*
|
|
61
|
+
* The AI SDK converts FileUIPart.url to base64 source data. When url is a file_id
|
|
62
|
+
* (starts with "file_"), the data field contains the file_id string.
|
|
63
|
+
* We detect this and convert to native Anthropic file source format.
|
|
64
|
+
*/
|
|
65
|
+
function transformMessagesForFileId(messages) {
|
|
66
|
+
let hasFileIds = false;
|
|
67
|
+
const transformedMessages = messages.map((msg) => {
|
|
68
|
+
if (!msg.content || !Array.isArray(msg.content)) {
|
|
69
|
+
return msg;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...msg,
|
|
73
|
+
content: msg.content.map((block) => {
|
|
74
|
+
// Check if this is an image or document with base64 source
|
|
75
|
+
if ((block.type === 'image' || block.type === 'document') &&
|
|
76
|
+
block.source?.type === 'base64' &&
|
|
77
|
+
typeof block.source.data === 'string' &&
|
|
78
|
+
block.source.data.startsWith('file_')) {
|
|
79
|
+
const fileId = block.source.data;
|
|
80
|
+
hasFileIds = true;
|
|
81
|
+
return {
|
|
82
|
+
...block,
|
|
83
|
+
source: {
|
|
84
|
+
type: 'file',
|
|
85
|
+
file_id: fileId,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return block;
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
return { messages: transformedMessages, hasFileIds };
|
|
94
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
1
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
3
2
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
4
3
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
5
4
|
import { createProviderRegistry } from 'ai';
|
|
5
|
+
import { createAnthropicWithFileSupport } from './anthropic-file-support.js';
|
|
6
6
|
export function buildProviderConfigs(settings) {
|
|
7
7
|
const configs = [];
|
|
8
8
|
if (settings.openaiApiKey) {
|
|
@@ -40,17 +40,14 @@ export function createAIProviderRegistry(configs, settings) {
|
|
|
40
40
|
providers['openai'] = createOpenAI({ apiKey: config.apiKey });
|
|
41
41
|
break;
|
|
42
42
|
case 'anthropic':
|
|
43
|
-
providers['anthropic'] =
|
|
43
|
+
providers['anthropic'] = createAnthropicWithFileSupport(config.apiKey);
|
|
44
44
|
break;
|
|
45
45
|
case 'google':
|
|
46
46
|
providers['google'] = createGoogleGenerativeAI({ apiKey: config.apiKey });
|
|
47
47
|
break;
|
|
48
48
|
case 'openai-compatible':
|
|
49
49
|
if (config.baseUrl) {
|
|
50
|
-
const customHeaders = settings?.openaiCompatibleHeaders?.
|
|
51
|
-
acc[header] = value;
|
|
52
|
-
return acc;
|
|
53
|
-
}, {}) ?? {};
|
|
50
|
+
const customHeaders = Object.fromEntries(settings?.openaiCompatibleHeaders?.map(({ header, value }) => [header, value]) ?? []);
|
|
54
51
|
providers['openai-compatible'] = createOpenAICompatible({
|
|
55
52
|
name: settings?.openaiCompatibleName ?? 'openai-compatible',
|
|
56
53
|
apiKey: config.apiKey,
|
|
@@ -11,13 +11,13 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
11
11
|
active: "active";
|
|
12
12
|
inactive: "inactive";
|
|
13
13
|
}>>;
|
|
14
|
-
trigger: z.ZodOptional<z.
|
|
14
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
15
15
|
operation: "operation";
|
|
16
16
|
schedule: "schedule";
|
|
17
17
|
event: "event";
|
|
18
18
|
webhook: "webhook";
|
|
19
19
|
manual: "manual";
|
|
20
|
-
}
|
|
20
|
+
}>>;
|
|
21
21
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
22
22
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
23
23
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -36,10 +36,10 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
36
36
|
}, z.core.$strip>>>;
|
|
37
37
|
date_created: z.ZodOptional<z.ZodString>;
|
|
38
38
|
user_created: z.ZodOptional<z.ZodString>;
|
|
39
|
-
accountability: z.ZodOptional<z.
|
|
39
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
40
40
|
all: "all";
|
|
41
41
|
activity: "activity";
|
|
42
|
-
}
|
|
42
|
+
}>>;
|
|
43
43
|
}, z.core.$strip>;
|
|
44
44
|
}, z.core.$strict>, z.ZodObject<{
|
|
45
45
|
action: z.ZodLiteral<"read">;
|
|
@@ -79,13 +79,13 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
79
79
|
active: "active";
|
|
80
80
|
inactive: "inactive";
|
|
81
81
|
}>>;
|
|
82
|
-
trigger: z.ZodOptional<z.
|
|
82
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
83
83
|
operation: "operation";
|
|
84
84
|
schedule: "schedule";
|
|
85
85
|
event: "event";
|
|
86
86
|
webhook: "webhook";
|
|
87
87
|
manual: "manual";
|
|
88
|
-
}
|
|
88
|
+
}>>;
|
|
89
89
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
90
90
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
91
91
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -104,10 +104,10 @@ export declare const FlowsValidateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
104
104
|
}, z.core.$strip>>>;
|
|
105
105
|
date_created: z.ZodOptional<z.ZodString>;
|
|
106
106
|
user_created: z.ZodOptional<z.ZodString>;
|
|
107
|
-
accountability: z.ZodOptional<z.
|
|
107
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
108
108
|
all: "all";
|
|
109
109
|
activity: "activity";
|
|
110
|
-
}
|
|
110
|
+
}>>;
|
|
111
111
|
}, z.core.$strip>;
|
|
112
112
|
query: z.ZodOptional<z.ZodObject<{
|
|
113
113
|
fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -176,13 +176,13 @@ export declare const FlowsInputSchema: z.ZodObject<{
|
|
|
176
176
|
active: "active";
|
|
177
177
|
inactive: "inactive";
|
|
178
178
|
}>>;
|
|
179
|
-
trigger: z.ZodOptional<z.
|
|
179
|
+
trigger: z.ZodOptional<z.ZodEnum<{
|
|
180
180
|
operation: "operation";
|
|
181
181
|
schedule: "schedule";
|
|
182
182
|
event: "event";
|
|
183
183
|
webhook: "webhook";
|
|
184
184
|
manual: "manual";
|
|
185
|
-
}
|
|
185
|
+
}>>;
|
|
186
186
|
options: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodAny>, z.ZodNull]>>;
|
|
187
187
|
operation: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
|
|
188
188
|
operations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -201,10 +201,10 @@ export declare const FlowsInputSchema: z.ZodObject<{
|
|
|
201
201
|
}, z.core.$strip>>>;
|
|
202
202
|
date_created: z.ZodOptional<z.ZodString>;
|
|
203
203
|
user_created: z.ZodOptional<z.ZodString>;
|
|
204
|
-
accountability: z.ZodOptional<z.
|
|
204
|
+
accountability: z.ZodOptional<z.ZodEnum<{
|
|
205
205
|
all: "all";
|
|
206
206
|
activity: "activity";
|
|
207
|
-
}
|
|
207
|
+
}>>;
|
|
208
208
|
}, z.core.$strip>>;
|
|
209
209
|
key: z.ZodOptional<z.ZodString>;
|
|
210
210
|
}, z.core.$strip>;
|
|
@@ -217,7 +217,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
217
217
|
color?: string | null | undefined;
|
|
218
218
|
description?: string | null | undefined;
|
|
219
219
|
status?: "active" | "inactive" | undefined;
|
|
220
|
-
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" |
|
|
220
|
+
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" | undefined;
|
|
221
221
|
options?: Record<string, any> | null | undefined;
|
|
222
222
|
operation?: string | null | undefined;
|
|
223
223
|
operations?: {
|
|
@@ -236,7 +236,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
236
236
|
}[] | undefined;
|
|
237
237
|
date_created?: string | undefined;
|
|
238
238
|
user_created?: string | undefined;
|
|
239
|
-
accountability?: "all" | "activity" |
|
|
239
|
+
accountability?: "all" | "activity" | undefined;
|
|
240
240
|
};
|
|
241
241
|
} | {
|
|
242
242
|
action: "read";
|
|
@@ -273,7 +273,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
273
273
|
color?: string | null | undefined;
|
|
274
274
|
description?: string | null | undefined;
|
|
275
275
|
status?: "active" | "inactive" | undefined;
|
|
276
|
-
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" |
|
|
276
|
+
trigger?: "operation" | "schedule" | "event" | "webhook" | "manual" | undefined;
|
|
277
277
|
options?: Record<string, any> | null | undefined;
|
|
278
278
|
operation?: string | null | undefined;
|
|
279
279
|
operations?: {
|
|
@@ -292,7 +292,7 @@ export declare const flows: import("../types.js").ToolConfig<{
|
|
|
292
292
|
}[] | undefined;
|
|
293
293
|
date_created?: string | undefined;
|
|
294
294
|
user_created?: string | undefined;
|
|
295
|
-
accountability?: "all" | "activity" |
|
|
295
|
+
accountability?: "all" | "activity" | undefined;
|
|
296
296
|
};
|
|
297
297
|
query?: {
|
|
298
298
|
fields?: string[] | undefined;
|