@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,494 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { OpenAIConversationAdapter } from './adapter';
|
|
3
|
+
import type {
|
|
4
|
+
TUniversalMessage,
|
|
5
|
+
IUserMessage,
|
|
6
|
+
ISystemMessage,
|
|
7
|
+
IAssistantMessage,
|
|
8
|
+
IToolMessage,
|
|
9
|
+
} from '@robota-sdk/agent-core';
|
|
10
|
+
|
|
11
|
+
describe('OpenAIConversationAdapter', () => {
|
|
12
|
+
describe('convertMessage', () => {
|
|
13
|
+
it('should convert user message correctly', () => {
|
|
14
|
+
const userMessage: IUserMessage = {
|
|
15
|
+
id: 'msg-1',
|
|
16
|
+
state: 'complete' as const,
|
|
17
|
+
role: 'user',
|
|
18
|
+
content: 'Hello, how are you?',
|
|
19
|
+
timestamp: new Date(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const result = OpenAIConversationAdapter.convertMessage(userMessage);
|
|
23
|
+
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: 'Hello, how are you?',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should convert system message correctly', () => {
|
|
31
|
+
const systemMessage: ISystemMessage = {
|
|
32
|
+
id: 'msg-1',
|
|
33
|
+
state: 'complete' as const,
|
|
34
|
+
role: 'system',
|
|
35
|
+
content: 'You are a helpful assistant.',
|
|
36
|
+
timestamp: new Date(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = OpenAIConversationAdapter.convertMessage(systemMessage);
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
role: 'system',
|
|
43
|
+
content: 'You are a helpful assistant.',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should convert regular assistant message with content correctly', () => {
|
|
48
|
+
const assistantMessage: IAssistantMessage = {
|
|
49
|
+
id: 'msg-1',
|
|
50
|
+
state: 'complete' as const,
|
|
51
|
+
role: 'assistant',
|
|
52
|
+
content: 'I can help you with that.',
|
|
53
|
+
timestamp: new Date(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
role: 'assistant',
|
|
60
|
+
content: 'I can help you with that.',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should convert assistant message with null content correctly', () => {
|
|
65
|
+
const assistantMessage: IAssistantMessage = {
|
|
66
|
+
id: 'msg-1',
|
|
67
|
+
state: 'complete' as const,
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
content: null,
|
|
70
|
+
timestamp: new Date(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
content: null,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should convert assistant message with empty content correctly', () => {
|
|
82
|
+
const assistantMessage: IAssistantMessage = {
|
|
83
|
+
id: 'msg-1',
|
|
84
|
+
state: 'complete' as const,
|
|
85
|
+
role: 'assistant',
|
|
86
|
+
content: '',
|
|
87
|
+
timestamp: new Date(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
91
|
+
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
role: 'assistant',
|
|
94
|
+
content: null, // Empty string is also converted to null
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should convert assistant message with tool calls and null content correctly', () => {
|
|
99
|
+
const assistantMessage: IAssistantMessage = {
|
|
100
|
+
id: 'msg-1',
|
|
101
|
+
state: 'complete' as const,
|
|
102
|
+
role: 'assistant',
|
|
103
|
+
content: null,
|
|
104
|
+
toolCalls: [
|
|
105
|
+
{
|
|
106
|
+
id: 'call_123',
|
|
107
|
+
type: 'function',
|
|
108
|
+
function: {
|
|
109
|
+
name: 'calculate',
|
|
110
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
118
|
+
|
|
119
|
+
expect(result).toEqual({
|
|
120
|
+
role: 'assistant',
|
|
121
|
+
content: null,
|
|
122
|
+
tool_calls: [
|
|
123
|
+
{
|
|
124
|
+
id: 'call_123',
|
|
125
|
+
type: 'function',
|
|
126
|
+
function: {
|
|
127
|
+
name: 'calculate',
|
|
128
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should convert assistant message with tool calls and empty content correctly', () => {
|
|
136
|
+
const assistantMessage: IAssistantMessage = {
|
|
137
|
+
id: 'msg-1',
|
|
138
|
+
state: 'complete' as const,
|
|
139
|
+
role: 'assistant',
|
|
140
|
+
content: '',
|
|
141
|
+
toolCalls: [
|
|
142
|
+
{
|
|
143
|
+
id: 'call_456',
|
|
144
|
+
type: 'function',
|
|
145
|
+
function: {
|
|
146
|
+
name: 'getWeather',
|
|
147
|
+
arguments: '{"city":"Seoul"}',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
timestamp: new Date(),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
155
|
+
|
|
156
|
+
expect(result).toEqual({
|
|
157
|
+
role: 'assistant',
|
|
158
|
+
content: null,
|
|
159
|
+
tool_calls: [
|
|
160
|
+
{
|
|
161
|
+
id: 'call_456',
|
|
162
|
+
type: 'function',
|
|
163
|
+
function: {
|
|
164
|
+
name: 'getWeather',
|
|
165
|
+
arguments: '{"city":"Seoul"}',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should convert assistant message with tool calls and meaningful content correctly', () => {
|
|
173
|
+
const assistantMessage: IAssistantMessage = {
|
|
174
|
+
id: 'msg-1',
|
|
175
|
+
state: 'complete' as const,
|
|
176
|
+
role: 'assistant',
|
|
177
|
+
content: 'Let me calculate that for you.',
|
|
178
|
+
toolCalls: [
|
|
179
|
+
{
|
|
180
|
+
id: 'call_789',
|
|
181
|
+
type: 'function',
|
|
182
|
+
function: {
|
|
183
|
+
name: 'calculate',
|
|
184
|
+
arguments: '{"operation":"multiply","a":7,"b":8}',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
timestamp: new Date(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const result = OpenAIConversationAdapter.convertMessage(assistantMessage);
|
|
192
|
+
|
|
193
|
+
expect(result).toEqual({
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
content: 'Let me calculate that for you.',
|
|
196
|
+
tool_calls: [
|
|
197
|
+
{
|
|
198
|
+
id: 'call_789',
|
|
199
|
+
type: 'function',
|
|
200
|
+
function: {
|
|
201
|
+
name: 'calculate',
|
|
202
|
+
arguments: '{"operation":"multiply","a":7,"b":8}',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should convert tool message correctly', () => {
|
|
210
|
+
const toolMessage: IToolMessage = {
|
|
211
|
+
id: 'msg-1',
|
|
212
|
+
state: 'complete' as const,
|
|
213
|
+
role: 'tool',
|
|
214
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
215
|
+
toolCallId: 'call_123',
|
|
216
|
+
name: 'calculate',
|
|
217
|
+
timestamp: new Date(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = OpenAIConversationAdapter.convertMessage(toolMessage);
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual({
|
|
223
|
+
role: 'tool',
|
|
224
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
225
|
+
tool_call_id: 'call_123',
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should throw error for tool message without toolCallId', () => {
|
|
230
|
+
const toolMessage: IToolMessage = {
|
|
231
|
+
id: 'msg-1',
|
|
232
|
+
state: 'complete' as const,
|
|
233
|
+
role: 'tool',
|
|
234
|
+
content: '{"result":8}',
|
|
235
|
+
toolCallId: '',
|
|
236
|
+
timestamp: new Date(),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(() => {
|
|
240
|
+
OpenAIConversationAdapter.convertMessage(toolMessage);
|
|
241
|
+
}).toThrow('Tool message missing toolCallId');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('toOpenAIFormat', () => {
|
|
246
|
+
it('should convert a complete conversation with tool calls correctly', () => {
|
|
247
|
+
const messages: TUniversalMessage[] = [
|
|
248
|
+
{
|
|
249
|
+
id: 'msg-1',
|
|
250
|
+
state: 'complete' as const,
|
|
251
|
+
role: 'system',
|
|
252
|
+
content: 'You are a helpful assistant.',
|
|
253
|
+
timestamp: new Date(),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'msg-2',
|
|
257
|
+
state: 'complete' as const,
|
|
258
|
+
role: 'user',
|
|
259
|
+
content: 'What is 5 plus 3?',
|
|
260
|
+
timestamp: new Date(),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 'msg-3',
|
|
264
|
+
state: 'complete' as const,
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
content: null,
|
|
267
|
+
toolCalls: [
|
|
268
|
+
{
|
|
269
|
+
id: 'call_123',
|
|
270
|
+
type: 'function',
|
|
271
|
+
function: {
|
|
272
|
+
name: 'calculate',
|
|
273
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
timestamp: new Date(),
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'msg-4',
|
|
281
|
+
state: 'complete' as const,
|
|
282
|
+
role: 'tool',
|
|
283
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
284
|
+
toolCallId: 'call_123',
|
|
285
|
+
name: 'calculate',
|
|
286
|
+
timestamp: new Date(),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'msg-5',
|
|
290
|
+
state: 'complete' as const,
|
|
291
|
+
role: 'assistant',
|
|
292
|
+
content: 'The result of 5 plus 3 is 8.',
|
|
293
|
+
timestamp: new Date(),
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
|
|
298
|
+
|
|
299
|
+
expect(result).toEqual([
|
|
300
|
+
{
|
|
301
|
+
role: 'system',
|
|
302
|
+
content: 'You are a helpful assistant.',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
role: 'user',
|
|
306
|
+
content: 'What is 5 plus 3?',
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
role: 'assistant',
|
|
310
|
+
content: null,
|
|
311
|
+
tool_calls: [
|
|
312
|
+
{
|
|
313
|
+
id: 'call_123',
|
|
314
|
+
type: 'function',
|
|
315
|
+
function: {
|
|
316
|
+
name: 'calculate',
|
|
317
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
role: 'tool',
|
|
324
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
325
|
+
tool_call_id: 'call_123',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
role: 'assistant',
|
|
329
|
+
content: 'The result of 5 plus 3 is 8.',
|
|
330
|
+
},
|
|
331
|
+
]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should filter out tool messages with invalid toolCallId', () => {
|
|
335
|
+
const messages: TUniversalMessage[] = [
|
|
336
|
+
{
|
|
337
|
+
id: 'msg-1',
|
|
338
|
+
state: 'complete' as const,
|
|
339
|
+
role: 'user',
|
|
340
|
+
content: 'Hello',
|
|
341
|
+
timestamp: new Date(),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: 'msg-2',
|
|
345
|
+
state: 'complete' as const,
|
|
346
|
+
role: 'tool',
|
|
347
|
+
content: 'Invalid tool message',
|
|
348
|
+
toolCallId: '',
|
|
349
|
+
timestamp: new Date(),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'msg-3',
|
|
353
|
+
state: 'complete' as const,
|
|
354
|
+
role: 'tool',
|
|
355
|
+
content: 'Another invalid tool message',
|
|
356
|
+
toolCallId: 'unknown',
|
|
357
|
+
timestamp: new Date(),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'msg-4',
|
|
361
|
+
state: 'complete' as const,
|
|
362
|
+
role: 'assistant',
|
|
363
|
+
content: 'Hi there!',
|
|
364
|
+
timestamp: new Date(),
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
|
|
369
|
+
|
|
370
|
+
expect(result).toEqual([
|
|
371
|
+
{
|
|
372
|
+
role: 'user',
|
|
373
|
+
content: 'Hello',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
content: 'Hi there!',
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle the tool execution loop prevention scenario correctly', () => {
|
|
383
|
+
const messages: TUniversalMessage[] = [
|
|
384
|
+
{
|
|
385
|
+
id: 'msg-1',
|
|
386
|
+
state: 'complete' as const,
|
|
387
|
+
role: 'system',
|
|
388
|
+
content: 'You are a helpful assistant.',
|
|
389
|
+
timestamp: new Date(),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'msg-2',
|
|
393
|
+
state: 'complete' as const,
|
|
394
|
+
role: 'user',
|
|
395
|
+
content: 'What is 5 plus 3?',
|
|
396
|
+
timestamp: new Date(),
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: 'msg-3',
|
|
400
|
+
state: 'complete' as const,
|
|
401
|
+
role: 'assistant',
|
|
402
|
+
content: null,
|
|
403
|
+
toolCalls: [
|
|
404
|
+
{
|
|
405
|
+
id: 'call_calculate_123',
|
|
406
|
+
type: 'function',
|
|
407
|
+
function: {
|
|
408
|
+
name: 'calculate',
|
|
409
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
timestamp: new Date(),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: 'msg-4',
|
|
417
|
+
state: 'complete' as const,
|
|
418
|
+
role: 'tool',
|
|
419
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
420
|
+
toolCallId: 'call_calculate_123',
|
|
421
|
+
name: 'calculate',
|
|
422
|
+
timestamp: new Date(),
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
const result = OpenAIConversationAdapter.toOpenAIFormat(messages);
|
|
427
|
+
|
|
428
|
+
expect(result[2]).toEqual({
|
|
429
|
+
role: 'assistant',
|
|
430
|
+
content: null,
|
|
431
|
+
tool_calls: [
|
|
432
|
+
{
|
|
433
|
+
id: 'call_calculate_123',
|
|
434
|
+
type: 'function',
|
|
435
|
+
function: {
|
|
436
|
+
name: 'calculate',
|
|
437
|
+
arguments: '{"operation":"add","a":5,"b":3}',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(result[3]).toEqual({
|
|
444
|
+
role: 'tool',
|
|
445
|
+
content: '{"result":8,"operation":"5 + 3 = 8"}',
|
|
446
|
+
tool_call_id: 'call_calculate_123',
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should demonstrate the difference between correct and incorrect content handling', () => {
|
|
451
|
+
const correctAssistantMessage: IAssistantMessage = {
|
|
452
|
+
id: 'msg-1',
|
|
453
|
+
state: 'complete' as const,
|
|
454
|
+
role: 'assistant',
|
|
455
|
+
content: null,
|
|
456
|
+
toolCalls: [
|
|
457
|
+
{
|
|
458
|
+
id: 'call_123',
|
|
459
|
+
type: 'function',
|
|
460
|
+
function: {
|
|
461
|
+
name: 'calculate',
|
|
462
|
+
arguments: '{"a":5,"b":3}',
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
timestamp: new Date(),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const incorrectAssistantMessage: IAssistantMessage = {
|
|
470
|
+
id: 'msg-2',
|
|
471
|
+
state: 'complete' as const,
|
|
472
|
+
role: 'assistant',
|
|
473
|
+
content: '',
|
|
474
|
+
toolCalls: [
|
|
475
|
+
{
|
|
476
|
+
id: 'call_123',
|
|
477
|
+
type: 'function',
|
|
478
|
+
function: {
|
|
479
|
+
name: 'calculate',
|
|
480
|
+
arguments: '{"a":5,"b":3}',
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
timestamp: new Date(),
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const correctResult = OpenAIConversationAdapter.convertMessage(correctAssistantMessage);
|
|
488
|
+
const incorrectResult = OpenAIConversationAdapter.convertMessage(incorrectAssistantMessage);
|
|
489
|
+
|
|
490
|
+
expect(correctResult.content).toBe(null);
|
|
491
|
+
expect(incorrectResult.content).toBe(null);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { TUniversalMessage, IAssistantMessage } from '@robota-sdk/agent-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenAI Conversation Adapter
|
|
6
|
+
*
|
|
7
|
+
* Converts between TUniversalMessage format and OpenAI native types.
|
|
8
|
+
* Provides bidirectional conversion for seamless integration.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export class OpenAIConversationAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Filter messages for OpenAI compatibility
|
|
15
|
+
*
|
|
16
|
+
* OpenAI has specific requirements:
|
|
17
|
+
* - Tool messages must have valid toolCallId
|
|
18
|
+
* - Messages must be in proper sequence
|
|
19
|
+
* - Tool messages without toolCallId should be excluded
|
|
20
|
+
*/
|
|
21
|
+
static filterMessagesForOpenAI(messages: TUniversalMessage[]): TUniversalMessage[] {
|
|
22
|
+
return messages.filter((msg) => {
|
|
23
|
+
// Always include user, assistant, and system messages
|
|
24
|
+
if (msg.role === 'user' || msg.role === 'assistant' || msg.role === 'system') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// For tool messages, only include if they have a valid toolCallId
|
|
29
|
+
if (msg.role === 'tool') {
|
|
30
|
+
// Must have toolCallId and it must not be empty or 'unknown'
|
|
31
|
+
return !!(msg.toolCallId && msg.toolCallId.trim() !== '' && msg.toolCallId !== 'unknown');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert TUniversalMessage array to OpenAI message format
|
|
40
|
+
* Now properly handles tool messages for OpenAI's tool calling feature
|
|
41
|
+
*/
|
|
42
|
+
static toOpenAIFormat(messages: TUniversalMessage[]): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
43
|
+
// First filter messages for OpenAI compatibility
|
|
44
|
+
const filteredMessages = this.filterMessagesForOpenAI(messages);
|
|
45
|
+
return filteredMessages.map((msg) => this.convertMessage(msg));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a single TUniversalMessage to OpenAI format
|
|
50
|
+
* Handles all message types including tool messages
|
|
51
|
+
*/
|
|
52
|
+
static convertMessage(msg: TUniversalMessage): OpenAI.Chat.ChatCompletionMessageParam {
|
|
53
|
+
const messageRole = msg.role;
|
|
54
|
+
|
|
55
|
+
if (messageRole === 'user') {
|
|
56
|
+
return {
|
|
57
|
+
role: 'user',
|
|
58
|
+
content: msg.content,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (messageRole === 'assistant') {
|
|
63
|
+
const assistantMsg = msg as IAssistantMessage;
|
|
64
|
+
|
|
65
|
+
// Handle tool_calls format
|
|
66
|
+
if (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0) {
|
|
67
|
+
const result: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
|
|
68
|
+
role: 'assistant',
|
|
69
|
+
// CRITICAL: OpenAI API requires content to be null (not empty string) when tool_calls are present
|
|
70
|
+
// VERIFIED: 2024-12 - This prevents "400 Bad Request" errors from OpenAI API
|
|
71
|
+
// DO NOT CHANGE without testing against actual OpenAI API
|
|
72
|
+
content: assistantMsg.content === '' ? null : assistantMsg.content || null,
|
|
73
|
+
tool_calls: assistantMsg.toolCalls.map((toolCall) => ({
|
|
74
|
+
id: toolCall.id,
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: {
|
|
77
|
+
name: toolCall.function.name,
|
|
78
|
+
arguments: toolCall.function.arguments,
|
|
79
|
+
},
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Regular assistant message (without tool calls)
|
|
86
|
+
// VERIFIED: OpenAI accepts both null and string content for regular messages
|
|
87
|
+
// We preserve null when content is null or empty string for API consistency
|
|
88
|
+
return {
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
content:
|
|
91
|
+
assistantMsg.content === null
|
|
92
|
+
? null
|
|
93
|
+
: assistantMsg.content === ''
|
|
94
|
+
? null
|
|
95
|
+
: assistantMsg.content || '',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (messageRole === 'system') {
|
|
100
|
+
return {
|
|
101
|
+
role: 'system',
|
|
102
|
+
content: msg.content,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle tool messages for OpenAI tool calling
|
|
107
|
+
if (messageRole === 'tool') {
|
|
108
|
+
if (!msg.toolCallId || msg.toolCallId.trim() === '') {
|
|
109
|
+
throw new Error(`Tool message missing toolCallId: ${JSON.stringify(msg)}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result: OpenAI.Chat.ChatCompletionToolMessageParam = {
|
|
113
|
+
role: 'tool',
|
|
114
|
+
content: msg.content,
|
|
115
|
+
tool_call_id: msg.toolCallId,
|
|
116
|
+
};
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const exhaustive: never = messageRole;
|
|
121
|
+
throw new Error(`Unsupported message role: ${exhaustive}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Add system prompt to message array if needed
|
|
126
|
+
*/
|
|
127
|
+
static addSystemPromptIfNeeded(
|
|
128
|
+
messages: OpenAI.Chat.ChatCompletionMessageParam[],
|
|
129
|
+
systemPrompt?: string,
|
|
130
|
+
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
131
|
+
if (!systemPrompt) {
|
|
132
|
+
return messages;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if system message already exists
|
|
136
|
+
const hasSystemMessage = messages.some((msg) => msg.role === 'system');
|
|
137
|
+
|
|
138
|
+
if (hasSystemMessage) {
|
|
139
|
+
return messages;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add system prompt at the beginning
|
|
143
|
+
return [{ role: 'system', content: systemPrompt }, ...messages];
|
|
144
|
+
}
|
|
145
|
+
}
|