@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,628 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type OpenAI from 'openai';
|
|
3
|
+
import { GemmaProvider } from './index';
|
|
4
|
+
import type {
|
|
5
|
+
IProviderNativeRawPayloadEvent,
|
|
6
|
+
IToolSchema,
|
|
7
|
+
TUniversalMessage,
|
|
8
|
+
} from '@robota-sdk/agent-core';
|
|
9
|
+
|
|
10
|
+
vi.mock('openai', () => {
|
|
11
|
+
const MockOpenAI = vi.fn().mockImplementation(() => ({
|
|
12
|
+
chat: {
|
|
13
|
+
completions: {
|
|
14
|
+
create: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
return { default: MockOpenAI };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const timestamp = new Date('2026-05-01T00:00:00.000Z');
|
|
22
|
+
|
|
23
|
+
function createUserMessage(content: string): TUniversalMessage {
|
|
24
|
+
return {
|
|
25
|
+
id: 'user-1',
|
|
26
|
+
role: 'user',
|
|
27
|
+
content,
|
|
28
|
+
state: 'complete',
|
|
29
|
+
timestamp,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function* asyncIterableFrom<T>(items: T[]): AsyncIterable<T> {
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
yield item;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createChunk(
|
|
40
|
+
content: string,
|
|
41
|
+
finishReason: OpenAI.Chat.ChatCompletionChunk.Choice['finish_reason'] = null,
|
|
42
|
+
): OpenAI.Chat.ChatCompletionChunk {
|
|
43
|
+
return {
|
|
44
|
+
id: 'chunk-1',
|
|
45
|
+
object: 'chat.completion.chunk',
|
|
46
|
+
created: 1,
|
|
47
|
+
model: 'supergemma4',
|
|
48
|
+
choices: [
|
|
49
|
+
{
|
|
50
|
+
index: 0,
|
|
51
|
+
delta: { content },
|
|
52
|
+
finish_reason: finishReason,
|
|
53
|
+
logprobs: null,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createToolCallChunk(toolName: string, args: string): OpenAI.Chat.ChatCompletionChunk {
|
|
60
|
+
return {
|
|
61
|
+
id: 'chunk-tool',
|
|
62
|
+
object: 'chat.completion.chunk',
|
|
63
|
+
created: 1,
|
|
64
|
+
model: 'supergemma4',
|
|
65
|
+
choices: [
|
|
66
|
+
{
|
|
67
|
+
index: 0,
|
|
68
|
+
delta: {
|
|
69
|
+
tool_calls: [
|
|
70
|
+
{
|
|
71
|
+
index: 0,
|
|
72
|
+
id: 'call-1',
|
|
73
|
+
type: 'function',
|
|
74
|
+
function: { name: toolName, arguments: args },
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
finish_reason: 'tool_calls',
|
|
79
|
+
logprobs: null,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createDeclaredToolSchema(): IToolSchema {
|
|
86
|
+
return {
|
|
87
|
+
name: 'DeclaredTool',
|
|
88
|
+
description: 'Declared test tool',
|
|
89
|
+
parameters: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
prompt: { type: 'string' },
|
|
93
|
+
mode: { type: 'string' },
|
|
94
|
+
background: { type: 'boolean' },
|
|
95
|
+
},
|
|
96
|
+
required: ['prompt'],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('GemmaProvider', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
vi.clearAllMocks();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('creates an OpenAI-compatible client with local endpoint options', async () => {
|
|
107
|
+
const OpenAIModule = await import('openai');
|
|
108
|
+
const OpenAIConstructor = vi.mocked(OpenAIModule.default);
|
|
109
|
+
|
|
110
|
+
const provider = new GemmaProvider({
|
|
111
|
+
apiKey: 'lm-studio',
|
|
112
|
+
baseURL: 'http://localhost:1234/v1',
|
|
113
|
+
timeout: 1000,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(provider.name).toBe('gemma');
|
|
117
|
+
expect(OpenAIConstructor).toHaveBeenCalledWith({
|
|
118
|
+
apiKey: 'lm-studio',
|
|
119
|
+
baseURL: 'http://localhost:1234/v1',
|
|
120
|
+
timeout: 1000,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('reports provider-native web tools as unsupported for LM Studio/Gemma chat completions', () => {
|
|
125
|
+
const provider = new GemmaProvider({
|
|
126
|
+
apiKey: 'lm-studio',
|
|
127
|
+
baseURL: 'http://localhost:1234/v1',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(provider.getCapabilities().nativeWebTools).toEqual({
|
|
131
|
+
webSearch: {
|
|
132
|
+
supported: false,
|
|
133
|
+
enabled: false,
|
|
134
|
+
source: 'openai-compatible-chat-completions',
|
|
135
|
+
reason:
|
|
136
|
+
'Gemma OpenAI-compatible endpoints support declared function tools, not provider-native web search.',
|
|
137
|
+
},
|
|
138
|
+
webFetch: {
|
|
139
|
+
supported: false,
|
|
140
|
+
enabled: false,
|
|
141
|
+
source: 'openai-compatible-chat-completions',
|
|
142
|
+
reason:
|
|
143
|
+
'Gemma OpenAI-compatible endpoints support declared function tools, not provider-native web fetch.',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rejects request-level native web tools before LM Studio transport execution', async () => {
|
|
149
|
+
const provider = new GemmaProvider({
|
|
150
|
+
apiKey: 'lm-studio',
|
|
151
|
+
baseURL: 'http://localhost:1234/v1',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
provider.chat([createUserMessage('Search the web')], {
|
|
156
|
+
model: 'supergemma4',
|
|
157
|
+
nativeWebTools: { webSearch: true },
|
|
158
|
+
}),
|
|
159
|
+
).rejects.toThrow(
|
|
160
|
+
'Provider gemma does not support native web search. Gemma OpenAI-compatible endpoints support declared function tools, not provider-native web search.',
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('filters Gemma reasoning markers from non-streaming chat content', async () => {
|
|
165
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
166
|
+
const client = (
|
|
167
|
+
provider as unknown as {
|
|
168
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
169
|
+
}
|
|
170
|
+
).client;
|
|
171
|
+
client.chat.completions.create.mockResolvedValue({
|
|
172
|
+
id: 'chatcmpl-test',
|
|
173
|
+
object: 'chat.completion',
|
|
174
|
+
created: 1,
|
|
175
|
+
model: 'supergemma4',
|
|
176
|
+
choices: [
|
|
177
|
+
{
|
|
178
|
+
index: 0,
|
|
179
|
+
message: {
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: '<|channel>thought\nhidden<channel|>Visible answer',
|
|
182
|
+
refusal: null,
|
|
183
|
+
},
|
|
184
|
+
finish_reason: 'stop',
|
|
185
|
+
logprobs: null,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = await provider.chat([createUserMessage('Hello')], { model: 'supergemma4' });
|
|
191
|
+
|
|
192
|
+
expect(result.content).toBe('Visible answer');
|
|
193
|
+
expect(result.metadata?.['gemmaReasoningFiltered']).toBe(true);
|
|
194
|
+
expect(result.metadata?.['gemmaRawContent']).toBe(
|
|
195
|
+
'<|channel>thought\nhidden<channel|>Visible answer',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('emits native Chat Completions request and response payloads before Gemma projection', async () => {
|
|
200
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
201
|
+
const client = (
|
|
202
|
+
provider as unknown as {
|
|
203
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
204
|
+
}
|
|
205
|
+
).client;
|
|
206
|
+
client.chat.completions.create.mockResolvedValue({
|
|
207
|
+
id: 'gemma-native',
|
|
208
|
+
object: 'chat.completion',
|
|
209
|
+
created: 1,
|
|
210
|
+
model: 'supergemma4',
|
|
211
|
+
choices: [
|
|
212
|
+
{
|
|
213
|
+
index: 0,
|
|
214
|
+
message: { role: 'assistant', content: 'Visible answer', refusal: null },
|
|
215
|
+
finish_reason: 'stop',
|
|
216
|
+
logprobs: null,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
const events: IProviderNativeRawPayloadEvent[] = [];
|
|
221
|
+
|
|
222
|
+
await provider.chat([createUserMessage('Hello')], {
|
|
223
|
+
model: 'supergemma4',
|
|
224
|
+
onProviderNativeRawPayload: (event) => events.push(event),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(events).toEqual([
|
|
228
|
+
expect.objectContaining({
|
|
229
|
+
provider: 'gemma',
|
|
230
|
+
apiSurface: 'chat-completions',
|
|
231
|
+
payloadKind: 'request',
|
|
232
|
+
}),
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
provider: 'gemma',
|
|
235
|
+
apiSurface: 'chat-completions',
|
|
236
|
+
payloadKind: 'response',
|
|
237
|
+
payload: expect.objectContaining({ id: 'gemma-native' }),
|
|
238
|
+
}),
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('filters Gemma reasoning markers from streaming chat assembly and deltas', async () => {
|
|
243
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
244
|
+
const client = (
|
|
245
|
+
provider as unknown as {
|
|
246
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
247
|
+
}
|
|
248
|
+
).client;
|
|
249
|
+
client.chat.completions.create.mockResolvedValue(
|
|
250
|
+
asyncIterableFrom([
|
|
251
|
+
createChunk('<|cha'),
|
|
252
|
+
createChunk('nnel>thought\nhidden'),
|
|
253
|
+
createChunk('<channel|>Visible'),
|
|
254
|
+
createChunk(' answer', 'stop'),
|
|
255
|
+
]),
|
|
256
|
+
);
|
|
257
|
+
const onTextDelta = vi.fn();
|
|
258
|
+
|
|
259
|
+
const result = await provider.chat([createUserMessage('Hello')], {
|
|
260
|
+
model: 'supergemma4',
|
|
261
|
+
onTextDelta,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result.content).toBe('Visible answer');
|
|
265
|
+
expect(onTextDelta).toHaveBeenCalledTimes(2);
|
|
266
|
+
expect(onTextDelta).toHaveBeenNthCalledWith(1, 'Visible');
|
|
267
|
+
expect(onTextDelta).toHaveBeenNthCalledWith(2, ' answer');
|
|
268
|
+
expect(result.metadata?.['gemmaReasoningFiltered']).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('emits ordered native Chat Completions stream chunks before Gemma projection', async () => {
|
|
272
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
273
|
+
const client = (
|
|
274
|
+
provider as unknown as {
|
|
275
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
276
|
+
}
|
|
277
|
+
).client;
|
|
278
|
+
client.chat.completions.create.mockResolvedValue(
|
|
279
|
+
asyncIterableFrom([createChunk('Visible'), createChunk(' answer', 'stop')]),
|
|
280
|
+
);
|
|
281
|
+
const events: IProviderNativeRawPayloadEvent[] = [];
|
|
282
|
+
|
|
283
|
+
await provider.chat([createUserMessage('Hello')], {
|
|
284
|
+
model: 'supergemma4',
|
|
285
|
+
onTextDelta: vi.fn(),
|
|
286
|
+
onProviderNativeRawPayload: (event) => events.push(event),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(events.map((event) => event.payloadKind)).toEqual([
|
|
290
|
+
'request',
|
|
291
|
+
'stream_event',
|
|
292
|
+
'stream_event',
|
|
293
|
+
]);
|
|
294
|
+
expect(
|
|
295
|
+
events.filter((event) => event.payloadKind === 'stream_event').map((event) => event.sequence),
|
|
296
|
+
).toEqual([0, 1]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('projects Gemma native tool-call text from non-streaming chat content', async () => {
|
|
300
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
301
|
+
const client = (
|
|
302
|
+
provider as unknown as {
|
|
303
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
304
|
+
}
|
|
305
|
+
).client;
|
|
306
|
+
client.chat.completions.create.mockResolvedValue({
|
|
307
|
+
id: 'chatcmpl-test',
|
|
308
|
+
object: 'chat.completion',
|
|
309
|
+
created: 1,
|
|
310
|
+
model: 'supergemma4',
|
|
311
|
+
choices: [
|
|
312
|
+
{
|
|
313
|
+
index: 0,
|
|
314
|
+
message: {
|
|
315
|
+
role: 'assistant',
|
|
316
|
+
content:
|
|
317
|
+
'<|tool_call>call:DeclaredTool{prompt:<|"|>analyze<|"|>,background:true}<tool_call|>',
|
|
318
|
+
refusal: null,
|
|
319
|
+
},
|
|
320
|
+
finish_reason: 'tool_calls',
|
|
321
|
+
logprobs: null,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const result = await provider.chat([createUserMessage('Hello')], {
|
|
327
|
+
model: 'supergemma4',
|
|
328
|
+
tools: [createDeclaredToolSchema()],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(result.content).toBe('');
|
|
332
|
+
expect(result.metadata?.['toolCallTextProjected']).toBe(true);
|
|
333
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
334
|
+
expect(result.toolCalls).toEqual([
|
|
335
|
+
{
|
|
336
|
+
id: 'gemma_call_0',
|
|
337
|
+
type: 'function',
|
|
338
|
+
function: {
|
|
339
|
+
name: 'DeclaredTool',
|
|
340
|
+
arguments: '{"prompt":"analyze","background":true}',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('passes undeclared native OpenAI-compatible tool calls to core for normal tool-result errors', async () => {
|
|
347
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
348
|
+
const client = (
|
|
349
|
+
provider as unknown as {
|
|
350
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
351
|
+
}
|
|
352
|
+
).client;
|
|
353
|
+
client.chat.completions.create.mockResolvedValue({
|
|
354
|
+
id: 'chatcmpl-test',
|
|
355
|
+
object: 'chat.completion',
|
|
356
|
+
created: 1,
|
|
357
|
+
model: 'supergemma4',
|
|
358
|
+
choices: [
|
|
359
|
+
{
|
|
360
|
+
index: 0,
|
|
361
|
+
message: {
|
|
362
|
+
role: 'assistant',
|
|
363
|
+
content: null,
|
|
364
|
+
refusal: null,
|
|
365
|
+
tool_calls: [
|
|
366
|
+
{
|
|
367
|
+
id: 'call-1',
|
|
368
|
+
type: 'function',
|
|
369
|
+
function: { name: 'agent', arguments: '{"prompt":"do work"}' },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
finish_reason: 'tool_calls',
|
|
374
|
+
logprobs: null,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const result = await provider.chat([createUserMessage('Hello')], {
|
|
380
|
+
model: 'supergemma4',
|
|
381
|
+
tools: [createDeclaredToolSchema()],
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
385
|
+
expect(result.toolCalls).toEqual([
|
|
386
|
+
{
|
|
387
|
+
id: 'call-1',
|
|
388
|
+
type: 'function',
|
|
389
|
+
function: { name: 'agent', arguments: '{"prompt":"do work"}' },
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('projects split Gemma native tool-call text from streaming chat assembly', async () => {
|
|
395
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
396
|
+
const client = (
|
|
397
|
+
provider as unknown as {
|
|
398
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
399
|
+
}
|
|
400
|
+
).client;
|
|
401
|
+
client.chat.completions.create.mockResolvedValue(
|
|
402
|
+
asyncIterableFrom([
|
|
403
|
+
createChunk('<|tool_call>call:Declar'),
|
|
404
|
+
createChunk('edTool{prompt:<|"|>analyze<|"|>,background:true}<tool_call|>'),
|
|
405
|
+
createChunk('', 'tool_calls'),
|
|
406
|
+
]),
|
|
407
|
+
);
|
|
408
|
+
const onTextDelta = vi.fn();
|
|
409
|
+
|
|
410
|
+
const result = await provider.chat([createUserMessage('Hello')], {
|
|
411
|
+
model: 'supergemma4',
|
|
412
|
+
tools: [createDeclaredToolSchema()],
|
|
413
|
+
onTextDelta,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(onTextDelta).not.toHaveBeenCalled();
|
|
417
|
+
expect(result.content).toBe('');
|
|
418
|
+
expect(result.metadata?.['toolCallTextProjected']).toBe(true);
|
|
419
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
420
|
+
expect(result.toolCalls).toEqual([
|
|
421
|
+
{
|
|
422
|
+
id: 'gemma_call_0',
|
|
423
|
+
type: 'function',
|
|
424
|
+
function: {
|
|
425
|
+
name: 'DeclaredTool',
|
|
426
|
+
arguments: '{"prompt":"analyze","background":true}',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('passes undeclared native OpenAI-compatible tool calls during streaming assembly', async () => {
|
|
433
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
434
|
+
const client = (
|
|
435
|
+
provider as unknown as {
|
|
436
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
437
|
+
}
|
|
438
|
+
).client;
|
|
439
|
+
client.chat.completions.create.mockResolvedValue(
|
|
440
|
+
asyncIterableFrom([createToolCallChunk('agent', '{"prompt":"do work"}')]),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const result = await provider.chat([createUserMessage('Hello')], {
|
|
444
|
+
model: 'supergemma4',
|
|
445
|
+
tools: [createDeclaredToolSchema()],
|
|
446
|
+
onTextDelta: vi.fn(),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
450
|
+
expect(result.toolCalls).toEqual([
|
|
451
|
+
{
|
|
452
|
+
id: 'call-1',
|
|
453
|
+
type: 'function',
|
|
454
|
+
function: { name: 'agent', arguments: '{"prompt":"do work"}' },
|
|
455
|
+
},
|
|
456
|
+
]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('projects split Gemma native tool-call text from direct chatStream chunks', async () => {
|
|
460
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
461
|
+
const client = (
|
|
462
|
+
provider as unknown as {
|
|
463
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
464
|
+
}
|
|
465
|
+
).client;
|
|
466
|
+
client.chat.completions.create.mockResolvedValue(
|
|
467
|
+
asyncIterableFrom([
|
|
468
|
+
createChunk('<|tool_call>call:Declar'),
|
|
469
|
+
createChunk('edTool{prompt:<|"|>analyze<|"|>}<tool_call|>'),
|
|
470
|
+
createChunk('', 'tool_calls'),
|
|
471
|
+
]),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const chunks: TUniversalMessage[] = [];
|
|
475
|
+
for await (const chunk of provider.chatStream?.([createUserMessage('Hello')], {
|
|
476
|
+
model: 'supergemma4',
|
|
477
|
+
tools: [createDeclaredToolSchema()],
|
|
478
|
+
}) ?? []) {
|
|
479
|
+
chunks.push(chunk);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
expect(chunks).toHaveLength(1);
|
|
483
|
+
const [toolCallChunk] = chunks;
|
|
484
|
+
expect(toolCallChunk?.content).toBe('');
|
|
485
|
+
if (!toolCallChunk || toolCallChunk.role !== 'assistant') {
|
|
486
|
+
throw new Error('Expected assistant message');
|
|
487
|
+
}
|
|
488
|
+
expect(toolCallChunk.toolCalls).toEqual([
|
|
489
|
+
{
|
|
490
|
+
id: 'gemma_call_0',
|
|
491
|
+
type: 'function',
|
|
492
|
+
function: {
|
|
493
|
+
name: 'DeclaredTool',
|
|
494
|
+
arguments: '{"prompt":"analyze"}',
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
]);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('encapsulates XML-like declared tool tags as tool calls in non-streaming chat', async () => {
|
|
501
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
502
|
+
const client = (
|
|
503
|
+
provider as unknown as {
|
|
504
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
505
|
+
}
|
|
506
|
+
).client;
|
|
507
|
+
client.chat.completions.create.mockResolvedValue({
|
|
508
|
+
id: 'chatcmpl-test',
|
|
509
|
+
object: 'chat.completion',
|
|
510
|
+
created: 1,
|
|
511
|
+
model: 'supergemma4',
|
|
512
|
+
choices: [
|
|
513
|
+
{
|
|
514
|
+
index: 0,
|
|
515
|
+
message: {
|
|
516
|
+
role: 'assistant',
|
|
517
|
+
content:
|
|
518
|
+
'<tool-launch-sequence>prepare tools</tool-launch-sequence><DeclaredTool prompt="analyze backlog" mode="plan" />',
|
|
519
|
+
refusal: null,
|
|
520
|
+
},
|
|
521
|
+
finish_reason: 'tool_calls',
|
|
522
|
+
logprobs: null,
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const result = await provider.chat([createUserMessage('Launch tools')], {
|
|
528
|
+
model: 'supergemma4',
|
|
529
|
+
tools: [createDeclaredToolSchema()],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
expect(result.content).toBe('');
|
|
533
|
+
expect(result.metadata?.['toolCallTextProjected']).toBe(true);
|
|
534
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
535
|
+
expect(result.toolCalls).toEqual([
|
|
536
|
+
{
|
|
537
|
+
id: 'gemma_call_0',
|
|
538
|
+
type: 'function',
|
|
539
|
+
function: {
|
|
540
|
+
name: 'DeclaredTool',
|
|
541
|
+
arguments: '{"prompt":"analyze backlog","mode":"plan"}',
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('does not synthesize tool calls from command-like text inside XML wrappers', async () => {
|
|
548
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
549
|
+
const client = (
|
|
550
|
+
provider as unknown as {
|
|
551
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
552
|
+
}
|
|
553
|
+
).client;
|
|
554
|
+
client.chat.completions.create.mockResolvedValue({
|
|
555
|
+
id: 'chatcmpl-test',
|
|
556
|
+
object: 'chat.completion',
|
|
557
|
+
created: 1,
|
|
558
|
+
model: 'supergemma4',
|
|
559
|
+
choices: [
|
|
560
|
+
{
|
|
561
|
+
index: 0,
|
|
562
|
+
message: {
|
|
563
|
+
role: 'assistant',
|
|
564
|
+
content: [
|
|
565
|
+
'<tool-launch>',
|
|
566
|
+
'parallel',
|
|
567
|
+
' worker=DeclaredTool:"Analyze implementation."',
|
|
568
|
+
' reviewer=DeclaredTool:"Analyze architecture."',
|
|
569
|
+
'</tool-launch>',
|
|
570
|
+
].join('\n'),
|
|
571
|
+
refusal: null,
|
|
572
|
+
},
|
|
573
|
+
finish_reason: 'tool_calls',
|
|
574
|
+
logprobs: null,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const result = await provider.chat([createUserMessage('Launch tools')], {
|
|
580
|
+
model: 'supergemma4',
|
|
581
|
+
tools: [createDeclaredToolSchema()],
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(result.content).toBe('');
|
|
585
|
+
expect(result.metadata?.['toolCallTextProjected']).toBe(true);
|
|
586
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
587
|
+
expect(result.toolCalls).toBeUndefined();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('encapsulates split XML-like declared tool tags during streaming assembly', async () => {
|
|
591
|
+
const provider = new GemmaProvider({ apiKey: 'lm-studio' });
|
|
592
|
+
const client = (
|
|
593
|
+
provider as unknown as {
|
|
594
|
+
client: { chat: { completions: { create: ReturnType<typeof vi.fn> } } };
|
|
595
|
+
}
|
|
596
|
+
).client;
|
|
597
|
+
client.chat.completions.create.mockResolvedValue(
|
|
598
|
+
asyncIterableFrom([
|
|
599
|
+
createChunk('<tool-launch-sequence>prepare'),
|
|
600
|
+
createChunk(' tools</tool-launch-sequence><DeclaredTool prompt="analyze backlog"'),
|
|
601
|
+
createChunk(' mode="worker" />'),
|
|
602
|
+
createChunk('', 'tool_calls'),
|
|
603
|
+
]),
|
|
604
|
+
);
|
|
605
|
+
const onTextDelta = vi.fn();
|
|
606
|
+
|
|
607
|
+
const result = await provider.chat([createUserMessage('Launch tools')], {
|
|
608
|
+
model: 'supergemma4',
|
|
609
|
+
tools: [createDeclaredToolSchema()],
|
|
610
|
+
onTextDelta,
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(onTextDelta).not.toHaveBeenCalled();
|
|
614
|
+
expect(result.content).toBe('');
|
|
615
|
+
expect(result.metadata?.['toolCallTextProjected']).toBe(true);
|
|
616
|
+
if (result.role !== 'assistant') throw new Error('Expected assistant message');
|
|
617
|
+
expect(result.toolCalls).toEqual([
|
|
618
|
+
{
|
|
619
|
+
id: 'gemma_call_0',
|
|
620
|
+
type: 'function',
|
|
621
|
+
function: {
|
|
622
|
+
name: 'DeclaredTool',
|
|
623
|
+
arguments: '{"prompt":"analyze backlog","mode":"worker"}',
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
]);
|
|
627
|
+
});
|
|
628
|
+
});
|