@copilotkit/runtime 0.0.0-0.0.0-max-changeset-10101010101010-20260109191632

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.eslintrc.js +7 -0
  2. package/CHANGELOG.md +2905 -0
  3. package/LICENSE +21 -0
  4. package/README.md +76 -0
  5. package/__snapshots__/schema/schema.graphql +371 -0
  6. package/dist/index.d.ts +1495 -0
  7. package/dist/index.js +5644 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +5601 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/langgraph.d.ts +284 -0
  12. package/dist/langgraph.js +211 -0
  13. package/dist/langgraph.js.map +1 -0
  14. package/dist/langgraph.mjs +206 -0
  15. package/dist/langgraph.mjs.map +1 -0
  16. package/dist/v2/index.d.ts +2 -0
  17. package/dist/v2/index.js +22 -0
  18. package/dist/v2/index.js.map +1 -0
  19. package/dist/v2/index.mjs +5 -0
  20. package/dist/v2/index.mjs.map +1 -0
  21. package/jest.config.js +10 -0
  22. package/package.json +143 -0
  23. package/scripts/generate-gql-schema.ts +13 -0
  24. package/src/agents/langgraph/event-source.ts +329 -0
  25. package/src/agents/langgraph/events.ts +377 -0
  26. package/src/graphql/inputs/action.input.ts +16 -0
  27. package/src/graphql/inputs/agent-session.input.ts +13 -0
  28. package/src/graphql/inputs/agent-state.input.ts +13 -0
  29. package/src/graphql/inputs/cloud-guardrails.input.ts +16 -0
  30. package/src/graphql/inputs/cloud.input.ts +8 -0
  31. package/src/graphql/inputs/context-property.input.ts +10 -0
  32. package/src/graphql/inputs/copilot-context.input.ts +10 -0
  33. package/src/graphql/inputs/custom-property.input.ts +15 -0
  34. package/src/graphql/inputs/extensions.input.ts +21 -0
  35. package/src/graphql/inputs/forwarded-parameters.input.ts +22 -0
  36. package/src/graphql/inputs/frontend.input.ts +14 -0
  37. package/src/graphql/inputs/generate-copilot-response.input.ts +59 -0
  38. package/src/graphql/inputs/load-agent-state.input.ts +10 -0
  39. package/src/graphql/inputs/message.input.ts +110 -0
  40. package/src/graphql/inputs/meta-event.input.ts +18 -0
  41. package/src/graphql/message-conversion/agui-to-gql.test.ts +1263 -0
  42. package/src/graphql/message-conversion/agui-to-gql.ts +333 -0
  43. package/src/graphql/message-conversion/gql-to-agui.test.ts +1580 -0
  44. package/src/graphql/message-conversion/gql-to-agui.ts +278 -0
  45. package/src/graphql/message-conversion/index.ts +2 -0
  46. package/src/graphql/message-conversion/roundtrip-conversion.test.ts +526 -0
  47. package/src/graphql/resolvers/copilot.resolver.ts +708 -0
  48. package/src/graphql/resolvers/state.resolver.ts +27 -0
  49. package/src/graphql/types/agents-response.type.ts +19 -0
  50. package/src/graphql/types/base/index.ts +10 -0
  51. package/src/graphql/types/converted/index.ts +176 -0
  52. package/src/graphql/types/copilot-response.type.ts +138 -0
  53. package/src/graphql/types/enums.ts +38 -0
  54. package/src/graphql/types/extensions-response.type.ts +23 -0
  55. package/src/graphql/types/guardrails-result.type.ts +20 -0
  56. package/src/graphql/types/load-agent-state-response.type.ts +17 -0
  57. package/src/graphql/types/message-status.type.ts +42 -0
  58. package/src/graphql/types/meta-events.type.ts +71 -0
  59. package/src/graphql/types/response-status.type.ts +66 -0
  60. package/src/index.ts +4 -0
  61. package/src/langgraph.ts +1 -0
  62. package/src/lib/cloud/index.ts +4 -0
  63. package/src/lib/error-messages.ts +200 -0
  64. package/src/lib/index.ts +52 -0
  65. package/src/lib/integrations/index.ts +6 -0
  66. package/src/lib/integrations/nest/index.ts +14 -0
  67. package/src/lib/integrations/nextjs/app-router.ts +38 -0
  68. package/src/lib/integrations/nextjs/pages-router.ts +39 -0
  69. package/src/lib/integrations/node-express/index.ts +14 -0
  70. package/src/lib/integrations/node-http/index.ts +143 -0
  71. package/src/lib/integrations/node-http/request-handler.ts +111 -0
  72. package/src/lib/integrations/shared.ts +161 -0
  73. package/src/lib/logger.ts +28 -0
  74. package/src/lib/observability.ts +160 -0
  75. package/src/lib/runtime/__tests__/copilot-runtime-error.test.ts +169 -0
  76. package/src/lib/runtime/__tests__/mcp-tools-utils.test.ts +464 -0
  77. package/src/lib/runtime/agent-integrations/langgraph/agent.ts +209 -0
  78. package/src/lib/runtime/agent-integrations/langgraph/consts.ts +34 -0
  79. package/src/lib/runtime/agent-integrations/langgraph/index.ts +2 -0
  80. package/src/lib/runtime/copilot-runtime.ts +710 -0
  81. package/src/lib/runtime/mcp-tools-utils.ts +254 -0
  82. package/src/lib/runtime/retry-utils.ts +96 -0
  83. package/src/lib/runtime/telemetry-agent-runner.ts +139 -0
  84. package/src/lib/runtime/types.ts +49 -0
  85. package/src/lib/runtime/utils.ts +87 -0
  86. package/src/lib/streaming.ts +202 -0
  87. package/src/lib/telemetry-client.ts +64 -0
  88. package/src/service-adapters/anthropic/anthropic-adapter.ts +452 -0
  89. package/src/service-adapters/anthropic/utils.ts +152 -0
  90. package/src/service-adapters/bedrock/bedrock-adapter.ts +73 -0
  91. package/src/service-adapters/conversion.ts +67 -0
  92. package/src/service-adapters/empty/empty-adapter.ts +38 -0
  93. package/src/service-adapters/events.ts +294 -0
  94. package/src/service-adapters/experimental/ollama/ollama-adapter.ts +84 -0
  95. package/src/service-adapters/google/google-genai-adapter.test.ts +104 -0
  96. package/src/service-adapters/google/google-genai-adapter.ts +88 -0
  97. package/src/service-adapters/groq/groq-adapter.ts +203 -0
  98. package/src/service-adapters/index.ts +18 -0
  99. package/src/service-adapters/langchain/langchain-adapter.ts +111 -0
  100. package/src/service-adapters/langchain/langserve.ts +88 -0
  101. package/src/service-adapters/langchain/types.ts +14 -0
  102. package/src/service-adapters/langchain/utils.ts +313 -0
  103. package/src/service-adapters/openai/openai-adapter.ts +283 -0
  104. package/src/service-adapters/openai/openai-assistant-adapter.ts +344 -0
  105. package/src/service-adapters/openai/utils.ts +199 -0
  106. package/src/service-adapters/service-adapter.ts +41 -0
  107. package/src/service-adapters/shared/error-utils.ts +61 -0
  108. package/src/service-adapters/shared/index.ts +1 -0
  109. package/src/service-adapters/unify/unify-adapter.ts +151 -0
  110. package/src/utils/failed-response-status-reasons.ts +70 -0
  111. package/src/utils/index.ts +1 -0
  112. package/src/v2/index.ts +3 -0
  113. package/tests/global.d.ts +13 -0
  114. package/tests/service-adapters/anthropic/allowlist-approach.test.ts +226 -0
  115. package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +389 -0
  116. package/tests/service-adapters/openai/allowlist-approach.test.ts +238 -0
  117. package/tests/service-adapters/openai/openai-adapter.test.ts +301 -0
  118. package/tests/setup.jest.ts +21 -0
  119. package/tests/tsconfig.json +10 -0
  120. package/tsconfig.json +13 -0
  121. package/tsup.config.ts +20 -0
  122. package/typedoc.json +4 -0
@@ -0,0 +1,161 @@
1
+ import { YogaInitialContext } from "graphql-yoga";
2
+ import { buildSchemaSync } from "type-graphql";
3
+ import { CopilotResolver } from "../../graphql/resolvers/copilot.resolver";
4
+ import { useDeferStream } from "@graphql-yoga/plugin-defer-stream";
5
+ import { CopilotRuntime } from "../runtime/copilot-runtime";
6
+ import { CopilotServiceAdapter } from "../../service-adapters";
7
+ import { CopilotCloudOptions } from "../cloud";
8
+ import { LogLevel, createLogger } from "../../lib/logger";
9
+ import { createYoga } from "graphql-yoga";
10
+ import telemetry from "../telemetry-client";
11
+ import { StateResolver } from "../../graphql/resolvers/state.resolver";
12
+ import * as packageJson from "../../../package.json";
13
+ import { CopilotKitError, CopilotKitErrorCode } from "@copilotkit/shared";
14
+
15
+ const logger = createLogger();
16
+
17
+ export const addCustomHeaderPlugin = {
18
+ onResponse({ response }) {
19
+ // Set your custom header; adjust the header name and value as needed
20
+ response.headers.set("X-CopilotKit-Runtime-Version", packageJson.version);
21
+ },
22
+ };
23
+
24
+ type AnyPrimitive = string | boolean | number | null;
25
+ export type CopilotRequestContextProperties = Record<
26
+ string,
27
+ AnyPrimitive | Record<string, AnyPrimitive>
28
+ >;
29
+
30
+ export type GraphQLContext = YogaInitialContext & {
31
+ _copilotkit: CreateCopilotRuntimeServerOptions;
32
+ properties: CopilotRequestContextProperties;
33
+ logger: typeof logger;
34
+ };
35
+
36
+ export interface CreateCopilotRuntimeServerOptions {
37
+ runtime: CopilotRuntime<any>;
38
+ serviceAdapter?: CopilotServiceAdapter;
39
+ endpoint: string;
40
+ baseUrl?: string;
41
+ cloud?: CopilotCloudOptions;
42
+ properties?: CopilotRequestContextProperties;
43
+ logLevel?: LogLevel;
44
+ }
45
+
46
+ export async function createContext(
47
+ initialContext: YogaInitialContext,
48
+ copilotKitContext: CreateCopilotRuntimeServerOptions,
49
+ contextLogger: typeof logger,
50
+ properties: CopilotRequestContextProperties = {},
51
+ ): Promise<Partial<GraphQLContext>> {
52
+ logger.debug({ copilotKitContext }, "Creating GraphQL context");
53
+ const ctx: GraphQLContext = {
54
+ ...initialContext,
55
+ _copilotkit: {
56
+ ...copilotKitContext,
57
+ },
58
+ properties: { ...properties },
59
+ logger: contextLogger,
60
+ };
61
+ return ctx;
62
+ }
63
+
64
+ export function buildSchema(
65
+ options: {
66
+ emitSchemaFile?: string;
67
+ } = {},
68
+ ) {
69
+ logger.debug("Building GraphQL schema...");
70
+ const schema = buildSchemaSync({
71
+ resolvers: [CopilotResolver, StateResolver],
72
+ emitSchemaFile: options.emitSchemaFile,
73
+ });
74
+ logger.debug("GraphQL schema built successfully");
75
+ return schema;
76
+ }
77
+
78
+ export type CommonConfig = {
79
+ logging: typeof logger;
80
+ schema: ReturnType<typeof buildSchema>;
81
+ plugins: Parameters<typeof createYoga>[0]["plugins"];
82
+ context: (ctx: YogaInitialContext) => Promise<Partial<GraphQLContext>>;
83
+ maskedErrors: {
84
+ maskError: (error: any, message: string, isDev?: boolean) => any;
85
+ };
86
+ };
87
+
88
+ export function getCommonConfig(options: CreateCopilotRuntimeServerOptions): CommonConfig {
89
+ const logLevel = (process.env.LOG_LEVEL as LogLevel) || (options.logLevel as LogLevel) || "error";
90
+ const logger = createLogger({ level: logLevel, component: "getCommonConfig" });
91
+
92
+ const contextLogger = createLogger({ level: logLevel });
93
+
94
+ if (options.cloud) {
95
+ telemetry.setCloudConfiguration({
96
+ publicApiKey: options.cloud.publicApiKey,
97
+ baseUrl: options.cloud.baseUrl,
98
+ });
99
+ }
100
+
101
+ if (options.properties?._copilotkit) {
102
+ telemetry.setGlobalProperties({
103
+ _copilotkit: {
104
+ ...(options.properties._copilotkit as Record<string, any>),
105
+ },
106
+ });
107
+ }
108
+
109
+ telemetry.setGlobalProperties({
110
+ runtime: {
111
+ serviceAdapter: options.serviceAdapter.constructor.name,
112
+ },
113
+ });
114
+
115
+ // User error codes that should not be logged as server errors
116
+ const userErrorCodes = [
117
+ CopilotKitErrorCode.AGENT_NOT_FOUND,
118
+ CopilotKitErrorCode.API_NOT_FOUND,
119
+ CopilotKitErrorCode.REMOTE_ENDPOINT_NOT_FOUND,
120
+ CopilotKitErrorCode.CONFIGURATION_ERROR,
121
+ CopilotKitErrorCode.MISSING_PUBLIC_API_KEY_ERROR,
122
+ ];
123
+
124
+ return {
125
+ logging: createLogger({ component: "Yoga GraphQL", level: logLevel }),
126
+ schema: buildSchema(),
127
+ plugins: [useDeferStream(), addCustomHeaderPlugin],
128
+ context: (ctx: YogaInitialContext): Promise<Partial<GraphQLContext>> =>
129
+ createContext(ctx, options, contextLogger, options.properties),
130
+ // Suppress logging for user configuration errors
131
+ maskedErrors: {
132
+ maskError: (error: any, message: string, isDev?: boolean) => {
133
+ // Check if this is a user configuration error (could be wrapped in GraphQLError)
134
+ const originalError = error.originalError || error;
135
+ const extensions = error.extensions;
136
+ const errorCode = extensions?.code;
137
+
138
+ // Suppress logging for user errors based on error code
139
+ if (errorCode && userErrorCodes.includes(errorCode)) {
140
+ // Log user configuration errors at debug level instead
141
+ console.debug("User configuration error:", error.message);
142
+ return error;
143
+ }
144
+
145
+ // Check if the original error is a user error
146
+ if (
147
+ originalError instanceof CopilotKitError &&
148
+ userErrorCodes.includes(originalError.code)
149
+ ) {
150
+ // Log user configuration errors at debug level instead
151
+ console.debug("User configuration error:", error.message);
152
+ return error;
153
+ }
154
+
155
+ // For application errors, log normally and mask if needed
156
+ console.error("Application error:", error);
157
+ return error;
158
+ },
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,28 @@
1
+ import createPinoLogger from "pino";
2
+ import pretty from "pino-pretty";
3
+
4
+ export type LogLevel = "debug" | "info" | "warn" | "error";
5
+
6
+ export type CopilotRuntimeLogger = ReturnType<typeof createLogger>;
7
+
8
+ export function createLogger(options?: { level?: LogLevel; component?: string }) {
9
+ const { level, component } = options || {};
10
+ const stream = pretty({ colorize: true });
11
+
12
+ const logger = createPinoLogger(
13
+ {
14
+ level: process.env.LOG_LEVEL || level || "error",
15
+ redact: {
16
+ paths: ["pid", "hostname"],
17
+ remove: true,
18
+ },
19
+ },
20
+ stream,
21
+ );
22
+
23
+ if (component) {
24
+ return logger.child({ component });
25
+ } else {
26
+ return logger;
27
+ }
28
+ }
@@ -0,0 +1,160 @@
1
+ import { RuntimeEventSource, RuntimeEventTypes } from "../service-adapters/events";
2
+
3
+ export interface LLMRequestData {
4
+ threadId?: string;
5
+ runId?: string;
6
+ model?: string;
7
+ messages: any[];
8
+ actions?: any[];
9
+ forwardedParameters?: any;
10
+ timestamp: number;
11
+ provider?: string;
12
+ [key: string]: any;
13
+ }
14
+
15
+ export interface LLMResponseData {
16
+ threadId: string;
17
+ runId?: string;
18
+ model?: string;
19
+ output: any;
20
+ latency: number;
21
+ timestamp: number;
22
+ provider?: string;
23
+ isProgressiveChunk?: boolean;
24
+ isFinalResponse?: boolean;
25
+ [key: string]: any;
26
+ }
27
+
28
+ export interface LLMErrorData {
29
+ threadId?: string;
30
+ runId?: string;
31
+ model?: string;
32
+ error: Error | string;
33
+ timestamp: number;
34
+ provider?: string;
35
+ [key: string]: any;
36
+ }
37
+
38
+ export interface CopilotObservabilityHooks {
39
+ handleRequest: (data: LLMRequestData) => void | Promise<void>;
40
+ handleResponse: (data: LLMResponseData) => void | Promise<void>;
41
+ handleError: (data: LLMErrorData) => void | Promise<void>;
42
+ }
43
+
44
+ /**
45
+ * Configuration for CopilotKit logging functionality.
46
+ *
47
+ * @remarks
48
+ * Custom logging handlers require a valid CopilotKit public API key.
49
+ * Sign up at https://docs.copilotkit.ai/quickstart#get-a-copilot-cloud-public-api-key to get your key.
50
+ */
51
+ export interface CopilotObservabilityConfig {
52
+ /**
53
+ * Enable or disable logging functionality.
54
+ *
55
+ * @default false
56
+ */
57
+ enabled: boolean;
58
+
59
+ /**
60
+ * Controls whether logs are streamed progressively or buffered.
61
+ * - When true: Each token and update is logged as it's generated (real-time)
62
+ * - When false: Complete responses are logged after completion (batched)
63
+ *
64
+ * @default true
65
+ */
66
+ progressive: boolean;
67
+
68
+ /**
69
+ * Custom observability hooks for request, response, and error events.
70
+ *
71
+ * @remarks
72
+ * Using custom observability hooks requires a valid CopilotKit public API key.
73
+ */
74
+ hooks: CopilotObservabilityHooks;
75
+ }
76
+
77
+ /**
78
+ * Setup progressive logging by wrapping the event stream
79
+ */
80
+ function setupProgressiveLogging(
81
+ eventSource: RuntimeEventSource,
82
+ streamedChunks: any[],
83
+ requestStartTime: number,
84
+ context: {
85
+ threadId?: string;
86
+ runId?: string;
87
+ model?: string;
88
+ provider?: string;
89
+ agentName?: string;
90
+ nodeName?: string;
91
+ },
92
+ publicApiKey?: string,
93
+ ): void {
94
+ if (this.observability?.enabled && this.observability.progressive && publicApiKey) {
95
+ // Keep reference to original stream function
96
+ const originalStream = eventSource.stream.bind(eventSource);
97
+
98
+ // Wrap the stream function to intercept events
99
+ eventSource.stream = async (callback) => {
100
+ await originalStream(async (eventStream$) => {
101
+ // Create subscription to capture streaming events
102
+ eventStream$.subscribe({
103
+ next: (event) => {
104
+ // Only log content chunks
105
+ if (event.type === RuntimeEventTypes.TextMessageContent) {
106
+ // Store the chunk
107
+ streamedChunks.push(event.content);
108
+
109
+ // Log each chunk separately for progressive mode
110
+ try {
111
+ const progressiveData: LLMResponseData = {
112
+ threadId: context.threadId || "",
113
+ runId: context.runId,
114
+ model: context.model,
115
+ output: event.content,
116
+ latency: Date.now() - requestStartTime,
117
+ timestamp: Date.now(),
118
+ provider: context.provider,
119
+ isProgressiveChunk: true,
120
+ agentName: context.agentName,
121
+ nodeName: context.nodeName,
122
+ };
123
+
124
+ // Use Promise to handle async logger without awaiting
125
+ Promise.resolve()
126
+ .then(() => {
127
+ this.observability.hooks.handleResponse(progressiveData);
128
+ })
129
+ .catch((error) => {
130
+ console.error("Error in progressive logging:", error);
131
+ });
132
+ } catch (error) {
133
+ console.error("Error preparing progressive log data:", error);
134
+ }
135
+ }
136
+ },
137
+ });
138
+
139
+ // Call the original callback with the event stream
140
+ await callback(eventStream$);
141
+ });
142
+ };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Log error if observability is enabled
148
+ */
149
+ async function logObservabilityError(
150
+ errorData: LLMErrorData,
151
+ publicApiKey?: string,
152
+ ): Promise<void> {
153
+ if (this.observability?.enabled && publicApiKey) {
154
+ try {
155
+ await this.observability.hooks.handleError(errorData);
156
+ } catch (logError) {
157
+ console.error("Error logging LLM error:", logError);
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,169 @@
1
+ import { CopilotErrorEvent, CopilotRequestContext, CopilotErrorHandler } from "@copilotkit/shared";
2
+
3
+ describe("CopilotRuntime onError types", () => {
4
+ it("should have correct CopilotTraceEvent type structure", () => {
5
+ const errorEvent: CopilotErrorEvent = {
6
+ type: "error",
7
+ timestamp: Date.now(),
8
+ context: {
9
+ threadId: "test-123",
10
+ source: "runtime",
11
+ request: {
12
+ operation: "test-operation",
13
+ startTime: Date.now(),
14
+ },
15
+ technical: {},
16
+ metadata: {},
17
+ },
18
+ error: new Error("Test error"),
19
+ };
20
+
21
+ expect(errorEvent.type).toBe("error");
22
+ expect(errorEvent.timestamp).toBeGreaterThan(0);
23
+ expect(errorEvent.context.threadId).toBe("test-123");
24
+ expect(errorEvent.error).toBeInstanceOf(Error);
25
+ });
26
+
27
+ it("should have correct CopilotRequestContext type structure", () => {
28
+ const context: CopilotRequestContext = {
29
+ threadId: "test-thread-456",
30
+ runId: "test-run-789",
31
+ source: "runtime",
32
+ request: {
33
+ operation: "processRuntimeRequest",
34
+ method: "POST",
35
+ url: "http://localhost:3000/api/copilotkit",
36
+ startTime: Date.now(),
37
+ },
38
+ response: {
39
+ status: 200,
40
+ endTime: Date.now(),
41
+ latency: 1200,
42
+ },
43
+ agent: {
44
+ name: "test-agent",
45
+ nodeName: "test-node",
46
+ state: { step: 1 },
47
+ },
48
+ messages: {
49
+ input: [],
50
+ output: [],
51
+ messageCount: 2,
52
+ },
53
+ technical: {
54
+ userAgent: "Mozilla/5.0...",
55
+ host: "localhost:3000",
56
+ environment: "test",
57
+ version: "1.0.0",
58
+ stackTrace: "Error: Test\n at test.js:1:1",
59
+ },
60
+ performance: {
61
+ requestDuration: 1200,
62
+ streamingDuration: 800,
63
+ actionExecutionTime: 400,
64
+ memoryUsage: 45.2,
65
+ },
66
+ metadata: {
67
+ testFlag: true,
68
+ version: "1.0.0",
69
+ },
70
+ };
71
+
72
+ expect(context.threadId).toBe("test-thread-456");
73
+ expect(context.agent?.name).toBe("test-agent");
74
+ expect(context.messages?.messageCount).toBe(2);
75
+ expect(context.technical?.stackTrace).toContain("Error: Test");
76
+ expect(context.metadata?.testFlag).toBe(true);
77
+ });
78
+
79
+ it("should support all error event types", () => {
80
+ const eventTypes: CopilotErrorEvent["type"][] = [
81
+ "error",
82
+ "request",
83
+ "response",
84
+ "agent_state",
85
+ "action",
86
+ "message",
87
+ "performance",
88
+ ];
89
+
90
+ eventTypes.forEach((type) => {
91
+ const event: CopilotErrorEvent = {
92
+ type,
93
+ timestamp: Date.now(),
94
+ context: {
95
+ threadId: `test-${type}`,
96
+ source: "runtime",
97
+ request: {
98
+ operation: "test",
99
+ startTime: Date.now(),
100
+ },
101
+ technical: {},
102
+ metadata: {},
103
+ },
104
+ };
105
+
106
+ expect(event.type).toBe(type);
107
+ });
108
+ });
109
+
110
+ describe("publicApiKey gating logic", () => {
111
+ type ShouldHandleError = (onError?: CopilotErrorHandler, publicApiKey?: string) => boolean;
112
+
113
+ const shouldHandleError: ShouldHandleError = (onError, publicApiKey) => {
114
+ return Boolean(onError && publicApiKey);
115
+ };
116
+
117
+ it("should return true when both onError and publicApiKey are provided", () => {
118
+ const onError = jest.fn();
119
+ const result = shouldHandleError(onError, "valid-api-key");
120
+ expect(result).toBe(true);
121
+ });
122
+
123
+ it("should return false when onError is missing", () => {
124
+ const result = shouldHandleError(undefined, "valid-api-key");
125
+ expect(result).toBe(false);
126
+ });
127
+
128
+ it("should return false when publicApiKey is missing", () => {
129
+ const onError = jest.fn();
130
+ const result = shouldHandleError(onError, undefined);
131
+ expect(result).toBe(false);
132
+ });
133
+
134
+ it("should return false when publicApiKey is empty string", () => {
135
+ const onError = jest.fn();
136
+ const result = shouldHandleError(onError, "");
137
+ expect(result).toBe(false);
138
+ });
139
+
140
+ it("should return false when both are missing", () => {
141
+ const result = shouldHandleError(undefined, undefined);
142
+ expect(result).toBe(false);
143
+ });
144
+
145
+ it("should extract publicApiKey from headers for both cloud and non-cloud requests", () => {
146
+ // Test the logic we just fixed in the GraphQL resolver
147
+ const mockHeaders = new Map([["x-copilotcloud-public-api-key", "test-key-123"]]);
148
+
149
+ // Simulate header extraction logic
150
+ const extractPublicApiKey = (headers: Map<string, string>, hasCloudConfig: boolean) => {
151
+ const publicApiKeyFromHeaders = headers.get("x-copilotcloud-public-api-key");
152
+ return publicApiKeyFromHeaders || null;
153
+ };
154
+
155
+ // Should work for cloud requests
156
+ const cloudKey = extractPublicApiKey(mockHeaders, true);
157
+ expect(cloudKey).toBe("test-key-123");
158
+
159
+ // Should also work for non-cloud requests (this was the bug)
160
+ const nonCloudKey = extractPublicApiKey(mockHeaders, false);
161
+ expect(nonCloudKey).toBe("test-key-123");
162
+
163
+ // Both should enable error handling when onError is present
164
+ const onError = jest.fn();
165
+ expect(shouldHandleError(onError, cloudKey)).toBe(true);
166
+ expect(shouldHandleError(onError, nonCloudKey)).toBe(true);
167
+ });
168
+ });
169
+ });