@btst/stack 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/components/markdown/index.d.cts +4 -2
- package/dist/components/markdown/index.d.mts +4 -2
- package/dist/components/markdown/index.d.ts +4 -2
- package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +102 -15
- package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +102 -15
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +8 -4
- package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +8 -4
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.cjs +3 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-input.mjs +3 -2
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +0 -1
- package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +0 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +7 -1
- package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +7 -1
- package/dist/packages/stack/src/plugins/blog/client/components/shared/markdown-content.cjs +6 -1
- package/dist/packages/stack/src/plugins/blog/client/components/shared/markdown-content.mjs +6 -1
- package/dist/packages/ui/src/components/markdown-content.cjs +23 -3
- package/dist/packages/ui/src/components/markdown-content.mjs +25 -5
- package/dist/plugins/ai-chat/api/index.d.cts +2 -2
- package/dist/plugins/ai-chat/api/index.d.mts +2 -2
- package/dist/plugins/ai-chat/api/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/client/index.d.cts +1 -1
- package/dist/plugins/ai-chat/client/index.d.mts +1 -1
- package/dist/plugins/ai-chat/client/index.d.ts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
- package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
- package/dist/plugins/blog/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +4 -2
- package/dist/plugins/blog/client/index.d.mts +4 -2
- package/dist/plugins/blog/client/index.d.ts +4 -2
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/cms/api/index.d.cts +1 -1
- package/dist/plugins/cms/api/index.d.mts +1 -1
- package/dist/plugins/cms/api/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +1 -1
- package/dist/plugins/form-builder/query-keys.d.mts +1 -1
- package/dist/plugins/form-builder/query-keys.d.ts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/ui-builder/client/index.d.cts +1 -1
- package/dist/plugins/ui-builder/client/index.d.mts +1 -1
- package/dist/plugins/ui-builder/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
- package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
- package/dist/shared/{stack.BTStHVTy.d.cts → stack.BGzgFx3R.d.cts} +5 -5
- package/dist/shared/{stack.CtrnoQgH.d.cts → stack.BOZRaVpu.d.cts} +35 -35
- package/dist/shared/{stack.94kN3pEK.d.mts → stack.CjCSDfWy.d.mts} +5 -5
- package/dist/shared/{stack.B2EYZyh_.d.ts → stack.DDQTbADz.d.ts} +4 -2
- package/dist/shared/{stack.lPKwd_v0.d.ts → stack.DlBoNqYu.d.ts} +35 -35
- package/dist/shared/{stack.DfrRYOIl.d.ts → stack.Dpg_JjIo.d.ts} +5 -5
- package/dist/shared/{stack.BggTHKlo.d.mts → stack.Dvy7FmaA.d.mts} +35 -35
- package/dist/shared/{stack.fF273jws.d.cts → stack.IY-ngxuw.d.cts} +4 -2
- package/dist/shared/{stack.DivuAnNI.d.mts → stack.mU9iMF2j.d.mts} +4 -2
- package/package.json +1 -1
- package/src/plugins/ai-chat/__tests__/chat-input-authorization.test.tsx +42 -0
- package/src/plugins/ai-chat/__tests__/compaction.test.ts +79 -19
- package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +129 -0
- package/src/plugins/ai-chat/api/compaction.ts +137 -13
- package/src/plugins/ai-chat/api/operations.ts +18 -4
- package/src/plugins/ai-chat/api/plugin.ts +10 -1
- package/src/plugins/ai-chat/client/components/chat-input.tsx +3 -3
- package/src/plugins/ai-chat/client/components/chat-interface.tsx +0 -1
- package/src/plugins/blog/__tests__/introduction-slot.test.tsx +86 -0
- package/src/plugins/blog/client/components/pages/post-page.internal.tsx +4 -1
- package/src/plugins/blog/client/components/shared/markdown-content.tsx +8 -1
- package/src/plugins/blog/client/overrides.ts +3 -0
- package/dist/shared/{stack.CyalJ0j6.d.ts → stack.BMlTZnZp.d.ts} +16 -16
- package/dist/shared/{stack.D6Pg-ufX.d.cts → stack.BV0uNV4f.d.cts} +16 -16
- package/dist/shared/{stack.BuxUQqL8.d.ts → stack.CGmh-Lpe.d.ts} +12 -12
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
- package/dist/shared/{stack.Egr3RWhA.d.mts → stack.Doy2_wgl.d.mts} +12 -12
- package/dist/shared/{stack.B8PG21zX.d.mts → stack.lsbdKRj7.d.mts} +16 -16
- package/dist/shared/{stack.CX83YNuS.d.cts → stack.zch70Loj.d.cts} +12 -12
|
@@ -2,7 +2,7 @@ import * as _btst_stack_plugins_client from '@btst/stack/plugins/client';
|
|
|
2
2
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
3
3
|
import * as zod from 'zod';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { c as createPostSchema, u as updatePostSchema } from './stack.
|
|
5
|
+
import { c as createPostSchema, u as updatePostSchema } from './stack.Csi-4Mgz.mjs';
|
|
6
6
|
import { P as Post, T as Tag, S as SerializedPost, a as SerializedTag } from './stack.Dsatjh0B.mjs';
|
|
7
7
|
import * as _btst_stack_plugins_api from '@btst/stack/plugins/api';
|
|
8
8
|
import { DeepReadonly, OperationContext, OperationErrorContext } from '@btst/stack/plugins/api';
|
|
@@ -89,12 +89,6 @@ declare const NextPreviousPostsQuerySchema: z.ZodObject<{
|
|
|
89
89
|
date: z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>;
|
|
90
90
|
}, z.core.$strip>;
|
|
91
91
|
declare const CreatePostOperationInputSchema: z.ZodObject<{
|
|
92
|
-
slug: z.ZodOptional<z.ZodString>;
|
|
93
|
-
title: z.ZodString;
|
|
94
|
-
content: z.ZodString;
|
|
95
|
-
excerpt: z.ZodString;
|
|
96
|
-
image: z.ZodOptional<z.ZodString>;
|
|
97
|
-
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
98
92
|
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
99
93
|
name: z.ZodString;
|
|
100
94
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -102,6 +96,12 @@ declare const CreatePostOperationInputSchema: z.ZodObject<{
|
|
|
102
96
|
name: z.ZodString;
|
|
103
97
|
slug: z.ZodString;
|
|
104
98
|
}, z.core.$strip>]>>>>;
|
|
99
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
100
|
+
title: z.ZodString;
|
|
101
|
+
content: z.ZodString;
|
|
102
|
+
excerpt: z.ZodString;
|
|
103
|
+
image: z.ZodOptional<z.ZodString>;
|
|
104
|
+
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
105
105
|
publishedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
106
106
|
createdAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
107
107
|
updatedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
@@ -109,12 +109,6 @@ declare const CreatePostOperationInputSchema: z.ZodObject<{
|
|
|
109
109
|
declare const UpdatePostOperationInputSchema: z.ZodObject<{
|
|
110
110
|
id: z.ZodString;
|
|
111
111
|
data: z.ZodObject<{
|
|
112
|
-
slug: z.ZodString;
|
|
113
|
-
title: z.ZodString;
|
|
114
|
-
content: z.ZodString;
|
|
115
|
-
excerpt: z.ZodString;
|
|
116
|
-
image: z.ZodOptional<z.ZodString>;
|
|
117
|
-
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
118
112
|
tags: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
119
113
|
name: z.ZodString;
|
|
120
114
|
}, z.core.$strip>, z.ZodObject<{
|
|
@@ -122,6 +116,12 @@ declare const UpdatePostOperationInputSchema: z.ZodObject<{
|
|
|
122
116
|
name: z.ZodString;
|
|
123
117
|
slug: z.ZodString;
|
|
124
118
|
}, z.core.$strip>]>>>>;
|
|
119
|
+
slug: z.ZodString;
|
|
120
|
+
title: z.ZodString;
|
|
121
|
+
content: z.ZodString;
|
|
122
|
+
excerpt: z.ZodString;
|
|
123
|
+
image: z.ZodOptional<z.ZodString>;
|
|
124
|
+
published: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
125
125
|
publishedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
126
126
|
createdAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
127
127
|
updatedAt: z.ZodOptional<z.ZodPipe<z.ZodCoercedDate<unknown>, z.ZodTransform<string, Date>>>;
|
|
@@ -386,9 +386,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
386
386
|
title: string;
|
|
387
387
|
content: string;
|
|
388
388
|
excerpt: string;
|
|
389
|
-
slug?: string | undefined;
|
|
390
|
-
image?: string | undefined;
|
|
391
|
-
published?: boolean | undefined;
|
|
392
389
|
tags?: ({
|
|
393
390
|
name: string;
|
|
394
391
|
} | {
|
|
@@ -396,6 +393,9 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
396
393
|
name: string;
|
|
397
394
|
slug: string;
|
|
398
395
|
})[] | undefined;
|
|
396
|
+
slug?: string | undefined;
|
|
397
|
+
image?: string | undefined;
|
|
398
|
+
published?: boolean | undefined;
|
|
399
399
|
publishedAt?: unknown;
|
|
400
400
|
createdAt?: unknown;
|
|
401
401
|
updatedAt?: unknown;
|
|
@@ -403,9 +403,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
403
403
|
title: string;
|
|
404
404
|
content: string;
|
|
405
405
|
excerpt: string;
|
|
406
|
-
slug?: string | undefined;
|
|
407
|
-
image?: string | undefined;
|
|
408
|
-
published?: boolean | undefined;
|
|
409
406
|
tags?: ({
|
|
410
407
|
name: string;
|
|
411
408
|
} | {
|
|
@@ -413,6 +410,9 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
413
410
|
name: string;
|
|
414
411
|
slug: string;
|
|
415
412
|
})[] | undefined;
|
|
413
|
+
slug?: string | undefined;
|
|
414
|
+
image?: string | undefined;
|
|
415
|
+
published?: boolean | undefined;
|
|
416
416
|
publishedAt?: unknown;
|
|
417
417
|
createdAt?: unknown;
|
|
418
418
|
updatedAt?: unknown;
|
|
@@ -448,8 +448,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
448
448
|
title: string;
|
|
449
449
|
content: string;
|
|
450
450
|
excerpt: string;
|
|
451
|
-
image?: string | undefined;
|
|
452
|
-
published?: boolean | undefined;
|
|
453
451
|
tags?: ({
|
|
454
452
|
name: string;
|
|
455
453
|
} | {
|
|
@@ -457,6 +455,8 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
457
455
|
name: string;
|
|
458
456
|
slug: string;
|
|
459
457
|
})[] | undefined;
|
|
458
|
+
image?: string | undefined;
|
|
459
|
+
published?: boolean | undefined;
|
|
460
460
|
publishedAt?: unknown;
|
|
461
461
|
createdAt?: unknown;
|
|
462
462
|
updatedAt?: unknown;
|
|
@@ -465,8 +465,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
465
465
|
title: string;
|
|
466
466
|
content: string;
|
|
467
467
|
excerpt: string;
|
|
468
|
-
image?: string | undefined;
|
|
469
|
-
published?: boolean | undefined;
|
|
470
468
|
tags?: ({
|
|
471
469
|
name: string;
|
|
472
470
|
} | {
|
|
@@ -474,6 +472,8 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
474
472
|
name: string;
|
|
475
473
|
slug: string;
|
|
476
474
|
})[] | undefined;
|
|
475
|
+
image?: string | undefined;
|
|
476
|
+
published?: boolean | undefined;
|
|
477
477
|
publishedAt?: unknown;
|
|
478
478
|
createdAt?: unknown;
|
|
479
479
|
updatedAt?: unknown;
|
|
@@ -627,12 +627,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
627
627
|
total: number;
|
|
628
628
|
}, false>;
|
|
629
629
|
readonly createPost: _btst_stack_plugins_api.Operation<zod.ZodObject<{
|
|
630
|
-
slug: zod.ZodOptional<zod.ZodString>;
|
|
631
|
-
title: zod.ZodString;
|
|
632
|
-
content: zod.ZodString;
|
|
633
|
-
excerpt: zod.ZodString;
|
|
634
|
-
image: zod.ZodOptional<zod.ZodString>;
|
|
635
|
-
published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
|
|
636
630
|
tags: zod.ZodDefault<zod.ZodOptional<zod.ZodArray<zod.ZodUnion<readonly [zod.ZodObject<{
|
|
637
631
|
name: zod.ZodString;
|
|
638
632
|
}, zod_v4_core.$strip>, zod.ZodObject<{
|
|
@@ -640,6 +634,12 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
640
634
|
name: zod.ZodString;
|
|
641
635
|
slug: zod.ZodString;
|
|
642
636
|
}, zod_v4_core.$strip>]>>>>;
|
|
637
|
+
slug: zod.ZodOptional<zod.ZodString>;
|
|
638
|
+
title: zod.ZodString;
|
|
639
|
+
content: zod.ZodString;
|
|
640
|
+
excerpt: zod.ZodString;
|
|
641
|
+
image: zod.ZodOptional<zod.ZodString>;
|
|
642
|
+
published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
|
|
643
643
|
publishedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
644
644
|
createdAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
645
645
|
updatedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
@@ -673,12 +673,6 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
673
673
|
readonly updatePost: _btst_stack_plugins_api.Operation<zod.ZodObject<{
|
|
674
674
|
id: zod.ZodString;
|
|
675
675
|
data: zod.ZodObject<{
|
|
676
|
-
slug: zod.ZodString;
|
|
677
|
-
title: zod.ZodString;
|
|
678
|
-
content: zod.ZodString;
|
|
679
|
-
excerpt: zod.ZodString;
|
|
680
|
-
image: zod.ZodOptional<zod.ZodString>;
|
|
681
|
-
published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
|
|
682
676
|
tags: zod.ZodDefault<zod.ZodOptional<zod.ZodArray<zod.ZodUnion<readonly [zod.ZodObject<{
|
|
683
677
|
name: zod.ZodString;
|
|
684
678
|
}, zod_v4_core.$strip>, zod.ZodObject<{
|
|
@@ -686,6 +680,12 @@ declare const blogBackendPlugin: (options?: BlogBackendOptions) => _btst_stack_p
|
|
|
686
680
|
name: zod.ZodString;
|
|
687
681
|
slug: zod.ZodString;
|
|
688
682
|
}, zod_v4_core.$strip>]>>>>;
|
|
683
|
+
slug: zod.ZodString;
|
|
684
|
+
title: zod.ZodString;
|
|
685
|
+
content: zod.ZodString;
|
|
686
|
+
excerpt: zod.ZodString;
|
|
687
|
+
image: zod.ZodOptional<zod.ZodString>;
|
|
688
|
+
published: zod.ZodDefault<zod.ZodOptional<zod.ZodBoolean>>;
|
|
689
689
|
publishedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
690
690
|
createdAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
691
691
|
updatedAt: zod.ZodOptional<zod.ZodPipe<zod.ZodCoercedDate<unknown>, zod.ZodTransform<string, Date>>>;
|
|
@@ -57,10 +57,12 @@ interface AiChatPageContentConfig {
|
|
|
57
57
|
description?: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** Opt-in context compaction for text
|
|
60
|
+
/** Opt-in context compaction for conversations containing text and images. */
|
|
61
61
|
interface AiChatCompactionConfig {
|
|
62
62
|
/** Actual context window. Configure the model's output limit at or below 20% of this. */
|
|
63
63
|
contextWindowTokens: number;
|
|
64
|
+
/** Estimated tokens per image, excluding encoded bytes. Default: 4096. Increase for models with higher image token costs. */
|
|
65
|
+
imageTokenEstimate?: number;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
type AiChatAccess = "authorized" | "public";
|
|
@@ -230,7 +232,7 @@ interface AiChatBackendConfig {
|
|
|
230
232
|
*/
|
|
231
233
|
access?: AiChatAccess;
|
|
232
234
|
systemPrompt?: string;
|
|
233
|
-
/** Automatically summarize older
|
|
235
|
+
/** Automatically summarize older text and image context; off by default. */
|
|
234
236
|
compaction?: AiChatCompactionConfig;
|
|
235
237
|
tools?: Record<string, Tool>;
|
|
236
238
|
/** Enable the standard readPage server tool for full page content. Reserves the readPage tool name. */
|
|
@@ -57,10 +57,12 @@ interface AiChatPageContentConfig {
|
|
|
57
57
|
description?: string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** Opt-in context compaction for text
|
|
60
|
+
/** Opt-in context compaction for conversations containing text and images. */
|
|
61
61
|
interface AiChatCompactionConfig {
|
|
62
62
|
/** Actual context window. Configure the model's output limit at or below 20% of this. */
|
|
63
63
|
contextWindowTokens: number;
|
|
64
|
+
/** Estimated tokens per image, excluding encoded bytes. Default: 4096. Increase for models with higher image token costs. */
|
|
65
|
+
imageTokenEstimate?: number;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
type AiChatAccess = "authorized" | "public";
|
|
@@ -230,7 +232,7 @@ interface AiChatBackendConfig {
|
|
|
230
232
|
*/
|
|
231
233
|
access?: AiChatAccess;
|
|
232
234
|
systemPrompt?: string;
|
|
233
|
-
/** Automatically summarize older
|
|
235
|
+
/** Automatically summarize older text and image context; off by default. */
|
|
234
236
|
compaction?: AiChatCompactionConfig;
|
|
235
237
|
tools?: Record<string, Tool>;
|
|
236
238
|
/** Enable the standard readPage server tool for full page content. Reserves the readPage tool name. */
|
package/package.json
CHANGED
|
@@ -37,6 +37,48 @@ afterEach(async () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
describe("AI Chat attachment authorization", () => {
|
|
40
|
+
it("uses the configured upload transport in public mode without requiring a saved-chat identity", async () => {
|
|
41
|
+
const uploadFile = vi.fn(async () => "https://files/public.png");
|
|
42
|
+
const onFilesAttached = vi.fn();
|
|
43
|
+
await act(async () => {
|
|
44
|
+
root.render(
|
|
45
|
+
<StackProvider
|
|
46
|
+
stack={createTestClientStack({
|
|
47
|
+
aiChat: aiChatClientPlugin({ mode: "public" }),
|
|
48
|
+
})}
|
|
49
|
+
overrides={{ aiChat: { uploadFile, allowedFileTypes: ["image"] } }}
|
|
50
|
+
>
|
|
51
|
+
<ChatInput
|
|
52
|
+
handleInputChange={() => {}}
|
|
53
|
+
handleSubmit={() => {}}
|
|
54
|
+
isLoading={false}
|
|
55
|
+
attachedFiles={[]}
|
|
56
|
+
onFilesAttached={onFilesAttached}
|
|
57
|
+
/>
|
|
58
|
+
</StackProvider>,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
const input =
|
|
62
|
+
container.querySelector<HTMLInputElement>('input[type="file"]');
|
|
63
|
+
expect(input).toBeTruthy();
|
|
64
|
+
Object.defineProperty(input!, "files", {
|
|
65
|
+
configurable: true,
|
|
66
|
+
value: [new File(["image"], "public.png", { type: "image/png" })],
|
|
67
|
+
});
|
|
68
|
+
await act(async () => {
|
|
69
|
+
input!.dispatchEvent(new Event("change", { bubbles: true }));
|
|
70
|
+
await Promise.resolve();
|
|
71
|
+
});
|
|
72
|
+
expect(uploadFile).toHaveBeenCalledOnce();
|
|
73
|
+
expect(onFilesAttached).toHaveBeenCalledWith([
|
|
74
|
+
{
|
|
75
|
+
url: "https://files/public.png",
|
|
76
|
+
mediaType: "image/png",
|
|
77
|
+
filename: "public.png",
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
40
82
|
it("checks the selected MIME type before calling the upload transport", async () => {
|
|
41
83
|
const observedMediaTypes: string[][] = [];
|
|
42
84
|
const authorization = defineAuthorization({
|
|
@@ -56,6 +56,85 @@ describe("public context compaction", () => {
|
|
|
56
56
|
expect(summarize).toHaveBeenCalledOnce();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it("counts image content separately from inline bytes and keeps tool output as text", () => {
|
|
60
|
+
const inline = {
|
|
61
|
+
role: "user",
|
|
62
|
+
parts: [
|
|
63
|
+
{
|
|
64
|
+
type: "file",
|
|
65
|
+
mediaType: "image/png",
|
|
66
|
+
url: "data:image/png;base64," + "A".repeat(100_000),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
const remote = {
|
|
71
|
+
role: "user",
|
|
72
|
+
parts: [
|
|
73
|
+
{
|
|
74
|
+
type: "file",
|
|
75
|
+
mediaType: "image/png",
|
|
76
|
+
url: "https://example.org/image.png",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
expect(estimateContextTokens([inline])).toBe(
|
|
81
|
+
estimateContextTokens([remote]),
|
|
82
|
+
);
|
|
83
|
+
expect(estimateContextTokens([inline])).toBeGreaterThanOrEqual(4096);
|
|
84
|
+
const toolOutput = {
|
|
85
|
+
role: "tool",
|
|
86
|
+
content: [
|
|
87
|
+
{ type: "tool-result", output: { type: "json", value: inline } },
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
expect(estimateContextTokens([toolOutput])).toBeGreaterThan(30_000);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("sends older images as vision input when summarizing instead of serialized bytes", async () => {
|
|
94
|
+
const png =
|
|
95
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jr1sAAAAASUVORK5CYII=";
|
|
96
|
+
const original = [
|
|
97
|
+
message("u1", "user", "Remember the number in this image"),
|
|
98
|
+
];
|
|
99
|
+
original[0]!.parts.push({ type: "file", mediaType: "image/png", url: png });
|
|
100
|
+
const model = new MockLanguageModelV2({
|
|
101
|
+
doGenerate: async ({ prompt }) => ({
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: JSON.stringify(prompt).includes('"type":"file"')
|
|
106
|
+
? "The image shows verification number 917."
|
|
107
|
+
: "The user wants to remember the image number.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
finishReason: "stop",
|
|
111
|
+
usage: { inputTokens: 100, outputTokens: 20, totalTokens: 120 },
|
|
112
|
+
warnings: [],
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
const before = structuredClone(original);
|
|
116
|
+
const summary = await createSummarizer(
|
|
117
|
+
model,
|
|
118
|
+
16_384,
|
|
119
|
+
)({ messages: original });
|
|
120
|
+
expect(summary).toContain("917");
|
|
121
|
+
expect(
|
|
122
|
+
model.doGenerateCalls.some((call) =>
|
|
123
|
+
call.prompt.some(
|
|
124
|
+
(message) =>
|
|
125
|
+
message.role === "user" &&
|
|
126
|
+
message.content.some(
|
|
127
|
+
(part) => part.type === "file" && part.mediaType === "image/png",
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
).toBe(true);
|
|
132
|
+
expect(JSON.stringify(model.doGenerateCalls)).not.toContain(
|
|
133
|
+
"data:image/png;base64",
|
|
134
|
+
);
|
|
135
|
+
expect(original).toEqual(before);
|
|
136
|
+
});
|
|
137
|
+
|
|
59
138
|
it("reuses a checkpoint only while all summarized messages are unchanged", () => {
|
|
60
139
|
const checkpoint = captureContextCheckpoint(history, "summary", "a1");
|
|
61
140
|
expect(contextRequest(history, checkpoint)).toEqual({
|
|
@@ -219,25 +298,6 @@ describe("public context compaction", () => {
|
|
|
219
298
|
body: JSON.stringify(body),
|
|
220
299
|
}),
|
|
221
300
|
);
|
|
222
|
-
const attachment = await send({
|
|
223
|
-
messages: [
|
|
224
|
-
{
|
|
225
|
-
id: "file",
|
|
226
|
-
role: "user",
|
|
227
|
-
parts: [
|
|
228
|
-
{
|
|
229
|
-
type: "file",
|
|
230
|
-
mediaType: "image/png",
|
|
231
|
-
url: "https://example.org/image.png",
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
},
|
|
235
|
-
],
|
|
236
|
-
});
|
|
237
|
-
expect(attachment.status).toBe(400);
|
|
238
|
-
expect(await attachment.text()).toContain("text-only");
|
|
239
|
-
expect(model.doGenerateCalls).toHaveLength(0);
|
|
240
|
-
expect(model.doStreamCalls).toHaveLength(0);
|
|
241
301
|
const response = await send({ messages: history });
|
|
242
302
|
expect(response.status).toBe(200);
|
|
243
303
|
const text = await response.text();
|
|
@@ -77,6 +77,7 @@ function backend(
|
|
|
77
77
|
return [
|
|
78
78
|
aiChat.stream.start.when(owns),
|
|
79
79
|
aiChat.message.send.when(owns),
|
|
80
|
+
aiChat.attachment.send.when(owns),
|
|
80
81
|
aiChat.message.retry.when(owns),
|
|
81
82
|
aiChat.message.edit.when(owns),
|
|
82
83
|
aiChat.tool.activate.when(owns),
|
|
@@ -162,6 +163,134 @@ async function seed(app: ReturnType<typeof backend>["app"]) {
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
describe("persisted context compaction", () => {
|
|
166
|
+
it.each([undefined, 1, "application/pdf"])(
|
|
167
|
+
"rejects unsupported attachment media type %s as a client error",
|
|
168
|
+
async (mediaType) => {
|
|
169
|
+
const { app, languageModel } = backend();
|
|
170
|
+
const response = await app.handler(
|
|
171
|
+
new Request("http://localhost/api/chat", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "content-type": "application/json" },
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
id: "bad-file",
|
|
178
|
+
role: "user",
|
|
179
|
+
parts: [
|
|
180
|
+
{ type: "file", mediaType, url: "https://example.org/file" },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}),
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
expect(response.status).toBe(400);
|
|
188
|
+
expect(languageModel.doGenerateCalls).toHaveLength(0);
|
|
189
|
+
expect(languageModel.doStreamCalls).toHaveLength(0);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
it.each(["public", "authorized"] as const)(
|
|
194
|
+
"keeps images usable across compaction and resume in %s chats",
|
|
195
|
+
async (access) => {
|
|
196
|
+
const { app, languageModel, errors } = backend(model(), {
|
|
197
|
+
access,
|
|
198
|
+
compaction: { contextWindowTokens: 16_384 },
|
|
199
|
+
});
|
|
200
|
+
const png =
|
|
201
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jr1sAAAAASUVORK5CYII=";
|
|
202
|
+
const first = text(
|
|
203
|
+
"image-1",
|
|
204
|
+
"user",
|
|
205
|
+
"Remember this image. " + "research ".repeat(2500),
|
|
206
|
+
);
|
|
207
|
+
first.parts.push({ type: "file", mediaType: "image/png", url: png });
|
|
208
|
+
const request = (body: unknown) =>
|
|
209
|
+
app.handler(
|
|
210
|
+
new Request("http://localhost/api/chat", {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: { "content-type": "application/json" },
|
|
213
|
+
body: JSON.stringify(body),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
const firstResponse = await request({ messages: [first] });
|
|
217
|
+
expect(firstResponse.status).toBe(200);
|
|
218
|
+
expect(await firstResponse.text()).toContain("ORCHID-917");
|
|
219
|
+
expect(languageModel.doGenerateCalls).toHaveLength(0);
|
|
220
|
+
const id = firstResponse.headers.get("X-Conversation-Id");
|
|
221
|
+
const previous =
|
|
222
|
+
access === "authorized"
|
|
223
|
+
? await resume(app, id!)
|
|
224
|
+
: [first, text("a1", "assistant", "ORCHID-917")];
|
|
225
|
+
const second = text(
|
|
226
|
+
"image-2",
|
|
227
|
+
"user",
|
|
228
|
+
"Compare the images. " + "context ".repeat(750),
|
|
229
|
+
);
|
|
230
|
+
second.parts.push({ type: "file", mediaType: "image/png", url: png });
|
|
231
|
+
const messages = [...previous, second];
|
|
232
|
+
const response = await request({
|
|
233
|
+
messages,
|
|
234
|
+
...(id ? { conversationId: id } : {}),
|
|
235
|
+
});
|
|
236
|
+
const stream = await response.text();
|
|
237
|
+
expect(response.status).toBe(200);
|
|
238
|
+
expect(stream).toContain("ORCHID-917");
|
|
239
|
+
expect(stream).toContain("data-context-status");
|
|
240
|
+
const visionCalls = languageModel.doGenerateCalls.filter((call) =>
|
|
241
|
+
call.prompt.some(
|
|
242
|
+
(message) =>
|
|
243
|
+
message.role === "user" &&
|
|
244
|
+
message.content.some(
|
|
245
|
+
(part) => part.type === "file" && part.mediaType === "image/png",
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
expect(visionCalls).toHaveLength(1);
|
|
250
|
+
const liveImages = languageModel.doStreamCalls
|
|
251
|
+
.at(-1)!
|
|
252
|
+
.prompt.flatMap((message) =>
|
|
253
|
+
message.role === "user"
|
|
254
|
+
? message.content.filter((part) => part.type === "file")
|
|
255
|
+
: [],
|
|
256
|
+
);
|
|
257
|
+
expect(liveImages).toHaveLength(1);
|
|
258
|
+
const summaries = languageModel.doGenerateCalls.length;
|
|
259
|
+
let continuation: Record<string, unknown>;
|
|
260
|
+
if (access === "authorized") {
|
|
261
|
+
const saved = await resume(app, id!);
|
|
262
|
+
expect(saved[0]!.parts).toEqual(first.parts);
|
|
263
|
+
expect(saved[2]!.parts).toEqual(second.parts);
|
|
264
|
+
continuation = {
|
|
265
|
+
conversationId: id,
|
|
266
|
+
messages: [...saved, text("u3", "user", "Repeat it")],
|
|
267
|
+
};
|
|
268
|
+
} else {
|
|
269
|
+
expect(messages[0]!.parts).toEqual(first.parts);
|
|
270
|
+
const line = stream
|
|
271
|
+
.split("\n")
|
|
272
|
+
.find((line) => line.includes('"data-context-checkpoint"'))!;
|
|
273
|
+
const checkpoint = JSON.parse(line.slice(6)).data;
|
|
274
|
+
const cut =
|
|
275
|
+
messages.findIndex(
|
|
276
|
+
(message) => message.id === checkpoint.throughMessageId,
|
|
277
|
+
) + 1;
|
|
278
|
+
continuation = {
|
|
279
|
+
contextSummary: checkpoint.summary,
|
|
280
|
+
messages: [
|
|
281
|
+
...messages.slice(cut),
|
|
282
|
+
text("a2", "assistant", "ORCHID-917"),
|
|
283
|
+
text("u3", "user", "Repeat it"),
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const continued = await request(continuation);
|
|
288
|
+
expect(await continued.text()).toContain("ORCHID-917");
|
|
289
|
+
expect(languageModel.doGenerateCalls).toHaveLength(summaries);
|
|
290
|
+
expect(errors).not.toHaveBeenCalled();
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
165
294
|
it("persists successful responses without content and calls the completion hook", async () => {
|
|
166
295
|
const languageModel = model();
|
|
167
296
|
languageModel.doStream = async () => ({
|