aiexecode 1.0.96 → 1.0.98

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.

@@ -7,6 +7,7 @@ import { Box, Text } from 'ink';
7
7
  import { theme } from '../design/themeColors.js';
8
8
  import { useKeypress, keyMatchers, Command } from '../hooks/useKeypress.js';
9
9
  import { useCompletion } from '../hooks/useCompletion.js';
10
+ import { useFileCompletion } from '../hooks/useFileCompletion.js';
10
11
  import { AutocompleteMenu } from './AutocompleteMenu.js';
11
12
  import { cpSlice, cpLen } from '../utils/inputBuffer.js';
12
13
  import { uiEvents } from '../../system/ui_events.js';
@@ -48,18 +49,31 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
48
49
  // placeholder가 제공되지 않으면 무작위로 선택 (컴포넌트 마운트 시 한 번만)
49
50
  const defaultPlaceholder = useMemo(() => placeholder || getRandomPlaceholder(), []);
50
51
 
51
- const completionRaw = useCompletion(buffer, commands);
52
+ const commandCompletionRaw = useCompletion(buffer, commands);
53
+ const fileCompletionRaw = useFileCompletion(buffer);
54
+
55
+ // Stabilize completion objects to prevent handleInput from being recreated
56
+ const commandCompletion = useMemo(() => commandCompletionRaw, [
57
+ commandCompletionRaw.showSuggestions,
58
+ commandCompletionRaw.suggestions.length,
59
+ commandCompletionRaw.activeSuggestionIndex,
60
+ commandCompletionRaw.handleAutocomplete,
61
+ commandCompletionRaw.navigateUp,
62
+ commandCompletionRaw.navigateDown
63
+ ]);
52
64
 
53
- // Stabilize completion object to prevent handleInput from being recreated
54
- const completion = useMemo(() => completionRaw, [
55
- completionRaw.showSuggestions,
56
- completionRaw.suggestions.length,
57
- completionRaw.activeSuggestionIndex,
58
- completionRaw.handleAutocomplete,
59
- completionRaw.navigateUp,
60
- completionRaw.navigateDown
65
+ const fileCompletion = useMemo(() => fileCompletionRaw, [
66
+ fileCompletionRaw.showSuggestions,
67
+ fileCompletionRaw.suggestions.length,
68
+ fileCompletionRaw.activeSuggestionIndex,
69
+ fileCompletionRaw.handleAutocomplete,
70
+ fileCompletionRaw.navigateUp,
71
+ fileCompletionRaw.navigateDown
61
72
  ]);
62
73
 
74
+ // 파일 자동완성 우선, 그 다음 명령어 자동완성
75
+ const completion = fileCompletion.showSuggestions ? fileCompletion : commandCompletion;
76
+
63
77
 
64
78
  const handleSubmitAndClear = useCallback((submittedValue) => {
65
79
  buffer.setText('');
@@ -139,9 +153,17 @@ function InputPromptComponent({ buffer, onSubmit, onClearScreen, onExit, command
139
153
  // If suggestions are shown, accept the active suggestion
140
154
  if (completion.showSuggestions && completion.suggestions.length > 0) {
141
155
  const targetIndex = completion.activeSuggestionIndex === -1 ? 0 : completion.activeSuggestionIndex;
156
+ const suggestion = completion.suggestions[targetIndex];
157
+
158
+ // 파일 자동완성인 경우 (@로 시작) 완성만 하고 submit하지 않음
159
+ if (suggestion.value.startsWith('@')) {
160
+ completion.handleAutocomplete(targetIndex);
161
+ return;
162
+ }
163
+
164
+ // 명령어 자동완성인 경우 완성 후 바로 실행
142
165
  completion.handleAutocomplete(targetIndex);
143
- // Execute the command immediately
144
- const completedCommand = completion.suggestions[targetIndex].value;
166
+ const completedCommand = suggestion.value;
145
167
  handleSubmitAndClear(completedCommand);
146
168
  return;
147
169
  }
@@ -40,7 +40,7 @@ export function ModelListView({ modelsByProvider }) {
40
40
  React.createElement(Text, { key: 'spacer2' }, null),
41
41
  React.createElement(Text, { key: 'usage', bold: true }, 'Usage:'),
42
42
  React.createElement(Text, { key: 'usage-cmd' }, ' /model <model-id>'),
43
- React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model gpt-5')
43
+ React.createElement(Text, { key: 'usage-example', dimColor: true }, ' Example: /model glm-4.5')
44
44
  );
45
45
 
46
46
  return React.createElement(Box, {
@@ -15,10 +15,8 @@ import { AI_MODELS, getAllModelIds, getModelsByProvider, DEFAULT_MODEL } from '.
15
15
  function detectProviderFromApiKey(apiKey) {
16
16
  if (!apiKey || typeof apiKey !== 'string') return null;
17
17
 
18
- if (apiKey.startsWith('sk-proj-')) {
19
- return 'openai';
20
- } else if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
21
- // Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
18
+ // Z.AI API 키 형식: 32자리 hex + '.' + 16자리 영숫자
19
+ if (/^[a-f0-9]{32}\.[A-Za-z0-9]{16}$/.test(apiKey)) {
22
20
  return 'zai';
23
21
  }
24
22
  return null;
@@ -73,7 +71,7 @@ export function SetupWizard({ onComplete, onCancel }) {
73
71
  const provider = detectProviderFromApiKey(apiKey);
74
72
  if (!provider) {
75
73
  // 유효하지 않은 API 키 형식
76
- setErrorMessage('Invalid API key. Please use OpenAI (sk-proj-...) or Z.AI format.');
74
+ setErrorMessage('Invalid API key format. Please use Z.AI API key.');
77
75
  setTextInput('');
78
76
  return;
79
77
  }
@@ -188,7 +186,7 @@ export function SetupWizard({ onComplete, onCancel }) {
188
186
  case STEPS.API_KEY:
189
187
  return React.createElement(Box, { flexDirection: 'column' },
190
188
  React.createElement(Text, { bold: true, color: theme.text.accent }, '1. API Key:'),
191
- React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://platform.openai.com/account/api-keys or https://z.ai/manage-apikey/apikey-list'),
189
+ React.createElement(Text, { color: theme.text.secondary }, ' Get your API key from: https://z.ai/manage-apikey/apikey-list'),
192
190
  React.createElement(Text, null),
193
191
  React.createElement(Box, {
194
192
  borderStyle: 'round',
@@ -0,0 +1,467 @@
1
+ /**
2
+ * File Completion Hook
3
+ *
4
+ * @ 문자 감지 시 파일/디렉토리 자동완성을 제공합니다.
5
+ * - 경로 모드: @src/ → src 디렉토리 내용 표시
6
+ * - 검색 모드: @glob → 프로젝트 전체에서 "glob" 포함 파일 검색
7
+ */
8
+
9
+ import { useState, useEffect, useCallback, useRef } from 'react';
10
+ import { resolve, dirname, basename, join, relative } from 'path';
11
+ import { safeReaddir, safeStat, safeExists } from '../../util/safe_fs.js';
12
+
13
+ // 검색에서 제외할 디렉토리 (성능 최적화)
14
+ const EXCLUDED_DIRS = new Set([
15
+ 'node_modules',
16
+ '.git',
17
+ '.svn',
18
+ '.hg',
19
+ 'dist',
20
+ 'build',
21
+ 'out',
22
+ 'output',
23
+ 'target',
24
+ '.next',
25
+ '.nuxt',
26
+ '.svelte-kit',
27
+ '__pycache__',
28
+ '.pytest_cache',
29
+ '.mypy_cache',
30
+ '.tox',
31
+ 'venv',
32
+ '.venv',
33
+ 'env',
34
+ '.virtualenv',
35
+ '.cache',
36
+ '.temp',
37
+ '.tmp',
38
+ 'coverage',
39
+ '.nyc_output',
40
+ '.turbo',
41
+ '.parcel-cache',
42
+ '.sass-cache',
43
+ '.idea',
44
+ '.vscode',
45
+ '.vs',
46
+ 'vendor',
47
+ 'Pods',
48
+ '.bundle',
49
+ 'bower_components',
50
+ 'jspm_packages',
51
+ 'bin',
52
+ 'obj',
53
+ 'logs'
54
+ ]);
55
+
56
+ // 검색 최대 깊이
57
+ const MAX_SEARCH_DEPTH = 5;
58
+
59
+ // 검색 최대 결과 수
60
+ const MAX_SEARCH_RESULTS = 20;
61
+
62
+ // 디바운스 시간 (ms)
63
+ const SEARCH_DEBOUNCE_MS = 150;
64
+
65
+ /**
66
+ * 텍스트에서 @ 참조를 파싱합니다.
67
+ * @param {string} text - 전체 텍스트
68
+ * @param {number} cursorPosition - 커서 위치
69
+ * @returns {Object} { isActive, prefix, startIndex, isSearchMode }
70
+ */
71
+ function parseAtReference(text, cursorPosition) {
72
+ const textBeforeCursor = text.slice(0, cursorPosition);
73
+ const match = textBeforeCursor.match(/@([^\s@]*)$/);
74
+
75
+ if (!match) {
76
+ return { isActive: false, prefix: '', startIndex: -1, isSearchMode: false };
77
+ }
78
+
79
+ const prefix = match[1];
80
+
81
+ // 검색 모드 판단: /가 없고 2글자 이상이면 검색 모드
82
+ const isSearchMode = prefix.length >= 2 && !prefix.includes('/');
83
+
84
+ return {
85
+ isActive: true,
86
+ prefix,
87
+ startIndex: match.index,
88
+ isSearchMode
89
+ };
90
+ }
91
+
92
+ /**
93
+ * 재귀적으로 파일을 검색합니다.
94
+ * @param {string} dirPath - 검색할 디렉토리
95
+ * @param {string} searchTerm - 검색어 (소문자)
96
+ * @param {string} basePath - 기준 경로 (상대 경로 계산용)
97
+ * @param {number} depth - 현재 깊이
98
+ * @param {Array} results - 결과 배열 (mutation)
99
+ * @param {number} maxResults - 최대 결과 수
100
+ */
101
+ async function searchFilesRecursive(dirPath, searchTerm, basePath, depth, results, maxResults) {
102
+ // 깊이 제한 또는 결과 수 제한 도달
103
+ if (depth > MAX_SEARCH_DEPTH || results.length >= maxResults) {
104
+ return;
105
+ }
106
+
107
+ try {
108
+ const entries = await safeReaddir(dirPath, { withFileTypes: true });
109
+
110
+ for (const entry of entries) {
111
+ if (results.length >= maxResults) break;
112
+
113
+ const name = entry.name;
114
+
115
+ // 숨김 파일/폴더 제외
116
+ if (name.startsWith('.')) continue;
117
+
118
+ // 제외 디렉토리 체크
119
+ if (entry.isDirectory() && EXCLUDED_DIRS.has(name)) continue;
120
+
121
+ const fullPath = join(dirPath, name);
122
+ const relativePath = relative(basePath, fullPath);
123
+ const nameLower = name.toLowerCase();
124
+
125
+ // 이름에 검색어가 포함되면 결과에 추가
126
+ if (nameLower.includes(searchTerm)) {
127
+ const isDirectory = entry.isDirectory();
128
+ results.push({
129
+ value: '@' + relativePath + (isDirectory ? '/' : ''),
130
+ displayValue: relativePath,
131
+ icon: isDirectory ? '📁' : '📄',
132
+ isDirectory,
133
+ description: isDirectory ? 'Directory' : 'File',
134
+ // 정렬용 점수: 이름이 검색어로 시작하면 높은 점수
135
+ score: nameLower.startsWith(searchTerm) ? 2 : (nameLower === searchTerm ? 3 : 1)
136
+ });
137
+ }
138
+
139
+ // 디렉토리면 재귀 탐색
140
+ if (entry.isDirectory() && results.length < maxResults) {
141
+ await searchFilesRecursive(fullPath, searchTerm, basePath, depth + 1, results, maxResults);
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // 접근 불가능한 디렉토리는 무시
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 프로젝트 전체에서 파일을 검색합니다.
151
+ * @param {string} searchTerm - 검색어
152
+ * @returns {Promise<Array>} 검색 결과
153
+ */
154
+ async function searchFiles(searchTerm) {
155
+ const basePath = process.cwd();
156
+ const searchTermLower = searchTerm.toLowerCase();
157
+ const results = [];
158
+
159
+ await searchFilesRecursive(basePath, searchTermLower, basePath, 0, results, MAX_SEARCH_RESULTS);
160
+
161
+ // 점수순 정렬 (높은 점수 우선), 같은 점수면 경로 길이 짧은 것 우선
162
+ results.sort((a, b) => {
163
+ if (b.score !== a.score) return b.score - a.score;
164
+ if (a.displayValue.length !== b.displayValue.length) {
165
+ return a.displayValue.length - b.displayValue.length;
166
+ }
167
+ return a.displayValue.localeCompare(b.displayValue);
168
+ });
169
+
170
+ return results.slice(0, MAX_SEARCH_RESULTS);
171
+ }
172
+
173
+ /**
174
+ * 디렉토리 내용을 가져와 필터링합니다.
175
+ * @param {string} prefix - @ 뒤의 경로 prefix
176
+ * @returns {Promise<Array>} 자동완성 항목 목록
177
+ */
178
+ async function fetchDirectoryContents(prefix) {
179
+ try {
180
+ let dirPath;
181
+ let filterPrefix;
182
+
183
+ if (!prefix) {
184
+ dirPath = process.cwd();
185
+ filterPrefix = '';
186
+ } else if (prefix.endsWith('/')) {
187
+ dirPath = resolve(process.cwd(), prefix);
188
+ filterPrefix = '';
189
+ } else {
190
+ const parentDir = dirname(prefix);
191
+ filterPrefix = basename(prefix).toLowerCase();
192
+
193
+ if (parentDir === '.' || parentDir === '') {
194
+ dirPath = process.cwd();
195
+ } else {
196
+ dirPath = resolve(process.cwd(), parentDir);
197
+ }
198
+ }
199
+
200
+ const exists = await safeExists(dirPath);
201
+ if (!exists) return [];
202
+
203
+ const stat = await safeStat(dirPath);
204
+ if (!stat.isDirectory()) return [];
205
+
206
+ const entries = await safeReaddir(dirPath, { withFileTypes: true });
207
+ const results = [];
208
+
209
+ for (const entry of entries) {
210
+ const name = entry.name;
211
+
212
+ if (name.startsWith('.')) continue;
213
+
214
+ if (filterPrefix && !name.toLowerCase().startsWith(filterPrefix)) continue;
215
+
216
+ const isDirectory = entry.isDirectory();
217
+
218
+ let completePath;
219
+ if (!prefix) {
220
+ completePath = name;
221
+ } else if (prefix.endsWith('/')) {
222
+ completePath = prefix + name;
223
+ } else {
224
+ const parentDir = dirname(prefix);
225
+ if (parentDir === '.' || parentDir === '') {
226
+ completePath = name;
227
+ } else {
228
+ completePath = join(parentDir, name);
229
+ }
230
+ }
231
+
232
+ results.push({
233
+ value: '@' + completePath + (isDirectory ? '/' : ''),
234
+ displayValue: name,
235
+ icon: isDirectory ? '📁' : '📄',
236
+ isDirectory,
237
+ description: isDirectory ? 'Directory' : 'File'
238
+ });
239
+ }
240
+
241
+ results.sort((a, b) => {
242
+ if (a.isDirectory && !b.isDirectory) return -1;
243
+ if (!a.isDirectory && b.isDirectory) return 1;
244
+ return a.displayValue.localeCompare(b.displayValue);
245
+ });
246
+
247
+ return results.slice(0, 20);
248
+
249
+ } catch (error) {
250
+ return [];
251
+ }
252
+ }
253
+
254
+ /**
255
+ * 파일 자동완성 훅
256
+ * @param {Object} buffer - Input buffer 객체
257
+ * @returns {Object} 자동완성 상태 및 핸들러
258
+ */
259
+ export function useFileCompletion(buffer) {
260
+ const [suggestions, setSuggestions] = useState([]);
261
+ const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1);
262
+ const [showSuggestions, setShowSuggestions] = useState(false);
263
+ const [atReference, setAtReference] = useState({ isActive: false, prefix: '', startIndex: -1, isSearchMode: false });
264
+ const lastTextRef = useRef('');
265
+ const fetchingRef = useRef(false);
266
+ const debounceTimerRef = useRef(null);
267
+ const lastSearchTermRef = useRef('');
268
+
269
+ useEffect(() => {
270
+ const text = buffer.text;
271
+
272
+ if (text === lastTextRef.current) {
273
+ return;
274
+ }
275
+ lastTextRef.current = text;
276
+
277
+ // 커서 위치 계산
278
+ const [row, col] = buffer.cursor;
279
+ const lines = buffer.lines;
280
+ let cursorPosition = 0;
281
+ for (let i = 0; i < row; i++) {
282
+ cursorPosition += lines[i].length + 1;
283
+ }
284
+ cursorPosition += col;
285
+
286
+ const ref = parseAtReference(text, cursorPosition);
287
+ setAtReference(ref);
288
+
289
+ if (!ref.isActive) {
290
+ if (showSuggestions) {
291
+ setSuggestions([]);
292
+ setShowSuggestions(false);
293
+ setActiveSuggestionIndex(-1);
294
+ }
295
+ // 디바운스 타이머 정리
296
+ if (debounceTimerRef.current) {
297
+ clearTimeout(debounceTimerRef.current);
298
+ debounceTimerRef.current = null;
299
+ }
300
+ return;
301
+ }
302
+
303
+ // 검색 모드
304
+ if (ref.isSearchMode) {
305
+ // 같은 검색어면 스킵
306
+ if (ref.prefix === lastSearchTermRef.current) {
307
+ return;
308
+ }
309
+
310
+ // 기존 타이머 취소
311
+ if (debounceTimerRef.current) {
312
+ clearTimeout(debounceTimerRef.current);
313
+ }
314
+
315
+ // 디바운스 적용
316
+ debounceTimerRef.current = setTimeout(async () => {
317
+ if (fetchingRef.current) return;
318
+
319
+ fetchingRef.current = true;
320
+ lastSearchTermRef.current = ref.prefix;
321
+
322
+ try {
323
+ const results = await searchFiles(ref.prefix);
324
+
325
+ // 텍스트가 변경되었으면 무시
326
+ if (buffer.text !== lastTextRef.current) {
327
+ fetchingRef.current = false;
328
+ return;
329
+ }
330
+
331
+ setSuggestions(results);
332
+ setShowSuggestions(results.length > 0);
333
+ setActiveSuggestionIndex(results.length > 0 ? 0 : -1);
334
+ } catch (error) {
335
+ // 에러 무시
336
+ } finally {
337
+ fetchingRef.current = false;
338
+ }
339
+ }, SEARCH_DEBOUNCE_MS);
340
+
341
+ return;
342
+ }
343
+
344
+ // 경로 모드 (기존 로직)
345
+ lastSearchTermRef.current = '';
346
+
347
+ if (fetchingRef.current) {
348
+ return;
349
+ }
350
+
351
+ fetchingRef.current = true;
352
+ fetchDirectoryContents(ref.prefix).then(results => {
353
+ fetchingRef.current = false;
354
+
355
+ if (buffer.text !== lastTextRef.current) {
356
+ return;
357
+ }
358
+
359
+ setSuggestions(results);
360
+ setShowSuggestions(results.length > 0);
361
+ setActiveSuggestionIndex(results.length > 0 ? 0 : -1);
362
+ }).catch(() => {
363
+ fetchingRef.current = false;
364
+ });
365
+
366
+ }, [buffer.text, buffer.cursor, buffer.lines, showSuggestions]);
367
+
368
+ // 컴포넌트 언마운트 시 타이머 정리
369
+ useEffect(() => {
370
+ return () => {
371
+ if (debounceTimerRef.current) {
372
+ clearTimeout(debounceTimerRef.current);
373
+ }
374
+ };
375
+ }, []);
376
+
377
+ const navigateUp = useCallback(() => {
378
+ setActiveSuggestionIndex(prev =>
379
+ prev > 0 ? prev - 1 : suggestions.length - 1
380
+ );
381
+ }, [suggestions.length]);
382
+
383
+ const navigateDown = useCallback(() => {
384
+ setActiveSuggestionIndex(prev =>
385
+ prev < suggestions.length - 1 ? prev + 1 : 0
386
+ );
387
+ }, [suggestions.length]);
388
+
389
+ const handleAutocomplete = useCallback((index) => {
390
+ if (index >= 0 && index < suggestions.length) {
391
+ const suggestion = suggestions[index];
392
+ const text = buffer.text;
393
+
394
+ const beforeAt = text.slice(0, atReference.startIndex);
395
+
396
+ const [row, col] = buffer.cursor;
397
+ const lines = buffer.lines;
398
+ let cursorPosition = 0;
399
+ for (let i = 0; i < row; i++) {
400
+ cursorPosition += lines[i].length + 1;
401
+ }
402
+ cursorPosition += col;
403
+ const afterCursor = text.slice(cursorPosition);
404
+
405
+ let newValue = suggestion.value;
406
+
407
+ if (!suggestion.isDirectory) {
408
+ newValue += ' ';
409
+ }
410
+
411
+ const newText = beforeAt + newValue + afterCursor;
412
+ buffer.setText(newText);
413
+
414
+ const newCursorPos = beforeAt.length + newValue.length;
415
+
416
+ let pos = 0;
417
+ let targetRow = 0;
418
+ let targetCol = 0;
419
+ const newLines = newText.split('\n');
420
+ for (let i = 0; i < newLines.length; i++) {
421
+ if (pos + newLines[i].length >= newCursorPos) {
422
+ targetRow = i;
423
+ targetCol = newCursorPos - pos;
424
+ break;
425
+ }
426
+ pos += newLines[i].length + 1;
427
+ }
428
+
429
+ if (typeof buffer.setCursor === 'function') {
430
+ buffer.setCursor(targetRow, targetCol);
431
+ } else {
432
+ buffer.move('end');
433
+ }
434
+
435
+ if (suggestion.isDirectory) {
436
+ // 디렉토리 선택 시 검색 모드 해제
437
+ lastSearchTermRef.current = '';
438
+ } else {
439
+ setSuggestions([]);
440
+ setShowSuggestions(false);
441
+ setActiveSuggestionIndex(-1);
442
+ }
443
+ }
444
+ }, [buffer, suggestions, atReference]);
445
+
446
+ const resetCompletionState = useCallback(() => {
447
+ setSuggestions([]);
448
+ setShowSuggestions(false);
449
+ setActiveSuggestionIndex(-1);
450
+ setAtReference({ isActive: false, prefix: '', startIndex: -1, isSearchMode: false });
451
+ lastSearchTermRef.current = '';
452
+ if (debounceTimerRef.current) {
453
+ clearTimeout(debounceTimerRef.current);
454
+ debounceTimerRef.current = null;
455
+ }
456
+ }, []);
457
+
458
+ return {
459
+ suggestions,
460
+ activeSuggestionIndex,
461
+ showSuggestions,
462
+ navigateUp,
463
+ navigateDown,
464
+ handleAutocomplete,
465
+ resetCompletionState
466
+ };
467
+ }
@@ -704,21 +704,46 @@ export async function request(taskName, requestPayload) {
704
704
  *
705
705
  * @see https://platform.openai.com/docs/guides/error-codes
706
706
  */
707
+ /*
708
+ * Z.AI/GLM 컨텍스트 초과 에러 샘플:
709
+ * {
710
+ * "error": {
711
+ * "code": "1210",
712
+ * "message": "Invalid API parameter, please check the documentation.Request 320006 input tokens exceeds the model's maximum context length 202750"
713
+ * },
714
+ * "request_id": "20260124000100026f0914d2e744f5"
715
+ * }
716
+ * HTTP_STATUS: 400
717
+ */
707
718
  export function shouldRetryWithTrim(error) {
708
719
  if (!error) return false;
709
720
 
710
- // OpenAI SDK의 공식 에러 코드 확인
721
+ // 에러 코드 메시지 추출
711
722
  const errorCode = error?.code || error?.error?.code;
723
+ const errorMessage = error?.message || error?.error?.message || '';
712
724
 
725
+ // OpenAI: context_length_exceeded
713
726
  if (errorCode === 'context_length_exceeded') {
714
- debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded');
727
+ debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded (OpenAI)');
728
+ return true;
729
+ }
730
+
731
+ // Z.AI/GLM: code "1210" (context length exceeded)
732
+ if (errorCode === '1210' || errorCode === 1210) {
733
+ debugLog('[shouldRetryWithTrim] Detected: code 1210 (GLM context exceeded)');
734
+ return true;
735
+ }
736
+
737
+ // 메시지에서 컨텍스트 초과 패턴 감지 (다양한 API 대응)
738
+ const contextExceededPattern = /exceeds.*(?:context|token).*(?:length|limit|maximum)/i;
739
+ if (contextExceededPattern.test(errorMessage)) {
740
+ debugLog(`[shouldRetryWithTrim] Detected context exceeded in message: ${errorMessage.substring(0, 100)}`);
715
741
  return true;
716
742
  }
717
743
 
718
744
  // 400 에러도 trim 후 재시도 대상
719
745
  if (error?.status === 400 || error?.response?.status === 400) {
720
746
  const errorType = error?.type || error?.error?.type;
721
- const errorMessage = error?.message || error?.error?.message || '';
722
747
  debugLog(
723
748
  `[shouldRetryWithTrim] Detected 400 error - ` +
724
749
  `Type: ${errorType}, Code: ${errorCode}, ` +