Skip to content

Commit 8c27669

Browse files
[9.2] Update dependency ai to v5 (#244675) (#245562)
# Backport This will backport the following commits from `main` to `9.2`: - [Update dependency ai to v5 (#244675)](#244675) <!--- Backport version: 10.2.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Samiul Monir","email":"150824886+Samiul-TheSoccerFan@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-12-08T17:26:35Z","message":"Update dependency ai to v5 (#244675)\n\n## Summary\n\n- Update dependency ai to v5\n- Addressed the breaking changes\n\n<img width=\"2552\" height=\"1267\" alt=\"Screenshot 2025-11-28 at 2 38\n46 PM\"\nsrc=\"https://github.com/user-attachments/assets/44f9c824-79c5-4e7e-a0ad-216e79673f9e\"\n/>\n\n### Security Checklist:\n**Purpose** – `@ai-sdk/langchain` provides the official adapter that\nconverts LangChain's `Runnable.stream()` output into the AI SDK v5 `UI\nmessage stream` format. We use it in the search playground's\nconversational chain so the existing LangChain pipeline can feed the new\ncreateUIMessageStream/SSE infrastructure without rewriting the entire\nchain logic.\n\n**Justification** – AI SDK v5 moved the LangChain adapter out of the\ncore `ai` package; the only supported way to keep LangChain models\nstreaming into UI chunks is this package. Using the upstream adapter\nkeeps us aligned with Vercel's protocol changes (LC stream events, chunk\nmetadata, etc.) and avoids maintaining fragile, duplicate conversion\ncode in Kibana.\n\n**Alternatives explored** –We looked at copying the old adapter logic,\nbut the new UI chunk format (text, tools, reasoning, data events)\nchanges quickly and is easy to get wrong. Reimplementing it ourselves\nwould be brittle and hard to maintain, so we rely on the upstream\npackage instead.\n\n**Existing dependencies** – Kibana already depends on LangChain itself\nand on the base `ai` SDK, but neither now exposes a LangChain to UI\nstream bridge. `@ai-sdk/langchain` is the upstream successor to what\nused to be bundled in `ai`, so it’s effectively continuing the same\ncapability with ongoing support, making it the preferred and only viable\noption.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"81c634708ab1405b64a5ca524b0f62d84d351467","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Search","backport:all-open","ci:project-deploy-elasticsearch","v9.3.0"],"title":"Update dependency ai to v5","number":244675,"url":"https://github.com/elastic/kibana/pull/244675","mergeCommit":{"message":"Update dependency ai to v5 (#244675)\n\n## Summary\n\n- Update dependency ai to v5\n- Addressed the breaking changes\n\n<img width=\"2552\" height=\"1267\" alt=\"Screenshot 2025-11-28 at 2 38\n46 PM\"\nsrc=\"https://github.com/user-attachments/assets/44f9c824-79c5-4e7e-a0ad-216e79673f9e\"\n/>\n\n### Security Checklist:\n**Purpose** – `@ai-sdk/langchain` provides the official adapter that\nconverts LangChain's `Runnable.stream()` output into the AI SDK v5 `UI\nmessage stream` format. We use it in the search playground's\nconversational chain so the existing LangChain pipeline can feed the new\ncreateUIMessageStream/SSE infrastructure without rewriting the entire\nchain logic.\n\n**Justification** – AI SDK v5 moved the LangChain adapter out of the\ncore `ai` package; the only supported way to keep LangChain models\nstreaming into UI chunks is this package. Using the upstream adapter\nkeeps us aligned with Vercel's protocol changes (LC stream events, chunk\nmetadata, etc.) and avoids maintaining fragile, duplicate conversion\ncode in Kibana.\n\n**Alternatives explored** –We looked at copying the old adapter logic,\nbut the new UI chunk format (text, tools, reasoning, data events)\nchanges quickly and is easy to get wrong. Reimplementing it ourselves\nwould be brittle and hard to maintain, so we rely on the upstream\npackage instead.\n\n**Existing dependencies** – Kibana already depends on LangChain itself\nand on the base `ai` SDK, but neither now exposes a LangChain to UI\nstream bridge. `@ai-sdk/langchain` is the upstream successor to what\nused to be bundled in `ai`, so it’s effectively continuing the same\ncapability with ongoing support, making it the preferred and only viable\noption.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"81c634708ab1405b64a5ca524b0f62d84d351467"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/244675","number":244675,"mergeCommit":{"message":"Update dependency ai to v5 (#244675)\n\n## Summary\n\n- Update dependency ai to v5\n- Addressed the breaking changes\n\n<img width=\"2552\" height=\"1267\" alt=\"Screenshot 2025-11-28 at 2 38\n46 PM\"\nsrc=\"https://github.com/user-attachments/assets/44f9c824-79c5-4e7e-a0ad-216e79673f9e\"\n/>\n\n### Security Checklist:\n**Purpose** – `@ai-sdk/langchain` provides the official adapter that\nconverts LangChain's `Runnable.stream()` output into the AI SDK v5 `UI\nmessage stream` format. We use it in the search playground's\nconversational chain so the existing LangChain pipeline can feed the new\ncreateUIMessageStream/SSE infrastructure without rewriting the entire\nchain logic.\n\n**Justification** – AI SDK v5 moved the LangChain adapter out of the\ncore `ai` package; the only supported way to keep LangChain models\nstreaming into UI chunks is this package. Using the upstream adapter\nkeeps us aligned with Vercel's protocol changes (LC stream events, chunk\nmetadata, etc.) and avoids maintaining fragile, duplicate conversion\ncode in Kibana.\n\n**Alternatives explored** –We looked at copying the old adapter logic,\nbut the new UI chunk format (text, tools, reasoning, data events)\nchanges quickly and is easy to get wrong. Reimplementing it ourselves\nwould be brittle and hard to maintain, so we rely on the upstream\npackage instead.\n\n**Existing dependencies** – Kibana already depends on LangChain itself\nand on the base `ai` SDK, but neither now exposes a LangChain to UI\nstream bridge. `@ai-sdk/langchain` is the upstream successor to what\nused to be bundled in `ai`, so it’s effectively continuing the same\ncapability with ongoing support, making it the preferred and only viable\noption.\n\n\n### Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [X] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"81c634708ab1405b64a5ca524b0f62d84d351467"}}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent dc32b50 commit 8c27669

13 files changed

Lines changed: 326 additions & 274 deletions

File tree

.buildkite/scripts/steps/security/third_party_packages.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ ajv-formats
1414
dompurify
1515
@axe-core/playwright
1616
magic-bytes.js
17+
@ai-sdk/langchain

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@
108108
"pkce-challenge": "3.1.0"
109109
},
110110
"dependencies": {
111-
"@a2a-js/sdk": "^0.3.0",
111+
"@a2a-js/sdk": "^0.3.4",
112+
"@ai-sdk/langchain": "^1.0.102",
112113
"@apidevtools/json-schema-ref-parser": "^14.1.1",
113114
"@appland/sql-parser": "^1.5.1",
114115
"@arizeai/openinference-semantic-conventions": "^1.1.0",
@@ -1203,7 +1204,7 @@
12031204
"@xstate5/react": "npm:@xstate/react@^5.0.3",
12041205
"@xyflow/react": "^12.8.5",
12051206
"adm-zip": "^0.5.16",
1206-
"ai": "^4.3.15",
1207+
"ai": "^5.0.102",
12071208
"ajv": "^8.17.1",
12081209
"ajv-formats": "^3.0.1",
12091210
"antlr4": "^4.13.2",

renovate.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2330,7 +2330,8 @@
23302330
{
23312331
"groupName": "search-ui ai dependency",
23322332
"matchDepNames": [
2333-
"ai"
2333+
"ai",
2334+
"@ai-sdk/langchain"
23342335
],
23352336
"reviewers": [
23362337
"team:search-kibana"

src/platform/packages/shared/kbn-test/jest-preset.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ module.exports = {
125125
transformIgnorePatterns: [
126126
// ignore all node_modules except monaco-editor, monaco-yaml which requires babel transforms to handle dynamic import()
127127
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
128-
'[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|monaco-languageserver-types|monaco-marker-data-provider|monaco-worker-manager|vscode-languageserver-types|d3-interpolate|d3-color|langchain|langsmith|@cfworker|gpt-tokenizer|flat|@langchain|eventsource-parser|fast-check|@fast-check/jest|@assemblyscript|quickselect|rbush|vega-interpreter|vega-util|vega-tooltip))[/\\\\].+\\.js$',
128+
'[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|monaco-languageserver-types|monaco-marker-data-provider|monaco-worker-manager|vscode-languageserver-types|d3-interpolate|d3-color|langchain|langsmith|@cfworker|gpt-tokenizer|flat|@langchain|eventsource-parser|fast-check|@fast-check/jest|@assemblyscript|quickselect|rbush|zod/v4|vega-interpreter|vega-util|vega-tooltip))[/\\\\].+\\.js$',
129129
'packages/kbn-pm/dist/index.js',
130-
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain))/dist/[/\\\\].+\\.js$',
131-
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain))/dist/util/[/\\\\].+\\.js$',
130+
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/[/\\\\].+\\.js$',
131+
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/util/[/\\\\].+\\.js$',
132132
],
133133

134134
// An array of regexp pattern strings that are matched against all source file paths, matched files to include/exclude for code coverage

src/platform/packages/shared/kbn-test/jest_integration_node/jest-preset.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ module.exports = {
2323
// An array of regexp pattern strings that are matched against, matched files will skip transformation:
2424
transformIgnorePatterns: [
2525
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
26-
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|gpt-tokenizer|flat|@langchain|eventsource-parser))[/\\\\].+\\.js$',
27-
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain))/dist/[/\\\\].+\\.js$',
28-
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain))/dist/util/[/\\\\].+\\.js$',
26+
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|gpt-tokenizer|flat|@langchain|eventsource-parser|zod/v4))[/\\\\].+\\.js$',
27+
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/[/\\\\].+\\.js$',
28+
'[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith|@langchain|zod/v4))/dist/util/[/\\\\].+\\.js$',
2929
],
3030
setupFilesAfterEnv: [
3131
'<rootDir>/src/platform/packages/shared/kbn-test/src/jest/setup/after_env.integration.js',

x-pack/solutions/search/plugins/search_playground/public/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,6 @@ export interface ElasticsearchIndex {
188188
uuid?: Uuid;
189189
}
190190

191-
export type JSONValue = null | string | number | boolean | { [x: string]: JSONValue } | JSONValue[];
192-
193191
export interface ChatRequestOptions {
194192
options?: RequestOptions;
195193
data?: ChatRequestData;

x-pack/solutions/search/plugins/search_playground/public/utils/api.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { v4 as uuidv4 } from 'uuid';
9+
import type { UIMessageChunk } from 'ai';
910
import { readDataStream } from './stream';
1011
import type { Annotation, Message } from '../types';
1112
import { MessageRole } from '../types';
@@ -99,41 +100,59 @@ export async function parseDataStream({
99100
const createdAt = getCurrentDate();
100101
const prefixMap: PrefixMap = {};
101102
let messageAnnotations: Annotation[] | undefined;
103+
const streamInstanceId = generateId();
104+
let messageSequence = 0;
105+
const allocateResponseId = (serverAssignedId?: string) => {
106+
messageSequence += 1;
107+
const baseId = `${streamInstanceId}-${messageSequence}`;
108+
return serverAssignedId ? `${baseId}:${serverAssignedId}` : baseId;
109+
};
110+
111+
let responseMessageId = allocateResponseId();
102112

103-
for await (const { type, value } of readDataStream(reader, {
113+
for await (const chunk of readDataStream(reader, {
104114
isAborted: () => abortControllerRef?.current === null,
105115
})) {
106-
if (type === 'text') {
116+
const { type } = chunk;
117+
118+
if (type === 'text-start') {
119+
responseMessageId =
120+
'id' in chunk && chunk.id ? allocateResponseId(chunk.id) : allocateResponseId();
121+
prefixMap.text = undefined;
122+
messageAnnotations = undefined;
123+
continue;
124+
} else if (type === 'text-delta' && 'delta' in chunk && typeof chunk.delta === 'string') {
107125
if (prefixMap.text) {
108126
prefixMap.text = {
109127
...prefixMap.text,
110-
content: (prefixMap.text.content || '') + value,
128+
content: (prefixMap.text.content || '') + chunk.delta,
111129
};
112130
} else {
113131
prefixMap.text = {
114-
id: generateId(),
132+
id: responseMessageId,
115133
role: MessageRole.assistant,
116-
content: value,
134+
content: chunk.delta,
117135
createdAt,
118136
};
119137
}
120-
} else if (type === 'error') {
121-
handleFailure(value);
138+
} else if (type === 'error' && 'errorText' in chunk) {
139+
handleFailure(chunk.errorText);
122140
break;
123-
}
124-
125-
let responseMessage = prefixMap.text;
126-
127-
if (type === 'message_annotations') {
141+
} else if (type === 'data-message_annotations' && 'data' in chunk) {
142+
const annotationsChunk = chunk as Extract<
143+
UIMessageChunk,
144+
{ type: `data-${string}`; data: unknown }
145+
>;
146+
const annotationValues = annotationsChunk.data as Annotation[];
128147
if (!messageAnnotations) {
129-
messageAnnotations = [...(value as unknown as Annotation[])];
148+
messageAnnotations = [...annotationValues];
130149
} else {
131-
messageAnnotations.push(...(value as unknown as Annotation[]));
150+
messageAnnotations.push(...annotationValues);
132151
}
133-
134-
responseMessage = assignAnnotationsToMessage(prefixMap.text, messageAnnotations);
135152
}
136153

154+
const responseMessage = prefixMap.text;
155+
137156
if (messageAnnotations?.length) {
138157
const messagePrefixKeys: Array<keyof PrefixMap> = ['text'];
139158
messagePrefixKeys.forEach((key) => {

x-pack/solutions/search/plugins/search_playground/public/utils/stream.ts

Lines changed: 53 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -5,182 +5,79 @@
55
* 2.0.
66
*/
77

8-
import type { JSONValue } from '../types';
8+
import type { UIMessageChunk } from 'ai';
99

10-
export interface StreamPart<CODE extends string, NAME extends string, TYPE> {
11-
code: CODE;
12-
name: NAME;
13-
parse: (value: JSONValue) => { type: NAME; value: TYPE };
10+
interface ReadDataStreamOptions {
11+
isAborted?: () => boolean;
1412
}
1513

16-
type StreamParts =
17-
| typeof textStreamPart
18-
| typeof errorStreamPart
19-
| typeof messageAnnotationsStreamPart;
20-
/**
21-
* Maps the type of a stream part to its value type.
22-
*/
23-
type StreamPartValueType = {
24-
[P in StreamParts as P['name']]: ReturnType<P['parse']>['value'];
25-
};
26-
27-
export type StreamPartType =
28-
| ReturnType<typeof textStreamPart.parse>
29-
| ReturnType<typeof errorStreamPart.parse>
30-
| ReturnType<typeof messageAnnotationsStreamPart.parse>
31-
| ReturnType<typeof bufferStreamPart.parse>;
32-
33-
const NEWLINE = '\n'.charCodeAt(0);
34-
35-
const concatChunks = (chunks: Uint8Array[], totalLength: number) => {
36-
const concatenatedChunks = new Uint8Array(totalLength);
37-
38-
let offset = 0;
39-
for (const chunk of chunks) {
40-
concatenatedChunks.set(chunk, offset);
41-
offset += chunk.length;
42-
}
43-
chunks.length = 0;
44-
45-
return concatenatedChunks;
46-
};
14+
const EVENT_SEPARATOR = '\n\n';
15+
const EVENT_DATA_PREFIX = 'data: ';
16+
const STREAM_END_PAYLOAD = '[DONE]';
4717

4818
export async function* readDataStream(
4919
reader: ReadableStreamDefaultReader<Uint8Array>,
50-
{ isAborted }: { isAborted?: () => boolean } = {}
51-
): AsyncGenerator<StreamPartType> {
20+
{ isAborted }: ReadDataStreamOptions = {}
21+
): AsyncGenerator<UIMessageChunk> {
5222
const decoder = new TextDecoder();
53-
const chunks: Uint8Array[] = [];
54-
let totalLength = 0;
23+
let buffer = '';
5524

5625
while (true) {
57-
const { value } = await reader.read();
26+
const { done, value } = await reader.read();
5827

5928
if (value) {
60-
chunks.push(value);
61-
totalLength += value.length;
62-
if (value[value.length - 1] !== NEWLINE) {
63-
continue;
29+
buffer += decoder.decode(value, { stream: true });
30+
31+
let separatorIndex = buffer.indexOf(EVENT_SEPARATOR);
32+
while (separatorIndex !== -1) {
33+
const event = buffer.slice(0, separatorIndex);
34+
buffer = buffer.slice(separatorIndex + EVENT_SEPARATOR.length);
35+
separatorIndex = buffer.indexOf(EVENT_SEPARATOR);
36+
37+
if (!event.startsWith(EVENT_DATA_PREFIX)) {
38+
continue;
39+
}
40+
41+
const payload = event.slice(EVENT_DATA_PREFIX.length);
42+
43+
if (payload === STREAM_END_PAYLOAD) {
44+
return;
45+
}
46+
47+
const message = JSON.parse(payload);
48+
if (isUIMessageChunk(message)) {
49+
yield message;
50+
} else {
51+
throw new Error(`Unsupported stream event: ${payload}`);
52+
}
6453
}
6554
}
6655

67-
if (chunks.length === 0) {
56+
if (done) {
57+
if (buffer.length) {
58+
let payload = buffer.trim();
59+
if (payload.startsWith(EVENT_DATA_PREFIX)) {
60+
payload = payload.slice(EVENT_DATA_PREFIX.length).trim();
61+
}
62+
if (payload && payload !== STREAM_END_PAYLOAD) {
63+
const message = JSON.parse(payload);
64+
if (isUIMessageChunk(message)) {
65+
yield message;
66+
} else {
67+
throw new Error(`Unsupported stream event: ${payload}`);
68+
}
69+
}
70+
}
6871
break;
6972
}
7073

71-
const concatenatedChunks = concatChunks(chunks, totalLength);
72-
totalLength = 0;
73-
74-
const streamParts = decoder
75-
.decode(concatenatedChunks, { stream: true })
76-
.split('\n')
77-
.filter((line) => line !== '')
78-
.map(parseStreamPart);
79-
80-
for (const streamPart of streamParts) {
81-
yield streamPart;
82-
}
83-
8474
if (isAborted?.()) {
85-
reader.cancel();
75+
await reader.cancel();
8676
break;
8777
}
8878
}
8979
}
9080

91-
const createStreamPart = <CODE extends string, NAME extends string, TYPE>(
92-
code: CODE,
93-
name: NAME,
94-
parse: (value: JSONValue) => { type: NAME; value: TYPE }
95-
): StreamPart<CODE, NAME, TYPE> => {
96-
return {
97-
code,
98-
name,
99-
parse,
100-
};
101-
};
102-
103-
const textStreamPart = createStreamPart('0', 'text', (value) => {
104-
if (typeof value !== 'string') {
105-
throw new Error('"text" parts expect a string value.');
106-
}
107-
return { type: 'text', value };
108-
});
109-
110-
const errorStreamPart = createStreamPart('3', 'error', (value) => {
111-
if (typeof value !== 'string') {
112-
throw new Error('"error" parts expect a string value.');
113-
}
114-
return { type: 'error', value };
115-
});
116-
117-
const messageAnnotationsStreamPart = createStreamPart('8', 'message_annotations', (value) => {
118-
if (!Array.isArray(value)) {
119-
throw new Error('"message_annotations" parts expect an array value.');
120-
}
121-
122-
return { type: 'message_annotations', value };
123-
});
124-
125-
const bufferStreamPart = createStreamPart('10', 'buffer', (value) => {
126-
if (typeof value !== 'string') {
127-
throw new Error('"buffer" parts expect a string value.');
128-
}
129-
130-
return { type: 'buffer', value };
131-
});
132-
133-
const streamParts = [
134-
textStreamPart,
135-
errorStreamPart,
136-
bufferStreamPart,
137-
messageAnnotationsStreamPart,
138-
] as const;
139-
140-
type StreamPartMap = {
141-
[P in StreamParts as P['code']]: P;
142-
};
143-
144-
const streamPartsByCode: StreamPartMap = streamParts.reduce(
145-
(acc, part) => ({
146-
...acc,
147-
[part.code]: part,
148-
}),
149-
{} as StreamPartMap
150-
);
151-
152-
const validCodes = streamParts.map((part) => part.code);
153-
154-
export const parseStreamPart = (line: string): StreamPartType => {
155-
const firstSeparatorIndex = line.indexOf(':');
156-
157-
if (firstSeparatorIndex === -1) {
158-
throw new Error('Failed to parse stream string. No separator found.');
159-
}
160-
161-
const prefix = line.slice(0, firstSeparatorIndex) as keyof StreamPartMap;
162-
163-
if (!validCodes.includes(prefix)) {
164-
throw new Error(`Failed to parse stream string. Invalid code ${prefix}.`);
165-
}
166-
167-
const code = prefix as keyof StreamPartMap;
168-
169-
const textValue = line.slice(firstSeparatorIndex + 1);
170-
const jsonValue: JSONValue = JSON.parse(textValue);
171-
172-
return streamPartsByCode[code].parse(jsonValue);
173-
};
174-
175-
export const formatStreamPart = <T extends keyof StreamPartValueType>(
176-
type: T,
177-
value: StreamPartValueType[T]
178-
): string => {
179-
const streamPart = streamParts.find((part) => part.name === type);
180-
181-
if (!streamPart) {
182-
throw new Error(`Invalid stream part type: ${type as string}`);
183-
}
184-
185-
return `${streamPart.code}:${JSON.stringify(value)}\n`;
186-
};
81+
function isUIMessageChunk(message: unknown): message is UIMessageChunk {
82+
return Boolean(message && typeof message === 'object' && 'type' in message);
83+
}

0 commit comments

Comments
 (0)