@copilotkit/runtime 1.55.3 → 1.56.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/dist/agent/converters/tanstack.cjs.map +1 -1
- package/dist/agent/converters/tanstack.d.cts +6 -19
- package/dist/agent/converters/tanstack.d.cts.map +1 -1
- package/dist/agent/converters/tanstack.d.mts +6 -19
- package/dist/agent/converters/tanstack.d.mts.map +1 -1
- package/dist/agent/converters/tanstack.mjs.map +1 -1
- package/dist/agent/index.cjs +14 -0
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +12 -1
- package/dist/agent/index.d.cts.map +1 -1
- package/dist/agent/index.d.mts +12 -1
- package/dist/agent/index.d.mts.map +1 -1
- package/dist/agent/index.mjs +14 -0
- package/dist/agent/index.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +1 -1
- package/dist/lib/index.cjs +1 -1
- package/dist/lib/index.d.cts +2 -1
- package/dist/lib/index.d.cts.map +1 -1
- package/dist/lib/index.d.mts +2 -1
- package/dist/lib/index.d.mts.map +1 -1
- package/dist/lib/index.mjs +1 -1
- package/dist/lib/integrations/shared.cjs +1 -1
- package/dist/lib/integrations/shared.d.cts +1 -1
- package/dist/lib/integrations/shared.d.mts +1 -1
- package/dist/lib/integrations/shared.mjs +1 -1
- package/dist/lib/runtime/copilot-runtime.cjs +14 -4
- package/dist/lib/runtime/copilot-runtime.cjs.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.cts +15 -3
- package/dist/lib/runtime/copilot-runtime.d.cts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.d.mts +15 -3
- package/dist/lib/runtime/copilot-runtime.d.mts.map +1 -1
- package/dist/lib/runtime/copilot-runtime.mjs +14 -4
- package/dist/lib/runtime/copilot-runtime.mjs.map +1 -1
- package/dist/package.cjs +6 -5
- package/dist/package.mjs +6 -5
- package/dist/service-adapters/openai/openai-adapter.cjs +1 -1
- package/dist/service-adapters/openai/openai-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-adapter.mjs +2 -2
- package/dist/service-adapters/openai/openai-adapter.mjs.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.cjs +8 -9
- package/dist/service-adapters/openai/openai-assistant-adapter.cjs.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.d.cts.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.d.mts.map +1 -1
- package/dist/service-adapters/openai/openai-assistant-adapter.mjs +9 -10
- package/dist/service-adapters/openai/openai-assistant-adapter.mjs.map +1 -1
- package/dist/service-adapters/openai/utils.cjs +53 -0
- package/dist/service-adapters/openai/utils.cjs.map +1 -1
- package/dist/service-adapters/openai/utils.mjs +51 -1
- package/dist/service-adapters/openai/utils.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -0
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/runtime/core/runtime.cjs +25 -0
- package/dist/v2/runtime/core/runtime.cjs.map +1 -1
- package/dist/v2/runtime/core/runtime.d.cts +53 -4
- package/dist/v2/runtime/core/runtime.d.cts.map +1 -1
- package/dist/v2/runtime/core/runtime.d.mts +53 -4
- package/dist/v2/runtime/core/runtime.d.mts.map +1 -1
- package/dist/v2/runtime/core/runtime.mjs +26 -2
- package/dist/v2/runtime/core/runtime.mjs.map +1 -1
- package/dist/v2/runtime/handlers/get-runtime-info.cjs +18 -10
- package/dist/v2/runtime/handlers/get-runtime-info.cjs.map +1 -1
- package/dist/v2/runtime/handlers/get-runtime-info.mjs +19 -11
- package/dist/v2/runtime/handlers/get-runtime-info.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs +1 -1
- package/dist/v2/runtime/handlers/handle-connect.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs +1 -1
- package/dist/v2/runtime/handlers/handle-connect.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.cjs +8 -2
- package/dist/v2/runtime/handlers/handle-run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-run.mjs +8 -2
- package/dist/v2/runtime/handlers/handle-run.mjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-stop.cjs +2 -1
- package/dist/v2/runtime/handlers/handle-stop.cjs.map +1 -1
- package/dist/v2/runtime/handlers/handle-stop.mjs +2 -1
- package/dist/v2/runtime/handlers/handle-stop.mjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.cjs +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.cjs.map +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.mjs +1 -1
- package/dist/v2/runtime/handlers/intelligence/thread-names.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs +3 -2
- package/dist/v2/runtime/handlers/shared/agent-utils.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs +3 -2
- package/dist/v2/runtime/handlers/shared/agent-utils.mjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs +40 -1
- package/dist/v2/runtime/handlers/shared/sse-response.cjs.map +1 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs +40 -1
- package/dist/v2/runtime/handlers/shared/sse-response.mjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.cjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.cjs.map +1 -1
- package/dist/v2/runtime/handlers/sse/run.mjs +3 -1
- package/dist/v2/runtime/handlers/sse/run.mjs.map +1 -1
- package/dist/v2/runtime/index.d.cts +1 -1
- package/dist/v2/runtime/index.d.mts +1 -1
- package/package.json +7 -6
- package/src/agent/__tests__/capabilities.test.ts +81 -0
- package/src/agent/converters/tanstack.ts +15 -7
- package/src/agent/index.ts +33 -0
- package/src/lib/runtime/__tests__/v1-agent-factory.test.ts +109 -0
- package/src/lib/runtime/copilot-runtime.ts +38 -2
- package/src/service-adapters/openai/__tests__/openai-v5-compat.test.ts +177 -0
- package/src/service-adapters/openai/openai-adapter.ts +3 -1
- package/src/service-adapters/openai/openai-assistant-adapter.ts +7 -9
- package/src/service-adapters/openai/utils.ts +100 -0
- package/src/v2/runtime/__tests__/agents-factory.test.ts +136 -0
- package/src/v2/runtime/__tests__/debug-sse-response.test.ts +302 -0
- package/src/v2/runtime/__tests__/get-runtime-info.test.ts +134 -1
- package/src/v2/runtime/core/runtime.ts +90 -2
- package/src/v2/runtime/handlers/get-runtime-info.ts +33 -8
- package/src/v2/runtime/handlers/handle-connect.ts +1 -1
- package/src/v2/runtime/handlers/handle-run.ts +16 -2
- package/src/v2/runtime/handlers/handle-stop.ts +2 -1
- package/src/v2/runtime/handlers/intelligence/thread-names.ts +1 -1
- package/src/v2/runtime/handlers/shared/agent-utils.ts +3 -2
- package/src/v2/runtime/handlers/shared/sse-response.ts +69 -0
- package/src/v2/runtime/handlers/sse/run.ts +9 -0
package/src/agent/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
StateSnapshotEvent,
|
|
21
21
|
StateDeltaEvent,
|
|
22
22
|
} from "@ag-ui/client";
|
|
23
|
+
import type { AgentCapabilities } from "@ag-ui/core";
|
|
23
24
|
import {
|
|
24
25
|
streamText,
|
|
25
26
|
LanguageModel,
|
|
@@ -817,6 +818,15 @@ export interface BuiltInAgentClassicConfig {
|
|
|
817
818
|
* Example: `{ openai: { reasoningEffort: "high" } }`
|
|
818
819
|
*/
|
|
819
820
|
providerOptions?: Record<string, any>;
|
|
821
|
+
/**
|
|
822
|
+
* Explicit agent capabilities. **Shallow-merged** at the category level on
|
|
823
|
+
* top of auto-inferred defaults — providing a category (e.g. `tools`)
|
|
824
|
+
* replaces that entire category, not individual fields within it.
|
|
825
|
+
*
|
|
826
|
+
* For example, `{ tools: { supported: true } }` will drop the inferred
|
|
827
|
+
* `clientProvided` value. Include all fields for any category you override.
|
|
828
|
+
*/
|
|
829
|
+
capabilities?: Partial<AgentCapabilities>;
|
|
820
830
|
}
|
|
821
831
|
|
|
822
832
|
/**
|
|
@@ -854,6 +864,29 @@ export class BuiltInAgent extends AbstractAgent {
|
|
|
854
864
|
return this.config?.overridableProperties?.includes(property) ?? false;
|
|
855
865
|
}
|
|
856
866
|
|
|
867
|
+
async getCapabilities(): Promise<AgentCapabilities> {
|
|
868
|
+
const inferred: AgentCapabilities = {
|
|
869
|
+
tools: {
|
|
870
|
+
supported: true,
|
|
871
|
+
clientProvided: true,
|
|
872
|
+
},
|
|
873
|
+
transport: {
|
|
874
|
+
streaming: true,
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
if (!this.config.capabilities) {
|
|
879
|
+
return inferred;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Shallow merge at the category level — explicit overrides replace
|
|
883
|
+
// entire categories when provided, inferred defaults fill the rest.
|
|
884
|
+
return {
|
|
885
|
+
...inferred,
|
|
886
|
+
...this.config.capabilities,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
857
890
|
run(input: RunAgentInput): Observable<BaseEvent> {
|
|
858
891
|
if (isFactoryConfig(this.config)) {
|
|
859
892
|
return this.runFactory(input, this.config);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
3
|
+
import { CopilotRuntime } from "../copilot-runtime";
|
|
4
|
+
import {
|
|
5
|
+
resolveAgents,
|
|
6
|
+
type AgentsConfig,
|
|
7
|
+
} from "../../../v2/runtime/core/runtime";
|
|
8
|
+
|
|
9
|
+
function createMockAgent(name = "test") {
|
|
10
|
+
return new HttpAgent({ url: `https://example.com/${name}` });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createMockRequest(headers?: Record<string, string>) {
|
|
14
|
+
return new Request("https://example.com/agent/test/run", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
17
|
+
body: JSON.stringify({ threadId: "thread-1", messages: [], state: {} }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("V1 CopilotRuntime with agent factory function", () => {
|
|
22
|
+
it("preserves factory function through constructor (does not spread to {})", () => {
|
|
23
|
+
const factory = vi.fn().mockReturnValue({
|
|
24
|
+
default: createMockAgent("default"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const runtime = new CopilotRuntime({ agents: factory });
|
|
28
|
+
|
|
29
|
+
// The V2 instance should receive a function (factory), not an empty object.
|
|
30
|
+
// Before the fix, spreading a function produced {}, losing all agents.
|
|
31
|
+
const v2Agents = runtime.instance.agents;
|
|
32
|
+
expect(typeof v2Agents).toBe("function");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("factory function resolves agents on each request", async () => {
|
|
36
|
+
const agentA = createMockAgent("tenant-a");
|
|
37
|
+
const agentB = createMockAgent("tenant-b");
|
|
38
|
+
|
|
39
|
+
const factory: AgentsConfig = ({ request }) => {
|
|
40
|
+
const tenantId = request.headers.get("x-tenant-id");
|
|
41
|
+
if (tenantId === "a") return { default: agentA };
|
|
42
|
+
return { default: agentB };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const runtime = new CopilotRuntime({ agents: factory });
|
|
46
|
+
const v2Agents = runtime.instance.agents;
|
|
47
|
+
|
|
48
|
+
const requestA = createMockRequest({ "x-tenant-id": "a" });
|
|
49
|
+
const resolvedA = await resolveAgents(v2Agents, requestA);
|
|
50
|
+
expect(resolvedA.default).toBe(agentA);
|
|
51
|
+
|
|
52
|
+
const requestB = createMockRequest({ "x-tenant-id": "b" });
|
|
53
|
+
const resolvedB = await resolveAgents(v2Agents, requestB);
|
|
54
|
+
expect(resolvedB.default).toBe(agentB);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("merges endpoint agents with factory-resolved agents", async () => {
|
|
58
|
+
const factoryAgent = createMockAgent("factory-agent");
|
|
59
|
+
const factory = vi.fn().mockReturnValue({
|
|
60
|
+
dynamic: factoryAgent,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Use remoteEndpoints to generate endpoint agents that should be merged
|
|
64
|
+
const runtime = new CopilotRuntime({
|
|
65
|
+
agents: factory,
|
|
66
|
+
remoteEndpoints: [
|
|
67
|
+
{
|
|
68
|
+
url: "https://example.com/endpoint",
|
|
69
|
+
onBeforeRequest: undefined,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const v2Agents = runtime.instance.agents;
|
|
75
|
+
expect(typeof v2Agents).toBe("function");
|
|
76
|
+
|
|
77
|
+
const request = createMockRequest();
|
|
78
|
+
const resolved = await resolveAgents(v2Agents, request);
|
|
79
|
+
|
|
80
|
+
// Factory agent should be present
|
|
81
|
+
expect(resolved.dynamic).toBe(factoryAgent);
|
|
82
|
+
// Factory should have been called with request context
|
|
83
|
+
expect(factory).toHaveBeenCalledWith({ request });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("static agents record still works after fix", async () => {
|
|
87
|
+
const agent = createMockAgent("static");
|
|
88
|
+
|
|
89
|
+
const runtime = new CopilotRuntime({
|
|
90
|
+
agents: { myAgent: agent },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const v2Agents = runtime.instance.agents;
|
|
94
|
+
const resolved = await resolveAgents(v2Agents);
|
|
95
|
+
expect(resolved.myAgent).toBe(agent);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("promised agents record still works after fix", async () => {
|
|
99
|
+
const agent = createMockAgent("promised");
|
|
100
|
+
|
|
101
|
+
const runtime = new CopilotRuntime({
|
|
102
|
+
agents: Promise.resolve({ myAgent: agent }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const v2Agents = runtime.instance.agents;
|
|
106
|
+
const resolved = await resolveAgents(v2Agents);
|
|
107
|
+
expect(resolved.myAgent).toBe(agent);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getZodParameters,
|
|
24
24
|
type PartialBy,
|
|
25
25
|
isTelemetryDisabled,
|
|
26
|
+
type DebugConfig,
|
|
26
27
|
} from "@copilotkit/shared";
|
|
27
28
|
import type { RunAgentInput } from "@ag-ui/core";
|
|
28
29
|
import { aguiToGQL } from "../../graphql/message-conversion/agui-to-gql";
|
|
@@ -35,8 +36,13 @@ import {
|
|
|
35
36
|
type CopilotRuntimeOptions,
|
|
36
37
|
type CopilotRuntimeOptions as CopilotRuntimeOptionsVNext,
|
|
37
38
|
type AgentRunner,
|
|
39
|
+
type AgentsConfig,
|
|
40
|
+
type AgentsFactory,
|
|
41
|
+
type AgentFactoryContext,
|
|
38
42
|
InMemoryAgentRunner,
|
|
39
43
|
} from "../../v2/runtime";
|
|
44
|
+
|
|
45
|
+
export type { AgentsConfig, AgentsFactory, AgentFactoryContext };
|
|
40
46
|
import { TelemetryAgentRunner } from "./telemetry-agent-runner";
|
|
41
47
|
import telemetry from "../telemetry-client";
|
|
42
48
|
|
|
@@ -287,6 +293,19 @@ export interface CopilotRuntimeConstructorParams_BASE<
|
|
|
287
293
|
|
|
288
294
|
onStopGeneration?: OnStopGenerationHandler;
|
|
289
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Enable debug logging for the runtime event pipeline.
|
|
298
|
+
* Pass `true` for full output, or an object for granular control:
|
|
299
|
+
*
|
|
300
|
+
* ```ts
|
|
301
|
+
* const runtime = new CopilotRuntime({
|
|
302
|
+
* debug: true,
|
|
303
|
+
* // or: debug: { events: true, lifecycle: true, verbose: false }
|
|
304
|
+
* });
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
debug?: DebugConfig;
|
|
308
|
+
|
|
290
309
|
// /** Optional transcription service for audio processing. */
|
|
291
310
|
// transcriptionService?: CopilotRuntimeOptionsVNext["transcriptionService"];
|
|
292
311
|
// /** Optional *before* middleware – callback function or webhook URL. */
|
|
@@ -318,7 +337,7 @@ interface CopilotRuntimeConstructorParams<T extends Parameter[] | [] = []>
|
|
|
318
337
|
* – the `MaybePromise<NonEmptyRecord<T>>` constraint in `CopilotRuntimeOptionsVNext`
|
|
319
338
|
* – the `Record<string, AbstractAgent>` constraint in `both
|
|
320
339
|
*/
|
|
321
|
-
agents?:
|
|
340
|
+
agents?: AgentsConfig;
|
|
322
341
|
}
|
|
323
342
|
|
|
324
343
|
/**
|
|
@@ -342,6 +361,22 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
342
361
|
params?.remoteEndpoints ?? [],
|
|
343
362
|
);
|
|
344
363
|
|
|
364
|
+
// Merge endpoint agents with user-provided agents.
|
|
365
|
+
// When agents is a factory function, wrap it so endpoint agents are merged
|
|
366
|
+
// at resolution time (spreading a function produces {} — silent data loss).
|
|
367
|
+
let mergedAgents: AgentsConfig;
|
|
368
|
+
if (typeof agents === "function") {
|
|
369
|
+
mergedAgents = async (ctx) => {
|
|
370
|
+
const resolved = await agents(ctx);
|
|
371
|
+
return { ...endpointAgents, ...resolved };
|
|
372
|
+
};
|
|
373
|
+
} else {
|
|
374
|
+
mergedAgents = Promise.resolve(agents).then((resolved) => ({
|
|
375
|
+
...endpointAgents,
|
|
376
|
+
...resolved,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
|
|
345
380
|
// Determine the base runner (user-provided or default)
|
|
346
381
|
const baseRunner = params?.runner ?? new InMemoryAgentRunner();
|
|
347
382
|
|
|
@@ -353,9 +388,10 @@ export class CopilotRuntime<const T extends Parameter[] | [] = []> {
|
|
|
353
388
|
: new TelemetryAgentRunner({ runner: baseRunner });
|
|
354
389
|
|
|
355
390
|
this.runtimeArgs = {
|
|
356
|
-
agents:
|
|
391
|
+
agents: mergedAgents,
|
|
357
392
|
runner,
|
|
358
393
|
licenseToken: params?.licenseToken,
|
|
394
|
+
debug: params?.debug,
|
|
359
395
|
// TODO: add support for transcriptionService from CopilotRuntimeOptionsVNext once it is ready
|
|
360
396
|
// transcriptionService: params?.transcriptionService,
|
|
361
397
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isOpenAIV5,
|
|
4
|
+
getChatCompletionsForStreaming,
|
|
5
|
+
retrieveThreadRun,
|
|
6
|
+
submitToolOutputsStream,
|
|
7
|
+
} from "../utils";
|
|
8
|
+
import type OpenAI from "openai";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tests for OpenAI SDK v4/v5 compatibility layer.
|
|
12
|
+
*
|
|
13
|
+
* In v5, the `beta.chat` namespace was removed and promoted to `chat`.
|
|
14
|
+
* In v5, methods with multiple path params switched to named params
|
|
15
|
+
* (e.g. runs.retrieve(runId, { thread_id }) instead of runs.retrieve(threadId, runId)).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function createMockV4Client() {
|
|
19
|
+
const streamFn = vi.fn();
|
|
20
|
+
return {
|
|
21
|
+
client: {
|
|
22
|
+
beta: {
|
|
23
|
+
chat: {
|
|
24
|
+
completions: { stream: streamFn },
|
|
25
|
+
},
|
|
26
|
+
threads: {
|
|
27
|
+
create: vi.fn(),
|
|
28
|
+
runs: {
|
|
29
|
+
retrieve: vi.fn().mockResolvedValue({ id: "run_xyz" }),
|
|
30
|
+
stream: vi.fn(),
|
|
31
|
+
submitToolOutputsStream: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
create: vi.fn(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
chat: {
|
|
39
|
+
completions: {
|
|
40
|
+
stream: vi.fn(), // exists but should NOT be used for streaming in v4
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
} as unknown as OpenAI,
|
|
44
|
+
streamFn,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createMockV5Client() {
|
|
49
|
+
const streamFn = vi.fn();
|
|
50
|
+
return {
|
|
51
|
+
client: {
|
|
52
|
+
// v5: beta.chat is gone, only beta.threads remains
|
|
53
|
+
beta: {
|
|
54
|
+
threads: {
|
|
55
|
+
create: vi.fn(),
|
|
56
|
+
runs: {
|
|
57
|
+
retrieve: vi.fn().mockResolvedValue({ id: "run_xyz" }),
|
|
58
|
+
stream: vi.fn(),
|
|
59
|
+
submitToolOutputsStream: vi.fn(),
|
|
60
|
+
},
|
|
61
|
+
messages: {
|
|
62
|
+
create: vi.fn(),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
chat: {
|
|
67
|
+
completions: {
|
|
68
|
+
stream: streamFn,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
} as unknown as OpenAI,
|
|
72
|
+
streamFn,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("isOpenAIV5", () => {
|
|
77
|
+
it("returns false for a v4 client (beta.chat exists)", () => {
|
|
78
|
+
const { client } = createMockV4Client();
|
|
79
|
+
expect(isOpenAIV5(client)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns true for a v5 client (beta.chat does not exist)", () => {
|
|
83
|
+
const { client } = createMockV5Client();
|
|
84
|
+
expect(isOpenAIV5(client)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns true when beta is entirely missing", () => {
|
|
88
|
+
const client = { chat: { completions: {} } } as unknown as OpenAI;
|
|
89
|
+
expect(isOpenAIV5(client)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("getChatCompletionsForStreaming", () => {
|
|
94
|
+
it("returns beta.chat.completions for a v4 client", () => {
|
|
95
|
+
const { client } = createMockV4Client();
|
|
96
|
+
const completions = getChatCompletionsForStreaming(client);
|
|
97
|
+
// Should be the beta version, not the top-level one
|
|
98
|
+
expect(completions).not.toBe(client.chat.completions);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns chat.completions for a v5 client", () => {
|
|
102
|
+
const { client } = createMockV5Client();
|
|
103
|
+
const completions = getChatCompletionsForStreaming(client);
|
|
104
|
+
expect(completions).toBe(client.chat.completions);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("stream() on v4 calls beta.chat.completions.stream", () => {
|
|
108
|
+
const { client, streamFn } = createMockV4Client();
|
|
109
|
+
const completions = getChatCompletionsForStreaming(client);
|
|
110
|
+
completions.stream({ model: "gpt-4o", messages: [] });
|
|
111
|
+
expect(streamFn).toHaveBeenCalledWith({ model: "gpt-4o", messages: [] });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("stream() on v5 calls chat.completions.stream", () => {
|
|
115
|
+
const { client, streamFn } = createMockV5Client();
|
|
116
|
+
const completions = getChatCompletionsForStreaming(client);
|
|
117
|
+
completions.stream({ model: "gpt-4o", messages: [] });
|
|
118
|
+
expect(streamFn).toHaveBeenCalledWith({ model: "gpt-4o", messages: [] });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("retrieveThreadRun", () => {
|
|
123
|
+
it("v4: calls retrieve with positional args (threadId, runId)", async () => {
|
|
124
|
+
const { client } = createMockV4Client();
|
|
125
|
+
const retrieveFn = client.beta.threads.runs.retrieve as ReturnType<
|
|
126
|
+
typeof vi.fn
|
|
127
|
+
>;
|
|
128
|
+
|
|
129
|
+
await retrieveThreadRun(client, "thread_abc", "run_xyz");
|
|
130
|
+
|
|
131
|
+
expect(retrieveFn).toHaveBeenCalledWith("thread_abc", "run_xyz");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("v5: calls retrieve with named path params (runId, { thread_id })", async () => {
|
|
135
|
+
const { client } = createMockV5Client();
|
|
136
|
+
const retrieveFn = client.beta.threads.runs.retrieve as ReturnType<
|
|
137
|
+
typeof vi.fn
|
|
138
|
+
>;
|
|
139
|
+
|
|
140
|
+
await retrieveThreadRun(client, "thread_abc", "run_xyz");
|
|
141
|
+
|
|
142
|
+
expect(retrieveFn).toHaveBeenCalledWith("run_xyz", {
|
|
143
|
+
thread_id: "thread_abc",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("submitToolOutputsStream", () => {
|
|
149
|
+
it("v4: calls with positional args (threadId, runId, body)", () => {
|
|
150
|
+
const { client } = createMockV4Client();
|
|
151
|
+
const submitFn = client.beta.threads.runs
|
|
152
|
+
.submitToolOutputsStream as ReturnType<typeof vi.fn>;
|
|
153
|
+
const body = {
|
|
154
|
+
tool_outputs: [{ tool_call_id: "tc_1", output: "result" }],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
submitToolOutputsStream(client, "thread_abc", "run_xyz", body);
|
|
158
|
+
|
|
159
|
+
expect(submitFn).toHaveBeenCalledWith("thread_abc", "run_xyz", body);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("v5: calls with named path params (runId, { thread_id, ...body })", () => {
|
|
163
|
+
const { client } = createMockV5Client();
|
|
164
|
+
const submitFn = client.beta.threads.runs
|
|
165
|
+
.submitToolOutputsStream as ReturnType<typeof vi.fn>;
|
|
166
|
+
const body = {
|
|
167
|
+
tool_outputs: [{ tool_call_id: "tc_1", output: "result" }],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
submitToolOutputsStream(client, "thread_abc", "run_xyz", body);
|
|
171
|
+
|
|
172
|
+
expect(submitFn).toHaveBeenCalledWith("run_xyz", {
|
|
173
|
+
thread_id: "thread_abc",
|
|
174
|
+
...body,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
convertActionInputToOpenAITool,
|
|
62
62
|
convertMessageToOpenAIMessage,
|
|
63
63
|
limitMessagesToTokenCount,
|
|
64
|
+
getChatCompletionsForStreaming,
|
|
64
65
|
} from "./utils";
|
|
65
66
|
import { randomUUID } from "@copilotkit/shared";
|
|
66
67
|
import { convertServiceAdapterError, getSdkClientOptions } from "../shared";
|
|
@@ -203,7 +204,8 @@ export class OpenAIAdapter implements CopilotServiceAdapter {
|
|
|
203
204
|
|
|
204
205
|
try {
|
|
205
206
|
const openai = this.ensureOpenAI();
|
|
206
|
-
const
|
|
207
|
+
const completions = getChatCompletionsForStreaming(openai);
|
|
208
|
+
const stream = completions.stream({
|
|
207
209
|
model: model,
|
|
208
210
|
stream: true,
|
|
209
211
|
messages: openaiMessages,
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
convertActionInputToOpenAITool,
|
|
44
44
|
convertMessageToOpenAIMessage,
|
|
45
45
|
convertSystemMessageToAssistantAPI,
|
|
46
|
+
retrieveThreadRun,
|
|
47
|
+
submitToolOutputsStream,
|
|
46
48
|
} from "./utils";
|
|
47
49
|
import { RuntimeEventSource } from "../events";
|
|
48
50
|
import { ActionInput } from "../../graphql/inputs/action.input";
|
|
@@ -186,7 +188,7 @@ export class OpenAIAssistantAdapter implements CopilotServiceAdapter {
|
|
|
186
188
|
eventSource: RuntimeEventSource,
|
|
187
189
|
) {
|
|
188
190
|
const openai = this.ensureOpenAI();
|
|
189
|
-
let run = await openai
|
|
191
|
+
let run = await retrieveThreadRun(openai, threadId, runId);
|
|
190
192
|
|
|
191
193
|
if (!run.required_action) {
|
|
192
194
|
throw new Error("No tool outputs required");
|
|
@@ -219,14 +221,10 @@ export class OpenAIAssistantAdapter implements CopilotServiceAdapter {
|
|
|
219
221
|
};
|
|
220
222
|
});
|
|
221
223
|
|
|
222
|
-
const stream =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
tool_outputs: toolOutputs,
|
|
227
|
-
...(this.disableParallelToolCalls && { parallel_tool_calls: false }),
|
|
228
|
-
},
|
|
229
|
-
);
|
|
224
|
+
const stream = submitToolOutputsStream(openai, threadId, runId, {
|
|
225
|
+
tool_outputs: toolOutputs,
|
|
226
|
+
...(this.disableParallelToolCalls && { parallel_tool_calls: false }),
|
|
227
|
+
});
|
|
230
228
|
|
|
231
229
|
await this.streamResponse(stream, eventSource);
|
|
232
230
|
return runId;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
1
2
|
import { Message } from "../../graphql/types/converted";
|
|
2
3
|
import { ActionInput } from "../../graphql/inputs/action.input";
|
|
3
4
|
import {
|
|
@@ -10,6 +11,105 @@ import {
|
|
|
10
11
|
} from "openai/resources/chat";
|
|
11
12
|
import { parseJson } from "@copilotkit/shared";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* OpenAI v4 exposes streaming completions under `beta.chat.completions`.
|
|
16
|
+
* v5 removed `beta.chat` and promoted streaming to `chat.completions`.
|
|
17
|
+
* These interfaces model the v4-specific shape so we can detect and access
|
|
18
|
+
* the beta namespace safely without `as any`.
|
|
19
|
+
*/
|
|
20
|
+
interface OpenAIV4BetaChat {
|
|
21
|
+
chat: {
|
|
22
|
+
completions: OpenAI["chat"]["completions"];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface OpenAIV4Beta extends OpenAI.Beta {
|
|
27
|
+
chat: OpenAIV4BetaChat["chat"];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard: checks whether the OpenAI client has the v4-era `beta.chat`
|
|
32
|
+
* namespace. Returns `false` for v5+ clients where `beta.chat` was removed.
|
|
33
|
+
*/
|
|
34
|
+
function hasV4BetaChat(beta: OpenAI["beta"] | undefined): beta is OpenAIV4Beta {
|
|
35
|
+
return beta != null && "chat" in beta && (beta as OpenAIV4Beta).chat != null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detects whether the provided OpenAI client is v5+ by checking for the
|
|
40
|
+
* removal of the `beta.chat` namespace (which was promoted to `chat` in v5).
|
|
41
|
+
*/
|
|
42
|
+
export function isOpenAIV5(openai: OpenAI): boolean {
|
|
43
|
+
return !hasV4BetaChat(openai.beta);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the chat completions object that supports `.stream()`.
|
|
48
|
+
* In v4 this lives under `openai.beta.chat.completions`;
|
|
49
|
+
* in v5 it was promoted to `openai.chat.completions`.
|
|
50
|
+
*/
|
|
51
|
+
export function getChatCompletionsForStreaming(
|
|
52
|
+
openai: OpenAI,
|
|
53
|
+
): OpenAI["chat"]["completions"] {
|
|
54
|
+
if (hasV4BetaChat(openai.beta)) {
|
|
55
|
+
return openai.beta.chat.completions;
|
|
56
|
+
}
|
|
57
|
+
return openai.chat.completions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves a thread run, handling the v4→v5 API signature change.
|
|
62
|
+
* v4: retrieve(threadId, runId)
|
|
63
|
+
* v5: retrieve(runId, { thread_id: threadId })
|
|
64
|
+
*/
|
|
65
|
+
export async function retrieveThreadRun(
|
|
66
|
+
openai: OpenAI,
|
|
67
|
+
threadId: string,
|
|
68
|
+
runId: string,
|
|
69
|
+
): Promise<OpenAI.Beta.Threads.Runs.Run> {
|
|
70
|
+
if (isOpenAIV5(openai)) {
|
|
71
|
+
// v5 switched to named path params. The type definitions from whichever
|
|
72
|
+
// SDK version is installed won't match both signatures, so we call through
|
|
73
|
+
// a generic function reference. This is the one unavoidable boundary
|
|
74
|
+
// between two incompatible SDK type surfaces.
|
|
75
|
+
const retrieve = openai.beta.threads.runs.retrieve as {
|
|
76
|
+
(...args: unknown[]): Promise<OpenAI.Beta.Threads.Runs.Run>;
|
|
77
|
+
};
|
|
78
|
+
return retrieve(runId, { thread_id: threadId });
|
|
79
|
+
}
|
|
80
|
+
return openai.beta.threads.runs.retrieve(threadId, runId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Submits tool outputs as a stream, handling the v4→v5 API signature change.
|
|
85
|
+
* v4: submitToolOutputsStream(threadId, runId, body)
|
|
86
|
+
* v5: submitToolOutputsStream(runId, { thread_id, ...body })
|
|
87
|
+
*/
|
|
88
|
+
export function submitToolOutputsStream(
|
|
89
|
+
openai: OpenAI,
|
|
90
|
+
threadId: string,
|
|
91
|
+
runId: string,
|
|
92
|
+
body: {
|
|
93
|
+
tool_outputs: Array<{ tool_call_id: string; output: string }>;
|
|
94
|
+
parallel_tool_calls?: false;
|
|
95
|
+
},
|
|
96
|
+
) {
|
|
97
|
+
if (isOpenAIV5(openai)) {
|
|
98
|
+
// Same boundary as retrieveThreadRun — v5 uses named path params.
|
|
99
|
+
const submit = openai.beta.threads.runs.submitToolOutputsStream as {
|
|
100
|
+
(
|
|
101
|
+
...args: unknown[]
|
|
102
|
+
): ReturnType<typeof openai.beta.threads.runs.submitToolOutputsStream>;
|
|
103
|
+
};
|
|
104
|
+
return submit(runId, { thread_id: threadId, ...body });
|
|
105
|
+
}
|
|
106
|
+
return openai.beta.threads.runs.submitToolOutputsStream(
|
|
107
|
+
threadId,
|
|
108
|
+
runId,
|
|
109
|
+
body,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
13
113
|
export function limitMessagesToTokenCount(
|
|
14
114
|
messages: any[],
|
|
15
115
|
tools: any[],
|