aiexecode 1.0.94 → 1.0.127

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 (80) hide show
  1. package/README.md +198 -88
  2. package/index.js +310 -86
  3. package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
  4. package/package.json +4 -4
  5. package/payload_viewer/out/404/index.html +1 -1
  6. package/payload_viewer/out/404.html +1 -1
  7. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  8. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  9. package/payload_viewer/out/index.html +1 -1
  10. package/payload_viewer/out/index.txt +3 -3
  11. package/payload_viewer/web_server.js +361 -0
  12. package/prompts/completion_judge.txt +4 -0
  13. package/prompts/orchestrator.txt +116 -3
  14. package/src/LLMClient/client.js +401 -18
  15. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  16. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  17. package/src/LLMClient/errors.js +30 -4
  18. package/src/LLMClient/index.js +5 -0
  19. package/src/ai_based/completion_judge.js +263 -186
  20. package/src/ai_based/orchestrator.js +171 -35
  21. package/src/commands/agents.js +70 -0
  22. package/src/commands/apikey.js +1 -1
  23. package/src/commands/bg.js +129 -0
  24. package/src/commands/commands.js +51 -0
  25. package/src/commands/debug.js +52 -0
  26. package/src/commands/help.js +11 -1
  27. package/src/commands/model.js +42 -7
  28. package/src/commands/reasoning_effort.js +2 -2
  29. package/src/commands/skills.js +46 -0
  30. package/src/config/ai_models.js +106 -6
  31. package/src/config/constants.js +71 -0
  32. package/src/config/feature_flags.js +6 -7
  33. package/src/frontend/App.js +108 -1
  34. package/src/frontend/components/AutocompleteMenu.js +7 -1
  35. package/src/frontend/components/BackgroundProcessList.js +175 -0
  36. package/src/frontend/components/ConversationItem.js +26 -10
  37. package/src/frontend/components/CurrentModelView.js +2 -2
  38. package/src/frontend/components/HelpView.js +106 -2
  39. package/src/frontend/components/Input.js +33 -11
  40. package/src/frontend/components/ModelListView.js +1 -1
  41. package/src/frontend/components/SetupWizard.js +51 -8
  42. package/src/frontend/hooks/useFileCompletion.js +467 -0
  43. package/src/frontend/utils/toolUIFormatter.js +261 -0
  44. package/src/system/agents_loader.js +289 -0
  45. package/src/system/ai_request.js +156 -12
  46. package/src/system/background_process.js +317 -0
  47. package/src/system/code_executer.js +496 -56
  48. package/src/system/command_parser.js +33 -3
  49. package/src/system/conversation_state.js +265 -0
  50. package/src/system/conversation_trimmer.js +132 -0
  51. package/src/system/custom_command_loader.js +386 -0
  52. package/src/system/file_integrity.js +73 -10
  53. package/src/system/log.js +10 -2
  54. package/src/system/output_helper.js +52 -9
  55. package/src/system/session.js +213 -58
  56. package/src/system/session_memory.js +30 -2
  57. package/src/system/skill_loader.js +318 -0
  58. package/src/system/system_info.js +254 -40
  59. package/src/system/tool_approval.js +10 -0
  60. package/src/system/tool_registry.js +15 -1
  61. package/src/system/ui_events.js +11 -0
  62. package/src/tools/code_editor.js +16 -10
  63. package/src/tools/file_reader.js +66 -9
  64. package/src/tools/glob.js +0 -3
  65. package/src/tools/ripgrep.js +5 -7
  66. package/src/tools/skill_tool.js +122 -0
  67. package/src/tools/web_downloader.js +0 -3
  68. package/src/util/clone.js +174 -0
  69. package/src/util/config.js +55 -2
  70. package/src/util/config_migration.js +174 -0
  71. package/src/util/debug_log.js +8 -2
  72. package/src/util/exit_handler.js +8 -0
  73. package/src/util/file_reference_parser.js +132 -0
  74. package/src/util/path_validator.js +178 -0
  75. package/src/util/prompt_loader.js +91 -1
  76. package/src/util/safe_fs.js +66 -3
  77. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  78. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Convert Responses API format to Z.AI (GLM) format
3
+ * Z.AI uses Anthropic Messages API compatible interface
4
+ * Base URL: https://api.z.ai/api/anthropic
5
+ */
6
+
7
+ import { getMaxTokens } from '../../config/ai_models.js';
8
+ import { createDebugLogger } from '../../util/debug_log.js';
9
+
10
+ const debugLog = createDebugLogger('zai_converter.log', 'zai_converter');
11
+
12
+ /**
13
+ * Convert snake_case to camelCase
14
+ * @param {string} str - snake_case string
15
+ * @returns {string} camelCase string
16
+ */
17
+ function snakeToCamel(str) {
18
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
19
+ }
20
+
21
+ /**
22
+ * Convert camelCase to snake_case
23
+ * @param {string} str - camelCase string
24
+ * @returns {string} snake_case string
25
+ */
26
+ function camelToSnake(str) {
27
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase();
28
+ }
29
+
30
+ /**
31
+ * Recursively add both snake_case and camelCase keys to an object
32
+ * This ensures compatibility with functions expecting either naming convention
33
+ * @param {any} obj - Object to process
34
+ * @returns {any} Object with both key formats
35
+ */
36
+ function addBothCaseKeys(obj) {
37
+ if (obj === null || typeof obj !== 'object') {
38
+ return obj;
39
+ }
40
+ if (Array.isArray(obj)) {
41
+ return obj.map(item => addBothCaseKeys(item));
42
+ }
43
+ const result = {};
44
+ for (const key of Object.keys(obj)) {
45
+ const value = addBothCaseKeys(obj[key]);
46
+ // Add original key
47
+ result[key] = value;
48
+ // Add camelCase version if key is snake_case
49
+ const camelKey = snakeToCamel(key);
50
+ if (camelKey !== key) {
51
+ result[camelKey] = value;
52
+ }
53
+ // Add snake_case version if key is camelCase
54
+ const snakeKey = camelToSnake(key);
55
+ if (snakeKey !== key) {
56
+ result[snakeKey] = value;
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Convert Responses API request to Z.AI format
64
+ * @param {Object} responsesRequest - Responses API format request
65
+ * @returns {Object} Z.AI (Anthropic-compatible) format request
66
+ */
67
+ export function convertResponsesRequestToZaiFormat(responsesRequest) {
68
+ const startTime = Date.now();
69
+ debugLog(`[convertRequest] START: model=${responsesRequest.model}, input_items=${responsesRequest.input?.length || 0}`);
70
+
71
+ const model = responsesRequest.model;
72
+ if (!model) {
73
+ throw new Error('Model name is required');
74
+ }
75
+
76
+ const defaultMaxTokens = getMaxTokens(model);
77
+
78
+ const zaiRequest = {
79
+ model: model,
80
+ max_tokens: responsesRequest.max_output_tokens || defaultMaxTokens
81
+ };
82
+
83
+ // Convert input to messages
84
+ const messages = [];
85
+
86
+ if (typeof responsesRequest.input === 'string') {
87
+ messages.push({
88
+ role: 'user',
89
+ content: responsesRequest.input
90
+ });
91
+ } else if (Array.isArray(responsesRequest.input)) {
92
+ for (const item of responsesRequest.input) {
93
+ // Handle output items (no role, has type)
94
+ if (!item.role && item.type) {
95
+ if (item.type === 'message') {
96
+ const textBlocks = [];
97
+ if (item.content && Array.isArray(item.content)) {
98
+ for (const contentBlock of item.content) {
99
+ if (contentBlock.type === 'output_text' && contentBlock.text) {
100
+ textBlocks.push({
101
+ type: 'text',
102
+ text: contentBlock.text
103
+ });
104
+ }
105
+ }
106
+ }
107
+ if (textBlocks.length > 0) {
108
+ messages.push({
109
+ role: 'assistant',
110
+ content: textBlocks
111
+ });
112
+ }
113
+ // Note: If message has no text content, it will be handled by the
114
+ // subsequent function_call items which create their own assistant messages
115
+ } else if (item.type === 'function_call') {
116
+ // Add placeholder text with tool_use for Z.AI/Anthropic API compatibility
117
+ messages.push({
118
+ role: 'assistant',
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: '(no content)'
123
+ },
124
+ {
125
+ type: 'tool_use',
126
+ id: item.call_id || item.id,
127
+ name: item.name,
128
+ input: JSON.parse(item.arguments || '{}')
129
+ }
130
+ ]
131
+ });
132
+ } else if (item.type === 'function_call_output') {
133
+ // Build tool_result object with proper error handling
134
+ const toolResult = {
135
+ type: 'tool_result',
136
+ tool_use_id: item.call_id,
137
+ content: typeof item.output === 'string' ? item.output : JSON.stringify(item.output)
138
+ };
139
+
140
+ // Add is_error flag if the output indicates an error
141
+ // Check for common error patterns in the output
142
+ if (item.is_error === true) {
143
+ toolResult.is_error = true;
144
+ } else if (typeof item.output === 'object' && item.output !== null) {
145
+ // Check for operation_successful: false pattern
146
+ if (item.output.operation_successful === false ||
147
+ item.output.stdout?.operation_successful === false) {
148
+ toolResult.is_error = true;
149
+ }
150
+ }
151
+
152
+ messages.push({
153
+ role: 'user',
154
+ content: [toolResult]
155
+ });
156
+ }
157
+ continue;
158
+ }
159
+
160
+ if (item.role && item.content) {
161
+ if (item.role === 'system') {
162
+ // Z.AI는 Anthropic API와 호환 - system을 배열로 지원 (캐시 제어 포함)
163
+ if (Array.isArray(item.content)) {
164
+ // 배열인 경우: 캐시 제어가 있는 블록들을 처리
165
+ const systemBlocks = item.content.map(c => {
166
+ const block = {
167
+ type: 'text',
168
+ text: c.type === 'input_text' || c.type === 'text' ? c.text : (typeof c === 'string' ? c : '')
169
+ };
170
+ // cache_control이 있으면 유지 (Claude Code 스타일)
171
+ if (c.cache_control) {
172
+ block.cache_control = c.cache_control;
173
+ }
174
+ return block;
175
+ }).filter(b => b.text);
176
+
177
+ // 캐시 제어가 있는 블록이 있으면 배열로, 없으면 단순 문자열로
178
+ const hasCacheControl = systemBlocks.some(b => b.cache_control);
179
+ if (hasCacheControl) {
180
+ zaiRequest.system = systemBlocks;
181
+ debugLog(`[convertRequest] System message with cache_control: ${systemBlocks.length} blocks`);
182
+ } else {
183
+ zaiRequest.system = systemBlocks.map(b => b.text).join('\n');
184
+ }
185
+ } else {
186
+ zaiRequest.system = item.content;
187
+ }
188
+ } else if (item.role === 'tool') {
189
+ const toolResult = {
190
+ type: 'tool_result',
191
+ tool_use_id: item.tool_call_id || item.id,
192
+ content: typeof item.content === 'string' ? item.content : JSON.stringify(item.content)
193
+ };
194
+
195
+ // Add is_error flag if present
196
+ if (item.is_error === true) {
197
+ toolResult.is_error = true;
198
+ }
199
+
200
+ messages.push({
201
+ role: 'user',
202
+ content: [toolResult]
203
+ });
204
+ } else if (item.role === 'assistant' && Array.isArray(item.content)) {
205
+ const textBlocks = [];
206
+ const toolUseBlocks = [];
207
+
208
+ for (const outputItem of item.content) {
209
+ if (outputItem.type === 'message' && outputItem.content) {
210
+ for (const contentBlock of outputItem.content) {
211
+ if (contentBlock.type === 'output_text' && contentBlock.text) {
212
+ textBlocks.push({
213
+ type: 'text',
214
+ text: contentBlock.text
215
+ });
216
+ }
217
+ }
218
+ } else if (outputItem.type === 'function_call') {
219
+ toolUseBlocks.push({
220
+ type: 'tool_use',
221
+ id: outputItem.call_id || outputItem.id,
222
+ name: outputItem.name,
223
+ input: JSON.parse(outputItem.arguments || '{}')
224
+ });
225
+ }
226
+ }
227
+
228
+ // If we have tool_use blocks but no text, add a placeholder text
229
+ // Z.AI/Anthropic API recommends having text content with tool calls
230
+ if (toolUseBlocks.length > 0 && textBlocks.length === 0) {
231
+ textBlocks.push({
232
+ type: 'text',
233
+ text: '(no content)'
234
+ });
235
+ }
236
+
237
+ const zaiContent = [...textBlocks, ...toolUseBlocks];
238
+
239
+ if (zaiContent.length > 0) {
240
+ messages.push({
241
+ role: 'assistant',
242
+ content: zaiContent
243
+ });
244
+ }
245
+ } else {
246
+ // 일반 user/assistant 메시지 처리
247
+ if (Array.isArray(item.content)) {
248
+ // 캐시 제어가 있는 content 블록 확인
249
+ const hasCacheControl = item.content.some(c => c.cache_control);
250
+
251
+ if (hasCacheControl) {
252
+ // 캐시 제어가 있는 경우: 블록 배열로 유지
253
+ const contentBlocks = item.content.map(c => {
254
+ const block = {
255
+ type: 'text',
256
+ text: c.type === 'input_text' || c.type === 'text' ? c.text : (typeof c === 'string' ? c : '')
257
+ };
258
+ if (c.cache_control) {
259
+ block.cache_control = c.cache_control;
260
+ }
261
+ return block;
262
+ }).filter(b => b.text);
263
+
264
+ messages.push({
265
+ role: item.role === 'assistant' ? 'assistant' : 'user',
266
+ content: contentBlocks
267
+ });
268
+ debugLog(`[convertRequest] User message with cache_control: ${contentBlocks.length} blocks`);
269
+ } else {
270
+ // 캐시 제어 없음: 단순 텍스트로 합침
271
+ const content = item.content.map(c => c.type === 'input_text' || c.type === 'text' ? c.text : c).filter(Boolean).join('\n');
272
+ messages.push({
273
+ role: item.role === 'assistant' ? 'assistant' : 'user',
274
+ content: content
275
+ });
276
+ }
277
+ } else {
278
+ messages.push({
279
+ role: item.role === 'assistant' ? 'assistant' : 'user',
280
+ content: item.content
281
+ });
282
+ }
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ // Merge consecutive messages with the same role
289
+ const mergedMessages = [];
290
+ for (let i = 0; i < messages.length; i++) {
291
+ const currentMsg = messages[i];
292
+
293
+ if (i < messages.length - 1 && messages[i + 1].role === currentMsg.role) {
294
+ const mergedContent = Array.isArray(currentMsg.content) ? [...currentMsg.content] : [currentMsg.content];
295
+
296
+ while (i < messages.length - 1 && messages[i + 1].role === currentMsg.role) {
297
+ i++;
298
+ const nextContent = messages[i].content;
299
+ if (Array.isArray(nextContent)) {
300
+ mergedContent.push(...nextContent);
301
+ } else {
302
+ mergedContent.push(nextContent);
303
+ }
304
+ }
305
+
306
+ mergedMessages.push({
307
+ role: currentMsg.role,
308
+ content: mergedContent
309
+ });
310
+ } else {
311
+ mergedMessages.push(currentMsg);
312
+ }
313
+ }
314
+
315
+ // Normalize content format for Z.AI/Anthropic API
316
+ // Content arrays must contain objects with {type: "text", text: "..."} format
317
+ for (const msg of mergedMessages) {
318
+ if (Array.isArray(msg.content)) {
319
+ msg.content = msg.content.map(item => {
320
+ // If item is already in correct format, keep it
321
+ if (typeof item === 'object' && item !== null && item.type) {
322
+ return item;
323
+ }
324
+ // Convert plain string to text block format
325
+ if (typeof item === 'string') {
326
+ return { type: 'text', text: item };
327
+ }
328
+ // Fallback: convert to string
329
+ return { type: 'text', text: String(item) };
330
+ });
331
+ }
332
+ }
333
+
334
+ zaiRequest.messages = mergedMessages;
335
+
336
+ // Handle instructions (system message)
337
+ if (responsesRequest.instructions) {
338
+ zaiRequest.system = responsesRequest.instructions;
339
+ }
340
+
341
+ // Convert tools from Responses API format to Z.AI (Anthropic) format
342
+ if (responsesRequest.tools && Array.isArray(responsesRequest.tools)) {
343
+ zaiRequest.tools = responsesRequest.tools.map(tool => {
344
+ if (tool.type === 'function') {
345
+ if (tool.function) {
346
+ return {
347
+ name: tool.function.name,
348
+ description: tool.function.description || `Function: ${tool.function.name}`,
349
+ input_schema: tool.function.parameters || {
350
+ type: 'object',
351
+ properties: {}
352
+ }
353
+ };
354
+ } else {
355
+ return {
356
+ name: tool.name,
357
+ description: tool.description || `Function: ${tool.name}`,
358
+ input_schema: tool.parameters || {
359
+ type: 'object',
360
+ properties: {}
361
+ }
362
+ };
363
+ }
364
+ } else if (tool.type === 'custom') {
365
+ return {
366
+ name: tool.name,
367
+ description: tool.description || `Tool: ${tool.name}`,
368
+ input_schema: tool.input_schema || {
369
+ type: 'object',
370
+ properties: {}
371
+ }
372
+ };
373
+ }
374
+ return {
375
+ name: tool.name,
376
+ description: tool.description,
377
+ input_schema: tool.input_schema
378
+ };
379
+ });
380
+ }
381
+
382
+ // Temperature - always set to 0 for consistent results
383
+ zaiRequest.temperature = 0;
384
+
385
+ // Stream - Z.AI requires this field (will be overridden by SDK if using stream method)
386
+ zaiRequest.stream = responsesRequest.stream || false;
387
+
388
+ // Tool choice
389
+ if (responsesRequest.tool_choice !== undefined) {
390
+ if (typeof responsesRequest.tool_choice === 'string') {
391
+ if (responsesRequest.tool_choice === 'auto') {
392
+ zaiRequest.tool_choice = { type: 'auto' };
393
+ } else if (responsesRequest.tool_choice === 'required') {
394
+ zaiRequest.tool_choice = { type: 'any' };
395
+ }
396
+ } else if (responsesRequest.tool_choice?.type === 'function' || responsesRequest.tool_choice?.type === 'custom') {
397
+ const toolName = responsesRequest.tool_choice.function?.name || responsesRequest.tool_choice.name;
398
+ zaiRequest.tool_choice = {
399
+ type: 'tool',
400
+ name: toolName
401
+ };
402
+ }
403
+ }
404
+
405
+ // Metadata
406
+ if (responsesRequest.metadata) {
407
+ zaiRequest.metadata = responsesRequest.metadata;
408
+ }
409
+
410
+ // Assistant Prefill 지원
411
+ // Z.AI/GLM에서 JSON 응답을 유도하기 위해 assistant 메시지를 미리 추가
412
+ if (responsesRequest.assistant_prefill) {
413
+ zaiRequest.messages.push({
414
+ role: 'assistant',
415
+ content: [
416
+ {
417
+ type: 'text',
418
+ text: responsesRequest.assistant_prefill
419
+ }
420
+ ]
421
+ });
422
+ debugLog(`[convertRequest] Assistant prefill added: ${responsesRequest.assistant_prefill}`);
423
+ }
424
+
425
+ // Handle json_schema format by converting to tool use
426
+ if (responsesRequest.text?.format?.type === 'json_schema') {
427
+ const schemaName = responsesRequest.text.format.name || 'output';
428
+ const schema = responsesRequest.text.format.schema;
429
+
430
+ const syntheticTool = {
431
+ name: schemaName,
432
+ description: `Generate structured output matching the ${schemaName} schema`,
433
+ input_schema: schema
434
+ };
435
+
436
+ zaiRequest.tools = [syntheticTool];
437
+
438
+ zaiRequest.tool_choice = {
439
+ type: 'tool',
440
+ name: schemaName
441
+ };
442
+
443
+ if (zaiRequest.messages.length > 0 && zaiRequest.messages[zaiRequest.messages.length - 1].role === 'assistant') {
444
+ zaiRequest.messages.push({
445
+ role: 'user',
446
+ content: [{ type: 'text', text: 'Please provide the structured output.' }]
447
+ });
448
+ }
449
+ }
450
+
451
+ const elapsed = Date.now() - startTime;
452
+ debugLog(`[convertRequest] END: ${elapsed}ms, messages=${zaiRequest.messages?.length}, system_len=${zaiRequest.system?.length || 0}, tools=${zaiRequest.tools?.length || 0}`);
453
+
454
+ return zaiRequest;
455
+ }
456
+
457
+ /**
458
+ * Convert Z.AI response to Responses API format
459
+ * @param {Object} zaiResponse - Z.AI (Anthropic-compatible) format response
460
+ * @param {string} model - Model name
461
+ * @param {Object} originalRequest - Original request for context
462
+ * @returns {Object} Responses API format response
463
+ */
464
+ export function convertZaiResponseToResponsesFormat(zaiResponse, model = 'glm-4.7', originalRequest = {}) {
465
+ const startTime = Date.now();
466
+ debugLog(`[convertResponse] START: id=${zaiResponse.id}, content_blocks=${zaiResponse.content?.length || 0}`);
467
+
468
+ const output = [];
469
+ let outputText = '';
470
+
471
+ const wasJsonSchemaRequest = originalRequest.text?.format?.type === 'json_schema';
472
+ const schemaName = originalRequest.text?.format?.name;
473
+
474
+ // Process content blocks
475
+ if (zaiResponse.content && Array.isArray(zaiResponse.content)) {
476
+ const messageContent = [];
477
+ let jsonSchemaOutput = null; // json_schema 요청 시 tool_use의 input 저장
478
+
479
+ // 먼저 tool_use 블록에서 json_schema 출력 확인
480
+ if (wasJsonSchemaRequest) {
481
+ for (const block of zaiResponse.content) {
482
+ if (block.type === 'tool_use' && block.name === schemaName) {
483
+ jsonSchemaOutput = JSON.stringify(block.input);
484
+ debugLog(`[convertResponse] Found json_schema tool_use: ${jsonSchemaOutput}`);
485
+ break;
486
+ }
487
+ }
488
+ }
489
+
490
+ for (const block of zaiResponse.content) {
491
+ if (block.type === 'thinking') {
492
+ // Z.AI/GLM thinking 블록 → Responses API reasoning 타입으로 변환
493
+ output.push({
494
+ id: `reasoning_${zaiResponse.id}_${output.length}`,
495
+ type: 'reasoning',
496
+ status: 'completed',
497
+ content: [
498
+ {
499
+ type: 'thinking',
500
+ thinking: block.thinking || ''
501
+ }
502
+ ]
503
+ });
504
+ debugLog(`[convertResponse] Converted thinking block: ${(block.thinking || '').length} chars`);
505
+ } else if (block.type === 'text') {
506
+ // json_schema 요청이고 tool_use가 있으면, text 블록은 output_text에 포함하지 않음
507
+ // (GLM 모델이 text + tool_use를 둘 다 반환하는 경우 대응)
508
+ if (wasJsonSchemaRequest && jsonSchemaOutput) {
509
+ debugLog(`[convertResponse] Skipping text block for json_schema request (tool_use found)`);
510
+ // text 블록은 messageContent에만 추가 (참고용)
511
+ messageContent.push({
512
+ type: 'output_text',
513
+ text: block.text,
514
+ annotations: []
515
+ });
516
+ // outputText에는 추가하지 않음!
517
+ } else {
518
+ messageContent.push({
519
+ type: 'output_text',
520
+ text: block.text,
521
+ annotations: []
522
+ });
523
+ outputText += block.text;
524
+ }
525
+ } else if (block.type === 'tool_use') {
526
+ if (wasJsonSchemaRequest && block.name === schemaName) {
527
+ // json_schema 요청의 tool_use는 outputText로만 변환
528
+ // Convert snake_case keys to camelCase
529
+ const convertedInput = addBothCaseKeys(block.input);
530
+ const jsonOutput = JSON.stringify(convertedInput);
531
+ messageContent.push({
532
+ type: 'output_text',
533
+ text: jsonOutput,
534
+ annotations: []
535
+ });
536
+ outputText = jsonOutput; // outputText를 JSON만으로 설정 (덮어쓰기)
537
+ } else {
538
+ // Z.AI uses 'call_' prefix for tool IDs
539
+ // Convert snake_case keys to camelCase (some models like GLM return snake_case)
540
+ const convertedInput = addBothCaseKeys(block.input);
541
+ const originalKeys = Object.keys(block.input || {}).join(',');
542
+ const convertedKeys = Object.keys(convertedInput || {}).join(',');
543
+ if (originalKeys !== convertedKeys) {
544
+ debugLog(`[convertResponse] Added both case keys: ${originalKeys} -> ${convertedKeys}`);
545
+ }
546
+ output.push({
547
+ id: `fc_${block.id}`,
548
+ type: 'function_call',
549
+ status: 'completed',
550
+ arguments: JSON.stringify(convertedInput),
551
+ call_id: block.id,
552
+ name: block.name
553
+ });
554
+ }
555
+ }
556
+ }
557
+
558
+ if (messageContent.length > 0) {
559
+ output.push({
560
+ id: `msg_${zaiResponse.id}`,
561
+ type: 'message',
562
+ status: 'completed',
563
+ role: 'assistant',
564
+ content: messageContent
565
+ });
566
+ }
567
+ }
568
+
569
+ if (output.length === 0) {
570
+ output.push({
571
+ id: `msg_${zaiResponse.id}`,
572
+ type: 'message',
573
+ status: 'completed',
574
+ role: 'assistant',
575
+ content: [
576
+ {
577
+ type: 'output_text',
578
+ text: outputText || ' ',
579
+ annotations: []
580
+ }
581
+ ]
582
+ });
583
+ }
584
+
585
+ // Determine status and incomplete_details based on stop_reason
586
+ const stopReason = zaiResponse.stop_reason;
587
+ const isCompleted = stopReason === 'end_turn' || stopReason === 'tool_use';
588
+ const isMaxTokens = stopReason === 'max_tokens';
589
+
590
+ // Build incomplete_details if response was truncated
591
+ let incompleteDetails = null;
592
+ if (isMaxTokens) {
593
+ incompleteDetails = {
594
+ reason: 'max_output_tokens',
595
+ message: 'Response was truncated because it reached the maximum output token limit'
596
+ };
597
+ debugLog(`[convertResponse] WARNING: Response truncated due to max_tokens`);
598
+ } else if (!isCompleted && stopReason) {
599
+ incompleteDetails = {
600
+ reason: stopReason,
601
+ message: `Response stopped with reason: ${stopReason}`
602
+ };
603
+ }
604
+
605
+ const responsesResponse = {
606
+ id: `resp_${zaiResponse.id}`,
607
+ object: 'response',
608
+ created_at: Math.floor(Date.now() / 1000),
609
+ status: isCompleted ? 'completed' : 'incomplete',
610
+ background: false,
611
+ billing: {
612
+ payer: 'developer'
613
+ },
614
+ error: null,
615
+ incomplete_details: incompleteDetails,
616
+ instructions: originalRequest.instructions || null,
617
+ max_output_tokens: originalRequest.max_output_tokens || null,
618
+ max_tool_calls: null,
619
+ model: model,
620
+ output: output,
621
+ parallel_tool_calls: true,
622
+ previous_response_id: null,
623
+ prompt_cache_key: null,
624
+ prompt_cache_retention: null,
625
+ reasoning: {
626
+ effort: originalRequest.reasoning?.effort || null,
627
+ summary: originalRequest.reasoning?.summary || null
628
+ },
629
+ safety_identifier: null,
630
+ service_tier: zaiResponse.usage?.service_tier || 'standard',
631
+ store: originalRequest.store !== undefined ? originalRequest.store : true,
632
+ temperature: originalRequest.temperature !== undefined ? originalRequest.temperature : 1,
633
+ text: {
634
+ format: {
635
+ type: 'text'
636
+ },
637
+ verbosity: 'medium'
638
+ },
639
+ tool_choice: originalRequest.tool_choice || 'auto',
640
+ tools: originalRequest.tools || [],
641
+ top_logprobs: 0,
642
+ top_p: originalRequest.top_p !== undefined ? originalRequest.top_p : 1,
643
+ truncation: 'disabled',
644
+ usage: {
645
+ input_tokens: zaiResponse.usage?.input_tokens || 0,
646
+ input_tokens_details: {
647
+ cached_tokens: zaiResponse.usage?.cache_read_input_tokens || 0
648
+ },
649
+ output_tokens: zaiResponse.usage?.output_tokens || 0,
650
+ output_tokens_details: {
651
+ reasoning_tokens: 0
652
+ },
653
+ total_tokens: (zaiResponse.usage?.input_tokens || 0) + (zaiResponse.usage?.output_tokens || 0),
654
+ // Z.AI 추가 정보
655
+ cache_read_input_tokens: zaiResponse.usage?.cache_read_input_tokens || 0,
656
+ server_tool_use: zaiResponse.usage?.server_tool_use || null
657
+ },
658
+ user: null,
659
+ metadata: {},
660
+ output_text: outputText
661
+ };
662
+
663
+ const elapsed = Date.now() - startTime;
664
+ debugLog(`[convertResponse] END: ${elapsed}ms, output_items=${output.length}, output_text_len=${outputText.length}`);
665
+
666
+ return responsesResponse;
667
+ }