@directus/api 33.3.1 → 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.
Files changed (98) hide show
  1. package/dist/ai/chat/lib/create-ui-stream.js +2 -1
  2. package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
  3. package/dist/ai/chat/lib/transform-file-parts.js +36 -0
  4. package/dist/ai/files/adapters/anthropic.d.ts +3 -0
  5. package/dist/ai/files/adapters/anthropic.js +25 -0
  6. package/dist/ai/files/adapters/google.d.ts +3 -0
  7. package/dist/ai/files/adapters/google.js +58 -0
  8. package/dist/ai/files/adapters/index.d.ts +3 -0
  9. package/dist/ai/files/adapters/index.js +3 -0
  10. package/dist/ai/files/adapters/openai.d.ts +3 -0
  11. package/dist/ai/files/adapters/openai.js +22 -0
  12. package/dist/ai/files/controllers/upload.d.ts +2 -0
  13. package/dist/ai/files/controllers/upload.js +101 -0
  14. package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
  15. package/dist/ai/files/lib/fetch-provider.js +23 -0
  16. package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
  17. package/dist/ai/files/lib/upload-to-provider.js +26 -0
  18. package/dist/ai/files/router.d.ts +1 -0
  19. package/dist/ai/files/router.js +5 -0
  20. package/dist/ai/files/types.d.ts +5 -0
  21. package/dist/ai/files/types.js +1 -0
  22. package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
  23. package/dist/ai/providers/anthropic-file-support.js +94 -0
  24. package/dist/ai/providers/registry.js +3 -6
  25. package/dist/ai/tools/flows/index.d.ts +16 -16
  26. package/dist/ai/tools/schema.d.ts +8 -8
  27. package/dist/ai/tools/schema.js +2 -2
  28. package/dist/app.js +10 -1
  29. package/dist/controllers/deployment-webhooks.d.ts +2 -0
  30. package/dist/controllers/deployment-webhooks.js +95 -0
  31. package/dist/controllers/deployment.js +61 -165
  32. package/dist/controllers/files.js +2 -1
  33. package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
  34. package/dist/database/helpers/date/dialects/oracle.js +2 -0
  35. package/dist/database/helpers/date/dialects/sqlite.js +2 -0
  36. package/dist/database/helpers/date/types.d.ts +1 -1
  37. package/dist/database/helpers/date/types.js +3 -1
  38. package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
  39. package/dist/database/helpers/fn/dialects/mssql.js +21 -0
  40. package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
  41. package/dist/database/helpers/fn/dialects/mysql.js +30 -0
  42. package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
  43. package/dist/database/helpers/fn/dialects/oracle.js +21 -0
  44. package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
  45. package/dist/database/helpers/fn/dialects/postgres.js +40 -0
  46. package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
  47. package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
  48. package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
  49. package/dist/database/helpers/fn/json/parse-function.js +66 -0
  50. package/dist/database/helpers/fn/types.d.ts +8 -0
  51. package/dist/database/helpers/fn/types.js +19 -0
  52. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
  53. package/dist/database/helpers/schema/dialects/mysql.js +11 -0
  54. package/dist/database/helpers/schema/types.d.ts +1 -0
  55. package/dist/database/helpers/schema/types.js +3 -0
  56. package/dist/database/index.js +2 -1
  57. package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
  58. package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
  59. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  60. package/dist/database/run-ast/lib/apply-query/filter/operator.js +17 -7
  61. package/dist/database/run-ast/lib/parse-current-level.js +8 -1
  62. package/dist/database/run-ast/run-ast.js +11 -1
  63. package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
  64. package/dist/database/run-ast/utils/get-column.js +13 -2
  65. package/dist/deployment/deployment.d.ts +25 -2
  66. package/dist/deployment/drivers/netlify.d.ts +6 -2
  67. package/dist/deployment/drivers/netlify.js +114 -12
  68. package/dist/deployment/drivers/vercel.d.ts +5 -2
  69. package/dist/deployment/drivers/vercel.js +84 -5
  70. package/dist/deployment.d.ts +5 -0
  71. package/dist/deployment.js +34 -0
  72. package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
  73. package/dist/request/is-denied-ip.js +24 -23
  74. package/dist/services/authentication.js +27 -22
  75. package/dist/services/collections.js +1 -0
  76. package/dist/services/deployment-projects.d.ts +31 -2
  77. package/dist/services/deployment-projects.js +109 -5
  78. package/dist/services/deployment-runs.d.ts +19 -1
  79. package/dist/services/deployment-runs.js +86 -0
  80. package/dist/services/deployment.d.ts +44 -3
  81. package/dist/services/deployment.js +263 -15
  82. package/dist/services/files/utils/get-metadata.js +6 -6
  83. package/dist/services/files.d.ts +3 -1
  84. package/dist/services/files.js +26 -3
  85. package/dist/services/graphql/resolvers/query.js +23 -6
  86. package/dist/services/payload.d.ts +6 -0
  87. package/dist/services/payload.js +27 -2
  88. package/dist/services/server.js +1 -1
  89. package/dist/services/users.js +6 -1
  90. package/dist/utils/get-field-relational-depth.d.ts +13 -0
  91. package/dist/utils/get-field-relational-depth.js +22 -0
  92. package/dist/utils/parse-value.d.ts +4 -0
  93. package/dist/utils/parse-value.js +11 -0
  94. package/dist/utils/sanitize-query.js +3 -2
  95. package/dist/utils/split-fields.d.ts +4 -0
  96. package/dist/utils/split-fields.js +32 -0
  97. package/dist/utils/validate-query.js +2 -1
  98. package/package.json +29 -29
@@ -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,3 @@
1
+ import type { ProviderFileRef } from '@directus/ai';
2
+ import type { UploadedFile } from '../types.js';
3
+ export declare function uploadToAnthropic(file: UploadedFile, apiKey: string): Promise<ProviderFileRef>;
@@ -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,3 @@
1
+ import type { ProviderFileRef } from '@directus/ai';
2
+ import type { UploadedFile } from '../types.js';
3
+ export declare function uploadToGoogle(file: UploadedFile, apiKey: string): Promise<ProviderFileRef>;
@@ -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,3 @@
1
+ export { uploadToOpenAI } from './openai.js';
2
+ export { uploadToAnthropic } from './anthropic.js';
3
+ export { uploadToGoogle } from './google.js';
@@ -0,0 +1,3 @@
1
+ export { uploadToOpenAI } from './openai.js';
2
+ export { uploadToAnthropic } from './anthropic.js';
3
+ export { uploadToGoogle } from './google.js';
@@ -0,0 +1,3 @@
1
+ import type { ProviderFileRef } from '@directus/ai';
2
+ import type { UploadedFile } from '../types.js';
3
+ export declare function uploadToOpenAI(file: UploadedFile, apiKey: string): Promise<ProviderFileRef>;
@@ -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,2 @@
1
+ import type { RequestHandler } from 'express';
2
+ export declare const aiFileUploadHandler: RequestHandler;
@@ -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,5 @@
1
+ export interface UploadedFile {
2
+ filename: string;
3
+ mimeType: string;
4
+ data: Buffer;
5
+ }
@@ -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'] = createAnthropic({ apiKey: config.apiKey });
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?.reduce((acc, { header, value }) => {
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.ZodUnion<readonly [z.ZodEnum<{
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
- }>, z.ZodNull]>>;
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.ZodUnion<readonly [z.ZodEnum<{
39
+ accountability: z.ZodOptional<z.ZodEnum<{
40
40
  all: "all";
41
41
  activity: "activity";
42
- }>, z.ZodNull]>>;
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.ZodUnion<readonly [z.ZodEnum<{
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
- }>, z.ZodNull]>>;
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.ZodUnion<readonly [z.ZodEnum<{
107
+ accountability: z.ZodOptional<z.ZodEnum<{
108
108
  all: "all";
109
109
  activity: "activity";
110
- }>, z.ZodNull]>>;
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.ZodUnion<readonly [z.ZodEnum<{
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
- }>, z.ZodNull]>>;
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.ZodUnion<readonly [z.ZodEnum<{
204
+ accountability: z.ZodOptional<z.ZodEnum<{
205
205
  all: "all";
206
206
  activity: "activity";
207
- }>, z.ZodNull]>>;
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" | null | undefined;
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" | null | undefined;
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" | null | undefined;
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" | null | undefined;
295
+ accountability?: "all" | "activity" | undefined;
296
296
  };
297
297
  query?: {
298
298
  fields?: string[] | undefined;