aiexecode 1.0.90 → 1.0.92
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.
Potentially problematic release.
This version of aiexecode might be problematic. Click here for more details.
- package/README.md +1 -0
- package/index.js +13 -11
- package/mcp-agent-lib/init.sh +3 -0
- package/mcp-agent-lib/package-lock.json +14 -1
- package/mcp-agent-lib/package.json +4 -6
- package/mcp-agent-lib/sampleFastMCPClient/client.py +25 -0
- package/mcp-agent-lib/sampleFastMCPClient/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServer/server.py +12 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/run.sh +3 -0
- package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/server.py +43 -0
- package/mcp-agent-lib/sampleFastMCPServerRootsRequest/server.py +63 -0
- package/mcp-agent-lib/sampleMCPHost/index.js +182 -63
- package/mcp-agent-lib/sampleMCPHost/mcp_config.json +7 -1
- package/mcp-agent-lib/sampleMCPHostFeatures/elicitation.js +151 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/index.js +166 -0
- package/mcp-agent-lib/sampleMCPHostFeatures/roots.js +197 -0
- package/mcp-agent-lib/src/mcp_client.js +129 -67
- package/mcp-agent-lib/src/mcp_message_logger.js +516 -0
- package/package.json +3 -1
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/src/LLMClient/client.js +992 -0
- package/src/LLMClient/converters/input-normalizer.js +238 -0
- package/src/LLMClient/converters/responses-to-claude.js +454 -0
- package/src/LLMClient/converters/responses-to-gemini.js +648 -0
- package/src/LLMClient/converters/responses-to-ollama.js +348 -0
- package/src/LLMClient/errors.js +372 -0
- package/src/LLMClient/index.js +31 -0
- package/src/commands/apikey.js +10 -22
- package/src/commands/model.js +28 -28
- package/src/commands/reasoning_effort.js +9 -23
- package/src/config/ai_models.js +212 -0
- package/src/config/feature_flags.js +1 -1
- package/src/frontend/App.js +5 -10
- package/src/frontend/components/CurrentModelView.js +0 -33
- package/src/frontend/components/Footer.js +3 -3
- package/src/frontend/components/ModelListView.js +30 -87
- package/src/frontend/components/ModelUpdatedView.js +7 -142
- package/src/frontend/components/SetupWizard.js +37 -32
- package/src/system/ai_request.js +57 -42
- package/src/util/config.js +26 -4
- package/src/util/setup_wizard.js +1 -6
- package/mcp-agent-lib/.claude/settings.local.json +0 -9
- package/src/config/openai_models.js +0 -152
- /package/payload_viewer/out/_next/static/{w4dMVYalgk7djrLxRxWiE → d0-fu2rgYnshgGFPxr1CR}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{w4dMVYalgk7djrLxRxWiE → d0-fu2rgYnshgGFPxr1CR}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{w4dMVYalgk7djrLxRxWiE → d0-fu2rgYnshgGFPxr1CR}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified LLM Client with Responses API Interface
|
|
3
|
+
* Supports OpenAI, Claude, Gemini, and Ollama with OpenAI Responses API-compatible interface
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import OpenAI from 'openai';
|
|
9
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
10
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
11
|
+
|
|
12
|
+
import { convertResponsesRequestToClaudeFormat, convertClaudeResponseToResponsesFormat } from './converters/responses-to-claude.js';
|
|
13
|
+
import { convertResponsesRequestToGeminiFormat, convertGeminiResponseToResponsesFormat } from './converters/responses-to-gemini.js';
|
|
14
|
+
import { convertResponsesRequestToOllamaFormat, convertOllamaResponseToResponsesFormat } from './converters/responses-to-ollama.js';
|
|
15
|
+
import { normalizeInput } from './converters/input-normalizer.js';
|
|
16
|
+
import { normalizeError, createErrorFromResponse } from './errors.js';
|
|
17
|
+
|
|
18
|
+
export class UnifiedLLMClient {
|
|
19
|
+
/**
|
|
20
|
+
* Create a unified LLM client
|
|
21
|
+
* @param {Object} config - Configuration object
|
|
22
|
+
* @param {string} config.provider - Provider name: 'openai', 'claude', 'gemini', or 'ollama' (auto-detected from model if not provided)
|
|
23
|
+
* @param {string} [config.apiKey] - API key for the provider
|
|
24
|
+
* @param {string} [config.baseUrl] - Base URL (for Ollama)
|
|
25
|
+
* @param {string} [config.model] - Default model to use
|
|
26
|
+
* @param {string} [config.logDir] - Directory path for logging request/response payloads (if not provided, logging is disabled)
|
|
27
|
+
*/
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.provider = config.provider;
|
|
30
|
+
this.apiKey = config.apiKey;
|
|
31
|
+
this.baseUrl = config.baseUrl;
|
|
32
|
+
this.defaultModel = config.model;
|
|
33
|
+
this.logDir = config.logDir;
|
|
34
|
+
this.explicitProvider = !!config.provider; // Track if provider was explicitly set
|
|
35
|
+
|
|
36
|
+
// Auto-detect provider from model if not explicitly provided
|
|
37
|
+
if (!this.provider && this.defaultModel) {
|
|
38
|
+
this.provider = this._detectProviderFromModel(this.defaultModel);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Default to openai if still no provider
|
|
42
|
+
this.provider = this.provider || 'openai';
|
|
43
|
+
|
|
44
|
+
// Create log directory if specified and doesn't exist
|
|
45
|
+
if (this.logDir) {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(this.logDir)) {
|
|
48
|
+
fs.mkdirSync(this.logDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`Failed to create log directory: ${error.message}`);
|
|
52
|
+
this.logDir = null; // Disable logging if directory creation fails
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._initializeClient();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Auto-detect provider from model name
|
|
61
|
+
*/
|
|
62
|
+
_detectProviderFromModel(model) {
|
|
63
|
+
if (model.startsWith('gpt-') || model.startsWith('o1-') || model.startsWith('o3-') || model.startsWith('text-')) {
|
|
64
|
+
return 'openai';
|
|
65
|
+
} else if (model.startsWith('claude-')) {
|
|
66
|
+
return 'claude';
|
|
67
|
+
} else if (model.startsWith('gemini-')) {
|
|
68
|
+
return 'gemini';
|
|
69
|
+
} else if (model.includes('llama') || model.includes('mistral') || model.includes('codellama')) {
|
|
70
|
+
return 'ollama';
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if model is GPT-5, o3, or other reasoning models
|
|
77
|
+
*/
|
|
78
|
+
_isReasoningModel(model) {
|
|
79
|
+
return model === 'gpt-5' || model === 'gpt-5-mini' || model === 'gpt-5-nano' ||
|
|
80
|
+
model === 'o3' || model === 'o3-mini';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_initializeClient() {
|
|
84
|
+
switch (this.provider) {
|
|
85
|
+
case 'openai':
|
|
86
|
+
this.client = new OpenAI({ apiKey: this.apiKey });
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'claude':
|
|
90
|
+
this.client = new Anthropic({ apiKey: this.apiKey });
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'gemini':
|
|
94
|
+
this.client = new GoogleGenerativeAI(this.apiKey);
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'ollama':
|
|
98
|
+
// Ollama doesn't have an official SDK, we'll use fetch
|
|
99
|
+
this.baseUrl = this.baseUrl || 'http://localhost:11434';
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`Unsupported provider: ${this.provider}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Log payload to file
|
|
109
|
+
* @param {Object} payload - Request or response payload
|
|
110
|
+
* @param {string} type - 'REQ' or 'RES'
|
|
111
|
+
* @param {string} providerName - Provider name
|
|
112
|
+
*/
|
|
113
|
+
_logPayload(payload, type, providerName) {
|
|
114
|
+
if (!this.logDir) return;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const now = new Date();
|
|
118
|
+
const year = now.getFullYear();
|
|
119
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
120
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
121
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
122
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
123
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
124
|
+
const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
|
|
125
|
+
|
|
126
|
+
const timestamp = `${year}${month}${day}-${hours}${minutes}${seconds}-${milliseconds}`;
|
|
127
|
+
const filename = `${timestamp}-${type}-${providerName}.json`;
|
|
128
|
+
const filepath = path.join(this.logDir, filename);
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(filepath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`Failed to log payload: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a response (Responses API-compatible interface)
|
|
138
|
+
* @param {Object} request - Responses API format request
|
|
139
|
+
* @param {Object} options - Additional options
|
|
140
|
+
* @param {AbortSignal} options.signal - AbortSignal for cancellation
|
|
141
|
+
* @returns {Promise<Object>|AsyncGenerator} Responses API format response or stream
|
|
142
|
+
*/
|
|
143
|
+
async response(request, options = {}) {
|
|
144
|
+
// Set default model if not provided
|
|
145
|
+
if (!request.model && this.defaultModel) {
|
|
146
|
+
request.model = this.defaultModel;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Convert instructions field to input array (Method 2)
|
|
150
|
+
// If instructions is provided, add it as a system role message at the beginning of input array
|
|
151
|
+
if (request.instructions) {
|
|
152
|
+
// Ensure input is an array
|
|
153
|
+
if (!request.input) {
|
|
154
|
+
request.input = [];
|
|
155
|
+
} else if (typeof request.input === 'string') {
|
|
156
|
+
// Convert string input to array format
|
|
157
|
+
request.input = [
|
|
158
|
+
{
|
|
159
|
+
role: 'user',
|
|
160
|
+
content: [{ type: 'input_text', text: request.input }]
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check if input already has a system message
|
|
166
|
+
const hasSystemMessage = Array.isArray(request.input) &&
|
|
167
|
+
request.input.some(item => item.role === 'system');
|
|
168
|
+
|
|
169
|
+
if (!hasSystemMessage) {
|
|
170
|
+
// Add instructions as system message at the beginning
|
|
171
|
+
const systemMessage = {
|
|
172
|
+
role: 'system',
|
|
173
|
+
content: typeof request.instructions === 'string'
|
|
174
|
+
? [{ type: 'input_text', text: request.instructions }]
|
|
175
|
+
: request.instructions
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
request.input.unshift(systemMessage);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Remove instructions field to avoid duplication
|
|
182
|
+
delete request.instructions;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Auto-detect provider from model name in request
|
|
186
|
+
// Only if provider was not explicitly set in constructor
|
|
187
|
+
let effectiveProvider = this.provider;
|
|
188
|
+
if (request.model && !this.explicitProvider) {
|
|
189
|
+
const detectedProvider = this._detectProviderFromModel(request.model);
|
|
190
|
+
if (detectedProvider) {
|
|
191
|
+
effectiveProvider = detectedProvider;
|
|
192
|
+
// Re-initialize client if provider changed
|
|
193
|
+
if (effectiveProvider !== this.provider) {
|
|
194
|
+
const oldProvider = this.provider;
|
|
195
|
+
this.provider = effectiveProvider;
|
|
196
|
+
this._initializeClient();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Log request payload
|
|
202
|
+
this._logPayload(request, 'REQ', effectiveProvider);
|
|
203
|
+
|
|
204
|
+
// Check if streaming is requested
|
|
205
|
+
const isStreaming = request.stream === true;
|
|
206
|
+
|
|
207
|
+
switch (effectiveProvider) {
|
|
208
|
+
case 'openai':
|
|
209
|
+
return isStreaming ? this._responseOpenAIStream(request, options) : await this._responseOpenAI(request, options);
|
|
210
|
+
|
|
211
|
+
case 'claude':
|
|
212
|
+
return isStreaming ? this._responseClaudeStream(request, options) : await this._responseClaude(request, options);
|
|
213
|
+
|
|
214
|
+
case 'gemini':
|
|
215
|
+
return isStreaming ? this._responseGeminiStream(request, options) : await this._responseGemini(request, options);
|
|
216
|
+
|
|
217
|
+
case 'ollama':
|
|
218
|
+
return isStreaming ? this._responseOllamaStream(request, options) : await this._responseOllama(request, options);
|
|
219
|
+
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`Unsupported provider: ${this.provider}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async _responseOpenAI(request, options = {}) {
|
|
226
|
+
try {
|
|
227
|
+
// Call OpenAI Responses API directly
|
|
228
|
+
const response = await this._callOpenAIResponsesAPI(request, options);
|
|
229
|
+
// Log response payload
|
|
230
|
+
this._logPayload(response, 'RES', 'openai');
|
|
231
|
+
return response;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw normalizeError(error, 'openai');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _callOpenAIResponsesAPI(request, options = {}) {
|
|
238
|
+
// Convert to proper Responses API format (like ai_request.js)
|
|
239
|
+
const openAIRequest = { ...request };
|
|
240
|
+
|
|
241
|
+
// 1. Normalize input to Responses API format
|
|
242
|
+
// Convert from string or Chat Completions format to proper input array
|
|
243
|
+
if (openAIRequest.input) {
|
|
244
|
+
openAIRequest.input = normalizeInput(openAIRequest.input);
|
|
245
|
+
|
|
246
|
+
// OpenAI Responses API uses role-based input with content arrays
|
|
247
|
+
// No conversion needed - keep the format as-is
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Tools conversion
|
|
251
|
+
// OpenAI uses: { type: 'function', name, description, parameters }
|
|
252
|
+
// Not: { type: 'custom', name, description, input_schema }
|
|
253
|
+
if (openAIRequest.tools && Array.isArray(openAIRequest.tools)) {
|
|
254
|
+
openAIRequest.tools = openAIRequest.tools.map(tool => {
|
|
255
|
+
let convertedTool;
|
|
256
|
+
|
|
257
|
+
if (tool.type === 'custom' && tool.input_schema) {
|
|
258
|
+
// Convert custom format to OpenAI Responses API format
|
|
259
|
+
convertedTool = {
|
|
260
|
+
type: 'function',
|
|
261
|
+
name: tool.name,
|
|
262
|
+
description: tool.description || `Tool: ${tool.name}`,
|
|
263
|
+
parameters: tool.input_schema
|
|
264
|
+
};
|
|
265
|
+
} else if (tool.type === 'function' && tool.function) {
|
|
266
|
+
// Convert Chat Completions format to Responses API format
|
|
267
|
+
convertedTool = {
|
|
268
|
+
type: 'function',
|
|
269
|
+
name: tool.function.name,
|
|
270
|
+
description: tool.function.description || `Function: ${tool.function.name}`,
|
|
271
|
+
parameters: tool.function.parameters
|
|
272
|
+
};
|
|
273
|
+
} else {
|
|
274
|
+
// Already in correct format or pass through
|
|
275
|
+
convertedTool = { ...tool };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Remove 'strict' field to prevent OpenAI SDK from auto-modifying 'required' fields
|
|
279
|
+
// This ensures all providers receive the same tool schema
|
|
280
|
+
delete convertedTool.strict;
|
|
281
|
+
|
|
282
|
+
return convertedTool;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Add reasoning configuration for reasoning models (like ai_request.js)
|
|
287
|
+
// Check if model supports reasoning
|
|
288
|
+
const reasoningModels = ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'o3', 'o3-mini'];
|
|
289
|
+
const currentModel = openAIRequest.model || 'gpt-4o-mini';
|
|
290
|
+
|
|
291
|
+
if (reasoningModels.some(m => currentModel.startsWith(m))) {
|
|
292
|
+
// Add reasoning configuration if not already present
|
|
293
|
+
if (!openAIRequest.reasoning) {
|
|
294
|
+
openAIRequest.reasoning = {
|
|
295
|
+
effort: 'medium',
|
|
296
|
+
summary: 'auto'
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Log raw request payload before API call
|
|
302
|
+
this._logPayload(openAIRequest, 'REQ-OPENAI-RAW', 'openai');
|
|
303
|
+
|
|
304
|
+
// Use OpenAI SDK to call Responses API (like ai_request.js)
|
|
305
|
+
const createOptions = {};
|
|
306
|
+
if (options.signal) {
|
|
307
|
+
createOptions.signal = options.signal;
|
|
308
|
+
}
|
|
309
|
+
const data = await this.client.responses.create(openAIRequest, createOptions);
|
|
310
|
+
|
|
311
|
+
// Log raw OpenAI API response before normalization
|
|
312
|
+
this._logPayload(data, 'RES-OPENAI-RAW', 'openai');
|
|
313
|
+
|
|
314
|
+
// Ensure all required fields are present and properly formatted
|
|
315
|
+
const normalizedResponse = {
|
|
316
|
+
id: data.id,
|
|
317
|
+
object: data.object || 'response',
|
|
318
|
+
created_at: data.created_at || Math.floor(Date.now() / 1000),
|
|
319
|
+
status: data.status || 'completed',
|
|
320
|
+
background: data.background !== undefined ? data.background : false,
|
|
321
|
+
billing: data.billing || { payer: 'developer' },
|
|
322
|
+
error: data.error || null,
|
|
323
|
+
incomplete_details: data.incomplete_details || null,
|
|
324
|
+
instructions: data.instructions || openAIRequest.instructions || null,
|
|
325
|
+
max_output_tokens: data.max_output_tokens || openAIRequest.max_output_tokens || null,
|
|
326
|
+
max_tool_calls: data.max_tool_calls || null,
|
|
327
|
+
model: data.model,
|
|
328
|
+
output: data.output || [],
|
|
329
|
+
parallel_tool_calls: data.parallel_tool_calls !== undefined ? data.parallel_tool_calls : true,
|
|
330
|
+
previous_response_id: data.previous_response_id || null,
|
|
331
|
+
prompt_cache_key: data.prompt_cache_key || null,
|
|
332
|
+
prompt_cache_retention: data.prompt_cache_retention || null,
|
|
333
|
+
reasoning: data.reasoning || { effort: openAIRequest.reasoning?.effort || null, summary: openAIRequest.reasoning?.summary || null },
|
|
334
|
+
safety_identifier: data.safety_identifier || null,
|
|
335
|
+
service_tier: data.service_tier || 'default',
|
|
336
|
+
store: data.store !== undefined ? data.store : (openAIRequest.store !== undefined ? openAIRequest.store : true),
|
|
337
|
+
temperature: data.temperature !== undefined ? data.temperature : (openAIRequest.temperature !== undefined ? openAIRequest.temperature : 1),
|
|
338
|
+
text: data.text || { format: { type: 'text' }, verbosity: 'medium' },
|
|
339
|
+
tool_choice: data.tool_choice || openAIRequest.tool_choice || 'auto',
|
|
340
|
+
tools: data.tools || openAIRequest.tools || [],
|
|
341
|
+
top_logprobs: data.top_logprobs !== undefined ? data.top_logprobs : 0,
|
|
342
|
+
top_p: data.top_p !== undefined ? data.top_p : (openAIRequest.top_p !== undefined ? openAIRequest.top_p : 1),
|
|
343
|
+
truncation: data.truncation || 'disabled',
|
|
344
|
+
usage: data.usage || {
|
|
345
|
+
input_tokens: 0,
|
|
346
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
347
|
+
output_tokens: 0,
|
|
348
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
349
|
+
total_tokens: 0
|
|
350
|
+
},
|
|
351
|
+
user: data.user || null,
|
|
352
|
+
metadata: data.metadata || {},
|
|
353
|
+
output_text: data.output_text !== undefined ? data.output_text : ''
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Normalize output items
|
|
357
|
+
if (normalizedResponse.output && Array.isArray(normalizedResponse.output)) {
|
|
358
|
+
for (const item of normalizedResponse.output) {
|
|
359
|
+
// Ensure function_call items have proper structure
|
|
360
|
+
if (item.type === 'function_call') {
|
|
361
|
+
// Ensure id field exists
|
|
362
|
+
if (!item.id) {
|
|
363
|
+
item.id = `fc_${item.call_id || Date.now()}`;
|
|
364
|
+
}
|
|
365
|
+
// Ensure status field exists
|
|
366
|
+
if (!item.status) {
|
|
367
|
+
item.status = 'completed';
|
|
368
|
+
}
|
|
369
|
+
// Keep arguments as string (per spec)
|
|
370
|
+
if (typeof item.arguments !== 'string' && item.input) {
|
|
371
|
+
item.arguments = JSON.stringify(item.input);
|
|
372
|
+
}
|
|
373
|
+
// Ensure call_id exists
|
|
374
|
+
if (!item.call_id && item.id) {
|
|
375
|
+
item.call_id = item.id.replace(/^fc_/, 'call_');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Ensure message items have proper structure
|
|
379
|
+
else if (item.type === 'message') {
|
|
380
|
+
if (!item.id) {
|
|
381
|
+
item.id = `msg_${Date.now()}`;
|
|
382
|
+
}
|
|
383
|
+
if (!item.status) {
|
|
384
|
+
item.status = 'completed';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Ensure reasoning items have proper structure
|
|
388
|
+
else if (item.type === 'reasoning') {
|
|
389
|
+
if (!item.id) {
|
|
390
|
+
item.id = `rs_${Date.now()}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Calculate output_text if not provided
|
|
397
|
+
if (normalizedResponse.output_text === '' && normalizedResponse.output) {
|
|
398
|
+
let outputText = '';
|
|
399
|
+
for (const item of normalizedResponse.output) {
|
|
400
|
+
if (item.type === 'message' && item.content) {
|
|
401
|
+
for (const content of item.content) {
|
|
402
|
+
if (content.type === 'output_text' && content.text) {
|
|
403
|
+
outputText += content.text;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
normalizedResponse.output_text = outputText;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Ensure usage has proper structure
|
|
412
|
+
if (!normalizedResponse.usage.input_tokens_details) {
|
|
413
|
+
normalizedResponse.usage.input_tokens_details = { cached_tokens: 0 };
|
|
414
|
+
}
|
|
415
|
+
if (!normalizedResponse.usage.output_tokens_details) {
|
|
416
|
+
normalizedResponse.usage.output_tokens_details = { reasoning_tokens: 0 };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return normalizedResponse;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async _responseClaude(request, options = {}) {
|
|
423
|
+
try {
|
|
424
|
+
const claudeRequest = convertResponsesRequestToClaudeFormat(request);
|
|
425
|
+
// Log raw request payload before API call
|
|
426
|
+
this._logPayload(claudeRequest, 'REQ-CLAUDE-RAW', 'claude');
|
|
427
|
+
|
|
428
|
+
// Use streaming internally to get the response
|
|
429
|
+
const streamOptions = {};
|
|
430
|
+
if (options.signal) {
|
|
431
|
+
streamOptions.signal = options.signal;
|
|
432
|
+
}
|
|
433
|
+
const stream = await this.client.messages.stream(claudeRequest, streamOptions);
|
|
434
|
+
|
|
435
|
+
// Accumulate streaming response into a complete response object
|
|
436
|
+
let messageId = null;
|
|
437
|
+
let model = request.model || 'claude-sonnet-4-5';
|
|
438
|
+
let stopReason = null;
|
|
439
|
+
let usage = { input_tokens: 0, output_tokens: 0 };
|
|
440
|
+
const contentBlocks = [];
|
|
441
|
+
let currentTextBlock = null;
|
|
442
|
+
let currentToolBlock = null;
|
|
443
|
+
|
|
444
|
+
for await (const event of stream) {
|
|
445
|
+
// Handle different stream event types
|
|
446
|
+
if (event.type === 'message_start') {
|
|
447
|
+
messageId = event.message.id;
|
|
448
|
+
model = event.message.model;
|
|
449
|
+
usage.input_tokens = event.message.usage?.input_tokens || 0;
|
|
450
|
+
} else if (event.type === 'content_block_start') {
|
|
451
|
+
if (event.content_block?.type === 'text') {
|
|
452
|
+
currentTextBlock = { type: 'text', text: '' };
|
|
453
|
+
} else if (event.content_block?.type === 'tool_use') {
|
|
454
|
+
currentToolBlock = {
|
|
455
|
+
type: 'tool_use',
|
|
456
|
+
id: event.content_block.id,
|
|
457
|
+
name: event.content_block.name,
|
|
458
|
+
input: {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
} else if (event.type === 'content_block_delta') {
|
|
462
|
+
if (event.delta?.type === 'text_delta') {
|
|
463
|
+
if (currentTextBlock) {
|
|
464
|
+
currentTextBlock.text += event.delta.text;
|
|
465
|
+
}
|
|
466
|
+
} else if (event.delta?.type === 'input_json_delta') {
|
|
467
|
+
if (currentToolBlock) {
|
|
468
|
+
// Accumulate JSON string for tool input
|
|
469
|
+
if (!currentToolBlock._inputJson) {
|
|
470
|
+
currentToolBlock._inputJson = '';
|
|
471
|
+
}
|
|
472
|
+
currentToolBlock._inputJson += event.delta.partial_json;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} else if (event.type === 'content_block_stop') {
|
|
476
|
+
if (currentTextBlock) {
|
|
477
|
+
contentBlocks.push(currentTextBlock);
|
|
478
|
+
currentTextBlock = null;
|
|
479
|
+
} else if (currentToolBlock) {
|
|
480
|
+
// Parse accumulated JSON for tool input
|
|
481
|
+
if (currentToolBlock._inputJson) {
|
|
482
|
+
try {
|
|
483
|
+
currentToolBlock.input = JSON.parse(currentToolBlock._inputJson);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
currentToolBlock.input = {};
|
|
486
|
+
}
|
|
487
|
+
delete currentToolBlock._inputJson;
|
|
488
|
+
}
|
|
489
|
+
contentBlocks.push(currentToolBlock);
|
|
490
|
+
currentToolBlock = null;
|
|
491
|
+
}
|
|
492
|
+
} else if (event.type === 'message_delta') {
|
|
493
|
+
stopReason = event.delta?.stop_reason || stopReason;
|
|
494
|
+
usage.output_tokens = event.usage?.output_tokens || usage.output_tokens;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Construct the complete Claude response object
|
|
499
|
+
const claudeResponse = {
|
|
500
|
+
id: messageId || `msg_${Date.now()}`,
|
|
501
|
+
type: 'message',
|
|
502
|
+
role: 'assistant',
|
|
503
|
+
content: contentBlocks,
|
|
504
|
+
model: model,
|
|
505
|
+
stop_reason: stopReason || 'end_turn',
|
|
506
|
+
usage: usage
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Log raw Claude API response before conversion
|
|
510
|
+
this._logPayload(claudeResponse, 'RES-CLAUDE-RAW', 'claude');
|
|
511
|
+
const response = convertClaudeResponseToResponsesFormat(claudeResponse, request.model, request);
|
|
512
|
+
// Log response payload
|
|
513
|
+
this._logPayload(response, 'RES', 'claude');
|
|
514
|
+
return response;
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw normalizeError(error, 'claude');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async _responseGemini(request, options = {}) {
|
|
521
|
+
try {
|
|
522
|
+
const geminiRequest = convertResponsesRequestToGeminiFormat(request);
|
|
523
|
+
// Log raw request payload before API call
|
|
524
|
+
this._logPayload(geminiRequest, 'REQ-RAW', 'gemini');
|
|
525
|
+
|
|
526
|
+
// Create model configuration
|
|
527
|
+
const modelConfig = { model: request.model || 'gemini-2.5-flash' };
|
|
528
|
+
|
|
529
|
+
// Add tools to model config if present
|
|
530
|
+
if (geminiRequest.tools && geminiRequest.tools.length > 0) {
|
|
531
|
+
modelConfig.tools = geminiRequest.tools;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Add system instruction to model config if present
|
|
535
|
+
if (geminiRequest.systemInstruction) {
|
|
536
|
+
modelConfig.systemInstruction = geminiRequest.systemInstruction;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const model = this.client.getGenerativeModel(modelConfig);
|
|
540
|
+
|
|
541
|
+
// Prepare generateContent request
|
|
542
|
+
const generateRequest = {
|
|
543
|
+
contents: geminiRequest.contents
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (geminiRequest.generationConfig) {
|
|
547
|
+
generateRequest.generationConfig = geminiRequest.generationConfig;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (geminiRequest.toolConfig) {
|
|
551
|
+
generateRequest.toolConfig = geminiRequest.toolConfig;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Add abort signal if provided
|
|
555
|
+
if (options.signal) {
|
|
556
|
+
generateRequest.signal = options.signal;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const result = await model.generateContent(generateRequest);
|
|
560
|
+
const response = await result.response;
|
|
561
|
+
|
|
562
|
+
// Log raw Gemini response for debugging
|
|
563
|
+
this._logPayload({
|
|
564
|
+
candidates: response.candidates,
|
|
565
|
+
usageMetadata: response.usageMetadata,
|
|
566
|
+
promptFeedback: response.promptFeedback
|
|
567
|
+
}, 'RES-GEMINI-RAW', 'gemini');
|
|
568
|
+
|
|
569
|
+
// Convert to Responses API format
|
|
570
|
+
const convertedResponse = convertGeminiResponseToResponsesFormat({
|
|
571
|
+
candidates: [
|
|
572
|
+
{
|
|
573
|
+
content: response.candidates?.[0]?.content,
|
|
574
|
+
finishReason: response.candidates?.[0]?.finishReason
|
|
575
|
+
}
|
|
576
|
+
],
|
|
577
|
+
usageMetadata: response.usageMetadata
|
|
578
|
+
}, request.model, request);
|
|
579
|
+
// Log response payload
|
|
580
|
+
this._logPayload(convertedResponse, 'RES', 'gemini');
|
|
581
|
+
return convertedResponse;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
// Log error for debugging
|
|
584
|
+
this._logPayload({
|
|
585
|
+
error_message: error.message,
|
|
586
|
+
error_stack: error.stack,
|
|
587
|
+
error_name: error.name,
|
|
588
|
+
full_error: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)))
|
|
589
|
+
}, 'ERROR-GEMINI', 'gemini');
|
|
590
|
+
throw normalizeError(error, 'gemini');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async _responseOllama(request, options = {}) {
|
|
595
|
+
try {
|
|
596
|
+
const { url, request: ollamaRequest } = convertResponsesRequestToOllamaFormat(request, this.baseUrl);
|
|
597
|
+
// Log raw request payload before API call
|
|
598
|
+
this._logPayload(ollamaRequest, 'REQ-RAW', 'ollama');
|
|
599
|
+
|
|
600
|
+
const fetchOptions = {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: {
|
|
603
|
+
'Content-Type': 'application/json'
|
|
604
|
+
},
|
|
605
|
+
body: JSON.stringify(ollamaRequest)
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// Add abort signal if provided
|
|
609
|
+
if (options.signal) {
|
|
610
|
+
fetchOptions.signal = options.signal;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const response = await fetch(url, fetchOptions);
|
|
614
|
+
|
|
615
|
+
if (!response.ok) {
|
|
616
|
+
const error = await createErrorFromResponse(response, 'ollama');
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const ollamaResponse = await response.json();
|
|
621
|
+
// Log Ollama's raw response for debugging
|
|
622
|
+
this._logPayload(ollamaResponse, 'RES-RAW', 'ollama');
|
|
623
|
+
const convertedResponse = convertOllamaResponseToResponsesFormat(ollamaResponse, request.model, request);
|
|
624
|
+
// Log response payload
|
|
625
|
+
this._logPayload(convertedResponse, 'RES', 'ollama');
|
|
626
|
+
return convertedResponse;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw normalizeError(error, 'ollama');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ============================================
|
|
633
|
+
// Streaming Methods
|
|
634
|
+
// ============================================
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* OpenAI streaming response
|
|
638
|
+
* @param {Object} request - Responses API format request
|
|
639
|
+
* @param {Object} options - Additional options
|
|
640
|
+
* @returns {AsyncGenerator} Responses API format stream
|
|
641
|
+
*/
|
|
642
|
+
async *_responseOpenAIStream(request, options = {}) {
|
|
643
|
+
const chunks = []; // Collect all chunks for logging
|
|
644
|
+
try {
|
|
645
|
+
// Prepare streaming request (same as non-streaming)
|
|
646
|
+
const streamRequest = { ...request, stream: true };
|
|
647
|
+
|
|
648
|
+
// 1. Normalize input to Responses API format
|
|
649
|
+
if (streamRequest.input) {
|
|
650
|
+
streamRequest.input = normalizeInput(streamRequest.input);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 2. Tools conversion
|
|
654
|
+
if (streamRequest.tools && Array.isArray(streamRequest.tools)) {
|
|
655
|
+
streamRequest.tools = streamRequest.tools.map(tool => {
|
|
656
|
+
let convertedTool;
|
|
657
|
+
|
|
658
|
+
if (tool.type === 'custom' && tool.input_schema) {
|
|
659
|
+
convertedTool = {
|
|
660
|
+
type: 'function',
|
|
661
|
+
name: tool.name,
|
|
662
|
+
description: tool.description || `Tool: ${tool.name}`,
|
|
663
|
+
parameters: tool.input_schema
|
|
664
|
+
};
|
|
665
|
+
} else if (tool.type === 'function' && tool.function) {
|
|
666
|
+
convertedTool = {
|
|
667
|
+
type: 'function',
|
|
668
|
+
name: tool.function.name,
|
|
669
|
+
description: tool.function.description || `Function: ${tool.function.name}`,
|
|
670
|
+
parameters: tool.function.parameters
|
|
671
|
+
};
|
|
672
|
+
} else {
|
|
673
|
+
convertedTool = { ...tool };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Remove 'strict' field to prevent OpenAI SDK from auto-modifying 'required' fields
|
|
677
|
+
delete convertedTool.strict;
|
|
678
|
+
|
|
679
|
+
return convertedTool;
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Add reasoning configuration for reasoning models
|
|
684
|
+
const reasoningModels = ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'o3', 'o3-mini'];
|
|
685
|
+
const currentModel = streamRequest.model || 'gpt-4o-mini';
|
|
686
|
+
|
|
687
|
+
if (reasoningModels.some(m => currentModel.startsWith(m))) {
|
|
688
|
+
if (!streamRequest.reasoning) {
|
|
689
|
+
streamRequest.reasoning = {
|
|
690
|
+
effort: 'medium',
|
|
691
|
+
summary: 'auto'
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Log raw request payload before API call
|
|
697
|
+
this._logPayload(streamRequest, 'REQ-RAW', 'openai');
|
|
698
|
+
|
|
699
|
+
// Use OpenAI SDK for streaming (like ai_request.js)
|
|
700
|
+
const createOptions = {};
|
|
701
|
+
if (options.signal) {
|
|
702
|
+
createOptions.signal = options.signal;
|
|
703
|
+
}
|
|
704
|
+
const stream = await this.client.responses.create(streamRequest, createOptions);
|
|
705
|
+
|
|
706
|
+
// Stream the response chunks and convert to consistent format
|
|
707
|
+
const streamId = `resp_${Date.now()}`;
|
|
708
|
+
const created = Math.floor(Date.now() / 1000);
|
|
709
|
+
let currentMessageId = null;
|
|
710
|
+
|
|
711
|
+
for await (const chunk of stream) {
|
|
712
|
+
// Convert OpenAI SDK streaming format to our standard format
|
|
713
|
+
if (chunk.type === 'response.output_text.delta') {
|
|
714
|
+
// Text delta
|
|
715
|
+
const deltaChunk = {
|
|
716
|
+
id: streamId,
|
|
717
|
+
object: 'response.delta',
|
|
718
|
+
created_at: created,
|
|
719
|
+
model: streamRequest.model,
|
|
720
|
+
delta: {
|
|
721
|
+
type: 'output_text',
|
|
722
|
+
message_id: chunk.item_id,
|
|
723
|
+
text: chunk.delta
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
chunks.push(deltaChunk);
|
|
727
|
+
yield deltaChunk;
|
|
728
|
+
} else if (chunk.type === 'response.done' || chunk.type === 'response.completed') {
|
|
729
|
+
// Stream completed
|
|
730
|
+
const doneChunk = {
|
|
731
|
+
id: streamId,
|
|
732
|
+
object: 'response.done',
|
|
733
|
+
created_at: created,
|
|
734
|
+
model: streamRequest.model,
|
|
735
|
+
status: 'completed'
|
|
736
|
+
};
|
|
737
|
+
chunks.push(doneChunk);
|
|
738
|
+
// Log all chunks
|
|
739
|
+
this._logPayload(chunks, 'RES', 'openai');
|
|
740
|
+
yield doneChunk;
|
|
741
|
+
}
|
|
742
|
+
// Ignore other chunk types for now (response.created, response.in_progress, etc.)
|
|
743
|
+
}
|
|
744
|
+
} catch (error) {
|
|
745
|
+
throw normalizeError(error, 'openai');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Claude streaming response
|
|
751
|
+
* @param {Object} request - Responses API format request
|
|
752
|
+
* @param {Object} options - Additional options
|
|
753
|
+
* @returns {AsyncGenerator} Responses API format stream
|
|
754
|
+
*/
|
|
755
|
+
async *_responseClaudeStream(request, options = {}) {
|
|
756
|
+
const chunks = []; // Collect all chunks for logging
|
|
757
|
+
try {
|
|
758
|
+
const claudeRequest = convertResponsesRequestToClaudeFormat(request);
|
|
759
|
+
// Log raw request payload before API call
|
|
760
|
+
this._logPayload(claudeRequest, 'REQ-RAW', 'claude');
|
|
761
|
+
|
|
762
|
+
const streamOptions = {};
|
|
763
|
+
if (options.signal) {
|
|
764
|
+
streamOptions.signal = options.signal;
|
|
765
|
+
}
|
|
766
|
+
const stream = await this.client.messages.stream(claudeRequest, streamOptions);
|
|
767
|
+
|
|
768
|
+
const streamId = `resp_${Date.now()}`;
|
|
769
|
+
const created = Math.floor(Date.now() / 1000);
|
|
770
|
+
let currentMessageId = `msg_${Date.now()}`;
|
|
771
|
+
|
|
772
|
+
for await (const event of stream) {
|
|
773
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
774
|
+
// Convert to Responses API streaming format
|
|
775
|
+
const deltaChunk = {
|
|
776
|
+
id: streamId,
|
|
777
|
+
object: 'response.delta',
|
|
778
|
+
created_at: created,
|
|
779
|
+
model: request.model || 'claude-sonnet-4-5',
|
|
780
|
+
delta: {
|
|
781
|
+
type: 'output_text',
|
|
782
|
+
message_id: currentMessageId,
|
|
783
|
+
text: event.delta.text
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
chunks.push(deltaChunk);
|
|
787
|
+
yield deltaChunk;
|
|
788
|
+
} else if (event.type === 'message_delta' && event.delta?.stop_reason) {
|
|
789
|
+
// Final chunk
|
|
790
|
+
const doneChunk = {
|
|
791
|
+
id: streamId,
|
|
792
|
+
object: 'response.done',
|
|
793
|
+
created_at: created,
|
|
794
|
+
model: request.model || 'claude-sonnet-4-5',
|
|
795
|
+
status: 'completed'
|
|
796
|
+
};
|
|
797
|
+
chunks.push(doneChunk);
|
|
798
|
+
// Log all chunks
|
|
799
|
+
this._logPayload(chunks, 'RES', 'claude');
|
|
800
|
+
yield doneChunk;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
throw normalizeError(error, 'claude');
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Gemini streaming response
|
|
810
|
+
* @param {Object} request - Responses API format request
|
|
811
|
+
* @param {Object} options - Additional options
|
|
812
|
+
* @returns {AsyncGenerator} Responses API format stream
|
|
813
|
+
*/
|
|
814
|
+
async *_responseGeminiStream(request, options = {}) {
|
|
815
|
+
const chunks = []; // Collect all chunks for logging
|
|
816
|
+
try {
|
|
817
|
+
const geminiRequest = convertResponsesRequestToGeminiFormat(request);
|
|
818
|
+
// Log raw request payload before API call
|
|
819
|
+
this._logPayload(geminiRequest, 'REQ-RAW', 'gemini');
|
|
820
|
+
|
|
821
|
+
// Create model configuration
|
|
822
|
+
const modelConfig = { model: request.model || 'gemini-2.5-flash' };
|
|
823
|
+
|
|
824
|
+
if (geminiRequest.tools && geminiRequest.tools.length > 0) {
|
|
825
|
+
modelConfig.tools = geminiRequest.tools;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (geminiRequest.systemInstruction) {
|
|
829
|
+
modelConfig.systemInstruction = geminiRequest.systemInstruction;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const model = this.client.getGenerativeModel(modelConfig);
|
|
833
|
+
|
|
834
|
+
const generateRequest = {
|
|
835
|
+
contents: geminiRequest.contents
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
if (geminiRequest.generationConfig) {
|
|
839
|
+
generateRequest.generationConfig = geminiRequest.generationConfig;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (geminiRequest.toolConfig) {
|
|
843
|
+
generateRequest.toolConfig = geminiRequest.toolConfig;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Add abort signal if provided
|
|
847
|
+
if (options.signal) {
|
|
848
|
+
generateRequest.signal = options.signal;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const result = await model.generateContentStream(generateRequest);
|
|
852
|
+
|
|
853
|
+
const streamId = `resp_${Date.now()}`;
|
|
854
|
+
const created = Math.floor(Date.now() / 1000);
|
|
855
|
+
const messageId = `msg_${Date.now()}`;
|
|
856
|
+
|
|
857
|
+
for await (const chunk of result.stream) {
|
|
858
|
+
const text = chunk.text();
|
|
859
|
+
|
|
860
|
+
if (text) {
|
|
861
|
+
const deltaChunk = {
|
|
862
|
+
id: streamId,
|
|
863
|
+
object: 'response.delta',
|
|
864
|
+
created_at: created,
|
|
865
|
+
model: request.model || 'gemini-2.5-flash',
|
|
866
|
+
delta: {
|
|
867
|
+
type: 'output_text',
|
|
868
|
+
message_id: messageId,
|
|
869
|
+
text: text
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
chunks.push(deltaChunk);
|
|
873
|
+
yield deltaChunk;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Final chunk
|
|
878
|
+
const doneChunk = {
|
|
879
|
+
id: streamId,
|
|
880
|
+
object: 'response.done',
|
|
881
|
+
created_at: created,
|
|
882
|
+
model: request.model || 'gemini-2.5-flash',
|
|
883
|
+
status: 'completed'
|
|
884
|
+
};
|
|
885
|
+
chunks.push(doneChunk);
|
|
886
|
+
// Log all chunks
|
|
887
|
+
this._logPayload(chunks, 'RES', 'gemini');
|
|
888
|
+
yield doneChunk;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
throw normalizeError(error, 'gemini');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Ollama streaming response
|
|
896
|
+
* @param {Object} request - Responses API format request
|
|
897
|
+
* @param {Object} options - Additional options
|
|
898
|
+
* @returns {AsyncGenerator} Responses API format stream
|
|
899
|
+
*/
|
|
900
|
+
async *_responseOllamaStream(request, options = {}) {
|
|
901
|
+
const chunks = []; // Collect all chunks for logging
|
|
902
|
+
try {
|
|
903
|
+
const { url, request: ollamaRequest } = convertResponsesRequestToOllamaFormat(request, this.baseUrl);
|
|
904
|
+
|
|
905
|
+
// Ensure stream is enabled
|
|
906
|
+
ollamaRequest.stream = true;
|
|
907
|
+
|
|
908
|
+
// Log raw request payload before API call
|
|
909
|
+
this._logPayload(ollamaRequest, 'REQ-RAW', 'ollama');
|
|
910
|
+
|
|
911
|
+
const fetchOptions = {
|
|
912
|
+
method: 'POST',
|
|
913
|
+
headers: {
|
|
914
|
+
'Content-Type': 'application/json'
|
|
915
|
+
},
|
|
916
|
+
body: JSON.stringify(ollamaRequest)
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// Add abort signal if provided
|
|
920
|
+
if (options.signal) {
|
|
921
|
+
fetchOptions.signal = options.signal;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const response = await fetch(url, fetchOptions);
|
|
925
|
+
|
|
926
|
+
if (!response.ok) {
|
|
927
|
+
throw new Error(`Ollama API error: ${response.statusText}`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const streamId = `resp_${Date.now()}`;
|
|
931
|
+
const created = Math.floor(Date.now() / 1000);
|
|
932
|
+
const messageId = `msg_${Date.now()}`;
|
|
933
|
+
|
|
934
|
+
// Parse streaming response (newline-delimited JSON)
|
|
935
|
+
const reader = response.body.getReader();
|
|
936
|
+
const decoder = new TextDecoder();
|
|
937
|
+
let buffer = '';
|
|
938
|
+
|
|
939
|
+
while (true) {
|
|
940
|
+
const { done, value } = await reader.read();
|
|
941
|
+
|
|
942
|
+
if (done) break;
|
|
943
|
+
|
|
944
|
+
buffer += decoder.decode(value, { stream: true });
|
|
945
|
+
const lines = buffer.split('\n');
|
|
946
|
+
buffer = lines.pop() || '';
|
|
947
|
+
|
|
948
|
+
for (const line of lines) {
|
|
949
|
+
if (line.trim()) {
|
|
950
|
+
try {
|
|
951
|
+
const chunk = JSON.parse(line);
|
|
952
|
+
|
|
953
|
+
if (chunk.message?.content) {
|
|
954
|
+
const deltaChunk = {
|
|
955
|
+
id: streamId,
|
|
956
|
+
object: 'response.delta',
|
|
957
|
+
created_at: created,
|
|
958
|
+
model: request.model,
|
|
959
|
+
delta: {
|
|
960
|
+
type: 'output_text',
|
|
961
|
+
message_id: messageId,
|
|
962
|
+
text: chunk.message.content
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
chunks.push(deltaChunk);
|
|
966
|
+
yield deltaChunk;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (chunk.done) {
|
|
970
|
+
const doneChunk = {
|
|
971
|
+
id: streamId,
|
|
972
|
+
object: 'response.done',
|
|
973
|
+
created_at: created,
|
|
974
|
+
model: request.model,
|
|
975
|
+
status: 'completed'
|
|
976
|
+
};
|
|
977
|
+
chunks.push(doneChunk);
|
|
978
|
+
// Log all chunks
|
|
979
|
+
this._logPayload(chunks, 'RES', 'ollama');
|
|
980
|
+
yield doneChunk;
|
|
981
|
+
}
|
|
982
|
+
} catch (parseError) {
|
|
983
|
+
console.error('Error parsing Ollama stream chunk:', parseError);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} catch (error) {
|
|
989
|
+
throw normalizeError(error, 'ollama');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|