@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,616 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { TUniversalMessage, IAssistantMessage, IToolSchema } from '@robota-sdk/agent-core';
|
|
3
|
+
import {
|
|
4
|
+
mapMessagePartsToGeminiParts,
|
|
5
|
+
convertToGeminiFormat,
|
|
6
|
+
convertToGeminiRequestFormat,
|
|
7
|
+
convertFromGeminiResponse,
|
|
8
|
+
convertToolsToGeminiFormat,
|
|
9
|
+
generateCallId,
|
|
10
|
+
} from './message-converter';
|
|
11
|
+
|
|
12
|
+
describe('mapMessagePartsToGeminiParts', () => {
|
|
13
|
+
it('converts text parts to Gemini text parts', () => {
|
|
14
|
+
const message: TUniversalMessage = {
|
|
15
|
+
id: 'msg-1',
|
|
16
|
+
state: 'complete' as const,
|
|
17
|
+
role: 'user',
|
|
18
|
+
content: '',
|
|
19
|
+
parts: [{ type: 'text', text: 'hello' }],
|
|
20
|
+
timestamp: new Date(),
|
|
21
|
+
};
|
|
22
|
+
const result = mapMessagePartsToGeminiParts(message);
|
|
23
|
+
expect(result).toEqual([{ text: 'hello' }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('converts inline image parts to Gemini inlineData parts', () => {
|
|
27
|
+
const message: TUniversalMessage = {
|
|
28
|
+
id: 'msg-1',
|
|
29
|
+
state: 'complete' as const,
|
|
30
|
+
role: 'user',
|
|
31
|
+
content: '',
|
|
32
|
+
parts: [{ type: 'image_inline', mimeType: 'image/png', data: 'abc123' }],
|
|
33
|
+
timestamp: new Date(),
|
|
34
|
+
};
|
|
35
|
+
const result = mapMessagePartsToGeminiParts(message);
|
|
36
|
+
expect(result).toEqual([{ inlineData: { mimeType: 'image/png', data: 'abc123' } }]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws on image_uri parts', () => {
|
|
40
|
+
const message: TUniversalMessage = {
|
|
41
|
+
id: 'msg-1',
|
|
42
|
+
state: 'complete' as const,
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: '',
|
|
45
|
+
parts: [{ type: 'image_uri', uri: 'https://example.com/img.png', mimeType: 'image/png' }],
|
|
46
|
+
timestamp: new Date(),
|
|
47
|
+
};
|
|
48
|
+
expect(() => mapMessagePartsToGeminiParts(message)).toThrow(
|
|
49
|
+
'Google provider does not support image URI parts directly: https://example.com/img.png',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to content when no parts are present', () => {
|
|
54
|
+
const message: TUniversalMessage = {
|
|
55
|
+
id: 'msg-1',
|
|
56
|
+
state: 'complete' as const,
|
|
57
|
+
role: 'user',
|
|
58
|
+
content: 'plain text',
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
};
|
|
61
|
+
const result = mapMessagePartsToGeminiParts(message);
|
|
62
|
+
expect(result).toEqual([{ text: 'plain text' }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns empty array when no parts and empty content', () => {
|
|
66
|
+
const message: TUniversalMessage = {
|
|
67
|
+
id: 'msg-1',
|
|
68
|
+
state: 'complete' as const,
|
|
69
|
+
role: 'user',
|
|
70
|
+
content: '',
|
|
71
|
+
timestamp: new Date(),
|
|
72
|
+
};
|
|
73
|
+
const result = mapMessagePartsToGeminiParts(message);
|
|
74
|
+
expect(result).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('converts mixed text and image parts', () => {
|
|
78
|
+
const message: TUniversalMessage = {
|
|
79
|
+
id: 'msg-1',
|
|
80
|
+
state: 'complete' as const,
|
|
81
|
+
role: 'user',
|
|
82
|
+
content: '',
|
|
83
|
+
parts: [
|
|
84
|
+
{ type: 'text', text: 'describe this' },
|
|
85
|
+
{ type: 'image_inline', mimeType: 'image/jpeg', data: 'base64data' },
|
|
86
|
+
],
|
|
87
|
+
timestamp: new Date(),
|
|
88
|
+
};
|
|
89
|
+
const result = mapMessagePartsToGeminiParts(message);
|
|
90
|
+
expect(result).toHaveLength(2);
|
|
91
|
+
expect(result[0]).toEqual({ text: 'describe this' });
|
|
92
|
+
expect(result[1]).toEqual({ inlineData: { mimeType: 'image/jpeg', data: 'base64data' } });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('convertToGeminiFormat', () => {
|
|
97
|
+
it('converts user messages with role "user"', () => {
|
|
98
|
+
const messages: TUniversalMessage[] = [
|
|
99
|
+
{
|
|
100
|
+
id: 'msg-1',
|
|
101
|
+
state: 'complete' as const,
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: 'hello',
|
|
104
|
+
timestamp: new Date(),
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const result = convertToGeminiFormat(messages);
|
|
108
|
+
expect(result).toHaveLength(1);
|
|
109
|
+
expect(result[0]?.role).toBe('user');
|
|
110
|
+
expect(result[0]?.parts).toEqual([{ text: 'hello' }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('converts assistant messages with role "model"', () => {
|
|
114
|
+
const messages: TUniversalMessage[] = [
|
|
115
|
+
{
|
|
116
|
+
id: 'msg-1',
|
|
117
|
+
state: 'complete' as const,
|
|
118
|
+
role: 'assistant',
|
|
119
|
+
content: 'hi there',
|
|
120
|
+
timestamp: new Date(),
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
const result = convertToGeminiFormat(messages);
|
|
124
|
+
expect(result).toHaveLength(1);
|
|
125
|
+
expect(result[0]?.role).toBe('model');
|
|
126
|
+
expect(result[0]?.parts).toEqual([{ text: 'hi there' }]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('converts assistant messages with tool calls to function call parts', () => {
|
|
130
|
+
const messages: TUniversalMessage[] = [
|
|
131
|
+
{
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
content: 'Let me check',
|
|
134
|
+
toolCalls: [
|
|
135
|
+
{
|
|
136
|
+
id: 'call_1',
|
|
137
|
+
type: 'function' as const,
|
|
138
|
+
function: {
|
|
139
|
+
name: 'get_weather',
|
|
140
|
+
arguments: '{"city":"Seoul"}',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
timestamp: new Date(),
|
|
145
|
+
} as IAssistantMessage,
|
|
146
|
+
];
|
|
147
|
+
const result = convertToGeminiFormat(messages);
|
|
148
|
+
expect(result[0]?.role).toBe('model');
|
|
149
|
+
expect(result[0]?.parts).toHaveLength(2);
|
|
150
|
+
expect(result[0]?.parts?.[0]).toEqual({ text: 'Let me check' });
|
|
151
|
+
expect(result[0]?.parts?.[1]).toEqual({
|
|
152
|
+
functionCall: { id: 'call_1', name: 'get_weather', args: { city: 'Seoul' } },
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('converts assistant messages with only tool calls and no content', () => {
|
|
157
|
+
const messages: TUniversalMessage[] = [
|
|
158
|
+
{
|
|
159
|
+
role: 'assistant',
|
|
160
|
+
content: null,
|
|
161
|
+
toolCalls: [
|
|
162
|
+
{
|
|
163
|
+
id: 'call_2',
|
|
164
|
+
type: 'function' as const,
|
|
165
|
+
function: {
|
|
166
|
+
name: 'search',
|
|
167
|
+
arguments: '{"query":"test"}',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
timestamp: new Date(),
|
|
172
|
+
} as IAssistantMessage,
|
|
173
|
+
];
|
|
174
|
+
const result = convertToGeminiFormat(messages);
|
|
175
|
+
expect(result[0]?.role).toBe('model');
|
|
176
|
+
// Should have only the function call part since content is null
|
|
177
|
+
expect(result[0]?.parts).toHaveLength(1);
|
|
178
|
+
expect(result[0]?.parts?.[0]).toHaveProperty('functionCall');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('converts tool messages to Gemini functionResponse parts', () => {
|
|
182
|
+
const messages: TUniversalMessage[] = [
|
|
183
|
+
{
|
|
184
|
+
id: 'msg-1',
|
|
185
|
+
state: 'complete' as const,
|
|
186
|
+
role: 'tool',
|
|
187
|
+
content: '{"temperature":20}',
|
|
188
|
+
toolCallId: 'call_1',
|
|
189
|
+
name: 'get_weather',
|
|
190
|
+
timestamp: new Date(),
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
const result = convertToGeminiFormat(messages);
|
|
194
|
+
expect(result[0]?.role).toBe('user');
|
|
195
|
+
expect(result[0]?.parts).toEqual([
|
|
196
|
+
{
|
|
197
|
+
functionResponse: {
|
|
198
|
+
id: 'call_1',
|
|
199
|
+
name: 'get_weather',
|
|
200
|
+
response: { temperature: 20 },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('excludes system messages from contents-only conversion', () => {
|
|
207
|
+
const messages: TUniversalMessage[] = [
|
|
208
|
+
{
|
|
209
|
+
id: 'msg-1',
|
|
210
|
+
state: 'complete' as const,
|
|
211
|
+
role: 'system',
|
|
212
|
+
content: 'You are helpful',
|
|
213
|
+
timestamp: new Date(),
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
const result = convertToGeminiFormat(messages);
|
|
217
|
+
expect(result).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('maps system messages to request-level systemInstruction', () => {
|
|
221
|
+
const messages: TUniversalMessage[] = [
|
|
222
|
+
{
|
|
223
|
+
id: 'msg-1',
|
|
224
|
+
state: 'complete' as const,
|
|
225
|
+
role: 'system',
|
|
226
|
+
content: 'You are helpful',
|
|
227
|
+
timestamp: new Date(),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'msg-2',
|
|
231
|
+
state: 'complete' as const,
|
|
232
|
+
role: 'user',
|
|
233
|
+
content: 'hello',
|
|
234
|
+
timestamp: new Date(),
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
const result = convertToGeminiRequestFormat(messages);
|
|
238
|
+
expect(result.systemInstruction).toBe('You are helpful');
|
|
239
|
+
expect(result.contents).toEqual([{ role: 'user', parts: [{ text: 'hello' }] }]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('combines system message text parts into systemInstruction', () => {
|
|
243
|
+
const messages: TUniversalMessage[] = [
|
|
244
|
+
{
|
|
245
|
+
id: 'msg-1',
|
|
246
|
+
state: 'complete' as const,
|
|
247
|
+
role: 'system',
|
|
248
|
+
content: '',
|
|
249
|
+
parts: [{ type: 'text', text: 'System instruction' }],
|
|
250
|
+
timestamp: new Date(),
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
const result = convertToGeminiRequestFormat(messages);
|
|
254
|
+
expect(result.systemInstruction).toBe('System instruction');
|
|
255
|
+
expect(result.contents).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('handles multiple messages in sequence', () => {
|
|
259
|
+
const messages: TUniversalMessage[] = [
|
|
260
|
+
{
|
|
261
|
+
id: 'msg-1',
|
|
262
|
+
state: 'complete' as const,
|
|
263
|
+
role: 'system',
|
|
264
|
+
content: 'Be helpful',
|
|
265
|
+
timestamp: new Date(),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'msg-2',
|
|
269
|
+
state: 'complete' as const,
|
|
270
|
+
role: 'user',
|
|
271
|
+
content: 'Hello',
|
|
272
|
+
timestamp: new Date(),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'msg-3',
|
|
276
|
+
state: 'complete' as const,
|
|
277
|
+
role: 'assistant',
|
|
278
|
+
content: 'Hi!',
|
|
279
|
+
timestamp: new Date(),
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
const result = convertToGeminiFormat(messages);
|
|
283
|
+
expect(result).toHaveLength(2);
|
|
284
|
+
expect(result[0]?.role).toBe('user');
|
|
285
|
+
expect(result[1]?.role).toBe('model');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('throws when tool message is missing a function name', () => {
|
|
289
|
+
const messages: TUniversalMessage[] = [
|
|
290
|
+
{
|
|
291
|
+
id: 'msg-1',
|
|
292
|
+
state: 'complete' as const,
|
|
293
|
+
role: 'tool',
|
|
294
|
+
content: 'tool result',
|
|
295
|
+
toolCallId: 'call_1',
|
|
296
|
+
timestamp: new Date(),
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
expect(() => convertToGeminiFormat(messages)).toThrow(
|
|
300
|
+
'Google provider tool message requires a function name.',
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('convertFromGeminiResponse', () => {
|
|
306
|
+
it('extracts text from response candidates', () => {
|
|
307
|
+
const response = {
|
|
308
|
+
candidates: [
|
|
309
|
+
{
|
|
310
|
+
content: {
|
|
311
|
+
parts: [{ text: 'Hello world' }],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
const result = convertFromGeminiResponse(response as never);
|
|
317
|
+
expect(result.role).toBe('assistant');
|
|
318
|
+
expect(result.content).toBe('Hello world');
|
|
319
|
+
expect(result.parts).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('throws when no candidates in response', () => {
|
|
323
|
+
const response = { candidates: [] };
|
|
324
|
+
expect(() => convertFromGeminiResponse(response as never)).toThrow(
|
|
325
|
+
'No candidate in Gemini response',
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('throws when candidates is undefined', () => {
|
|
330
|
+
const response = {};
|
|
331
|
+
expect(() => convertFromGeminiResponse(response as never)).toThrow(
|
|
332
|
+
'No candidate in Gemini response',
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('throws when no content in response', () => {
|
|
337
|
+
const response = {
|
|
338
|
+
candidates: [{ content: { parts: [] } }],
|
|
339
|
+
};
|
|
340
|
+
expect(() => convertFromGeminiResponse(response as never)).toThrow(
|
|
341
|
+
'No content in Gemini response',
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('throws when content is undefined', () => {
|
|
346
|
+
const response = {
|
|
347
|
+
candidates: [{}],
|
|
348
|
+
};
|
|
349
|
+
expect(() => convertFromGeminiResponse(response as never)).toThrow(
|
|
350
|
+
'No content in Gemini response',
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('extracts function calls from response', () => {
|
|
355
|
+
const response = {
|
|
356
|
+
candidates: [
|
|
357
|
+
{
|
|
358
|
+
content: {
|
|
359
|
+
parts: [
|
|
360
|
+
{
|
|
361
|
+
functionCall: {
|
|
362
|
+
id: 'gemini-call-1',
|
|
363
|
+
name: 'get_weather',
|
|
364
|
+
args: { city: 'Seoul' },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
const result = convertFromGeminiResponse(response as never) as IAssistantMessage;
|
|
373
|
+
expect(result.toolCalls).toBeDefined();
|
|
374
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
375
|
+
expect(result.toolCalls![0]?.function.name).toBe('get_weather');
|
|
376
|
+
expect(result.toolCalls![0]?.function.arguments).toBe('{"city":"Seoul"}');
|
|
377
|
+
expect(result.toolCalls![0]?.type).toBe('function');
|
|
378
|
+
expect(result.toolCalls![0]?.id).toBe('gemini-call-1');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('generates a fallback function call id when Gemini response omits one', () => {
|
|
382
|
+
const response = {
|
|
383
|
+
candidates: [
|
|
384
|
+
{
|
|
385
|
+
content: {
|
|
386
|
+
parts: [
|
|
387
|
+
{
|
|
388
|
+
functionCall: {
|
|
389
|
+
name: 'get_weather',
|
|
390
|
+
args: { city: 'Seoul' },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
const result = convertFromGeminiResponse(response as never) as IAssistantMessage;
|
|
399
|
+
expect(result.toolCalls?.[0]?.id).toMatch(/^call_/);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('handles mixed text and function call parts', () => {
|
|
403
|
+
const response = {
|
|
404
|
+
candidates: [
|
|
405
|
+
{
|
|
406
|
+
content: {
|
|
407
|
+
parts: [
|
|
408
|
+
{ text: 'Let me check the weather' },
|
|
409
|
+
{
|
|
410
|
+
functionCall: {
|
|
411
|
+
name: 'get_weather',
|
|
412
|
+
args: { city: 'Tokyo' },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
const result = convertFromGeminiResponse(response as never) as IAssistantMessage;
|
|
421
|
+
expect(result.content).toBe('Let me check the weather');
|
|
422
|
+
expect(result.parts).toHaveLength(1); // only text parts in messageParts
|
|
423
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('handles inline image parts in response', () => {
|
|
427
|
+
const response = {
|
|
428
|
+
candidates: [
|
|
429
|
+
{
|
|
430
|
+
content: {
|
|
431
|
+
parts: [
|
|
432
|
+
{ text: 'Here is the image' },
|
|
433
|
+
{ inlineData: { mimeType: 'image/png', data: 'base64data' } },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
const result = convertFromGeminiResponse(response as never);
|
|
440
|
+
expect(result.parts).toHaveLength(2);
|
|
441
|
+
expect(result.parts![0]).toEqual({ type: 'text', text: 'Here is the image' });
|
|
442
|
+
expect(result.parts![1]).toEqual({
|
|
443
|
+
type: 'image_inline',
|
|
444
|
+
data: 'base64data',
|
|
445
|
+
mimeType: 'image/png',
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('includes usage metadata when present', () => {
|
|
450
|
+
const response = {
|
|
451
|
+
candidates: [
|
|
452
|
+
{
|
|
453
|
+
content: {
|
|
454
|
+
parts: [{ text: 'response' }],
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
usageMetadata: {
|
|
459
|
+
promptTokenCount: 10,
|
|
460
|
+
candidatesTokenCount: 20,
|
|
461
|
+
totalTokenCount: 30,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
const result = convertFromGeminiResponse(response as never);
|
|
465
|
+
expect(result.metadata).toEqual({
|
|
466
|
+
promptTokens: 10,
|
|
467
|
+
completionTokens: 20,
|
|
468
|
+
totalTokens: 30,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('does not include metadata when usageMetadata is absent', () => {
|
|
473
|
+
const response = {
|
|
474
|
+
candidates: [
|
|
475
|
+
{
|
|
476
|
+
content: {
|
|
477
|
+
parts: [{ text: 'response' }],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
const result = convertFromGeminiResponse(response as never);
|
|
483
|
+
expect(result.metadata).toBeUndefined();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('joins multiple text parts into one content string', () => {
|
|
487
|
+
const response = {
|
|
488
|
+
candidates: [
|
|
489
|
+
{
|
|
490
|
+
content: {
|
|
491
|
+
parts: [{ text: 'Hello ' }, { text: 'world' }],
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
};
|
|
496
|
+
const result = convertFromGeminiResponse(response as never);
|
|
497
|
+
expect(result.content).toBe('Hello world');
|
|
498
|
+
expect(result.parts).toHaveLength(2);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('sets content to null when no text parts exist', () => {
|
|
502
|
+
const response = {
|
|
503
|
+
candidates: [
|
|
504
|
+
{
|
|
505
|
+
content: {
|
|
506
|
+
parts: [
|
|
507
|
+
{
|
|
508
|
+
functionCall: {
|
|
509
|
+
name: 'search',
|
|
510
|
+
args: { q: 'test' },
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
};
|
|
518
|
+
const result = convertFromGeminiResponse(response as never);
|
|
519
|
+
expect(result.content).toBeNull();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('handles multiple function calls', () => {
|
|
523
|
+
const response = {
|
|
524
|
+
candidates: [
|
|
525
|
+
{
|
|
526
|
+
content: {
|
|
527
|
+
parts: [
|
|
528
|
+
{ functionCall: { name: 'fn_a', args: { a: 1 } } },
|
|
529
|
+
{ functionCall: { name: 'fn_b', args: { b: 2 } } },
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
const result = convertFromGeminiResponse(response as never) as IAssistantMessage;
|
|
536
|
+
expect(result.toolCalls).toHaveLength(2);
|
|
537
|
+
expect(result.toolCalls![0]?.function.name).toBe('fn_a');
|
|
538
|
+
expect(result.toolCalls![1]?.function.name).toBe('fn_b');
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('convertToolsToGeminiFormat', () => {
|
|
543
|
+
it('converts tool schemas to Gemini function declarations', () => {
|
|
544
|
+
const tools: IToolSchema[] = [
|
|
545
|
+
{
|
|
546
|
+
name: 'get_weather',
|
|
547
|
+
description: 'Get weather information',
|
|
548
|
+
parameters: {
|
|
549
|
+
type: 'object',
|
|
550
|
+
properties: {
|
|
551
|
+
city: { type: 'string', description: 'City name' },
|
|
552
|
+
},
|
|
553
|
+
required: ['city'],
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
const result = convertToolsToGeminiFormat(tools);
|
|
558
|
+
expect(result).toHaveLength(1);
|
|
559
|
+
expect(result[0]?.name).toBe('get_weather');
|
|
560
|
+
expect(result[0]?.description).toBe('Get weather information');
|
|
561
|
+
expect(result[0]?.parameters).toEqual({
|
|
562
|
+
type: 'OBJECT',
|
|
563
|
+
properties: {
|
|
564
|
+
city: { type: 'STRING', description: 'City name' },
|
|
565
|
+
},
|
|
566
|
+
required: ['city'],
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('converts multiple tools', () => {
|
|
571
|
+
const tools: IToolSchema[] = [
|
|
572
|
+
{
|
|
573
|
+
name: 'tool_a',
|
|
574
|
+
description: 'Tool A',
|
|
575
|
+
parameters: { type: 'object', properties: {} },
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'tool_b',
|
|
579
|
+
description: 'Tool B',
|
|
580
|
+
parameters: { type: 'object', properties: { x: { type: 'number' } } },
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
const result = convertToolsToGeminiFormat(tools);
|
|
584
|
+
expect(result).toHaveLength(2);
|
|
585
|
+
expect(result[0]?.name).toBe('tool_a');
|
|
586
|
+
expect(result[1]?.name).toBe('tool_b');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('returns empty array for empty tools', () => {
|
|
590
|
+
const result = convertToolsToGeminiFormat([]);
|
|
591
|
+
expect(result).toEqual([]);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe('generateCallId', () => {
|
|
596
|
+
it('returns a string starting with "call_"', () => {
|
|
597
|
+
const id = generateCallId();
|
|
598
|
+
expect(id).toMatch(/^call_/);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('generates unique IDs across calls', () => {
|
|
602
|
+
const ids = new Set(Array.from({ length: 50 }, () => generateCallId()));
|
|
603
|
+
expect(ids.size).toBe(50);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('contains a timestamp component', () => {
|
|
607
|
+
const before = Date.now();
|
|
608
|
+
const id = generateCallId();
|
|
609
|
+
const after = Date.now();
|
|
610
|
+
// Extract timestamp between "call_" and the second "_"
|
|
611
|
+
const parts = id.split('_');
|
|
612
|
+
const timestamp = Number(parts[1]);
|
|
613
|
+
expect(timestamp).toBeGreaterThanOrEqual(before);
|
|
614
|
+
expect(timestamp).toBeLessThanOrEqual(after);
|
|
615
|
+
});
|
|
616
|
+
});
|