@directus/api 33.0.0 → 33.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +122 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/items/index.js +4 -1
  22. package/dist/ai/tools/items/prompt.md +7 -9
  23. package/dist/ai/tools/schema.js +1 -1
  24. package/dist/app.js +4 -0
  25. package/dist/auth/drivers/ldap.d.ts +1 -1
  26. package/dist/auth/drivers/ldap.js +142 -137
  27. package/dist/cache.d.ts +12 -0
  28. package/dist/cache.js +25 -1
  29. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  30. package/dist/controllers/deployment.d.ts +2 -0
  31. package/dist/controllers/deployment.js +481 -0
  32. package/dist/controllers/fields.js +6 -4
  33. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  34. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  35. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  36. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  37. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  38. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  39. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  40. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  41. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  43. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  44. package/dist/deployment/deployment.d.ts +94 -0
  45. package/dist/deployment/deployment.js +29 -0
  46. package/dist/deployment/drivers/index.d.ts +1 -0
  47. package/dist/deployment/drivers/index.js +1 -0
  48. package/dist/deployment/drivers/vercel.d.ts +32 -0
  49. package/dist/deployment/drivers/vercel.js +208 -0
  50. package/dist/deployment/index.d.ts +2 -0
  51. package/dist/deployment/index.js +2 -0
  52. package/dist/deployment.d.ts +24 -0
  53. package/dist/deployment.js +39 -0
  54. package/dist/middleware/respond.js +27 -14
  55. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  56. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
  57. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
  58. package/dist/server.js +2 -1
  59. package/dist/services/deployment-projects.d.ts +20 -0
  60. package/dist/services/deployment-projects.js +34 -0
  61. package/dist/services/deployment-runs.d.ts +13 -0
  62. package/dist/services/deployment-runs.js +6 -0
  63. package/dist/services/deployment.d.ts +40 -0
  64. package/dist/services/deployment.js +202 -0
  65. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  66. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  67. package/dist/services/index.d.ts +3 -0
  68. package/dist/services/index.js +3 -0
  69. package/dist/services/server.js +1 -0
  70. package/dist/services/specifications.js +2 -2
  71. package/dist/services/versions.js +1 -1
  72. package/dist/telemetry/lib/get-report.js +2 -0
  73. package/dist/telemetry/types/report.d.ts +8 -0
  74. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  75. package/dist/telemetry/utils/get-settings.js +5 -0
  76. package/dist/utils/deep-map-response.d.ts +1 -1
  77. package/dist/utils/deep-map-response.js +1 -1
  78. package/dist/utils/get-column-path.js +1 -1
  79. package/dist/utils/get-service.js +7 -1
  80. package/dist/utils/is-field-allowed.d.ts +4 -0
  81. package/dist/utils/is-field-allowed.js +9 -0
  82. package/dist/utils/versioning/handle-version.js +1 -1
  83. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  84. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  85. package/dist/websocket/collab/collab.d.ts +63 -0
  86. package/dist/websocket/collab/collab.js +481 -0
  87. package/dist/websocket/collab/constants.d.ts +1 -0
  88. package/dist/websocket/collab/constants.js +13 -0
  89. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  90. package/dist/websocket/collab/filter-to-fields.js +11 -0
  91. package/dist/websocket/collab/messenger.d.ts +43 -0
  92. package/dist/websocket/collab/messenger.js +225 -0
  93. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  94. package/dist/websocket/collab/payload-permissions.js +158 -0
  95. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  96. package/dist/websocket/collab/permissions-cache.js +204 -0
  97. package/dist/websocket/collab/room.d.ts +125 -0
  98. package/dist/websocket/collab/room.js +593 -0
  99. package/dist/websocket/collab/store.d.ts +7 -0
  100. package/dist/websocket/collab/store.js +33 -0
  101. package/dist/websocket/collab/types.d.ts +21 -0
  102. package/dist/websocket/collab/types.js +1 -0
  103. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  104. package/dist/websocket/collab/verify-permissions.js +100 -0
  105. package/dist/websocket/handlers/index.d.ts +2 -0
  106. package/dist/websocket/handlers/index.js +9 -0
  107. package/dist/websocket/utils/items.d.ts +2 -2
  108. package/dist/websocket/utils/message.d.ts +1 -1
  109. package/dist/websocket/utils/message.js +2 -2
  110. package/package.json +32 -30
  111. package/dist/utils/get-relation-info.d.ts +0 -6
  112. package/dist/utils/get-relation-info.js +0 -43
  113. package/dist/utils/get-relation-type.d.ts +0 -6
  114. package/dist/utils/get-relation-type.js +0 -18
  115. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  116. package/dist/utils/versioning/deep-map-with-schema.js +0 -81
@@ -13,7 +13,21 @@ export const aiChatPostHandler = async (req, res, _next) => {
13
13
  if (!parseResult.success) {
14
14
  throw new InvalidPayloadError({ reason: fromZodError(parseResult.error).message });
15
15
  }
16
- const { provider, model, messages: rawMessages, tools: requestedTools, toolApprovals } = parseResult.data;
16
+ const { provider, model, messages: rawMessages, tools: requestedTools, toolApprovals, context } = parseResult.data;
17
+ const aiSettings = res.locals['ai'].settings;
18
+ const allowedModelsMap = {
19
+ openai: aiSettings.openaiAllowedModels,
20
+ anthropic: aiSettings.anthropicAllowedModels,
21
+ google: aiSettings.googleAllowedModels,
22
+ };
23
+ // For standard providers: null/empty = no models allowed, must be in list
24
+ // openai-compatible skips validation entirely
25
+ if (provider !== 'openai-compatible') {
26
+ const allowedModels = allowedModelsMap[provider];
27
+ if (!allowedModels || allowedModels.length === 0 || !allowedModels.includes(model)) {
28
+ throw new ForbiddenError({ reason: 'Model not allowed for this provider' });
29
+ }
30
+ }
17
31
  if (rawMessages.length === 0) {
18
32
  throw new InvalidPayloadError({ reason: `"messages" must not be empty` });
19
33
  }
@@ -33,12 +47,13 @@ export const aiChatPostHandler = async (req, res, _next) => {
33
47
  if (validationResult.success === false) {
34
48
  throw new InvalidPayloadError({ reason: validationResult.error.message });
35
49
  }
36
- const stream = createUiStream(validationResult.data, {
50
+ const stream = await createUiStream(validationResult.data, {
37
51
  provider,
38
52
  model,
39
- tools: tools,
40
- apiKeys: res.locals['ai'].apiKeys,
53
+ tools,
54
+ aiSettings,
41
55
  systemPrompt: res.locals['ai'].systemPrompt,
56
+ ...(context && { context }),
42
57
  onUsage: (usage) => {
43
58
  res.write(`data: ${JSON.stringify({ type: 'data-usage', data: usage })}\n\n`);
44
59
  },
@@ -1,15 +1,16 @@
1
+ import type { ProviderType } from '@directus/ai';
1
2
  import { type LanguageModelUsage, type StreamTextResult, type Tool, type UIMessage } from 'ai';
3
+ import { type AISettings } from '../../providers/index.js';
4
+ import type { ChatContext } from '../models/chat-request.js';
2
5
  export interface CreateUiStreamOptions {
3
- provider: 'openai' | 'anthropic';
6
+ provider: ProviderType;
4
7
  model: string;
5
8
  tools: {
6
9
  [x: string]: Tool;
7
10
  };
8
- apiKeys: {
9
- openai: string | null;
10
- anthropic: string | null;
11
- };
11
+ aiSettings: AISettings;
12
12
  systemPrompt?: string;
13
+ context?: ChatContext;
13
14
  onUsage?: (usage: Pick<LanguageModelUsage, 'inputTokens' | 'outputTokens' | 'totalTokens'>) => void | Promise<void>;
14
15
  }
15
- export declare const createUiStream: (messages: UIMessage[], { provider, model, tools, apiKeys, systemPrompt, onUsage }: CreateUiStreamOptions) => StreamTextResult<Record<string, Tool<any, any>>, any>;
16
+ export declare const createUiStream: (messages: UIMessage[], { provider, model, tools, aiSettings, systemPrompt, context, onUsage }: CreateUiStreamOptions) => Promise<StreamTextResult<Record<string, Tool<any, any>>, any>>;
@@ -1,36 +1,39 @@
1
- import { createAnthropic } from '@ai-sdk/anthropic';
2
- import { createOpenAI } from '@ai-sdk/openai';
3
1
  import { ServiceUnavailableError } from '@directus/errors';
4
2
  import { convertToModelMessages, stepCountIs, streamText, } from 'ai';
3
+ import { buildProviderConfigs, createAIProviderRegistry, getProviderOptions, } from '../../providers/index.js';
5
4
  import { SYSTEM_PROMPT } from '../constants/system-prompt.js';
6
- export const createUiStream = (messages, { provider, model, tools, apiKeys, systemPrompt, onUsage }) => {
7
- if (apiKeys[provider] === null) {
5
+ import { formatContextForSystemPrompt } from '../utils/format-context.js';
6
+ export const createUiStream = async (messages, { provider, model, tools, aiSettings, systemPrompt, context, onUsage }) => {
7
+ const configs = buildProviderConfigs(aiSettings);
8
+ const providerConfig = configs.find((c) => c.type === provider);
9
+ if (!providerConfig) {
8
10
  throw new ServiceUnavailableError({ service: provider, reason: 'No API key configured for LLM provider' });
9
11
  }
10
- let modelProvider;
11
- if (provider === 'openai') {
12
- modelProvider = createOpenAI({ apiKey: apiKeys.openai });
13
- }
14
- else if (provider === 'anthropic') {
15
- modelProvider = createAnthropic({ apiKey: apiKeys.anthropic });
16
- }
17
- else {
18
- throw new Error(`Unexpected provider given: "${provider}"`);
19
- }
20
- systemPrompt ||= SYSTEM_PROMPT;
12
+ const registry = createAIProviderRegistry(configs, aiSettings);
13
+ const baseSystemPrompt = systemPrompt || SYSTEM_PROMPT;
14
+ const contextBlock = context ? formatContextForSystemPrompt(context) : null;
15
+ const providerOptions = getProviderOptions(provider, model, aiSettings);
16
+ // Compute the full system prompt once to avoid re-computing on each step
17
+ const fullSystemPrompt = contextBlock ? baseSystemPrompt + contextBlock : baseSystemPrompt;
21
18
  const stream = streamText({
22
- system: systemPrompt,
23
- model: modelProvider(model),
24
- messages: convertToModelMessages(messages),
19
+ system: baseSystemPrompt,
20
+ model: registry.languageModel(`${provider}:${model}`),
21
+ messages: await convertToModelMessages(messages),
25
22
  stopWhen: [stepCountIs(10)],
26
- providerOptions: {
27
- openai: {
28
- reasoningSummary: 'auto',
29
- store: false,
30
- include: ['reasoning.encrypted_content'],
31
- },
32
- },
23
+ providerOptions,
33
24
  tools,
25
+ /**
26
+ * prepareStep is called before each AI step to prepare the system prompt.
27
+ * When context exists, we override the system prompt to include context attachments.
28
+ * This allows the initial system prompt to be simple while ensuring all steps
29
+ * (including tool continuation steps) receive the full context.
30
+ */
31
+ prepareStep: () => {
32
+ if (contextBlock) {
33
+ return { system: fullSystemPrompt };
34
+ }
35
+ return {};
36
+ },
34
37
  onFinish({ usage }) {
35
38
  if (onUsage) {
36
39
  const { inputTokens, outputTokens, totalTokens } = usage;
@@ -4,15 +4,39 @@ export const loadSettings = async (_req, res, next) => {
4
4
  const service = new SettingsService({
5
5
  schema: await getSchema(),
6
6
  });
7
- const { ai_openai_api_key, ai_anthropic_api_key, ai_system_prompt } = await service.readSingleton({
8
- fields: ['ai_openai_api_key', 'ai_anthropic_api_key', 'ai_system_prompt'],
7
+ const settings = await service.readSingleton({
8
+ fields: [
9
+ 'ai_openai_api_key',
10
+ 'ai_anthropic_api_key',
11
+ 'ai_google_api_key',
12
+ 'ai_openai_compatible_api_key',
13
+ 'ai_openai_compatible_base_url',
14
+ 'ai_openai_compatible_name',
15
+ 'ai_openai_compatible_models',
16
+ 'ai_openai_compatible_headers',
17
+ 'ai_openai_allowed_models',
18
+ 'ai_anthropic_allowed_models',
19
+ 'ai_google_allowed_models',
20
+ 'ai_system_prompt',
21
+ ],
9
22
  });
23
+ const aiSettings = {
24
+ openaiApiKey: settings['ai_openai_api_key'] ?? null,
25
+ anthropicApiKey: settings['ai_anthropic_api_key'] ?? null,
26
+ googleApiKey: settings['ai_google_api_key'] ?? null,
27
+ openaiCompatibleApiKey: settings['ai_openai_compatible_api_key'] ?? null,
28
+ openaiCompatibleBaseUrl: settings['ai_openai_compatible_base_url'] ?? null,
29
+ openaiCompatibleName: settings['ai_openai_compatible_name'] ?? null,
30
+ openaiCompatibleModels: settings['ai_openai_compatible_models'] ?? null,
31
+ openaiCompatibleHeaders: settings['ai_openai_compatible_headers'] ?? null,
32
+ openaiAllowedModels: settings['ai_openai_allowed_models'] ?? null,
33
+ anthropicAllowedModels: settings['ai_anthropic_allowed_models'] ?? null,
34
+ googleAllowedModels: settings['ai_google_allowed_models'] ?? null,
35
+ systemPrompt: settings['ai_system_prompt'] ?? null,
36
+ };
10
37
  res.locals['ai'] = {
11
- apiKeys: {
12
- openai: ai_openai_api_key,
13
- anthropic: ai_anthropic_api_key,
14
- },
15
- systemPrompt: ai_system_prompt,
38
+ settings: aiSettings,
39
+ systemPrompt: settings['ai_system_prompt'],
16
40
  };
17
41
  return next();
18
42
  };
@@ -12,12 +12,103 @@ export declare const ToolApprovalMode: z.ZodEnum<{
12
12
  disabled: "disabled";
13
13
  }>;
14
14
  export type ToolApprovalMode = z.infer<typeof ToolApprovalMode>;
15
+ export declare const ContextAttachment: z.ZodDiscriminatedUnion<[z.ZodObject<{
16
+ type: z.ZodLiteral<"item">;
17
+ display: z.ZodString;
18
+ data: z.ZodObject<{
19
+ collection: z.ZodString;
20
+ key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
21
+ }, z.core.$strip>;
22
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
23
+ }, z.core.$strip>, z.ZodObject<{
24
+ type: z.ZodLiteral<"visual-element">;
25
+ display: z.ZodString;
26
+ data: z.ZodObject<{
27
+ key: z.ZodString;
28
+ collection: z.ZodString;
29
+ item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
30
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
31
+ rect: z.ZodOptional<z.ZodObject<{
32
+ top: z.ZodNumber;
33
+ left: z.ZodNumber;
34
+ width: z.ZodNumber;
35
+ height: z.ZodNumber;
36
+ }, z.core.$strip>>;
37
+ }, z.core.$strip>;
38
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
39
+ }, z.core.$strip>, z.ZodObject<{
40
+ type: z.ZodLiteral<"prompt">;
41
+ display: z.ZodString;
42
+ data: z.ZodObject<{
43
+ text: z.ZodString;
44
+ prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
45
+ values: z.ZodRecord<z.ZodString, z.ZodString>;
46
+ }, z.core.$strip>;
47
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
48
+ }, z.core.$strip>], "type">;
49
+ export type ContextAttachment = z.infer<typeof ContextAttachment>;
50
+ export declare const PageContext: z.ZodObject<{
51
+ path: z.ZodString;
52
+ collection: z.ZodOptional<z.ZodString>;
53
+ item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
54
+ module: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strip>;
56
+ export type PageContext = z.infer<typeof PageContext>;
57
+ export declare const ChatContext: z.ZodObject<{
58
+ attachments: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
59
+ type: z.ZodLiteral<"item">;
60
+ display: z.ZodString;
61
+ data: z.ZodObject<{
62
+ collection: z.ZodString;
63
+ key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
64
+ }, z.core.$strip>;
65
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
66
+ }, z.core.$strip>, z.ZodObject<{
67
+ type: z.ZodLiteral<"visual-element">;
68
+ display: z.ZodString;
69
+ data: z.ZodObject<{
70
+ key: z.ZodString;
71
+ collection: z.ZodString;
72
+ item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
73
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
74
+ rect: z.ZodOptional<z.ZodObject<{
75
+ top: z.ZodNumber;
76
+ left: z.ZodNumber;
77
+ width: z.ZodNumber;
78
+ height: z.ZodNumber;
79
+ }, z.core.$strip>>;
80
+ }, z.core.$strip>;
81
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ type: z.ZodLiteral<"prompt">;
84
+ display: z.ZodString;
85
+ data: z.ZodObject<{
86
+ text: z.ZodString;
87
+ prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
88
+ values: z.ZodRecord<z.ZodString, z.ZodString>;
89
+ }, z.core.$strip>;
90
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
91
+ }, z.core.$strip>], "type">>>;
92
+ page: z.ZodOptional<z.ZodObject<{
93
+ path: z.ZodString;
94
+ collection: z.ZodOptional<z.ZodString>;
95
+ item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
96
+ module: z.ZodOptional<z.ZodString>;
97
+ }, z.core.$strip>>;
98
+ }, z.core.$strip>;
99
+ export type ChatContext = z.infer<typeof ChatContext>;
15
100
  export declare const ChatRequest: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
16
101
  provider: z.ZodLiteral<"openai">;
17
- model: z.ZodUnion<readonly [z.ZodLiteral<"gpt-5">, z.ZodLiteral<"gpt-5-nano">, z.ZodLiteral<"gpt-5-mini">, z.ZodLiteral<"gpt-5-pro">]>;
102
+ model: z.ZodString;
18
103
  }, z.core.$strip>, z.ZodObject<{
19
104
  provider: z.ZodLiteral<"anthropic">;
20
- model: z.ZodUnion<readonly [z.ZodLiteral<"claude-sonnet-4-5">, z.ZodLiteral<"claude-haiku-4-5">, z.ZodLiteral<"claude-opus-4-1">]>;
105
+ model: z.ZodString;
106
+ }, z.core.$strip>, z.ZodObject<{
107
+ provider: z.ZodLiteral<"google">;
108
+ model: z.ZodString;
109
+ }, z.core.$strip>, z.ZodObject<{
110
+ provider: z.ZodLiteral<"openai-compatible">;
111
+ model: z.ZodString;
21
112
  }, z.core.$strip>], "provider">, z.ZodObject<{
22
113
  tools: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
23
114
  name: z.ZodString;
@@ -30,5 +121,47 @@ export declare const ChatRequest: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.Z
30
121
  ask: "ask";
31
122
  disabled: "disabled";
32
123
  }>>>;
124
+ context: z.ZodOptional<z.ZodObject<{
125
+ attachments: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
126
+ type: z.ZodLiteral<"item">;
127
+ display: z.ZodString;
128
+ data: z.ZodObject<{
129
+ collection: z.ZodString;
130
+ key: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
131
+ }, z.core.$strip>;
132
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
133
+ }, z.core.$strip>, z.ZodObject<{
134
+ type: z.ZodLiteral<"visual-element">;
135
+ display: z.ZodString;
136
+ data: z.ZodObject<{
137
+ key: z.ZodString;
138
+ collection: z.ZodString;
139
+ item: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
140
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
141
+ rect: z.ZodOptional<z.ZodObject<{
142
+ top: z.ZodNumber;
143
+ left: z.ZodNumber;
144
+ width: z.ZodNumber;
145
+ height: z.ZodNumber;
146
+ }, z.core.$strip>>;
147
+ }, z.core.$strip>;
148
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
149
+ }, z.core.$strip>, z.ZodObject<{
150
+ type: z.ZodLiteral<"prompt">;
151
+ display: z.ZodString;
152
+ data: z.ZodObject<{
153
+ text: z.ZodString;
154
+ prompt: z.ZodRecord<z.ZodString, z.ZodUnknown>;
155
+ values: z.ZodRecord<z.ZodString, z.ZodString>;
156
+ }, z.core.$strip>;
157
+ snapshot: z.ZodRecord<z.ZodString, z.ZodUnknown>;
158
+ }, z.core.$strip>], "type">>>;
159
+ page: z.ZodOptional<z.ZodObject<{
160
+ path: z.ZodString;
161
+ collection: z.ZodOptional<z.ZodString>;
162
+ item: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
163
+ module: z.ZodOptional<z.ZodString>;
164
+ }, z.core.$strip>>;
165
+ }, z.core.$strip>>;
33
166
  }, z.core.$strip>>;
34
167
  export type ChatRequest = z.infer<typeof ChatRequest>;
@@ -1,7 +1,7 @@
1
1
  import {} from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { parseJsonSchema7 } from '../utils/parse-json-schema-7.js';
4
- import { ProviderAnthropic, ProviderOpenAi } from './providers.js';
4
+ import { ProviderAnthropic, ProviderGoogle, ProviderOpenAi, ProviderOpenAiCompatible } from './providers.js';
5
5
  export const ChatRequestTool = z.union([
6
6
  z.string(),
7
7
  z.object({
@@ -19,8 +19,62 @@ export const ChatRequestTool = z.union([
19
19
  }),
20
20
  ]);
21
21
  export const ToolApprovalMode = z.enum(['always', 'ask', 'disabled']);
22
- export const ChatRequest = z.intersection(z.discriminatedUnion('provider', [ProviderOpenAi, ProviderAnthropic]), z.object({
22
+ const ItemContextData = z.object({
23
+ collection: z.string(),
24
+ key: z.union([z.string(), z.number()]),
25
+ });
26
+ const VisualElementContextData = z.object({
27
+ key: z.string(),
28
+ collection: z.string(),
29
+ item: z.union([z.string(), z.number()]),
30
+ fields: z.array(z.string()).optional(),
31
+ rect: z
32
+ .object({
33
+ top: z.number(),
34
+ left: z.number(),
35
+ width: z.number(),
36
+ height: z.number(),
37
+ })
38
+ .optional(),
39
+ });
40
+ const PromptContextData = z.object({
41
+ text: z.string(),
42
+ prompt: z.record(z.string(), z.unknown()),
43
+ values: z.record(z.string(), z.string()),
44
+ });
45
+ export const ContextAttachment = z.discriminatedUnion('type', [
46
+ z.object({
47
+ type: z.literal('item'),
48
+ display: z.string(),
49
+ data: ItemContextData,
50
+ snapshot: z.record(z.string(), z.unknown()),
51
+ }),
52
+ z.object({
53
+ type: z.literal('visual-element'),
54
+ display: z.string(),
55
+ data: VisualElementContextData,
56
+ snapshot: z.record(z.string(), z.unknown()),
57
+ }),
58
+ z.object({
59
+ type: z.literal('prompt'),
60
+ display: z.string(),
61
+ data: PromptContextData,
62
+ snapshot: z.record(z.string(), z.unknown()),
63
+ }),
64
+ ]);
65
+ export const PageContext = z.object({
66
+ path: z.string(),
67
+ collection: z.string().optional(),
68
+ item: z.union([z.string(), z.number()]).optional(),
69
+ module: z.string().optional(),
70
+ });
71
+ export const ChatContext = z.object({
72
+ attachments: z.array(ContextAttachment).max(10).optional(),
73
+ page: PageContext.optional(),
74
+ });
75
+ export const ChatRequest = z.intersection(z.discriminatedUnion('provider', [ProviderOpenAi, ProviderAnthropic, ProviderGoogle, ProviderOpenAiCompatible]), z.object({
23
76
  tools: z.array(ChatRequestTool),
24
77
  messages: z.array(z.looseObject({})),
25
78
  toolApprovals: z.record(z.string(), ToolApprovalMode).optional(),
79
+ context: ChatContext.optional(),
26
80
  }));
@@ -1,9 +1,23 @@
1
1
  import { z } from 'zod';
2
+ export declare const ProviderTypeSchema: z.ZodEnum<{
3
+ openai: "openai";
4
+ anthropic: "anthropic";
5
+ google: "google";
6
+ "openai-compatible": "openai-compatible";
7
+ }>;
2
8
  export declare const ProviderOpenAi: z.ZodObject<{
3
9
  provider: z.ZodLiteral<"openai">;
4
- model: z.ZodUnion<readonly [z.ZodLiteral<"gpt-5">, z.ZodLiteral<"gpt-5-nano">, z.ZodLiteral<"gpt-5-mini">, z.ZodLiteral<"gpt-5-pro">]>;
10
+ model: z.ZodString;
5
11
  }, z.core.$strip>;
6
12
  export declare const ProviderAnthropic: z.ZodObject<{
7
13
  provider: z.ZodLiteral<"anthropic">;
8
- model: z.ZodUnion<readonly [z.ZodLiteral<"claude-sonnet-4-5">, z.ZodLiteral<"claude-haiku-4-5">, z.ZodLiteral<"claude-opus-4-1">]>;
14
+ model: z.ZodString;
15
+ }, z.core.$strip>;
16
+ export declare const ProviderGoogle: z.ZodObject<{
17
+ provider: z.ZodLiteral<"google">;
18
+ model: z.ZodString;
19
+ }, z.core.$strip>;
20
+ export declare const ProviderOpenAiCompatible: z.ZodObject<{
21
+ provider: z.ZodLiteral<"openai-compatible">;
22
+ model: z.ZodString;
9
23
  }, z.core.$strip>;
@@ -1,9 +1,23 @@
1
1
  import { z } from 'zod';
2
+ export const ProviderTypeSchema = z.enum([
3
+ 'openai',
4
+ 'anthropic',
5
+ 'google',
6
+ 'openai-compatible',
7
+ ]);
2
8
  export const ProviderOpenAi = z.object({
3
9
  provider: z.literal('openai'),
4
- model: z.union([z.literal('gpt-5'), z.literal('gpt-5-nano'), z.literal('gpt-5-mini'), z.literal('gpt-5-pro')]),
10
+ model: z.string(),
5
11
  });
6
12
  export const ProviderAnthropic = z.object({
7
13
  provider: z.literal('anthropic'),
8
- model: z.union([z.literal('claude-sonnet-4-5'), z.literal('claude-haiku-4-5'), z.literal('claude-opus-4-1')]),
14
+ model: z.string(),
15
+ });
16
+ export const ProviderGoogle = z.object({
17
+ provider: z.literal('google'),
18
+ model: z.string(),
19
+ });
20
+ export const ProviderOpenAiCompatible = z.object({
21
+ provider: z.literal('openai-compatible'),
22
+ model: z.string(),
9
23
  });
@@ -1,6 +1,6 @@
1
1
  import { InvalidPayloadError } from '@directus/errors';
2
2
  import {} from '@directus/types';
3
- import { jsonSchema, tool } from 'ai';
3
+ import { jsonSchema, tool, zodSchema } from 'ai';
4
4
  import { fromZodError } from 'zod-validation-error';
5
5
  import { ALL_TOOLS } from '../../tools/index.js';
6
6
  export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, schema, toolApprovals, }) => {
@@ -13,10 +13,10 @@ export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, sc
13
13
  // Default to 'ask' (needs approval) if not specified
14
14
  const approvalMode = toolApprovals?.[chatRequestTool] ?? 'ask';
15
15
  const needsApproval = approvalMode !== 'always';
16
+ const inputSchema = zodSchema(directusTool.inputSchema);
16
17
  return tool({
17
- name: directusTool.name,
18
18
  description: directusTool.description,
19
- inputSchema: directusTool.inputSchema,
19
+ inputSchema,
20
20
  needsApproval,
21
21
  execute: async (rawArgs) => {
22
22
  const { error, data: args } = directusTool.validateSchema?.safeParse(rawArgs) ?? {
@@ -31,7 +31,6 @@ export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, sc
31
31
  }
32
32
  // Local/client-side tool (schema only, executed on client)
33
33
  return tool({
34
- name: chatRequestTool.name,
35
34
  description: chatRequestTool.description,
36
35
  inputSchema: jsonSchema(chatRequestTool.inputSchema),
37
36
  });
@@ -0,0 +1,5 @@
1
+ import type { ChatContext } from '../models/chat-request.js';
2
+ /**
3
+ * Format context for appending to system prompt
4
+ */
5
+ export declare function formatContextForSystemPrompt(context: ChatContext): string;
@@ -0,0 +1,122 @@
1
+ function escapeAngleBrackets(text) {
2
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
+ }
4
+ function groupAttachments(attachments) {
5
+ const groups = { visualElements: [], prompts: [], items: [] };
6
+ for (const att of attachments) {
7
+ switch (att.type) {
8
+ case 'visual-element':
9
+ groups.visualElements.push(att);
10
+ break;
11
+ case 'prompt':
12
+ groups.prompts.push(att);
13
+ break;
14
+ case 'item':
15
+ groups.items.push(att);
16
+ break;
17
+ }
18
+ }
19
+ return groups;
20
+ }
21
+ function formatVisualElement(att) {
22
+ const fields = att.data.fields?.length ? att.data.fields.map((f) => escapeAngleBrackets(f)).join(', ') : 'all';
23
+ const collection = escapeAngleBrackets(String(att.data.collection));
24
+ const item = escapeAngleBrackets(String(att.data.item));
25
+ const display = escapeAngleBrackets(att.display);
26
+ return `### ${collection}/${item} — "${display}"
27
+ Editable fields: ${fields}
28
+ \`\`\`json
29
+ ${escapeAngleBrackets(JSON.stringify(att.snapshot, null, 2))}
30
+ \`\`\``;
31
+ }
32
+ function formatPrompt(att) {
33
+ const snapshot = att.snapshot;
34
+ const lines = [];
35
+ const display = escapeAngleBrackets(att.display);
36
+ if (snapshot.text) {
37
+ lines.push(escapeAngleBrackets(snapshot.text));
38
+ }
39
+ if (snapshot.messages?.length) {
40
+ lines.push('\n### Example Exchange');
41
+ for (const msg of snapshot.messages) {
42
+ const role = escapeAngleBrackets(msg.role);
43
+ const text = escapeAngleBrackets(msg.text);
44
+ lines.push(`**${role}**: ${text}`);
45
+ }
46
+ }
47
+ return `### ${display}\n${lines.join('\n')}`;
48
+ }
49
+ function formatItem(att) {
50
+ const display = escapeAngleBrackets(att.display);
51
+ const collectionLabel = att.data.collection ? ` (${escapeAngleBrackets(att.data.collection)})` : '';
52
+ const keyLabel = ` — key: ${escapeAngleBrackets(String(att.data.key))}`;
53
+ const collection = att.data.collection ? escapeAngleBrackets(att.data.collection) : '';
54
+ const updateHint = att.data.collection
55
+ ? `\nTo update this item, use the items tool with: collection="${collection}", keys=["${escapeAngleBrackets(String(att.data.key))}"], action="update"`
56
+ : '\nUse the items tool to update this item.';
57
+ return `[Item: ${display}${collectionLabel}${keyLabel}]${updateHint}\n${escapeAngleBrackets(JSON.stringify(att.snapshot, null, 2))}`;
58
+ }
59
+ /**
60
+ * Format context for appending to system prompt
61
+ */
62
+ export function formatContextForSystemPrompt(context) {
63
+ const attachments = context.attachments ?? [];
64
+ const groups = groupAttachments(attachments);
65
+ const parts = [];
66
+ // 1. Custom instructions (prompts) - highest priority, placed first
67
+ if (groups.prompts.length > 0) {
68
+ const promptBlocks = groups.prompts.map(formatPrompt).join('\n\n');
69
+ parts.push(`<custom_instructions>
70
+ The user has applied the following prompt(s) to guide your behavior:
71
+
72
+ ${promptBlocks}
73
+ </custom_instructions>`);
74
+ }
75
+ // 2. User context (current page + items)
76
+ const sections = [];
77
+ const now = new Date();
78
+ sections.push(`## Current Date\n${now.toISOString().split('T')[0]}`);
79
+ if (context.page) {
80
+ const page = context.page;
81
+ const pageLines = [`Path: ${escapeAngleBrackets(String(page.path))}`];
82
+ if (page.collection)
83
+ pageLines.push(`Collection: ${escapeAngleBrackets(String(page.collection))}`);
84
+ if (page.item !== undefined)
85
+ pageLines.push(`Item: ${escapeAngleBrackets(String(page.item))}`);
86
+ if (page.module)
87
+ pageLines.push(`Module: ${escapeAngleBrackets(String(page.module))}`);
88
+ sections.push(`## Current Page\n${pageLines.join('\n')}`);
89
+ }
90
+ if (groups.items.length > 0) {
91
+ const itemLines = groups.items.map(formatItem).join('\n\n');
92
+ sections.push(`## User-Added Context
93
+ The user has attached these items as reference for their request.
94
+ All root-level fields the user has access to are shown below — use these exact field names when updating.
95
+ Use the items tool to fetch additional fields or update items when asked.
96
+
97
+ ${itemLines}`);
98
+ }
99
+ if (sections.length > 0) {
100
+ parts.push(`<user_context>\n${sections.join('\n\n')}\n</user_context>`);
101
+ }
102
+ // 3. Visual editing context
103
+ if (groups.visualElements.length > 0) {
104
+ const elementLines = groups.visualElements.map(formatVisualElement).join('\n\n');
105
+ parts.push(`<visual_editing>
106
+ ## Selected Elements
107
+ The user selected these elements for editing in the visual editor.
108
+
109
+ ${elementLines}
110
+ </visual_editing>`);
111
+ }
112
+ // 4. Attachment rules (only if any attachments exist)
113
+ if (attachments.length > 0) {
114
+ parts.push(`## Attachment Rules
115
+ - User-added attachments have HIGHER PRIORITY than page context.
116
+ - To modify attached items, ALWAYS use the items tool with action: 'update'. NEVER use form-values tools for attached items.
117
+ - form-values tools ONLY affect the currently open page form, which may be a DIFFERENT item than what the user attached.`);
118
+ }
119
+ if (parts.length === 0)
120
+ return '';
121
+ return '\n\n' + parts.join('\n\n');
122
+ }