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.
- package/config_template/settings.json +1 -3
- package/index.js +46 -71
- package/package.json +1 -12
- package/payload_viewer/out/404/index.html +1 -1
- package/payload_viewer/out/404.html +1 -1
- package/payload_viewer/out/index.html +1 -1
- package/payload_viewer/out/index.txt +1 -1
- package/payload_viewer/web_server.js +0 -163
- package/prompts/completion_judge.txt +11 -7
- package/src/ai_based/completion_judge.js +97 -6
- package/src/ai_based/orchestrator.js +71 -3
- package/src/ai_based/pip_package_installer.js +14 -12
- package/src/ai_based/pip_package_lookup.js +13 -10
- package/src/commands/apikey.js +8 -34
- package/src/commands/help.js +3 -4
- package/src/commands/model.js +17 -74
- package/src/commands/reasoning_effort.js +1 -1
- package/src/config/feature_flags.js +0 -12
- package/src/{ui → frontend}/App.js +23 -25
- package/src/frontend/README.md +81 -0
- package/src/{ui/components/SuggestionsDisplay.js → frontend/components/AutocompleteMenu.js} +3 -3
- package/src/{ui/components/HistoryItemDisplay.js → frontend/components/ConversationItem.js} +37 -89
- package/src/{ui → frontend}/components/CurrentModelView.js +3 -5
- package/src/{ui → frontend}/components/Footer.js +4 -6
- package/src/{ui → frontend}/components/Header.js +2 -5
- package/src/{ui/components/InputPrompt.js → frontend/components/Input.js} +16 -54
- package/src/frontend/components/ModelListView.js +106 -0
- package/src/{ui → frontend}/components/ModelUpdatedView.js +3 -5
- package/src/{ui → frontend}/components/SessionSpinner.js +3 -3
- package/src/{ui → frontend}/components/SetupWizard.js +8 -101
- package/src/{ui → frontend}/components/ToolApprovalPrompt.js +16 -14
- package/src/frontend/design/themeColors.js +42 -0
- package/src/{ui → frontend}/index.js +7 -7
- package/src/frontend/utils/inputBuffer.js +441 -0
- package/src/{ui/utils/markdownRenderer.js → frontend/utils/markdownParser.js} +3 -3
- package/src/{ui/utils/ConsolePatcher.js → frontend/utils/outputRedirector.js} +9 -9
- package/src/{ui/utils/codeColorizer.js → frontend/utils/syntaxHighlighter.js} +2 -3
- package/src/system/ai_request.js +145 -595
- package/src/system/code_executer.js +111 -16
- package/src/system/file_integrity.js +5 -7
- package/src/system/log.js +3 -3
- package/src/system/mcp_integration.js +15 -13
- package/src/system/output_helper.js +0 -20
- package/src/system/session.js +97 -23
- package/src/system/session_memory.js +2 -82
- package/src/system/system_info.js +1 -1
- package/src/system/ui_events.js +0 -43
- package/src/tools/code_editor.js +17 -2
- package/src/tools/file_reader.js +17 -2
- package/src/tools/glob.js +9 -1
- package/src/tools/response_message.js +0 -2
- package/src/tools/ripgrep.js +9 -1
- package/src/tools/web_downloader.js +9 -1
- package/src/util/config.js +3 -8
- package/src/util/debug_log.js +4 -11
- package/src/util/mcp_config_manager.js +3 -5
- package/src/util/output_formatter.js +0 -47
- package/src/util/prompt_loader.js +3 -4
- package/src/util/safe_fs.js +60 -0
- package/src/util/setup_wizard.js +1 -3
- package/src/util/text_formatter.js +0 -86
- package/src/config/claude_models.js +0 -195
- package/src/ui/README.md +0 -208
- package/src/ui/api.js +0 -167
- package/src/ui/components/AgenticProgressDisplay.js +0 -126
- package/src/ui/components/Composer.js +0 -55
- package/src/ui/components/LoadingIndicator.js +0 -54
- package/src/ui/components/ModelListView.js +0 -214
- package/src/ui/components/Notifications.js +0 -55
- package/src/ui/components/StreamingIndicator.js +0 -36
- package/src/ui/contexts/AppContext.js +0 -25
- package/src/ui/contexts/StreamingContext.js +0 -20
- package/src/ui/contexts/UIStateContext.js +0 -117
- package/src/ui/example-usage.js +0 -180
- package/src/ui/hooks/useTerminalResize.js +0 -39
- package/src/ui/themes/semantic-tokens.js +0 -73
- package/src/ui/utils/text-buffer.js +0 -975
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_buildManifest.js +0 -0
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_clientMiddlewareManifest.json +0 -0
- /package/payload_viewer/out/_next/static/{t0WTsjXST7ISD1Boa6ifx → 7FgHZugvVp3pn1XStUYUJ}/_ssgManifest.js +0 -0
- /package/src/{ui → frontend}/components/BlankLine.js +0 -0
- /package/src/{ui → frontend}/components/FileDiffViewer.js +0 -0
- /package/src/{ui → frontend}/components/HelpView.js +0 -0
- /package/src/{ui → frontend}/hooks/useCompletion.js +0 -0
- /package/src/{ui → frontend}/hooks/useKeypress.js +0 -0
- /package/src/{ui → frontend}/utils/diffUtils.js +0 -0
- /package/src/{ui → frontend}/utils/renderInkComponent.js +0 -0
package/src/system/ai_request.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
debugLog('[getModelForProvider] Called');
|
|
113
71
|
const settings = await loadSettings();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
354
|
+
} else {
|
|
355
|
+
debugLog(`[request] Effort ${reasoningEffort} is supported by ${currentModel}`);
|
|
356
|
+
}
|
|
764
357
|
|
|
765
|
-
//
|
|
766
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
811
|
-
|
|
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
|
-
|
|
836
|
-
|
|
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
|
-
// 로그는 원본 포맷으로 저장
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
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
|
}
|