@copilotkit/runtime 0.0.0-fix-restore-handle-method-node-http-20251222114321 → 0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142

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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
- "version": "0.0.0-fix-restore-handle-method-node-http-20251222114321",
12
+ "version": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
13
13
  "sideEffects": false,
14
14
  "main": "./dist/index.js",
15
15
  "module": "./dist/index.mjs",
@@ -46,15 +46,13 @@
46
46
  "tsup": "^6.7.0",
47
47
  "typescript": "^5.2.3",
48
48
  "vitest": "^3.2.4",
49
+ "@copilotkit/shared": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
50
+ "@copilotkitnext/agent": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
51
+ "@copilotkitnext/runtime": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
49
52
  "eslint-config-custom": "1.4.6",
50
53
  "tsconfig": "1.4.6"
51
54
  },
52
55
  "dependencies": {
53
- "@ag-ui/client": "^0.0.42",
54
- "@ag-ui/core": "^0.0.42",
55
- "@ag-ui/langgraph": "^0.0.20",
56
- "@copilotkitnext/agent": "0.0.33",
57
- "@copilotkitnext/runtime": "0.0.33",
58
56
  "@graphql-yoga/plugin-defer-stream": "^3.3.1",
59
57
  "@hono/node-server": "^1.13.5",
60
58
  "@scarf/scarf": "^1.3.0",
@@ -71,10 +69,12 @@
71
69
  "reflect-metadata": "^0.2.2",
72
70
  "rxjs": "7.8.1",
73
71
  "type-graphql": "2.0.0-rc.1",
74
- "zod": "^3.23.3",
75
- "@copilotkit/shared": "0.0.0-fix-restore-handle-method-node-http-20251222114321"
72
+ "zod": "^3.23.3"
76
73
  },
77
74
  "peerDependencies": {
75
+ "@ag-ui/client": "^0.0.42",
76
+ "@ag-ui/core": "^0.0.42",
77
+ "@ag-ui/langgraph": "^0.0.20",
78
78
  "@anthropic-ai/sdk": "^0.57.0",
79
79
  "@langchain/aws": ">=0.1.9",
80
80
  "@langchain/core": ">=0.3.66",
@@ -84,7 +84,10 @@
84
84
  "@langchain/openai": ">=0.4.2",
85
85
  "groq-sdk": ">=0.3.0 <1.0.0",
86
86
  "langchain": ">=0.3.3",
87
- "openai": "^4.85.1"
87
+ "openai": "^4.85.1",
88
+ "@copilotkit/shared": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
89
+ "@copilotkitnext/agent": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142",
90
+ "@copilotkitnext/runtime": "0.0.0-fix-runtime-openai-api-key-passthrough-20260115003142"
88
91
  },
89
92
  "peerDependenciesMeta": {
90
93
  "@anthropic-ai/sdk": {
@@ -128,7 +131,7 @@
128
131
  "textarea"
129
132
  ],
130
133
  "scripts": {
131
- "build": "tsup --onSuccess \"pnpm run generate-graphql-schema\"",
134
+ "build": "pnpm run generate-graphql-schema && tsup",
132
135
  "dev": "tsup --watch --onSuccess \"pnpm run generate-graphql-schema\"",
133
136
  "test": "jest --passWithNoTests",
134
137
  "check-types": "tsc --noEmit",
@@ -1,6 +1,17 @@
1
1
  import { CreateCopilotRuntimeServerOptions, getCommonConfig } from "../shared";
2
2
  import telemetry, { getRuntimeInstanceTelemetryInfo } from "../../telemetry-client";
3
3
  import { createCopilotEndpointSingleRoute } from "@copilotkitnext/runtime";
4
+ import type { IncomingMessage, ServerResponse } from "node:http";
5
+ import {
6
+ getFullUrl,
7
+ IncomingWithBody,
8
+ isDisturbedOrLockedError,
9
+ isStreamConsumed,
10
+ nodeStreamToReadableStream,
11
+ readableStreamToNodeStream,
12
+ synthesizeBodyFromParsedBody,
13
+ toHeaders,
14
+ } from "./request-handler";
4
15
 
5
16
  export function copilotRuntimeNodeHttpEndpoint(options: CreateCopilotRuntimeServerOptions) {
6
17
  const commonConfig = getCommonConfig(options);
@@ -32,5 +43,101 @@ export function copilotRuntimeNodeHttpEndpoint(options: CreateCopilotRuntimeServ
32
43
  basePath: options.baseUrl ?? options.endpoint,
33
44
  });
34
45
 
35
- return honoApp.fetch;
46
+ const handle = async function handler(req: IncomingWithBody, res: ServerResponse) {
47
+ const url = getFullUrl(req);
48
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
49
+
50
+ const baseHeaders = toHeaders(req.headers);
51
+ const parsedBody = req.body;
52
+
53
+ const streamConsumed = isStreamConsumed(req) || parsedBody !== undefined;
54
+ const canStream = hasBody && !streamConsumed;
55
+
56
+ let requestBody: BodyInit | null | undefined = undefined;
57
+ let useDuplex = false;
58
+
59
+ if (hasBody && canStream) {
60
+ requestBody = nodeStreamToReadableStream(req);
61
+ useDuplex = true;
62
+ }
63
+
64
+ if (hasBody && streamConsumed) {
65
+ if (parsedBody !== undefined) {
66
+ const synthesized = synthesizeBodyFromParsedBody(parsedBody, baseHeaders);
67
+ requestBody = synthesized.body ?? undefined;
68
+ baseHeaders.delete("content-length");
69
+
70
+ if (synthesized.contentType) {
71
+ baseHeaders.set("content-type", synthesized.contentType);
72
+ }
73
+
74
+ logger.debug("Request stream already consumed; using parsed req.body to rebuild request.");
75
+ } else {
76
+ logger.warn("Request stream consumed with no available body; sending empty payload.");
77
+ requestBody = undefined;
78
+ }
79
+ }
80
+
81
+ const buildRequest = (body: BodyInit | null | undefined, headers: Headers, duplex: boolean) =>
82
+ new Request(url, {
83
+ method: req.method,
84
+ headers,
85
+ body,
86
+ duplex: duplex ? "half" : undefined,
87
+ } as RequestInit);
88
+
89
+ let response: Response;
90
+ try {
91
+ response = await honoApp.fetch(buildRequest(requestBody, baseHeaders, useDuplex));
92
+ } catch (error) {
93
+ if (isDisturbedOrLockedError(error) && hasBody) {
94
+ logger.warn(
95
+ "Encountered disturbed/locked request body; rebuilding request using parsed body or empty payload.",
96
+ );
97
+
98
+ const fallbackHeaders = new Headers(baseHeaders);
99
+ let fallbackBody: BodyInit | null | undefined;
100
+
101
+ if (parsedBody !== undefined) {
102
+ const synthesized = synthesizeBodyFromParsedBody(parsedBody, fallbackHeaders);
103
+ fallbackBody = synthesized.body ?? undefined;
104
+ fallbackHeaders.delete("content-length");
105
+
106
+ if (synthesized.contentType) {
107
+ fallbackHeaders.set("content-type", synthesized.contentType);
108
+ }
109
+ } else {
110
+ fallbackBody = undefined;
111
+ }
112
+
113
+ response = await honoApp.fetch(buildRequest(fallbackBody, fallbackHeaders, false));
114
+ } else {
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ res.statusCode = response.status;
120
+ response.headers.forEach((value, key) => {
121
+ res.setHeader(key, value);
122
+ });
123
+
124
+ if (response.body) {
125
+ readableStreamToNodeStream(response.body).pipe(res);
126
+ } else {
127
+ res.end();
128
+ }
129
+ };
130
+
131
+ return function (
132
+ reqOrRequest: IncomingMessage | Request,
133
+ res?: ServerResponse,
134
+ ): Promise<void> | Promise<Response> | Response {
135
+ if (reqOrRequest instanceof Request) {
136
+ return honoApp.fetch(reqOrRequest as Request);
137
+ }
138
+ if (!res) {
139
+ throw new TypeError("ServerResponse is required for Node HTTP requests");
140
+ }
141
+ return handle(reqOrRequest as IncomingMessage, res);
142
+ };
36
143
  }
@@ -0,0 +1,111 @@
1
+ import type { IncomingMessage } from "http";
2
+ import { Readable } from "node:stream";
3
+
4
+ export type IncomingWithBody = IncomingMessage & { body?: unknown; complete?: boolean };
5
+
6
+ export function readableStreamToNodeStream(webStream: ReadableStream): Readable {
7
+ const reader = webStream.getReader();
8
+
9
+ return new Readable({
10
+ async read() {
11
+ try {
12
+ const { done, value } = await reader.read();
13
+ if (done) {
14
+ this.push(null);
15
+ } else {
16
+ this.push(Buffer.from(value));
17
+ }
18
+ } catch (err) {
19
+ this.destroy(err as Error);
20
+ }
21
+ },
22
+ });
23
+ }
24
+
25
+ export function nodeStreamToReadableStream(nodeStream: Readable): ReadableStream<Uint8Array> {
26
+ return new ReadableStream({
27
+ start(controller) {
28
+ nodeStream.on("data", (chunk) => {
29
+ controller.enqueue(chunk instanceof Buffer ? new Uint8Array(chunk) : chunk);
30
+ });
31
+ nodeStream.on("end", () => {
32
+ controller.close();
33
+ });
34
+ nodeStream.on("error", (err) => {
35
+ controller.error(err);
36
+ });
37
+ },
38
+ cancel() {
39
+ nodeStream.destroy();
40
+ },
41
+ });
42
+ }
43
+
44
+ export function getFullUrl(req: IncomingMessage): string {
45
+ // Use req.url (path relative to mount point) for Hono routing to work correctly.
46
+ // Express sets req.url to the path after the mount point (e.g., "/" when mounted at "/copilotkit").
47
+ // Pure Node HTTP sets req.url to the full path.
48
+ const path = req.url || "/";
49
+ const host =
50
+ (req.headers["x-forwarded-host"] as string) || (req.headers.host as string) || "localhost";
51
+ const proto =
52
+ (req.headers["x-forwarded-proto"] as string) ||
53
+ ((req.socket as any).encrypted ? "https" : "http");
54
+
55
+ return `${proto}://${host}${path}`;
56
+ }
57
+
58
+ export function toHeaders(rawHeaders: IncomingMessage["headers"]): Headers {
59
+ const headers = new Headers();
60
+
61
+ for (const [key, value] of Object.entries(rawHeaders)) {
62
+ if (value === undefined) continue;
63
+
64
+ if (Array.isArray(value)) {
65
+ value.forEach((entry) => headers.append(key, entry));
66
+ continue;
67
+ }
68
+
69
+ headers.append(key, value);
70
+ }
71
+
72
+ return headers;
73
+ }
74
+
75
+ export function isStreamConsumed(req: IncomingWithBody): boolean {
76
+ const readableState = (req as any)._readableState;
77
+
78
+ return Boolean(
79
+ req.readableEnded || req.complete || readableState?.ended || readableState?.endEmitted,
80
+ );
81
+ }
82
+
83
+ export function synthesizeBodyFromParsedBody(
84
+ parsedBody: unknown,
85
+ headers: Headers,
86
+ ): { body: BodyInit | null; contentType?: string } {
87
+ if (parsedBody === null || parsedBody === undefined) {
88
+ return { body: null };
89
+ }
90
+
91
+ if (parsedBody instanceof Buffer || parsedBody instanceof Uint8Array) {
92
+ return { body: parsedBody };
93
+ }
94
+
95
+ if (typeof parsedBody === "string") {
96
+ return { body: parsedBody, contentType: headers.get("content-type") ?? "text/plain" };
97
+ }
98
+
99
+ return {
100
+ body: JSON.stringify(parsedBody),
101
+ contentType: "application/json",
102
+ };
103
+ }
104
+
105
+ export function isDisturbedOrLockedError(error: unknown): boolean {
106
+ return (
107
+ error instanceof TypeError &&
108
+ typeof error.message === "string" &&
109
+ (error.message.includes("disturbed") || error.message.includes("locked"))
110
+ );
111
+ }
@@ -480,7 +480,7 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
480
480
  // }
481
481
 
482
482
  // TODO: replace hooksParams top argument type with BeforeRequestMiddlewareParameters when exported
483
- params?.beforeRequestMiddleware?.(hookParams);
483
+ const middlewareResult = await params?.beforeRequestMiddleware?.(hookParams);
484
484
 
485
485
  if (params?.middleware?.onBeforeRequest) {
486
486
  const { request, runtime, path } = hookParams;
@@ -504,6 +504,8 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
504
504
  url: request.url,
505
505
  } satisfies OnBeforeRequestOptions);
506
506
  }
507
+
508
+ return middlewareResult;
507
509
  };
508
510
  }
509
511