@genkit-ai/express 1.36.0 → 1.38.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/lib/index.js +23 -13
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +23 -13
- package/lib/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +38 -15
- package/tests/express_test.ts +17 -1
package/lib/index.js
CHANGED
|
@@ -60,9 +60,10 @@ function expressHandler(action, opts) {
|
|
|
60
60
|
context = await opts?.contextProvider?.({
|
|
61
61
|
method: request.method,
|
|
62
62
|
headers: Object.fromEntries(
|
|
63
|
-
Object.entries(request.headers).map(([key, value]) => [
|
|
63
|
+
Object.entries(request.headers).filter(([, value]) => value !== void 0).map(([key, value]) => [
|
|
64
64
|
key.toLowerCase(),
|
|
65
|
-
|
|
65
|
+
// RFC 9110 5.3: combine repeated field lines with a comma.
|
|
66
|
+
Array.isArray(value) ? value.join(", ") : String(value)
|
|
66
67
|
])
|
|
67
68
|
),
|
|
68
69
|
input
|
|
@@ -82,7 +83,7 @@ ${e.stack}`
|
|
|
82
83
|
request.on("timeout", () => {
|
|
83
84
|
abortController.abort();
|
|
84
85
|
});
|
|
85
|
-
if (request.get("Accept")
|
|
86
|
+
if (request.get("Accept")?.toLowerCase().includes("text/event-stream") || stream === "true") {
|
|
86
87
|
const streamManager = opts?.streamManager;
|
|
87
88
|
if (streamManager && streamId) {
|
|
88
89
|
await subscribeToStream(streamManager, streamId, response);
|
|
@@ -136,6 +137,7 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
136
137
|
}
|
|
137
138
|
try {
|
|
138
139
|
let onChunk = (chunk) => {
|
|
140
|
+
if (response.destroyed) return;
|
|
139
141
|
response.write(
|
|
140
142
|
"data: " + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
141
143
|
);
|
|
@@ -156,10 +158,12 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
156
158
|
taskQueue.enqueue(() => durableStream.done(result.result));
|
|
157
159
|
await taskQueue.merge();
|
|
158
160
|
}
|
|
159
|
-
response.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
if (!response.destroyed) {
|
|
162
|
+
response.write(
|
|
163
|
+
"data: " + JSON.stringify({ result: result.result }) + streamDelimiter
|
|
164
|
+
);
|
|
165
|
+
response.end();
|
|
166
|
+
}
|
|
163
167
|
} catch (e) {
|
|
164
168
|
if (durableStream) {
|
|
165
169
|
taskQueue.enqueue(() => durableStream.error(e));
|
|
@@ -169,23 +173,27 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
169
173
|
`Streaming request failed with error: ${e.message}
|
|
170
174
|
${e.stack}`
|
|
171
175
|
);
|
|
172
|
-
response.
|
|
173
|
-
|
|
174
|
-
error:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
if (!response.destroyed) {
|
|
177
|
+
response.write(
|
|
178
|
+
`error: ${JSON.stringify({
|
|
179
|
+
error: (0, import_context.getCallableJSON)(e)
|
|
180
|
+
})}${streamDelimiter}`
|
|
181
|
+
);
|
|
182
|
+
response.end();
|
|
183
|
+
}
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
186
|
async function subscribeToStream(streamManager, streamId, response) {
|
|
181
187
|
try {
|
|
182
188
|
await streamManager.subscribe(streamId, {
|
|
183
189
|
onChunk: (chunk) => {
|
|
190
|
+
if (response.destroyed) return;
|
|
184
191
|
response.write(
|
|
185
192
|
"data: " + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
186
193
|
);
|
|
187
194
|
},
|
|
188
195
|
onDone: (output) => {
|
|
196
|
+
if (response.destroyed) return;
|
|
189
197
|
response.write(
|
|
190
198
|
"data: " + JSON.stringify({ result: output }) + streamDelimiter
|
|
191
199
|
);
|
|
@@ -196,6 +204,7 @@ async function subscribeToStream(streamManager, streamId, response) {
|
|
|
196
204
|
`Streaming request failed with error: ${err.message}
|
|
197
205
|
${err.stack}`
|
|
198
206
|
);
|
|
207
|
+
if (response.destroyed) return;
|
|
199
208
|
response.write(
|
|
200
209
|
`error: ${JSON.stringify({
|
|
201
210
|
error: (0, import_context.getCallableJSON)(err)
|
|
@@ -205,6 +214,7 @@ ${err.stack}`
|
|
|
205
214
|
}
|
|
206
215
|
});
|
|
207
216
|
} catch (e) {
|
|
217
|
+
if (response.destroyed) return;
|
|
208
218
|
if (e instanceof import_beta.StreamNotFoundError) {
|
|
209
219
|
response.status(204).end();
|
|
210
220
|
return;
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers)\n // Skip headers explicitly set to undefined so they don't become\n // the literal string \"undefined\" via String(value).\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [\n key.toLowerCase(),\n // RFC 9110 5.3: combine repeated field lines with a comma.\n Array.isArray(value) ? value.join(', ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (\n request.get('Accept')?.toLowerCase().includes('text/event-stream') ||\n stream === 'true'\n ) {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n // The client may have disconnected mid-stream; writing to a destroyed\n // response would throw.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n if (!response.destroyed) {\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n }\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n if (!response.destroyed) {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n if (response.destroyed) return;\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
|
package/lib/index.mjs
CHANGED
|
@@ -29,9 +29,10 @@ function expressHandler(action, opts) {
|
|
|
29
29
|
context = await opts?.contextProvider?.({
|
|
30
30
|
method: request.method,
|
|
31
31
|
headers: Object.fromEntries(
|
|
32
|
-
Object.entries(request.headers).map(([key, value]) => [
|
|
32
|
+
Object.entries(request.headers).filter(([, value]) => value !== void 0).map(([key, value]) => [
|
|
33
33
|
key.toLowerCase(),
|
|
34
|
-
|
|
34
|
+
// RFC 9110 5.3: combine repeated field lines with a comma.
|
|
35
|
+
Array.isArray(value) ? value.join(", ") : String(value)
|
|
35
36
|
])
|
|
36
37
|
),
|
|
37
38
|
input
|
|
@@ -51,7 +52,7 @@ ${e.stack}`
|
|
|
51
52
|
request.on("timeout", () => {
|
|
52
53
|
abortController.abort();
|
|
53
54
|
});
|
|
54
|
-
if (request.get("Accept")
|
|
55
|
+
if (request.get("Accept")?.toLowerCase().includes("text/event-stream") || stream === "true") {
|
|
55
56
|
const streamManager = opts?.streamManager;
|
|
56
57
|
if (streamManager && streamId) {
|
|
57
58
|
await subscribeToStream(streamManager, streamId, response);
|
|
@@ -105,6 +106,7 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
105
106
|
}
|
|
106
107
|
try {
|
|
107
108
|
let onChunk = (chunk) => {
|
|
109
|
+
if (response.destroyed) return;
|
|
108
110
|
response.write(
|
|
109
111
|
"data: " + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
110
112
|
);
|
|
@@ -125,10 +127,12 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
125
127
|
taskQueue.enqueue(() => durableStream.done(result.result));
|
|
126
128
|
await taskQueue.merge();
|
|
127
129
|
}
|
|
128
|
-
response.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
if (!response.destroyed) {
|
|
131
|
+
response.write(
|
|
132
|
+
"data: " + JSON.stringify({ result: result.result }) + streamDelimiter
|
|
133
|
+
);
|
|
134
|
+
response.end();
|
|
135
|
+
}
|
|
132
136
|
} catch (e) {
|
|
133
137
|
if (durableStream) {
|
|
134
138
|
taskQueue.enqueue(() => durableStream.error(e));
|
|
@@ -138,23 +142,27 @@ async function runActionWithDurableStreaming(action, streamManager, streamId, in
|
|
|
138
142
|
`Streaming request failed with error: ${e.message}
|
|
139
143
|
${e.stack}`
|
|
140
144
|
);
|
|
141
|
-
response.
|
|
142
|
-
|
|
143
|
-
error:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
if (!response.destroyed) {
|
|
146
|
+
response.write(
|
|
147
|
+
`error: ${JSON.stringify({
|
|
148
|
+
error: getCallableJSON(e)
|
|
149
|
+
})}${streamDelimiter}`
|
|
150
|
+
);
|
|
151
|
+
response.end();
|
|
152
|
+
}
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
async function subscribeToStream(streamManager, streamId, response) {
|
|
150
156
|
try {
|
|
151
157
|
await streamManager.subscribe(streamId, {
|
|
152
158
|
onChunk: (chunk) => {
|
|
159
|
+
if (response.destroyed) return;
|
|
153
160
|
response.write(
|
|
154
161
|
"data: " + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
155
162
|
);
|
|
156
163
|
},
|
|
157
164
|
onDone: (output) => {
|
|
165
|
+
if (response.destroyed) return;
|
|
158
166
|
response.write(
|
|
159
167
|
"data: " + JSON.stringify({ result: output }) + streamDelimiter
|
|
160
168
|
);
|
|
@@ -165,6 +173,7 @@ async function subscribeToStream(streamManager, streamId, response) {
|
|
|
165
173
|
`Streaming request failed with error: ${err.message}
|
|
166
174
|
${err.stack}`
|
|
167
175
|
);
|
|
176
|
+
if (response.destroyed) return;
|
|
168
177
|
response.write(
|
|
169
178
|
`error: ${JSON.stringify({
|
|
170
179
|
error: getCallableJSON(err)
|
|
@@ -174,6 +183,7 @@ ${err.stack}`
|
|
|
174
183
|
}
|
|
175
184
|
});
|
|
176
185
|
} catch (e) {
|
|
186
|
+
if (response.destroyed) return;
|
|
177
187
|
if (e instanceof StreamNotFoundError) {
|
|
178
188
|
response.status(204).end();
|
|
179
189
|
return;
|
package/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { type CorsOptions } from 'cors';\nimport { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n const input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers)\n // Skip headers explicitly set to undefined so they don't become\n // the literal string \"undefined\" via String(value).\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [\n key.toLowerCase(),\n // RFC 9110 5.3: combine repeated field lines with a comma.\n Array.isArray(value) ? value.join(', ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\n });\n\n if (\n request.get('Accept')?.toLowerCase().includes('text/event-stream') ||\n stream === 'true'\n ) {\n const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\n );\n } else {\n try {\n const result = await action.run(input, {\n context,\n abortSignal: abortController.signal,\n });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n // The client may have disconnected mid-stream; writing to a destroyed\n // response would throw.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n if (!response.destroyed) {\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n }\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n if (!response.destroyed) {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n if (response.destroyed) return;\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n if (response.destroyed) return;\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n // The subscribing client may have disconnected; skip writes to a\n // destroyed response to avoid throwing.\n if (response.destroyed) return;\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? Number.parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"genai",
|
|
10
10
|
"generative-ai"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.38.0",
|
|
13
13
|
"type": "commonjs",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"express": "^4.21.1 || ^5",
|
|
27
|
-
"genkit": "^1.
|
|
27
|
+
"genkit": "^1.38.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"get-port": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -85,10 +85,15 @@ export function expressHandler<
|
|
|
85
85
|
(await opts?.contextProvider?.({
|
|
86
86
|
method: request.method as RequestData['method'],
|
|
87
87
|
headers: Object.fromEntries(
|
|
88
|
-
Object.entries(request.headers)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
Object.entries(request.headers)
|
|
89
|
+
// Skip headers explicitly set to undefined so they don't become
|
|
90
|
+
// the literal string "undefined" via String(value).
|
|
91
|
+
.filter(([, value]) => value !== undefined)
|
|
92
|
+
.map(([key, value]) => [
|
|
93
|
+
key.toLowerCase(),
|
|
94
|
+
// RFC 9110 5.3: combine repeated field lines with a comma.
|
|
95
|
+
Array.isArray(value) ? value.join(', ') : String(value),
|
|
96
|
+
])
|
|
92
97
|
),
|
|
93
98
|
input,
|
|
94
99
|
})) || {};
|
|
@@ -109,7 +114,10 @@ export function expressHandler<
|
|
|
109
114
|
abortController.abort();
|
|
110
115
|
});
|
|
111
116
|
|
|
112
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
request.get('Accept')?.toLowerCase().includes('text/event-stream') ||
|
|
119
|
+
stream === 'true'
|
|
120
|
+
) {
|
|
113
121
|
const streamManager = opts?.streamManager;
|
|
114
122
|
if (streamManager && streamId) {
|
|
115
123
|
await subscribeToStream(streamManager, streamId, response);
|
|
@@ -180,6 +188,9 @@ async function runActionWithDurableStreaming<
|
|
|
180
188
|
}
|
|
181
189
|
try {
|
|
182
190
|
let onChunk = (chunk: z.infer<S>) => {
|
|
191
|
+
// The client may have disconnected mid-stream; writing to a destroyed
|
|
192
|
+
// response would throw.
|
|
193
|
+
if (response.destroyed) return;
|
|
183
194
|
response.write(
|
|
184
195
|
'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
185
196
|
);
|
|
@@ -200,10 +211,12 @@ async function runActionWithDurableStreaming<
|
|
|
200
211
|
taskQueue!.enqueue(() => durableStream!.done(result.result));
|
|
201
212
|
await taskQueue!.merge();
|
|
202
213
|
}
|
|
203
|
-
response.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
214
|
+
if (!response.destroyed) {
|
|
215
|
+
response.write(
|
|
216
|
+
'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
|
|
217
|
+
);
|
|
218
|
+
response.end();
|
|
219
|
+
}
|
|
207
220
|
} catch (e) {
|
|
208
221
|
if (durableStream) {
|
|
209
222
|
taskQueue!.enqueue(() => durableStream!.error(e));
|
|
@@ -214,12 +227,14 @@ async function runActionWithDurableStreaming<
|
|
|
214
227
|
(e as Error).stack
|
|
215
228
|
}`
|
|
216
229
|
);
|
|
217
|
-
response.
|
|
218
|
-
|
|
219
|
-
error:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
if (!response.destroyed) {
|
|
231
|
+
response.write(
|
|
232
|
+
`error: ${JSON.stringify({
|
|
233
|
+
error: getCallableJSON(e),
|
|
234
|
+
})}${streamDelimiter}`
|
|
235
|
+
);
|
|
236
|
+
response.end();
|
|
237
|
+
}
|
|
223
238
|
}
|
|
224
239
|
}
|
|
225
240
|
|
|
@@ -231,11 +246,15 @@ async function subscribeToStream(
|
|
|
231
246
|
try {
|
|
232
247
|
await streamManager.subscribe(streamId, {
|
|
233
248
|
onChunk: (chunk) => {
|
|
249
|
+
// The subscribing client may have disconnected; skip writes to a
|
|
250
|
+
// destroyed response to avoid throwing.
|
|
251
|
+
if (response.destroyed) return;
|
|
234
252
|
response.write(
|
|
235
253
|
'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
|
|
236
254
|
);
|
|
237
255
|
},
|
|
238
256
|
onDone: (output) => {
|
|
257
|
+
if (response.destroyed) return;
|
|
239
258
|
response.write(
|
|
240
259
|
'data: ' + JSON.stringify({ result: output }) + streamDelimiter
|
|
241
260
|
);
|
|
@@ -247,6 +266,7 @@ async function subscribeToStream(
|
|
|
247
266
|
(err as Error).stack
|
|
248
267
|
}`
|
|
249
268
|
);
|
|
269
|
+
if (response.destroyed) return;
|
|
250
270
|
response.write(
|
|
251
271
|
`error: ${JSON.stringify({
|
|
252
272
|
error: getCallableJSON(err),
|
|
@@ -256,6 +276,9 @@ async function subscribeToStream(
|
|
|
256
276
|
},
|
|
257
277
|
});
|
|
258
278
|
} catch (e: any) {
|
|
279
|
+
// The subscribing client may have disconnected; skip writes to a
|
|
280
|
+
// destroyed response to avoid throwing.
|
|
281
|
+
if (response.destroyed) return;
|
|
259
282
|
if (e instanceof StreamNotFoundError) {
|
|
260
283
|
response.status(204).end();
|
|
261
284
|
return;
|
package/tests/express_test.ts
CHANGED
|
@@ -405,7 +405,7 @@ describe('expressHandler', async () => {
|
|
|
405
405
|
assert.strictEqual(await subscription.output, 'Echo: durable');
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
it
|
|
408
|
+
it('should return 204 for a non-existent stream', async () => {
|
|
409
409
|
try {
|
|
410
410
|
const result = streamFlow({
|
|
411
411
|
url: `http://localhost:${port}/streamingFlowDurable`,
|
|
@@ -422,6 +422,22 @@ describe('expressHandler', async () => {
|
|
|
422
422
|
}
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
+
it('detects streaming for a multi-value, mixed-case Accept header', async () => {
|
|
426
|
+
// Clients/proxies can send a media-type list and mixed casing, e.g.
|
|
427
|
+
// "Text/Event-Stream, */*"; the handler should still stream rather than
|
|
428
|
+
// fall back to a single JSON response.
|
|
429
|
+
const response = await fetch(`http://localhost:${port}/streamingFlow`, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
headers: {
|
|
432
|
+
'Content-Type': 'application/json',
|
|
433
|
+
Accept: 'Text/Event-Stream, */*',
|
|
434
|
+
},
|
|
435
|
+
body: JSON.stringify({ data: { question: 'hi' } }),
|
|
436
|
+
});
|
|
437
|
+
const text = await response.text();
|
|
438
|
+
assert.match(text, /^data: /m); // SSE frames, not a single JSON body
|
|
439
|
+
});
|
|
440
|
+
|
|
425
441
|
it('stream a model', async () => {
|
|
426
442
|
const result = streamFlow({
|
|
427
443
|
url: `http://localhost:${port}/echoModel`,
|