@robota-sdk/agent-provider 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- package/src/shared/openai-compatible/types.ts +59 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { OpenAIStreamHandler } from './stream-handler';
|
|
3
|
+
import type { IPayloadLogger } from '../interfaces/payload-logger';
|
|
4
|
+
import type { ILogger } from '@robota-sdk/agent-core';
|
|
5
|
+
import type { IOpenAIStreamRequestParams, IOpenAIChatRequestParams } from '../types/api-types';
|
|
6
|
+
import type OpenAI from 'openai';
|
|
7
|
+
|
|
8
|
+
function createMockLogger(): ILogger {
|
|
9
|
+
return {
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
warn: vi.fn(),
|
|
12
|
+
error: vi.fn(),
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
log: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createMockPayloadLogger(enabled = true): IPayloadLogger {
|
|
19
|
+
return {
|
|
20
|
+
isEnabled: vi.fn().mockReturnValue(enabled),
|
|
21
|
+
logPayload: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create an async iterable from an array of chunks
|
|
26
|
+
async function* asyncIterableFrom<T>(items: T[]): AsyncIterable<T> {
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
yield item;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createMockClient(chunks: OpenAI.Chat.ChatCompletionChunk[]): OpenAI {
|
|
33
|
+
return {
|
|
34
|
+
chat: {
|
|
35
|
+
completions: {
|
|
36
|
+
create: vi.fn().mockResolvedValue(asyncIterableFrom(chunks)),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
} as unknown as OpenAI;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createStreamChunk(
|
|
43
|
+
content: string,
|
|
44
|
+
finishReason: string | null = null,
|
|
45
|
+
): OpenAI.Chat.ChatCompletionChunk {
|
|
46
|
+
return {
|
|
47
|
+
id: 'chatcmpl-chunk',
|
|
48
|
+
object: 'chat.completion.chunk',
|
|
49
|
+
created: Date.now(),
|
|
50
|
+
model: 'gpt-4',
|
|
51
|
+
choices: [
|
|
52
|
+
{
|
|
53
|
+
index: 0,
|
|
54
|
+
delta: { content },
|
|
55
|
+
finish_reason: finishReason,
|
|
56
|
+
logprobs: null,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
} as OpenAI.Chat.ChatCompletionChunk;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('OpenAIStreamHandler', () => {
|
|
63
|
+
let mockClient: OpenAI;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('handleStream', () => {
|
|
70
|
+
it('should yield parsed messages from stream', async () => {
|
|
71
|
+
const chunks = [
|
|
72
|
+
createStreamChunk('Hello'),
|
|
73
|
+
createStreamChunk(' world'),
|
|
74
|
+
createStreamChunk('', 'stop'),
|
|
75
|
+
];
|
|
76
|
+
mockClient = createMockClient(chunks);
|
|
77
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
78
|
+
|
|
79
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
80
|
+
model: 'gpt-4',
|
|
81
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
82
|
+
stream: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const results: Array<{ content: string | null }> = [];
|
|
86
|
+
for await (const msg of handler.handleStream(requestParams)) {
|
|
87
|
+
results.push(msg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(results).toHaveLength(3);
|
|
91
|
+
expect(results[0].content).toBe('Hello');
|
|
92
|
+
expect(results[1].content).toBe(' world');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should log payload when payload logger is enabled', async () => {
|
|
96
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
97
|
+
mockClient = createMockClient(chunks);
|
|
98
|
+
const payloadLogger = createMockPayloadLogger(true);
|
|
99
|
+
const handler = new OpenAIStreamHandler(mockClient, payloadLogger);
|
|
100
|
+
|
|
101
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
102
|
+
model: 'gpt-4',
|
|
103
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
104
|
+
stream: true,
|
|
105
|
+
temperature: 0.5,
|
|
106
|
+
max_tokens: 100,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const results = [];
|
|
110
|
+
for await (const msg of handler.handleStream(requestParams)) {
|
|
111
|
+
results.push(msg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(payloadLogger.logPayload).toHaveBeenCalledWith(
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
model: 'gpt-4',
|
|
117
|
+
messagesCount: 1,
|
|
118
|
+
hasTools: false,
|
|
119
|
+
temperature: 0.5,
|
|
120
|
+
maxTokens: 100,
|
|
121
|
+
}),
|
|
122
|
+
'stream',
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should not log payload when payload logger is disabled', async () => {
|
|
127
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
128
|
+
mockClient = createMockClient(chunks);
|
|
129
|
+
const payloadLogger = createMockPayloadLogger(false);
|
|
130
|
+
const handler = new OpenAIStreamHandler(mockClient, payloadLogger);
|
|
131
|
+
|
|
132
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
133
|
+
model: 'gpt-4',
|
|
134
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
135
|
+
stream: true,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
for await (const _msg of handler.handleStream(requestParams)) {
|
|
139
|
+
// consume stream
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
expect(payloadLogger.logPayload).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should pass tools and tool_choice to API when provided', async () => {
|
|
146
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
147
|
+
mockClient = createMockClient(chunks);
|
|
148
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
149
|
+
|
|
150
|
+
const tools: OpenAI.Chat.ChatCompletionTool[] = [
|
|
151
|
+
{
|
|
152
|
+
type: 'function',
|
|
153
|
+
function: {
|
|
154
|
+
name: 'search',
|
|
155
|
+
description: 'Search the web',
|
|
156
|
+
parameters: { type: 'object', properties: {} },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
162
|
+
model: 'gpt-4',
|
|
163
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
164
|
+
stream: true,
|
|
165
|
+
tools,
|
|
166
|
+
tool_choice: 'auto',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
for await (const _msg of handler.handleStream(requestParams)) {
|
|
170
|
+
// consume
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const createCall = vi.mocked(mockClient.chat.completions.create);
|
|
174
|
+
expect(createCall).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
tools,
|
|
177
|
+
tool_choice: 'auto',
|
|
178
|
+
stream: true,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should throw with descriptive error when API call fails', async () => {
|
|
184
|
+
mockClient = {
|
|
185
|
+
chat: {
|
|
186
|
+
completions: {
|
|
187
|
+
create: vi.fn().mockRejectedValue(new Error('API rate limit')),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
} as unknown as OpenAI;
|
|
191
|
+
|
|
192
|
+
const logger = createMockLogger();
|
|
193
|
+
const handler = new OpenAIStreamHandler(mockClient, undefined, logger);
|
|
194
|
+
|
|
195
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
196
|
+
model: 'gpt-4',
|
|
197
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
198
|
+
stream: true,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const results = [];
|
|
202
|
+
await expect(async () => {
|
|
203
|
+
for await (const msg of handler.handleStream(requestParams)) {
|
|
204
|
+
results.push(msg);
|
|
205
|
+
}
|
|
206
|
+
}).rejects.toThrow('OpenAI streaming failed: API rate limit');
|
|
207
|
+
expect(logger.error).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle empty stream', async () => {
|
|
211
|
+
mockClient = createMockClient([]);
|
|
212
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
213
|
+
|
|
214
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
215
|
+
model: 'gpt-4',
|
|
216
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
217
|
+
stream: true,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const results = [];
|
|
221
|
+
for await (const msg of handler.handleStream(requestParams)) {
|
|
222
|
+
results.push(msg);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expect(results).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should work without payload logger', async () => {
|
|
229
|
+
const chunks = [createStreamChunk('Hello', 'stop')];
|
|
230
|
+
mockClient = createMockClient(chunks);
|
|
231
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
232
|
+
|
|
233
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
234
|
+
model: 'gpt-4',
|
|
235
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
236
|
+
stream: true,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const results = [];
|
|
240
|
+
for await (const msg of handler.handleStream(requestParams)) {
|
|
241
|
+
results.push(msg);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
expect(results).toHaveLength(1);
|
|
245
|
+
expect(results[0].content).toBe('Hello');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('generateStreamingResponse', () => {
|
|
250
|
+
it('should convert chat request params and delegate to handleStream', async () => {
|
|
251
|
+
const chunks = [createStreamChunk('Response'), createStreamChunk('', 'stop')];
|
|
252
|
+
mockClient = createMockClient(chunks);
|
|
253
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
254
|
+
|
|
255
|
+
const request: IOpenAIChatRequestParams = {
|
|
256
|
+
model: 'gpt-4',
|
|
257
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
258
|
+
temperature: 0.8,
|
|
259
|
+
max_tokens: 200,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const results = [];
|
|
263
|
+
for await (const msg of handler.generateStreamingResponse(request)) {
|
|
264
|
+
results.push(msg);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
expect(results).toHaveLength(2);
|
|
268
|
+
expect(results[0].content).toBe('Response');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should default model to gpt-4o-mini when not specified', async () => {
|
|
272
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
273
|
+
mockClient = createMockClient(chunks);
|
|
274
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
275
|
+
|
|
276
|
+
const request: IOpenAIChatRequestParams = {
|
|
277
|
+
model: '',
|
|
278
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for await (const _msg of handler.generateStreamingResponse(request)) {
|
|
282
|
+
// consume
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const createCall = vi.mocked(mockClient.chat.completions.create);
|
|
286
|
+
expect(createCall).toHaveBeenCalledWith(expect.objectContaining({ model: 'gpt-4o-mini' }));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should include tools when provided in request', async () => {
|
|
290
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
291
|
+
mockClient = createMockClient(chunks);
|
|
292
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
293
|
+
|
|
294
|
+
const tools: OpenAI.Chat.ChatCompletionTool[] = [
|
|
295
|
+
{
|
|
296
|
+
type: 'function',
|
|
297
|
+
function: {
|
|
298
|
+
name: 'calc',
|
|
299
|
+
description: 'Calculate',
|
|
300
|
+
parameters: { type: 'object', properties: {} },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const request: IOpenAIChatRequestParams = {
|
|
306
|
+
model: 'gpt-4',
|
|
307
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
308
|
+
tools,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
for await (const _msg of handler.generateStreamingResponse(request)) {
|
|
312
|
+
// consume
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const createCall = vi.mocked(mockClient.chat.completions.create);
|
|
316
|
+
expect(createCall).toHaveBeenCalledWith(
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
tools,
|
|
319
|
+
tool_choice: 'auto',
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should throw and log error when streaming fails', async () => {
|
|
325
|
+
mockClient = {
|
|
326
|
+
chat: {
|
|
327
|
+
completions: {
|
|
328
|
+
create: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
} as unknown as OpenAI;
|
|
332
|
+
|
|
333
|
+
const logger = createMockLogger();
|
|
334
|
+
const handler = new OpenAIStreamHandler(mockClient, undefined, logger);
|
|
335
|
+
|
|
336
|
+
const request: IOpenAIChatRequestParams = {
|
|
337
|
+
model: 'gpt-4',
|
|
338
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
await expect(async () => {
|
|
342
|
+
for await (const _msg of handler.generateStreamingResponse(request)) {
|
|
343
|
+
// consume
|
|
344
|
+
}
|
|
345
|
+
}).rejects.toThrow();
|
|
346
|
+
expect(logger.error).toHaveBeenCalled();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should handle empty messages array', async () => {
|
|
350
|
+
const chunks = [createStreamChunk('Hi', 'stop')];
|
|
351
|
+
mockClient = createMockClient(chunks);
|
|
352
|
+
const handler = new OpenAIStreamHandler(mockClient);
|
|
353
|
+
|
|
354
|
+
const request: IOpenAIChatRequestParams = {
|
|
355
|
+
model: 'gpt-4',
|
|
356
|
+
messages: [],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const results = [];
|
|
360
|
+
for await (const msg of handler.generateStreamingResponse(request)) {
|
|
361
|
+
results.push(msg);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
expect(results).toHaveLength(1);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
3
|
+
import type { IPayloadLogger } from '../interfaces/payload-logger';
|
|
4
|
+
import type { IOpenAIChatRequestParams, IOpenAIStreamRequestParams } from '../types/api-types';
|
|
5
|
+
import { SilentLogger, type ILogger } from '@robota-sdk/agent-core';
|
|
6
|
+
import { OpenAIResponseParser } from '../parsers/response-parser';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenAI streaming response handler
|
|
10
|
+
*
|
|
11
|
+
* Handles streaming chat completions from OpenAI API.
|
|
12
|
+
* Extracts streaming logic from the main provider for better modularity.
|
|
13
|
+
*/
|
|
14
|
+
export class OpenAIStreamHandler {
|
|
15
|
+
private readonly logger: ILogger;
|
|
16
|
+
private readonly parser: OpenAIResponseParser;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly client: OpenAI,
|
|
20
|
+
private readonly payloadLogger?: IPayloadLogger,
|
|
21
|
+
logger?: ILogger,
|
|
22
|
+
) {
|
|
23
|
+
this.logger = logger || SilentLogger;
|
|
24
|
+
this.parser = new OpenAIResponseParser(logger);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handle streaming response for OpenAI chat completions
|
|
29
|
+
*
|
|
30
|
+
* @param requestParams - OpenAI API request parameters
|
|
31
|
+
* @returns AsyncGenerator yielding universal messages
|
|
32
|
+
*/
|
|
33
|
+
async *handleStream(
|
|
34
|
+
requestParams: IOpenAIStreamRequestParams,
|
|
35
|
+
): AsyncGenerator<TUniversalMessage, void, undefined> {
|
|
36
|
+
try {
|
|
37
|
+
// Log payload for debugging if logger is available
|
|
38
|
+
if (this.payloadLogger?.isEnabled()) {
|
|
39
|
+
const logData = {
|
|
40
|
+
model: requestParams.model,
|
|
41
|
+
messagesCount: requestParams.messages.length,
|
|
42
|
+
hasTools: !!requestParams.tools,
|
|
43
|
+
temperature: requestParams.temperature,
|
|
44
|
+
maxTokens: requestParams.max_tokens,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
await this.payloadLogger.logPayload(logData, 'stream');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create streaming chat completion with proper type-safe parameters
|
|
51
|
+
const streamParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {
|
|
52
|
+
model: requestParams.model,
|
|
53
|
+
messages: requestParams.messages,
|
|
54
|
+
stream: true,
|
|
55
|
+
...(requestParams.temperature !== undefined && { temperature: requestParams.temperature }),
|
|
56
|
+
...(requestParams.max_tokens !== undefined && { max_tokens: requestParams.max_tokens }),
|
|
57
|
+
...(requestParams.tools && {
|
|
58
|
+
tools: requestParams.tools,
|
|
59
|
+
tool_choice: requestParams.tool_choice || 'auto',
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
const response = await this.client.chat.completions.create(streamParams);
|
|
63
|
+
|
|
64
|
+
// Process each chunk in the stream
|
|
65
|
+
for await (const chunk of response) {
|
|
66
|
+
const parsed = this.parser.parseStreamingChunk(chunk);
|
|
67
|
+
if (parsed) {
|
|
68
|
+
yield parsed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const errorMessage =
|
|
73
|
+
error instanceof Error ? error.message : 'OpenAI streaming request failed';
|
|
74
|
+
this.logger.error('Stream creation failed', { error: errorMessage });
|
|
75
|
+
throw new Error(`OpenAI streaming failed: ${errorMessage}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate streaming response using raw request payload (for agents package compatibility)
|
|
81
|
+
*
|
|
82
|
+
* @param request - Raw request payload from ConversationService
|
|
83
|
+
* @returns AsyncGenerator yielding universal messages
|
|
84
|
+
*/
|
|
85
|
+
async *generateStreamingResponse(
|
|
86
|
+
request: IOpenAIChatRequestParams,
|
|
87
|
+
): AsyncGenerator<TUniversalMessage, void, undefined> {
|
|
88
|
+
try {
|
|
89
|
+
// Extract parameters from request payload
|
|
90
|
+
const model = request.model;
|
|
91
|
+
const messages = request.messages || [];
|
|
92
|
+
const temperature = request.temperature;
|
|
93
|
+
const maxTokens = request.max_tokens;
|
|
94
|
+
const tools = request.tools;
|
|
95
|
+
|
|
96
|
+
// Build OpenAI request parameters
|
|
97
|
+
const requestParams: IOpenAIStreamRequestParams = {
|
|
98
|
+
model: model || 'gpt-4o-mini',
|
|
99
|
+
messages,
|
|
100
|
+
temperature,
|
|
101
|
+
max_tokens: maxTokens,
|
|
102
|
+
stream: true,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Add tools if provided
|
|
106
|
+
if (tools && Array.isArray(tools) && tools.length > 0) {
|
|
107
|
+
requestParams.tools = tools;
|
|
108
|
+
requestParams.tool_choice = 'auto';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Use existing stream handler
|
|
112
|
+
yield* this.handleStream(requestParams);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
115
|
+
this.logger.error('OpenAI generateStreamingResponse error:', { message: errorMessage });
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI API Request Parameters
|
|
5
|
+
* Replaces any types with specific OpenAI API structure
|
|
6
|
+
*/
|
|
7
|
+
export interface IOpenAIChatRequestParams {
|
|
8
|
+
model: string;
|
|
9
|
+
messages: OpenAI.Chat.ChatCompletionMessageParam[];
|
|
10
|
+
temperature?: number | undefined;
|
|
11
|
+
max_tokens?: number | undefined;
|
|
12
|
+
tools?: OpenAI.Chat.ChatCompletionTool[] | undefined;
|
|
13
|
+
tool_choice?: 'auto' | 'none' | OpenAI.Chat.ChatCompletionNamedToolChoice | undefined;
|
|
14
|
+
stream?: boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OpenAI API streaming request parameters
|
|
19
|
+
*/
|
|
20
|
+
export interface IOpenAIStreamRequestParams extends Omit<IOpenAIChatRequestParams, 'stream'> {
|
|
21
|
+
stream: true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* OpenAI API tool call structure
|
|
26
|
+
*/
|
|
27
|
+
export interface IOpenAIToolCall {
|
|
28
|
+
id: string;
|
|
29
|
+
type: 'function';
|
|
30
|
+
function: {
|
|
31
|
+
name: string;
|
|
32
|
+
arguments: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* OpenAI API message structure with tool calls
|
|
38
|
+
*/
|
|
39
|
+
export interface IOpenAIAssistantMessage {
|
|
40
|
+
role: 'assistant';
|
|
41
|
+
content: string | null;
|
|
42
|
+
tool_calls?: IOpenAIToolCall[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* OpenAI API tool message structure
|
|
47
|
+
*/
|
|
48
|
+
export interface IOpenAIToolMessage {
|
|
49
|
+
role: 'tool';
|
|
50
|
+
content: string;
|
|
51
|
+
tool_call_id: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* OpenAI streaming chunk delta structure
|
|
56
|
+
*/
|
|
57
|
+
export interface IOpenAIStreamDelta {
|
|
58
|
+
role?: 'assistant';
|
|
59
|
+
content?: string | null;
|
|
60
|
+
tool_calls?: Array<{
|
|
61
|
+
index: number;
|
|
62
|
+
id?: string;
|
|
63
|
+
type?: 'function';
|
|
64
|
+
function?: {
|
|
65
|
+
name?: string;
|
|
66
|
+
arguments?: string;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* OpenAI streaming chunk structure
|
|
73
|
+
*/
|
|
74
|
+
export interface IOpenAIStreamChunk {
|
|
75
|
+
id: string;
|
|
76
|
+
object: 'chat.completion.chunk';
|
|
77
|
+
created: number;
|
|
78
|
+
model: string;
|
|
79
|
+
choices: Array<{
|
|
80
|
+
index: number;
|
|
81
|
+
delta: IOpenAIStreamDelta;
|
|
82
|
+
finish_reason?: string | null;
|
|
83
|
+
}>;
|
|
84
|
+
usage?: {
|
|
85
|
+
prompt_tokens: number;
|
|
86
|
+
completion_tokens: number;
|
|
87
|
+
total_tokens: number;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* OpenAI error structure for type-safe error handling
|
|
93
|
+
*/
|
|
94
|
+
export interface IOpenAIError {
|
|
95
|
+
message: string;
|
|
96
|
+
type?: string;
|
|
97
|
+
param?: string | null;
|
|
98
|
+
code?: string | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Payload logging data structure
|
|
103
|
+
*/
|
|
104
|
+
export interface IOpenAILogData {
|
|
105
|
+
model: string;
|
|
106
|
+
messagesCount: number;
|
|
107
|
+
hasTools: boolean;
|
|
108
|
+
temperature?: number | undefined;
|
|
109
|
+
maxTokens?: number | undefined;
|
|
110
|
+
timestamp: string;
|
|
111
|
+
requestId?: string | undefined;
|
|
112
|
+
}
|