aiexecode 1.0.66 → 1.0.69

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 (87) hide show
  1. package/config_template/settings.json +1 -3
  2. package/index.js +46 -71
  3. package/package.json +1 -12
  4. package/payload_viewer/out/404/index.html +1 -1
  5. package/payload_viewer/out/404.html +1 -1
  6. package/payload_viewer/out/index.html +1 -1
  7. package/payload_viewer/out/index.txt +1 -1
  8. package/payload_viewer/web_server.js +0 -163
  9. package/prompts/completion_judge.txt +11 -7
  10. package/src/ai_based/completion_judge.js +97 -6
  11. package/src/ai_based/orchestrator.js +71 -3
  12. package/src/ai_based/pip_package_installer.js +14 -12
  13. package/src/ai_based/pip_package_lookup.js +13 -10
  14. package/src/commands/apikey.js +8 -34
  15. package/src/commands/help.js +3 -4
  16. package/src/commands/model.js +17 -74
  17. package/src/commands/reasoning_effort.js +1 -1
  18. package/src/config/feature_flags.js +0 -12
  19. package/src/{ui → frontend}/App.js +23 -25
  20. package/src/frontend/README.md +81 -0
  21. package/src/{ui/components/SuggestionsDisplay.js → frontend/components/AutocompleteMenu.js} +3 -3
  22. package/src/{ui/components/HistoryItemDisplay.js → frontend/components/ConversationItem.js} +37 -89
  23. package/src/{ui → frontend}/components/CurrentModelView.js +3 -5
  24. package/src/{ui → frontend}/components/Footer.js +4 -6
  25. package/src/{ui → frontend}/components/Header.js +2 -5
  26. package/src/{ui/components/InputPrompt.js → frontend/components/Input.js} +16 -54
  27. package/src/frontend/components/ModelListView.js +106 -0
  28. package/src/{ui → frontend}/components/ModelUpdatedView.js +3 -5
  29. package/src/{ui → frontend}/components/SessionSpinner.js +3 -3
  30. package/src/{ui → frontend}/components/SetupWizard.js +8 -101
  31. package/src/{ui → frontend}/components/ToolApprovalPrompt.js +16 -14
  32. package/src/frontend/design/themeColors.js +42 -0
  33. package/src/{ui → frontend}/index.js +7 -7
  34. package/src/frontend/utils/inputBuffer.js +441 -0
  35. package/src/{ui/utils/markdownRenderer.js → frontend/utils/markdownParser.js} +3 -3
  36. package/src/{ui/utils/ConsolePatcher.js → frontend/utils/outputRedirector.js} +9 -9
  37. package/src/{ui/utils/codeColorizer.js → frontend/utils/syntaxHighlighter.js} +2 -3
  38. package/src/system/ai_request.js +145 -595
  39. package/src/system/code_executer.js +111 -16
  40. package/src/system/file_integrity.js +5 -7
  41. package/src/system/log.js +3 -3
  42. package/src/system/mcp_integration.js +15 -13
  43. package/src/system/output_helper.js +0 -20
  44. package/src/system/session.js +97 -23
  45. package/src/system/session_memory.js +2 -82
  46. package/src/system/system_info.js +1 -1
  47. package/src/system/ui_events.js +0 -43
  48. package/src/tools/code_editor.js +17 -2
  49. package/src/tools/file_reader.js +17 -2
  50. package/src/tools/glob.js +9 -1
  51. package/src/tools/response_message.js +0 -2
  52. package/src/tools/ripgrep.js +9 -1
  53. package/src/tools/web_downloader.js +9 -1
  54. package/src/util/config.js +3 -8
  55. package/src/util/debug_log.js +4 -11
  56. package/src/util/mcp_config_manager.js +3 -5
  57. package/src/util/output_formatter.js +0 -47
  58. package/src/util/prompt_loader.js +3 -4
  59. package/src/util/safe_fs.js +60 -0
  60. package/src/util/setup_wizard.js +1 -3
  61. package/src/util/text_formatter.js +0 -86
  62. package/src/config/claude_models.js +0 -195
  63. package/src/ui/README.md +0 -208
  64. package/src/ui/api.js +0 -167
  65. package/src/ui/components/AgenticProgressDisplay.js +0 -126
  66. package/src/ui/components/Composer.js +0 -55
  67. package/src/ui/components/LoadingIndicator.js +0 -54
  68. package/src/ui/components/ModelListView.js +0 -214
  69. package/src/ui/components/Notifications.js +0 -55
  70. package/src/ui/components/StreamingIndicator.js +0 -36
  71. package/src/ui/contexts/AppContext.js +0 -25
  72. package/src/ui/contexts/StreamingContext.js +0 -20
  73. package/src/ui/contexts/UIStateContext.js +0 -117
  74. package/src/ui/example-usage.js +0 -180
  75. package/src/ui/hooks/useTerminalResize.js +0 -39
  76. package/src/ui/themes/semantic-tokens.js +0 -73
  77. package/src/ui/utils/text-buffer.js +0 -975
  78. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_ssgManifest.js +0 -0
  81. /package/src/{ui → frontend}/components/BlankLine.js +0 -0
  82. /package/src/{ui → frontend}/components/FileDiffViewer.js +0 -0
  83. /package/src/{ui → frontend}/components/HelpView.js +0 -0
  84. /package/src/{ui → frontend}/hooks/useCompletion.js +0 -0
  85. /package/src/{ui → frontend}/hooks/useKeypress.js +0 -0
  86. /package/src/{ui → frontend}/utils/diffUtils.js +0 -0
  87. /package/src/{ui → frontend}/utils/renderInkComponent.js +0 -0
@@ -1,132 +1,85 @@
1
1
  import OpenAI from "openai";
2
- import Anthropic from "@anthropic-ai/sdk";
3
2
  import dotenv from "dotenv";
4
3
  import path from 'path';
5
- // import process from 'process';
6
4
  import { safeWriteFile } from '../util/safe_fs.js';
7
5
  import { logger } from "./log.js";
8
6
  import { ensureConfigDirectory, loadSettings, SETTINGS_FILE } from "../util/config.js";
9
- import { getClaudeMaxTokens, DEFAULT_CLAUDE_MODEL } from "../config/claude_models.js";
10
7
  import { getReasoningModels, supportsReasoningEffort, DEFAULT_OPENAI_MODEL } from "../config/openai_models.js";
11
- import { formatReadFileStdout } from "../util/output_formatter.js";
12
8
  import { createDebugLogger } from "../util/debug_log.js";
9
+ import { formatReadFileStdout } from "../util/output_formatter.js";
13
10
 
14
11
  const debugLog = createDebugLogger('ai_request.log', 'ai_request');
15
12
 
16
- // Planner·Orchestrator·Verifier가 동일한 방식으로 모델을 호출할 수 있도록 공통 게이트웨이를 제공합니다.
17
- // OpenAI 및 Anthropic SDK를 사용하기 위한 환경변수를 불러옵니다.
13
+ // OpenAI SDK를 사용하기 위한 환경변수를 불러옵니다.
18
14
  dotenv.config({ quiet: true });
19
15
 
20
16
  // AI 클라이언트를 초기화합니다.
21
17
  let openaiClient = null;
22
- let anthropicClient = null;
23
- let currentProvider = null;
24
18
  let currentAbortController = null;
25
- function consolelog() { }
26
19
 
27
20
  async function getOpenAIClient() {
21
+ debugLog('[getOpenAIClient] Called');
28
22
  if (openaiClient) {
23
+ debugLog('[getOpenAIClient] Using cached client');
29
24
  return openaiClient;
30
25
  }
31
26
 
27
+ debugLog('[getOpenAIClient] Creating new client');
32
28
  // Settings 로드 및 환경변수 설정
33
29
  await ensureConfigDirectory();
34
30
  const settings = await loadSettings();
31
+ debugLog(`[getOpenAIClient] Settings loaded: AI_PROVIDER=${settings?.AI_PROVIDER}, OPENAI_MODEL=${settings?.OPENAI_MODEL}, OPENAI_REASONING_EFFORT=${settings?.OPENAI_REASONING_EFFORT}`);
35
32
 
36
33
  if (!process.env.OPENAI_API_KEY && settings?.OPENAI_API_KEY) {
37
34
  process.env.OPENAI_API_KEY = settings.OPENAI_API_KEY;
35
+ debugLog('[getOpenAIClient] OPENAI_API_KEY loaded from settings');
38
36
  }
39
37
 
40
38
  // 모델 설정도 환경변수에 로드
41
39
  if (!process.env.OPENAI_MODEL && settings?.OPENAI_MODEL) {
42
40
  process.env.OPENAI_MODEL = settings.OPENAI_MODEL;
41
+ debugLog(`[getOpenAIClient] OPENAI_MODEL set to: ${settings.OPENAI_MODEL}`);
43
42
  }
44
43
 
45
44
  // reasoning_effort 설정도 환경변수에 로드
46
45
  if (!process.env.OPENAI_REASONING_EFFORT && settings?.OPENAI_REASONING_EFFORT) {
47
46
  process.env.OPENAI_REASONING_EFFORT = settings.OPENAI_REASONING_EFFORT;
47
+ debugLog(`[getOpenAIClient] OPENAI_REASONING_EFFORT set to: ${settings.OPENAI_REASONING_EFFORT}`);
48
48
  }
49
49
 
50
50
  if (!process.env.OPENAI_API_KEY) {
51
+ debugLog('[getOpenAIClient] ERROR: OPENAI_API_KEY not configured');
51
52
  throw new Error(`OPENAI_API_KEY is not configured. Please update ${SETTINGS_FILE}.`);
52
53
  }
53
54
 
55
+ debugLog('[getOpenAIClient] Initializing OpenAI client with API key (first 10 chars): ' + process.env.OPENAI_API_KEY.substring(0, 10) + '...');
54
56
  openaiClient = new OpenAI({
55
57
  apiKey: process.env.OPENAI_API_KEY
56
58
  });
57
59
 
60
+ debugLog('[getOpenAIClient] Client created successfully');
58
61
  return openaiClient;
59
62
  }
60
63
 
61
- async function getAnthropicClient() {
62
- if (anthropicClient) {
63
- return anthropicClient;
64
- }
65
-
66
- // Settings 로드 및 환경변수 설정
67
- await ensureConfigDirectory();
68
- const settings = await loadSettings();
69
-
70
- if (!process.env.ANTHROPIC_API_KEY && settings?.ANTHROPIC_API_KEY) {
71
- process.env.ANTHROPIC_API_KEY = settings.ANTHROPIC_API_KEY;
72
- }
73
-
74
- // 모델 설정도 환경변수에 로드
75
- if (!process.env.ANTHROPIC_MODEL && settings?.ANTHROPIC_MODEL) {
76
- process.env.ANTHROPIC_MODEL = settings.ANTHROPIC_MODEL;
77
- }
78
-
79
- if (!process.env.ANTHROPIC_API_KEY) {
80
- throw new Error(`ANTHROPIC_API_KEY is not configured. Please update ${SETTINGS_FILE}.`);
81
- }
82
-
83
- anthropicClient = new Anthropic({
84
- apiKey: process.env.ANTHROPIC_API_KEY
85
- });
86
-
87
- return anthropicClient;
88
- }
89
-
90
64
  async function getCurrentProvider() {
91
- if (currentProvider) {
92
- return currentProvider;
93
- }
94
-
95
- await ensureConfigDirectory();
96
- const settings = await loadSettings();
97
- currentProvider = settings?.AI_PROVIDER || 'openai';
98
-
99
- // 환경변수로 설정된 경우 우선 적용
100
- if (process.env.AI_PROVIDER) {
101
- currentProvider = process.env.AI_PROVIDER;
102
- } else if (settings?.AI_PROVIDER) {
103
- // Settings에서 환경변수로 로드
104
- process.env.AI_PROVIDER = settings.AI_PROVIDER;
105
- }
106
-
107
- return currentProvider;
65
+ return 'openai';
108
66
  }
109
67
 
110
68
  // Provider에 맞는 모델 이름 가져오기
111
69
  async function getModelForProvider() {
112
- const provider = await getCurrentProvider();
70
+ debugLog('[getModelForProvider] Called');
113
71
  const settings = await loadSettings();
114
-
115
- if (provider === 'anthropic') {
116
- // Anthropic 모델 사용
117
- return process.env.ANTHROPIC_MODEL || settings?.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL;
118
- } else {
119
- // OpenAI 모델 사용 (기본값)
120
- return process.env.OPENAI_MODEL || settings?.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
121
- }
72
+ const model = process.env.OPENAI_MODEL || settings?.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
73
+ debugLog(`[getModelForProvider] Model: ${model} (from: ${process.env.OPENAI_MODEL ? 'env' : settings?.OPENAI_MODEL ? 'settings' : 'default'})`);
74
+ return model;
122
75
  }
123
76
 
124
77
  // 클라이언트 및 캐시 리셋 함수 (모델 변경 시 사용)
125
78
  export function resetAIClients() {
79
+ debugLog('[resetAIClients] Resetting AI client cache');
126
80
  openaiClient = null;
127
- anthropicClient = null;
128
- currentProvider = null;
129
81
  currentAbortController = null;
82
+ debugLog('[resetAIClients] Client cache cleared');
130
83
  }
131
84
 
132
85
  // 현재 진행 중인 AI 요청을 중단하는 함수
@@ -140,279 +93,6 @@ export function abortCurrentRequest() {
140
93
  // export로 다른 파일에서 사용 가능하도록
141
94
  export { getModelForProvider };
142
95
 
143
- // OpenAI 포맷을 Anthropic 포맷으로 변환
144
- function convertOpenAIToAnthropic(requestPayload) {
145
- // 모델 이름 매핑 (OpenAI 모델이 지정된 경우 Anthropic 모델로 변환)
146
- let modelName = requestPayload.model;
147
- if (modelName && modelName.startsWith('gpt-')) {
148
- // OpenAI 모델이면 기본 Anthropic 모델로 변경
149
- modelName = process.env.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL;
150
- }
151
-
152
- // 모델별 max_tokens 설정 (중앙 설정에서 가져옴)
153
- const maxTokens = getClaudeMaxTokens(modelName);
154
-
155
- const anthropicRequest = {
156
- model: modelName,
157
- max_tokens: maxTokens, // Anthropic requires max_tokens
158
- messages: []
159
- };
160
-
161
- // tool_use와 tool_result 간의 매칭을 위한 ID 수집
162
- const toolUseIds = new Set();
163
- const toolResultIds = new Set();
164
-
165
- for (const msg of requestPayload.input) {
166
- if (msg.type === 'function_call') {
167
- toolUseIds.add(msg.id || msg.call_id);
168
- }
169
- if (msg.type === 'function_call_output') {
170
- toolResultIds.add(msg.call_id);
171
- }
172
- }
173
-
174
- // 양방향 매칭 필터: tool_use와 tool_result가 모두 존재하는 ID만 유효
175
- const validToolIds = new Set(
176
- [...toolUseIds].filter(id => toolResultIds.has(id))
177
- );
178
-
179
- // input을 messages로 변환
180
- if (requestPayload.input && Array.isArray(requestPayload.input)) {
181
- for (const msg of requestPayload.input) {
182
- // system 메시지는 별도 처리
183
- if (msg.role === 'system') {
184
- if (msg.content && Array.isArray(msg.content)) {
185
- const systemText = msg.content.map(c => c.text || c).join('\n');
186
- anthropicRequest.system = systemText;
187
- continue;
188
- } else if (typeof msg.content === 'string') {
189
- anthropicRequest.system = msg.content;
190
- continue;
191
- }
192
- }
193
-
194
- // function_call_output을 tool_result로 변환
195
- if (msg.type === 'function_call_output') {
196
- // 고아 tool_result 방지: 대응하는 tool_use가 없으면 건너뛰기
197
- if (!validToolIds.has(msg.call_id)) {
198
- continue;
199
- }
200
- const lastMsg = anthropicRequest.messages[anthropicRequest.messages.length - 1];
201
- if (lastMsg && lastMsg.role === 'user') {
202
- // 이미 user 메시지가 있으면 content에 추가
203
- if (!Array.isArray(lastMsg.content)) {
204
- // 문자열 content를 제대로 된 배열로 변환
205
- lastMsg.content = [{ type: 'text', text: lastMsg.content }];
206
- }
207
- lastMsg.content.push({
208
- type: 'tool_result',
209
- tool_use_id: msg.call_id,
210
- content: msg.output || ''
211
- });
212
- } else {
213
- // 새 user 메시지 생성
214
- anthropicRequest.messages.push({
215
- role: 'user',
216
- content: [{
217
- type: 'tool_result',
218
- tool_use_id: msg.call_id,
219
- content: msg.output || ''
220
- }]
221
- });
222
- }
223
- continue;
224
- }
225
-
226
- // function_call을 tool_use로 변환 (assistant 메시지의 일부)
227
- if (msg.type === 'function_call') {
228
- // 고아 tool_use 방지: 대응하는 tool_result가 없으면 건너뛰기
229
- const toolId = msg.id || msg.call_id;
230
- if (!validToolIds.has(toolId)) {
231
- continue;
232
- }
233
-
234
- const lastMsg = anthropicRequest.messages[anthropicRequest.messages.length - 1];
235
- if (lastMsg && lastMsg.role === 'assistant') {
236
- // 이미 assistant 메시지가 있으면 content에 추가
237
- if (!Array.isArray(lastMsg.content)) {
238
- // 문자열 content를 제대로 된 배열로 변환
239
- lastMsg.content = [{ type: 'text', text: lastMsg.content }];
240
- }
241
- lastMsg.content.push({
242
- type: 'tool_use',
243
- id: toolId,
244
- name: msg.name,
245
- input: JSON.parse(msg.arguments || '{}')
246
- });
247
- } else {
248
- // 새 assistant 메시지 생성
249
- anthropicRequest.messages.push({
250
- role: 'assistant',
251
- content: [{
252
- type: 'tool_use',
253
- id: toolId,
254
- name: msg.name,
255
- input: JSON.parse(msg.arguments || '{}')
256
- }]
257
- });
258
- }
259
- continue;
260
- }
261
-
262
- // reasoning 타입은 건너뛰기 (Anthropic에 없는 타입)
263
- if (msg.type === 'reasoning') {
264
- continue;
265
- }
266
-
267
- // 일반 메시지 처리
268
- const anthropicMsg = {
269
- role: msg.role,
270
- content: []
271
- };
272
-
273
- // content 변환
274
- if (msg.content && Array.isArray(msg.content)) {
275
- for (const c of msg.content) {
276
- if (c.type === 'input_text' || c.type === 'text' || c.type === 'output_text') {
277
- // 빈 text는 건너뛰기 (Anthropic은 빈 text를 허용하지 않음)
278
- if (c.text && c.text.trim()) {
279
- anthropicMsg.content.push({
280
- type: 'text',
281
- text: c.text
282
- });
283
- }
284
- } else if (c.text) {
285
- if (c.text.trim()) {
286
- anthropicMsg.content.push({
287
- type: 'text',
288
- text: c.text
289
- });
290
- }
291
- }
292
- }
293
- } else if (typeof msg.content === 'string') {
294
- anthropicMsg.content = msg.content;
295
- }
296
-
297
- // content가 있을 때만 메시지 추가 (빈 배열이나 빈 문자열은 제외)
298
- if (anthropicMsg.content.length > 0 ||
299
- (typeof anthropicMsg.content === 'string' && anthropicMsg.content.trim())) {
300
- anthropicRequest.messages.push(anthropicMsg);
301
- }
302
- }
303
- }
304
-
305
- // tools 변환
306
- if (requestPayload.tools && Array.isArray(requestPayload.tools)) {
307
- anthropicRequest.tools = requestPayload.tools.map(tool => ({
308
- name: tool.name,
309
- description: tool.description,
310
- input_schema: tool.input_schema || tool.parameters
311
- }));
312
- }
313
-
314
- // tool_choice 변환
315
- if (requestPayload.tool_choice) {
316
- if (requestPayload.tool_choice === 'auto') {
317
- anthropicRequest.tool_choice = { type: 'auto' };
318
- } else if (requestPayload.tool_choice === 'required') {
319
- anthropicRequest.tool_choice = { type: 'any' };
320
- } else if (typeof requestPayload.tool_choice === 'object') {
321
- anthropicRequest.tool_choice = requestPayload.tool_choice;
322
- }
323
- }
324
-
325
- // temperature와 top_p는 동시에 사용할 수 없음 (Anthropic API 제약)
326
- // temperature가 있으면 temperature 사용, 없으면 top_p 사용
327
- if (requestPayload.temperature !== undefined) {
328
- anthropicRequest.temperature = requestPayload.temperature;
329
- } else if (requestPayload.top_p !== undefined) {
330
- anthropicRequest.top_p = requestPayload.top_p;
331
- }
332
-
333
- return anthropicRequest;
334
- }
335
-
336
- // Anthropic 응답을 OpenAI 포맷으로 변환
337
- function convertAnthropicToOpenAI(anthropicResponse) {
338
- const openaiResponse = {
339
- output: [],
340
- output_text: ''
341
- };
342
-
343
- let fullText = '';
344
- let hasTextContent = false;
345
-
346
- // content 변환
347
- if (anthropicResponse.content && Array.isArray(anthropicResponse.content)) {
348
- // Anthropic 세션 종료 신호: content가 비어있고 stop_reason이 end_turn
349
- // OpenAI 로직과 호환되도록 빈 message를 추가
350
- if (anthropicResponse.content.length === 0 && anthropicResponse.stop_reason === 'end_turn') {
351
- openaiResponse.output.push({
352
- role: 'assistant',
353
- type: 'message',
354
- content: [{
355
- type: 'output_text',
356
- text: ''
357
- }]
358
- });
359
- }
360
-
361
- // 일반적인 content 처리
362
- for (const content of anthropicResponse.content) {
363
- if (content.type === 'text') {
364
- fullText += content.text;
365
- hasTextContent = true;
366
- openaiResponse.output.push({
367
- role: 'assistant',
368
- type: 'message',
369
- content: [{
370
- type: 'output_text',
371
- text: content.text
372
- }]
373
- });
374
- } else if (content.type === 'tool_use') {
375
- openaiResponse.output.push({
376
- role: 'assistant', // role 추가
377
- type: 'function_call',
378
- id: content.id,
379
- name: content.name,
380
- arguments: JSON.stringify(content.input),
381
- call_id: content.id // call_id도 추가 (일관성)
382
- });
383
- }
384
- }
385
- }
386
-
387
- openaiResponse.output_text = fullText;
388
-
389
- // Anthropic 응답의 메타데이터 복사
390
- if (anthropicResponse.id) {
391
- openaiResponse.id = anthropicResponse.id;
392
- }
393
- if (anthropicResponse.model) {
394
- openaiResponse.model = anthropicResponse.model;
395
- }
396
- if (anthropicResponse.type) {
397
- openaiResponse.object = anthropicResponse.type;
398
- }
399
-
400
- // stop_reason 처리
401
- if (anthropicResponse.stop_reason) {
402
- openaiResponse.stop_reason = anthropicResponse.stop_reason;
403
- }
404
- if (anthropicResponse.stop_sequence) {
405
- openaiResponse.stop_sequence = anthropicResponse.stop_sequence;
406
- }
407
-
408
- // usage 정보 복사 (cache 토큰 포함)
409
- if (anthropicResponse.usage) {
410
- openaiResponse.usage = anthropicResponse.usage;
411
- }
412
-
413
- return openaiResponse;
414
- }
415
-
416
96
  export function structureFixer(response) {
417
97
  for (let i = 0; i < response.output.length; i++) {
418
98
  if (response.output[i].type === 'reasoning') {
@@ -489,139 +169,43 @@ function toAbsolutePath(anyPath) {
489
169
 
490
170
  // 모든 AI 요청 과정에서 요청/응답 로그를 남기고 결과를 돌려줍니다.
491
171
  export async function request(taskName, requestPayload) {
172
+ debugLog(`[request] ========== START: ${taskName} ==========`);
492
173
  const provider = await getCurrentProvider();
174
+ debugLog(`[request] Provider: ${provider}`);
493
175
 
494
176
  // requestPayload를 deep copy하여 원본 보호
495
177
  let payloadCopy = JSON.parse(JSON.stringify(requestPayload));
178
+ debugLog(`[request] Payload copied - model: ${payloadCopy.model}, input messages: ${payloadCopy.input?.length || 0}`);
496
179
 
497
180
  // _internal_only 속성 제거 (API 요청에 무효한 속성)
181
+ debugLog(`[request] Checking for _internal_only attributes...`);
182
+ let removedInternalOnlyCount = 0;
498
183
  if (payloadCopy.input && Array.isArray(payloadCopy.input)) {
499
184
  for (const msg of payloadCopy.input) {
500
185
  if (msg._internal_only) {
501
186
  delete msg._internal_only;
187
+ removedInternalOnlyCount++;
502
188
  }
503
189
  }
504
190
  }
505
-
506
- // function_call_output의 output에서 original_result 제거
507
- if (false && payloadCopy.input && Array.isArray(payloadCopy.input)) {
508
- for (const msg of payloadCopy.input) {
509
- if (msg.type === 'function_call_output' && msg.output) {
510
- try {
511
- const outputObj = JSON.parse(msg.output);
512
- if (outputObj && typeof outputObj === 'object' && 'original_result' in outputObj) {
513
- delete outputObj.original_result;
514
-
515
- // tool이 response_message인 경우 stdout 변경
516
- if (outputObj.tool === 'response_message') {
517
- outputObj.stdout = 'response complete';
518
- }
519
-
520
- msg.output = JSON.stringify(outputObj);
521
- }
522
- } catch {
523
- // JSON 파싱 실패 시 무시
524
- }
525
- }
526
- }
191
+ if (removedInternalOnlyCount > 0) {
192
+ debugLog(`[request] Removed _internal_only from ${removedInternalOnlyCount} messages`);
527
193
  }
528
194
 
529
- // read_file + edit_file_range: 같은 파일의 최신 모습만 유지
530
- if (false && payloadCopy.input && Array.isArray(payloadCopy.input)) {
531
- // 1. absolute_file_path를 가진 모든 function_call_output 수집
532
- const pathGroups = new Map(); // absolute_file_path -> [{index, msg, outputObj, stdoutObj, toolName}]
533
-
534
- payloadCopy.input.forEach((msg, index) => {
535
- if (msg.type === 'function_call_output' && msg.output) {
536
- try {
537
- const outputObj = JSON.parse(msg.output);
538
- const toolName = outputObj?.tool;
539
-
540
- if (outputObj && typeof outputObj === 'object' && outputObj.stdout) {
541
- let stdoutObj = null;
542
- let absolutePath = null;
543
-
544
- // edit_file_range: stdout이 JSON 객체
545
- if (toolName === 'edit_file_range') {
546
- stdoutObj = JSON.parse(outputObj.stdout);
547
- absolutePath = stdoutObj?.absolute_file_path;
548
- }
549
- // read_file: stdout이 JSON 객체 (orchestrator에서 생성)
550
- else if (toolName === 'read_file') {
551
- stdoutObj = JSON.parse(outputObj.stdout);
552
- absolutePath = stdoutObj?.absolute_file_path;
553
- }
554
-
555
- if (stdoutObj && absolutePath) {
556
- if (!pathGroups.has(absolutePath)) {
557
- pathGroups.set(absolutePath, []);
558
- }
559
- pathGroups.get(absolutePath).push({ index, msg, outputObj, stdoutObj, toolName });
560
- }
561
- }
562
- } catch {
563
- // JSON 파싱 실패 시 무시
564
- }
565
- }
566
- });
567
-
568
- // 2. 각 그룹에서 index 기준 마지막만 내용 유지
569
- for (const [path, group] of pathGroups.entries()) {
570
- // index 기준 정렬 (시간순)
571
- group.sort((a, b) => a.index - b.index);
572
-
573
- // 마지막 요소의 index
574
- const lastIndex = group[group.length - 1].index;
575
-
576
- group.forEach((item) => {
577
- const { index, outputObj, stdoutObj, msg, toolName } = item;
578
- const isLast = (index === lastIndex);
579
-
580
- if (toolName === 'edit_file_range') {
581
- // 마지막이 아닌 요소는 updated_content 제거
582
- if (!isLast) {
583
- if (stdoutObj.file_stats && 'updated_content' in stdoutObj.file_stats) {
584
- delete stdoutObj.file_stats.updated_content;
585
- }
586
- } else {
587
- // 마지막 요소는 updated_content에 formatReadFileStdout 적용
588
- if (stdoutObj.file_stats && 'updated_content' in stdoutObj.file_stats) {
589
- const content = stdoutObj.file_stats.updated_content;
590
- const lines = content.split('\n');
591
- const formattedContent = formatReadFileStdout({ file_lines: lines });
592
- stdoutObj.file_stats.updated_content = formattedContent;
593
- }
594
- }
595
-
596
- // absolute_file_path 제거 후 JSON으로 직렬화
597
- delete stdoutObj.absolute_file_path;
598
- outputObj.stdout = JSON.stringify(stdoutObj);
599
- msg.output = JSON.stringify(outputObj);
600
-
601
- } else if (toolName === 'read_file') {
602
- // 마지막이 아닌 요소는 content를 "(내용 표시 안함)"으로 대체
603
- if (!isLast) {
604
- if ('content' in stdoutObj) {
605
- stdoutObj.content = '(내용 표시 안함)';
606
- }
607
- }
608
-
609
- // absolute_file_path 제거 후 문자열로 직렬화
610
- delete stdoutObj.absolute_file_path;
611
- outputObj.stdout = stdoutObj.content;
612
- msg.output = JSON.stringify(outputObj);
613
- }
614
- });
615
- }
616
- }
195
+ debugLog(`[request] Starting payload transformation...`);
617
196
  if (payloadCopy.input && Array.isArray(payloadCopy.input)) {
618
197
  const marker = {};
619
198
  const remove_call_id = {};//[];
620
199
  const response_message_call_id = {};
200
+
201
+ debugLog(`[request] Processing ${payloadCopy.input.length} input messages for deduplication and formatting`);
202
+ let processedCount = 0;
203
+
621
204
  for (let i = payloadCopy.input.length - 1; i >= 0; i--) {
622
205
  const msg = payloadCopy.input[i];
623
206
  const { type, call_id, output } = msg;
624
207
  if (type !== 'function_call_output') continue;
208
+ processedCount++;
625
209
 
626
210
  debugLog(`[ai_request] Processing function_call_output, call_id: ${call_id}, output length: ${output?.length || 0}`);
627
211
 
@@ -695,48 +279,34 @@ export async function request(taskName, requestPayload) {
695
279
  debugLog(`[ai_request] OTHER TOOL (${tool}) - AFTER: output length: ${msg.output.length} bytes (stdout: ${stdout?.length || 0}, stderr: ${stderr?.length || 0})`);
696
280
  }
697
281
  }
282
+
283
+ debugLog(`[request] Processed ${processedCount} function_call_output entries`);
284
+ debugLog(`[request] Marked files for deduplication: ${Object.keys(marker).length}`);
285
+
286
+ let responseMessageCount = 0;
698
287
  for (let i = payloadCopy.input.length - 1; i >= 0; i--) {
699
288
  const msg = payloadCopy.input[i];
700
289
  const { type, call_id, name } = msg;
701
290
  if (type !== 'function_call') continue;
702
291
  if (name !== 'response_message') continue;
703
292
  response_message_call_id[call_id] = true;
293
+ responseMessageCount++;
704
294
  }
295
+ debugLog(`[request] Found ${responseMessageCount} response_message function calls`);
296
+
297
+ let simplifiedResponseMessages = 0;
705
298
  for (let i = payloadCopy.input.length - 1; i >= 0; i--) {
706
299
  const msg = payloadCopy.input[i];
707
300
  const { type, call_id } = msg;
708
301
  if (type !== 'function_call_output') continue;
709
302
  if (!response_message_call_id[call_id]) continue;
710
303
  msg.output = JSON.stringify({ message_displayed: true });
304
+ simplifiedResponseMessages++;
711
305
  }
712
- if (!true) {
713
- const newArr = [];
714
- payloadCopy.input.forEach(msg => {
715
- if (msg.call_id && remove_call_id[msg.call_id]) return;
716
- newArr.push(msg)
717
- });
718
- if (!process.__counter) process.__counter = 1;
719
- process.__counter++;
720
- await safeWriteFile(`debugging_${process.__counter}.txt`, JSON.stringify(newArr, null, 2), 'utf8');
721
- payloadCopy = JSON.parse(JSON.stringify({ ...payloadCopy, input: newArr }, null, 2)); //⭐️
722
- } else {
723
- // await safeWriteFile('debugging3.txt', JSON.stringify(payloadCopy, null, 2), 'utf8');
724
-
725
- }
726
-
306
+ debugLog(`[request] Simplified ${simplifiedResponseMessages} response_message outputs`);
727
307
  }
728
- if (false && payloadCopy.input && Array.isArray(payloadCopy.input)) {
729
- for (const msg of payloadCopy.input) {
730
- if (msg.type === 'function_call_output') {
731
- const parsed = JSON.parse(msg.output);
732
- msg.output = parsed.stdout;
733
- // [
734
- // `stdout:`
735
- // ]
736
- }
737
308
 
738
- }
739
- }
309
+ debugLog(`[request] Payload transformation complete`);
740
310
  let response;
741
311
  let originalRequest;
742
312
  let originalResponse;
@@ -745,167 +315,147 @@ export async function request(taskName, requestPayload) {
745
315
  currentAbortController = new AbortController();
746
316
 
747
317
  try {
748
- if (provider === 'anthropic') {
749
- // Anthropic API 사용
750
- const anthropic = await getAnthropicClient();
751
- const anthropicRequest = convertOpenAIToAnthropic(JSON.parse(JSON.stringify(payloadCopy)));
752
- originalRequest = JSON.parse(JSON.stringify(anthropicRequest)); // 원본 Anthropic 요청
753
-
754
- // 로그는 원본 Anthropic 포맷으로 저장 (API 호출 전)
755
- await logger(`${taskName}_REQ`, originalRequest, provider);
756
-
757
- const anthropicResponse = await anthropic.messages.create(
758
- anthropicRequest,
759
- {
760
- signal: currentAbortController.signal
318
+ // OpenAI API 사용
319
+ debugLog('[request] Getting OpenAI client');
320
+ const openai = await getOpenAIClient();
321
+
322
+ // reasoning 설정 추가 (OpenAI 추론 모델용)
323
+ const settings = await loadSettings();
324
+ const reasoningEffort = settings?.OPENAI_REASONING_EFFORT || process.env.OPENAI_REASONING_EFFORT || 'medium';
325
+ debugLog(`[request] Reasoning effort: ${reasoningEffort}`);
326
+
327
+ // reasoning을 지원하는 모델인지 확인
328
+ const reasoningModels = getReasoningModels();
329
+ const currentModel = payloadCopy.model || settings?.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
330
+ debugLog(`[request] Current model: ${currentModel}`);
331
+
332
+ if (reasoningModels.some(m => currentModel.startsWith(m))) {
333
+ debugLog(`[request] Model supports reasoning - checking effort compatibility`);
334
+ let effectiveEffort = reasoningEffort;
335
+
336
+ // 모델이 해당 effort를 지원하는지 확인하고 필요하면 fallback
337
+ if (!supportsReasoningEffort(currentModel, reasoningEffort)) {
338
+ debugLog(`[request] Effort ${reasoningEffort} not supported by ${currentModel}, finding fallback`);
339
+ // high를 지원하는지 확인 (gpt-5-pro 등)
340
+ if (supportsReasoningEffort(currentModel, 'high')) {
341
+ effectiveEffort = 'high';
342
+ debugLog(`[request] Fallback to 'high'`);
343
+ }
344
+ // medium을 지원하는지 확인
345
+ else if (supportsReasoningEffort(currentModel, 'medium')) {
346
+ effectiveEffort = 'medium';
347
+ debugLog(`[request] Fallback to 'medium'`);
348
+ }
349
+ // low를 지원하는지 확인
350
+ else if (supportsReasoningEffort(currentModel, 'low')) {
351
+ effectiveEffort = 'low';
352
+ debugLog(`[request] Fallback to 'low'`);
761
353
  }
762
- );
763
- originalResponse = JSON.parse(JSON.stringify(anthropicResponse)); // 원본 Anthropic 응답
354
+ } else {
355
+ debugLog(`[request] Effort ${reasoningEffort} is supported by ${currentModel}`);
356
+ }
764
357
 
765
- // OpenAI 포맷으로 변환
766
- response = convertAnthropicToOpenAI(anthropicResponse);
358
+ // reasoning 객체로 설정
359
+ payloadCopy.reasoning = {
360
+ effort: effectiveEffort,
361
+ summary: 'auto'
362
+ };
363
+ debugLog(`[request] Applied reasoning config: effort=${effectiveEffort}, summary=auto`);
767
364
  } else {
768
- // OpenAI API 사용 (기본값)
769
- const openai = await getOpenAIClient();
770
-
771
- // reasoning 설정 추가 (OpenAI 추론 모델용)
772
- const settings = await loadSettings();
773
- const reasoningEffort = settings?.OPENAI_REASONING_EFFORT || process.env.OPENAI_REASONING_EFFORT || 'medium';
774
-
775
- // reasoning을 지원하는 모델인지 확인
776
- const reasoningModels = getReasoningModels();
777
- const currentModel = payloadCopy.model || settings?.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
778
-
779
- if (reasoningModels.some(m => currentModel.startsWith(m))) {
780
- let effectiveEffort = reasoningEffort;
365
+ debugLog(`[request] Model does not support reasoning`);
366
+ }
781
367
 
782
- // 모델이 해당 effort를 지원하는지 확인하고 필요하면 fallback
783
- if (!supportsReasoningEffort(currentModel, reasoningEffort)) {
784
- // high를 지원하는지 확인 (gpt-5-pro 등)
785
- if (supportsReasoningEffort(currentModel, 'high')) {
786
- effectiveEffort = 'high';
787
- }
788
- // medium을 지원하는지 확인
789
- else if (supportsReasoningEffort(currentModel, 'medium')) {
790
- effectiveEffort = 'medium';
791
- }
792
- // low를 지원하는지 확인
793
- else if (supportsReasoningEffort(currentModel, 'low')) {
794
- effectiveEffort = 'low';
795
- }
796
- }
368
+ originalRequest = JSON.parse(JSON.stringify(payloadCopy)); // 원본 OpenAI 요청
369
+ debugLog(`[request] Request prepared - logging to file`);
797
370
 
798
- // reasoning 객체로 설정
799
- payloadCopy.reasoning = {
800
- effort: effectiveEffort,
801
- summary: 'auto'
802
- };
803
- }
371
+ // 로그는 원본 OpenAI 포맷으로 저장 (API 호출 전)
372
+ await logger(`${taskName}_REQ`, originalRequest, provider);
373
+ debugLog(`[request] Request logged - calling OpenAI API`);
804
374
 
805
- originalRequest = JSON.parse(JSON.stringify(payloadCopy)); // 원본 OpenAI 요청
806
-
807
- // 로그는 원본 OpenAI 포맷으로 저장 (API 호출 전)
808
- await logger(`${taskName}_REQ`, originalRequest, provider);
375
+ response = await openai.responses.create(originalRequest, {
376
+ signal: currentAbortController.signal
377
+ });
378
+ debugLog(`[request] Response received - id: ${response?.id}, status: ${response?.status}, output items: ${response?.output?.length || 0}`);
809
379
 
810
- response = await openai.responses.create(originalRequest, {
811
- signal: currentAbortController.signal
812
- });
813
- // 원본 응답을 깊은 복사로 보존 (이후 수정으로부터 보호)
814
- originalResponse = JSON.parse(JSON.stringify(response));
815
- }
380
+ // 원본 응답을 깊은 복사로 보존 (이후 수정으로부터 보호)
381
+ originalResponse = JSON.parse(JSON.stringify(response));
382
+ debugLog(`[request] Response copied for logging`);
816
383
  } catch (error) {
817
384
  // AbortController 정리
818
385
  currentAbortController = null;
819
386
 
387
+ debugLog(`[request] ERROR occurred: ${error.name} - ${error.message}`);
388
+ debugLog(`[request] Error code: ${error.code}, status: ${error.status}`);
389
+
820
390
  // Abort 에러인 경우 명확하게 처리
821
391
  if (error.name === 'AbortError' || error.code === 'ABORT_ERR' || error.message?.includes('aborted')) {
392
+ debugLog(`[request] Request was aborted by user`);
822
393
  const abortError = new Error('Request aborted by user');
823
394
  abortError.name = 'AbortError';
824
395
  throw abortError;
825
396
  }
826
397
 
398
+ debugLog(`[request] Re-throwing error: ${error.stack}`);
827
399
  // 기타 에러는 그대로 throw
828
400
  throw error;
829
401
  }
830
402
 
831
403
  // 요청 완료 후 AbortController 정리
832
404
  currentAbortController = null;
405
+ debugLog(`[request] AbortController cleaned up`);
833
406
 
834
407
  if (taskName === 'orchestrator') {
835
- if (false) {
836
- response.output.forEach(obj => {
837
- consolelog('OUTPUT', obj.type, '|', obj.name);
838
- });
839
- }
840
- try {
841
- if (false) response.output.forEach(obj => {
842
- if (obj.type !== 'function_call') return;
843
- if (obj.name !== 'response_message') return;
844
- const text = JSON.parse(obj.arguments).message;
845
- Object.keys(obj).forEach(key => delete obj[key]);
846
- delete obj.id;
847
- obj.role = 'assistant';
848
- obj.type = 'message';
849
- obj.content = [{ text, "type": "output_text", }];
850
- });
851
- } catch {
852
- }
408
+ debugLog(`[request] Post-processing orchestrator response`);
409
+ debugLog(`[request] Sorting edit_file_range calls by start_line (descending)`);
853
410
  response.output = sortByStartLineDescending(response.output, 'function_call', 'edit_file_range');
411
+ debugLog(`[request] Sorted - output items: ${response.output.length}`);
854
412
  }
413
+
414
+ debugLog(`[request] Applying structure fixer to response`);
855
415
  structureFixer(response);
416
+ debugLog(`[request] Structure fixed - output items: ${response.output.length}`);
856
417
 
857
- // 로그는 원본 포맷으로 저장 (Anthropic이면 Anthropic 응답, OpenAI면 OpenAI 응답)
418
+ // 로그는 원본 포맷으로 저장
419
+ debugLog(`[request] Logging response to file`);
858
420
  await logger(`${taskName}_RES`, originalResponse, provider);
421
+ debugLog(`[request] ========== END: ${taskName} ==========`);
859
422
 
860
423
  return response;
861
424
  }
862
425
 
426
+ /**
427
+ * OpenAI API의 컨텍스트 윈도우 초과 에러인지 확인합니다.
428
+ *
429
+ * 이 함수는 OpenAI API의 공식 에러 코드만을 신뢰하며,
430
+ * 에러 메시지 문자열 매칭은 사용하지 않습니다.
431
+ *
432
+ * @param {Error | Object} error - 확인할 에러 객체
433
+ * @returns {boolean} 컨텍스트 윈도우 초과 에러 여부
434
+ *
435
+ * @see https://platform.openai.com/docs/guides/error-codes
436
+ */
863
437
  export function isContextWindowError(error) {
864
438
  if (!error) return false;
865
439
 
866
- // OpenAI 에러 코드 확인
867
- const errorCode = typeof error?.error?.code === 'string' ? error.error.code : typeof error?.code === 'string' ? error.code : '';
868
- if (errorCode && errorCode.toLowerCase() === 'context_length_exceeded') {
869
- return true;
870
- }
871
-
872
- // Anthropic 에러 타입 확인
873
- const errorType = typeof error?.error?.type === 'string' ? error.error.type : typeof error?.type === 'string' ? error.type : '';
874
-
875
- // Anthropic의 context window 초과 에러
876
- if (errorType === 'invalid_request_error') {
877
- const errorParam = typeof error?.error?.param === 'string' ? error.error.param : typeof error?.param === 'string' ? error.param : '';
440
+ // OpenAI SDK의 공식 에러 코드 확인
441
+ // error.code 또는 error.error.code에서 확인
442
+ const errorCode = error?.code || error?.error?.code;
878
443
 
879
- if (errorParam) {
880
- const normalizedParam = errorParam.toLowerCase();
881
- if (normalizedParam.includes('message') || normalizedParam.includes('prompt') || normalizedParam.includes('input')) {
882
- const maybeStatus = typeof error?.response?.status === 'number' ? error.response.status : typeof error?.status === 'number' ? error.status : null;
883
- if (maybeStatus === 400) {
884
- return true;
885
- }
886
- }
887
- }
444
+ if (errorCode === 'context_length_exceeded') {
445
+ debugLog('[isContextWindowError] Detected: context_length_exceeded');
446
+ return true;
888
447
  }
889
448
 
890
- // 에러 메시지 확인
891
- const message = typeof error === 'string'
892
- ? error
893
- : typeof error?.error?.message === 'string'
894
- ? error.error.message
895
- : typeof error?.message === 'string'
896
- ? error.message
897
- : '';
898
-
899
- if (!message) {
900
- return false;
449
+ // 감지되지 않은 에러 - 디버깅을 위해 로그 남김
450
+ if (error?.status === 400 || error?.response?.status === 400) {
451
+ const errorType = error?.type || error?.error?.type;
452
+ const errorMessage = error?.message || error?.error?.message || '';
453
+ debugLog(
454
+ `[isContextWindowError] Not detected - ` +
455
+ `Status: 400, Type: ${errorType}, Code: ${errorCode}, ` +
456
+ `Message: ${errorMessage.substring(0, 200)}`
457
+ );
901
458
  }
902
459
 
903
- const lowerMessage = message.toLowerCase();
904
- return lowerMessage.includes('context length')
905
- || lowerMessage.includes('maximum context length')
906
- || lowerMessage.includes('context window')
907
- || lowerMessage.includes('prompt is too long')
908
- || (lowerMessage.includes('token') && lowerMessage.includes('exceed'))
909
- || lowerMessage.includes('too much context')
910
- || lowerMessage.includes('too many tokens');
460
+ return false;
911
461
  }