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.

Files changed (50) hide show
  1. package/README.md +1 -0
  2. package/index.js +13 -11
  3. package/mcp-agent-lib/init.sh +3 -0
  4. package/mcp-agent-lib/package-lock.json +14 -1
  5. package/mcp-agent-lib/package.json +4 -6
  6. package/mcp-agent-lib/sampleFastMCPClient/client.py +25 -0
  7. package/mcp-agent-lib/sampleFastMCPClient/run.sh +3 -0
  8. package/mcp-agent-lib/sampleFastMCPServer/run.sh +3 -0
  9. package/mcp-agent-lib/sampleFastMCPServer/server.py +12 -0
  10. package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/run.sh +3 -0
  11. package/mcp-agent-lib/sampleFastMCPServerElicitationRequest/server.py +43 -0
  12. package/mcp-agent-lib/sampleFastMCPServerRootsRequest/server.py +63 -0
  13. package/mcp-agent-lib/sampleMCPHost/index.js +182 -63
  14. package/mcp-agent-lib/sampleMCPHost/mcp_config.json +7 -1
  15. package/mcp-agent-lib/sampleMCPHostFeatures/elicitation.js +151 -0
  16. package/mcp-agent-lib/sampleMCPHostFeatures/index.js +166 -0
  17. package/mcp-agent-lib/sampleMCPHostFeatures/roots.js +197 -0
  18. package/mcp-agent-lib/src/mcp_client.js +129 -67
  19. package/mcp-agent-lib/src/mcp_message_logger.js +516 -0
  20. package/package.json +3 -1
  21. package/payload_viewer/out/404/index.html +1 -1
  22. package/payload_viewer/out/404.html +1 -1
  23. package/payload_viewer/out/index.html +1 -1
  24. package/payload_viewer/out/index.txt +1 -1
  25. package/src/LLMClient/client.js +992 -0
  26. package/src/LLMClient/converters/input-normalizer.js +238 -0
  27. package/src/LLMClient/converters/responses-to-claude.js +454 -0
  28. package/src/LLMClient/converters/responses-to-gemini.js +648 -0
  29. package/src/LLMClient/converters/responses-to-ollama.js +348 -0
  30. package/src/LLMClient/errors.js +372 -0
  31. package/src/LLMClient/index.js +31 -0
  32. package/src/commands/apikey.js +10 -22
  33. package/src/commands/model.js +28 -28
  34. package/src/commands/reasoning_effort.js +9 -23
  35. package/src/config/ai_models.js +212 -0
  36. package/src/config/feature_flags.js +1 -1
  37. package/src/frontend/App.js +5 -10
  38. package/src/frontend/components/CurrentModelView.js +0 -33
  39. package/src/frontend/components/Footer.js +3 -3
  40. package/src/frontend/components/ModelListView.js +30 -87
  41. package/src/frontend/components/ModelUpdatedView.js +7 -142
  42. package/src/frontend/components/SetupWizard.js +37 -32
  43. package/src/system/ai_request.js +57 -42
  44. package/src/util/config.js +26 -4
  45. package/src/util/setup_wizard.js +1 -6
  46. package/mcp-agent-lib/.claude/settings.local.json +0 -9
  47. package/src/config/openai_models.js +0 -152
  48. /package/payload_viewer/out/_next/static/{w4dMVYalgk7djrLxRxWiE → d0-fu2rgYnshgGFPxr1CR}/_buildManifest.js +0 -0
  49. /package/payload_viewer/out/_next/static/{w4dMVYalgk7djrLxRxWiE → d0-fu2rgYnshgGFPxr1CR}/_clientMiddlewareManifest.json +0 -0
  50. /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
+ }