deel-local-cli 0.9.0 → 1.0.0
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.
- package/README.en.md +470 -27
- package/README.md +479 -31
- package/bin/deel.js +234 -186
- package/package.json +3 -2
- package/src/agent/compact.js +131 -138
- package/src/agent/effort.js +44 -11
- package/src/agent/loop.js +574 -251
- package/src/agent/memory.js +152 -0
- package/src/agent/mention.js +164 -0
- package/src/agent/modes.js +11 -3
- package/src/agent/recall.js +209 -0
- package/src/agent/salvage.js +182 -0
- package/src/agent/session.js +140 -10
- package/src/agent/store.js +36 -11
- package/src/backend/adapter.js +266 -183
- package/src/backend/ctxsize.js +133 -61
- package/src/backend/detect.js +10 -1
- package/src/backend/learn.js +102 -0
- package/src/backend/mcp.js +304 -0
- package/src/commands.js +493 -53
- package/src/oneshot.js +327 -0
- package/src/repl.js +715 -479
- package/src/report.js +19 -5
- package/src/safety/guard.js +123 -7
- package/src/safety/undo.js +96 -11
- package/src/tools/encoding.js +24 -2
- package/src/tools/fsutil.js +70 -0
- package/src/tools/index.js +471 -27
- package/src/tools/webfetch.js +37 -1
- package/src/ui/ansi.js +13 -1
- package/src/ui/diff.js +255 -0
- package/src/ui/level.js +3 -1
- package/src/ui/prompt.js +13 -1
- package/src/ui/screen.js +169 -0
- package/src/ui/status.js +81 -17
- package/src/ui/tui.js +419 -0
- package/src/version.js +28 -0
package/src/backend/ctxsize.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
// 모델이 한 번에 받아 줄 수 있는 길이(컨텍스트)
|
|
1
|
+
// 모델이 한 번에 받아 줄 수 있는 길이(컨텍스트)와 한 번에 낼 수 있는 답 길이를
|
|
2
|
+
// 서버에서 알아낸다.
|
|
2
3
|
//
|
|
3
4
|
// 왜 파일을 따로 뒀는가:
|
|
4
5
|
//
|
|
5
|
-
// 이
|
|
6
|
-
// 대화가 금방
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// 가진 것의 5% 만 쓰는 셈이다.
|
|
6
|
+
// 이 숫자들이 프로그램 전체를 좌우한다. 컨텍스트가 작으면 파일을 몇 개 못 읽히고
|
|
7
|
+
// 대화가 금방 접힌다. 답 길이 상한이 작으면 **큰 파일이 안 만들어진다.**
|
|
8
|
+
// 못 알아내면 32,768 로 깔고 앉는데, 요즘 로컬 모델은 262,144 · 655,360 이 흔하다.
|
|
9
|
+
// 그 상태로 쓰면 모델이 가진 것의 5% 만 쓰는 셈이다.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// 서버 규격을 하나하나 아는 방식으로는 못 따라간다:
|
|
12
|
+
// LM Studio · llama.cpp · vLLM · TGI · KoboldCpp · LocalAI · LiteLLM ·
|
|
13
|
+
// OpenRouter · 사내 게이트웨이, 그리고 아직 나오지 않은 것들.
|
|
14
|
+
// 그래서 세 겹으로 간다.
|
|
14
15
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// 1겹. 아는 자리를 훑는다 ← 이 파일
|
|
17
|
+
// 2겹. 우리가 원하는 값을 지시한다 ← backend/adapter.js 의 num_ctx
|
|
18
|
+
// 3겹. 거절당하면 그 말에서 배운다 ← backend/learn.js ★ 이게 진짜 답
|
|
19
|
+
//
|
|
20
|
+
// 3겹이 핵심이다. 서버는 거절할 때 정답을 알려 준다 —
|
|
21
|
+
// "This model's maximum context length is 8192 tokens, however you requested 41003"
|
|
22
|
+
// 이 방식은 **처음 보는 서버에서도 통한다.** 규격을 몰라도 되고, 새 서버가
|
|
23
|
+
// 나와도 코드를 안 고쳐도 된다.
|
|
20
24
|
//
|
|
21
25
|
// '올린 길이' 와 '모델 최대' 를 구분한다. LM Studio 는 모델이 655,360 까지
|
|
22
26
|
// 되더라도 8,192 로 올려 둘 수 있다. 그 상태에서 655,360 을 믿고 보내면
|
|
@@ -24,19 +28,37 @@
|
|
|
24
28
|
// 따로 알려 준다 — 사용자가 서버에서 더 올린 다음 /ctx 로 맞출 수 있게.
|
|
25
29
|
import { req, headersFor } from './http.js';
|
|
26
30
|
|
|
27
|
-
// 이 이름들 중 하나면 컨텍스트
|
|
31
|
+
// 이 이름들 중 하나면 '모델이 낼 수 있는 최대 컨텍스트' 다. 순서가 곧 우선순위다.
|
|
28
32
|
const 최대이름 = [
|
|
29
33
|
'max_context_length', // LM Studio
|
|
30
34
|
'context_window', // 일부 게이트웨이
|
|
31
35
|
'context_length', // llama.cpp·HF 계열
|
|
32
36
|
'max_model_len', // vLLM
|
|
33
37
|
'max_input_tokens', // LiteLLM·OpenRouter 계열
|
|
34
|
-
'
|
|
38
|
+
'n_ctx_train', // llama.cpp — 학습 때 길이
|
|
35
39
|
'max_position_embeddings', // 모델 config 원본
|
|
36
40
|
'max_sequence_length',
|
|
37
41
|
'max_seq_len',
|
|
38
42
|
];
|
|
39
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* '지금 서버에 실제로 올려 둔 길이'.
|
|
46
|
+
*
|
|
47
|
+
* context_length 를 여기서 뺐다. 그건 **모델 최대**이지 올린 길이가 아니다.
|
|
48
|
+
* Ollama /api/show 가 그 이름으로 131,072 를 주는데 서버는 8,192 만 받는다.
|
|
49
|
+
* 그대로 믿으면 확신에 찬 오답이 된다 — 화면에는 "131,072 · 서버에서 읽음" 이
|
|
50
|
+
* 뜨고, 긴 대화에서 조용히 거절당한다. 못 찾았으면 못 찾았다고 해야 한다.
|
|
51
|
+
*
|
|
52
|
+
* 진짜 올린 길이는 대개 parameters 라는 **글자 덩어리** 안에 있다.
|
|
53
|
+
* 그래서 아래 글에서찾기() 가 필요하다.
|
|
54
|
+
*/
|
|
55
|
+
const 올린이름 = ['loaded_context_length', 'num_ctx', 'n_ctx'];
|
|
56
|
+
|
|
57
|
+
// 한 번에 낼 수 있는 답 길이. 컨텍스트와 다른 축인데 전에는 아예 안 봤다.
|
|
58
|
+
const 출력이름 = [
|
|
59
|
+
'max_output_tokens', 'max_completion_tokens', 'max_tokens',
|
|
60
|
+
'num_predict', 'n_predict', 'max_output_length',
|
|
61
|
+
];
|
|
40
62
|
|
|
41
63
|
// 사람이 알아볼 수 없는 값은 안 받는다. 512 미만은 오독, 1000만 초과는 단위 착각.
|
|
42
64
|
const 최소 = 512;
|
|
@@ -50,28 +72,66 @@ function 성한수(v) {
|
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
/**
|
|
53
|
-
*
|
|
75
|
+
* 글자 덩어리 안에서 값을 찾는다.
|
|
76
|
+
*
|
|
77
|
+
* 서버가 값을 JSON 으로 안 주고 글로 주는 일이 흔하다. Ollama 가 그렇다 —
|
|
78
|
+
* "parameters": "num_ctx 8192\nstop \"<|im_end|>\""
|
|
79
|
+
* 이건 JSON 으로 보면 그냥 긴 문자열 하나다. 파보기() 로는 영영 못 찾는다.
|
|
80
|
+
* 실제로 이것 때문에 8,192 서버에 131,072 를 보내고 있었다.
|
|
81
|
+
*
|
|
82
|
+
* llama.cpp 의 /props 나 일부 게이트웨이의 설명 필드도 같은 모양이다.
|
|
83
|
+
*/
|
|
84
|
+
export function 글에서찾기(s, 이름들) {
|
|
85
|
+
const 글 = String(s ?? '');
|
|
86
|
+
if (!글 || 글.length > 200000) return null;
|
|
87
|
+
for (const 이름 of 이름들) {
|
|
88
|
+
// 이름 뒤에 : = 공백 무엇이 와도 받는다. 값에 따옴표나 자릿점이 붙어도 받는다.
|
|
89
|
+
const re = new RegExp(`(?:^|[^a-z0-9_])${이름}["']?\\s*[:=]?\\s*["']?(\\d[\\d,_]*)`, 'i');
|
|
90
|
+
const m = re.exec(글);
|
|
91
|
+
if (m) {
|
|
92
|
+
const v = 성한수(m[1].replace(/[,_]/g, ''));
|
|
93
|
+
if (v) return { value: v, key: 이름 };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 객체 어디에 박혀 있어도 찾는다. 글자 덩어리 안까지 본다.
|
|
54
101
|
*
|
|
55
102
|
* 서버가 { data: { meta: { config: { max_position_embeddings: 655360 } } } } 처럼
|
|
56
103
|
* 깊이 넣어 두는 일이 흔하다. 그래서 이름으로 훑는다. 다만 아무 데나 파고들면
|
|
57
104
|
* 엉뚱한 숫자를 집으므로 깊이를 5 로 막는다.
|
|
105
|
+
*
|
|
106
|
+
* 어느 이름에서 찾았는지도 같이 돌려준다 — 값이 이상할 때 사람이 원인을
|
|
107
|
+
* 짚을 수 있어야 한다. /ctx 자세히 가 이걸 보여 준다.
|
|
58
108
|
*/
|
|
59
|
-
function 파보기(obj, 이름들, depth = 0) {
|
|
109
|
+
export function 파보기(obj, 이름들, { 글도 = true, depth = 0 } = {}) {
|
|
60
110
|
if (!obj || typeof obj !== 'object' || depth > 5) return null;
|
|
61
111
|
for (const key of 이름들) {
|
|
62
112
|
const v = 성한수(obj[key]);
|
|
63
|
-
if (v) return v;
|
|
113
|
+
if (v) return { value: v, key };
|
|
64
114
|
}
|
|
65
|
-
// 이름이 정확히 안 맞으면 '…context_length' 처럼 끝나는 것도 본다 (Ollama).
|
|
66
115
|
for (const [k, v] of Object.entries(obj)) {
|
|
67
|
-
|
|
116
|
+
// 이름이 정확히 안 맞으면 '…context_length' 처럼 끝나는 것도 본다 (Ollama).
|
|
117
|
+
if (이름들 === 최대이름 && /(^|[._])(context_length|context_window|n_ctx)$/i.test(k)) {
|
|
68
118
|
const n = 성한수(v);
|
|
69
|
-
if (n) return n;
|
|
119
|
+
if (n) return { value: n, key: k };
|
|
120
|
+
}
|
|
121
|
+
// 글자 덩어리 안까지 본다. 여기가 Ollama 의 진짜 num_ctx 가 사는 자리다.
|
|
122
|
+
//
|
|
123
|
+
// 찾는 이름은 객체를 볼 때와 **똑같이** 맞춘다. 위의 특별 취급(…context_length
|
|
124
|
+
// 로 끝나는 이름)까지 글에서도 그대로 본다. 두 잣대가 갈리면 '객체로 오면
|
|
125
|
+
// 찾고 글로 오면 못 찾는' 설명 못 할 상태가 된다.
|
|
126
|
+
if (글도 && typeof v === 'string') {
|
|
127
|
+
const 볼이름 = 이름들 === 최대이름 ? [...이름들, 'n_ctx', 'context_window'] : 이름들;
|
|
128
|
+
const 글것 = 글에서찾기(v, 볼이름);
|
|
129
|
+
if (글것) return { value: 글것.value, key: `${k}.${글것.key}` };
|
|
70
130
|
}
|
|
71
131
|
}
|
|
72
132
|
for (const v of Object.values(obj)) {
|
|
73
133
|
if (v && typeof v === 'object') {
|
|
74
|
-
const found = 파보기(v, 이름들, depth + 1);
|
|
134
|
+
const found = 파보기(v, 이름들, { 글도, depth: depth + 1 });
|
|
75
135
|
if (found) return found;
|
|
76
136
|
}
|
|
77
137
|
}
|
|
@@ -86,67 +146,79 @@ function 목록에서찾기(json, model) {
|
|
|
86
146
|
}
|
|
87
147
|
|
|
88
148
|
/**
|
|
89
|
-
* 서버에 물어
|
|
149
|
+
* 서버에 물어 길이를 알아낸다.
|
|
150
|
+
*
|
|
151
|
+
* 두드릴 자리를 **한꺼번에** 두드린다. 전에는 차례로 갔다 — 다섯 곳이 각각
|
|
152
|
+
* 시간 제한에 걸리면 켤 때마다 그만큼 멈춰 있었다. 서로 상관없는 요청이라
|
|
153
|
+
* 순서를 지킬 이유가 없다. 우선순위는 결과를 모아 놓고 정하면 된다.
|
|
90
154
|
*
|
|
91
|
-
* @
|
|
92
|
-
* @returns {Promise<{value:number|null, max:number|null, loaded:number|null, source:string|null, tried:Array}>}
|
|
155
|
+
* @returns {Promise<{value, max, loaded, out, source, outSource, tried, why}>}
|
|
93
156
|
*/
|
|
94
|
-
export async function probeCtx(conn, { timeout =
|
|
157
|
+
export async function probeCtx(conn, { timeout = 6000 } = {}) {
|
|
95
158
|
const H = () => headersFor(conn.auth, conn.key);
|
|
96
159
|
const base = String(conn.base ?? '').replace(/\/+$/, '');
|
|
97
160
|
const origin = base.replace(/\/v\d+$/, '').replace(/\/api$/, '');
|
|
98
161
|
const model = conn.model;
|
|
99
162
|
const tried = [];
|
|
100
|
-
let max = null;
|
|
101
|
-
let loaded = null;
|
|
102
|
-
let source = null;
|
|
103
163
|
|
|
104
164
|
const 본다 = async (label, url, opts = {}) => {
|
|
105
|
-
if (max && loaded) return null;
|
|
106
165
|
try {
|
|
107
166
|
const r = await req(url, { headers: H(), timeout, ...opts });
|
|
108
167
|
tried.push({ label, url, status: r.status ?? 0, ok: !!r.ok });
|
|
109
|
-
return r.ok ? r.json : null;
|
|
168
|
+
return { label, json: r.ok ? r.json : null };
|
|
110
169
|
} catch (err) {
|
|
111
170
|
tried.push({ label, url, status: 0, ok: false, why: String(err?.message ?? err) });
|
|
112
|
-
return null;
|
|
171
|
+
return { label, json: null };
|
|
113
172
|
}
|
|
114
173
|
};
|
|
115
174
|
|
|
116
|
-
|
|
117
|
-
|
|
175
|
+
// 두드릴 자리. 순서가 곧 우선순위다 — 앞엣것이 더 믿을 만하다.
|
|
176
|
+
const 자리 = conn.kind === 'ollama'
|
|
177
|
+
? [
|
|
178
|
+
['Ollama 모델 정보', `${origin}/api/show`, { method: 'POST', body: { model } }, {}],
|
|
179
|
+
]
|
|
180
|
+
: [
|
|
181
|
+
// 이 모델만 콕 집어 묻는다. 있으면 가장 정확하다.
|
|
182
|
+
['모델 상세', `${base}/models/${encodeURIComponent(model)}`, {}, {}],
|
|
183
|
+
// LM Studio 는 자기 규격에만 '올린 길이' 를 준다. 이게 실제로 쓸 값이다.
|
|
184
|
+
['LM Studio', `${origin}/api/v0/models/${encodeURIComponent(model)}`, {}, {}],
|
|
185
|
+
['LM Studio 목록', `${origin}/api/v0/models`, {}, { 목록: true }],
|
|
186
|
+
// 목록 응답 안에 들어 있는 경우 (vLLM 의 max_model_len 이 여기 있다)
|
|
187
|
+
['모델 목록', `${base}/models`, {}, { 목록: true }],
|
|
188
|
+
// llama.cpp 는 /props 로만 알려 준다. TGI 는 /info 다.
|
|
189
|
+
['llama.cpp /props', `${origin}/props`, {}, { 올린것도: true }],
|
|
190
|
+
['TGI /info', `${origin}/info`, {}, {}],
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const 답들 = await Promise.all(자리.map(([label, url, opts]) => 본다(label, url, opts)));
|
|
194
|
+
|
|
195
|
+
let max = null; let loaded = null; let out = null;
|
|
196
|
+
let source = null; let outSource = null; let maxKey = null; let loadedKey = null;
|
|
197
|
+
|
|
198
|
+
for (const [i, [label]] of 자리.entries()) {
|
|
199
|
+
const json = 답들[i].json;
|
|
200
|
+
if (!json) continue;
|
|
201
|
+
const { 목록 = false, 올린것도 = false } = 자리[i][3] ?? {};
|
|
118
202
|
const 대상 = 목록 ? 목록에서찾기(json, model) : json;
|
|
119
|
-
if (!대상)
|
|
203
|
+
if (!대상) continue;
|
|
204
|
+
|
|
120
205
|
const m = 파보기(대상, 최대이름);
|
|
121
206
|
const l = 파보기(대상, 올린이름);
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
// 1) 이 모델만 콕 집어 묻는다. 있으면 가장 정확하다.
|
|
130
|
-
담기(await 본다('모델 상세', `${base}/models/${encodeURIComponent(model)}`), '모델 상세');
|
|
131
|
-
|
|
132
|
-
// 2) LM Studio 는 자기 규격에만 '올린 길이' 를 준다. 이게 실제로 쓸 값이다.
|
|
133
|
-
담기(await 본다('LM Studio 상세', `${origin}/api/v0/models/${encodeURIComponent(model)}`), 'LM Studio');
|
|
134
|
-
담기(await 본다('LM Studio 목록', `${origin}/api/v0/models`), 'LM Studio', { 목록: true });
|
|
135
|
-
|
|
136
|
-
// 3) 목록 응답 안에 들어 있는 경우 (vLLM 의 max_model_len 이 여기 있다)
|
|
137
|
-
담기(await 본다('모델 목록', `${base}/models`), '모델 목록', { 목록: true });
|
|
138
|
-
|
|
139
|
-
// 4) llama.cpp 서버는 /props 로만 알려 준다.
|
|
140
|
-
const props = await 본다('llama.cpp /props', `${origin}/props`);
|
|
141
|
-
if (props) {
|
|
142
|
-
const n = 파보기(props, ['n_ctx', 'context_length']);
|
|
143
|
-
if (n) { if (!max) { max = n; source = 'llama.cpp'; } if (!loaded) loaded = n; }
|
|
144
|
-
}
|
|
207
|
+
const o = 파보기(대상, 출력이름);
|
|
208
|
+
if (m && !max) { max = m.value; maxKey = m.key; source = label; }
|
|
209
|
+
if (l && !loaded) { loaded = l.value; loadedKey = l.key; }
|
|
210
|
+
// llama.cpp 의 n_ctx 는 '지금 올린 길이' 다. 그 서버에서는 최대도 그 값으로 본다.
|
|
211
|
+
if (올린것도 && m && !loaded) { loaded = m.value; loadedKey = m.key; }
|
|
212
|
+
if (o && !out) { out = o.value; outSource = label; }
|
|
145
213
|
}
|
|
146
214
|
|
|
147
215
|
// 실제로 쓸 값: 올려 둔 길이가 있으면 그것. 없으면 모델 최대.
|
|
148
216
|
const value = loaded ?? max ?? null;
|
|
149
|
-
|
|
217
|
+
const why = value ? null
|
|
218
|
+
: tried.some((t) => t.ok) ? '서버가 응답은 했지만 길이를 안 알려 줍니다'
|
|
219
|
+
: '두드린 자리에서 아무 응답도 못 받았습니다';
|
|
220
|
+
|
|
221
|
+
return { value, max, loaded, out, source, outSource, maxKey, loadedKey, tried, why };
|
|
150
222
|
}
|
|
151
223
|
|
|
152
224
|
/**
|
package/src/backend/detect.js
CHANGED
|
@@ -27,7 +27,16 @@ async function tryOpenAI(base, key) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// 401/403 이면 규격은 맞고 인증만 틀린 것 — 다음 방식으로 계속.
|
|
30
|
-
|
|
30
|
+
//
|
|
31
|
+
// 그 밖의 오류(500 등)는 '규격은 맞는데 서버가 지금 화가 난 것' 으로 본다.
|
|
32
|
+
// 그래야 서버가 한 말을 사람에게 그대로 보여 줄 수 있다.
|
|
33
|
+
//
|
|
34
|
+
// 단, **200 인데 JSON 이 아니면 여기 해당하지 않는다.** 사내 프록시가
|
|
35
|
+
// 로그인 페이지를 200 으로 내주는 일이 흔한데, 예전에는 그걸 "OpenAI 호환
|
|
36
|
+
// 서버 · 모델 0개" 로 잡았다. 그러면 사람은 모델이 안 올라온 줄 알고
|
|
37
|
+
// 엉뚱한 데를 파게 된다 — 실제로는 인증 페이지에 막힌 것이다.
|
|
38
|
+
const 성공인데JSON아님 = r.status >= 200 && r.status < 300 && !r.json;
|
|
39
|
+
if (r.status && !성공인데JSON아님 && ![401, 403, 0, 404].includes(r.status)) {
|
|
31
40
|
return { kind: 'openai', base, auth: style.id, models: [], ms: r.ms, warn: serverMessage(r) };
|
|
32
41
|
}
|
|
33
42
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// 서버가 거절할 때 하는 말에서 진짜 한계를 배운다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 이게 핵심인가:
|
|
4
|
+
//
|
|
5
|
+
// 서버 규격을 하나하나 아는 방식으로는 영영 못 따라간다. LM Studio · llama.cpp ·
|
|
6
|
+
// vLLM · TGI · KoboldCpp · LocalAI · LiteLLM · OpenRouter · 사내 게이트웨이,
|
|
7
|
+
// 그리고 아직 나오지 않은 것들. 새 서버가 나올 때마다 코드를 고쳐야 한다면
|
|
8
|
+
// 그 코드는 늘 한 발 늦는다.
|
|
9
|
+
//
|
|
10
|
+
// 그런데 서버는 거절할 때 **정답을 그대로 말해 준다.**
|
|
11
|
+
//
|
|
12
|
+
// "This model's maximum context length is 8192 tokens, however you
|
|
13
|
+
// requested 41003 tokens (33003 in the messages, 8000 in the completion)."
|
|
14
|
+
//
|
|
15
|
+
// 숫자가 둘 다 들어 있다 — 한계도, 우리가 얼마나 넘겼는지도. 규격을 몰라도
|
|
16
|
+
// 되고, 이름이 무엇인지도 알 필요가 없다. **처음 보는 서버에서도 통한다.**
|
|
17
|
+
//
|
|
18
|
+
// 그런데 지금까지 그 문장을 버리고 있었다. 스트리밍이면 본문을 안 읽고
|
|
19
|
+
// HTTP 400 만 던졌다. 화면에는 `✗ HTTP 400` 한 줄만 남았다.
|
|
20
|
+
// 사용자를 구할 수 있었던 문장이 그 자리에서 사라진 것이다.
|
|
21
|
+
//
|
|
22
|
+
// 여기서는 문장에서 숫자만 뽑는다. 뽑은 값으로 무엇을 할지는 loop.js 가 정한다.
|
|
23
|
+
|
|
24
|
+
const 최소 = 512;
|
|
25
|
+
const 최대허용 = 10_000_000;
|
|
26
|
+
|
|
27
|
+
const 성한수 = (v) => {
|
|
28
|
+
const n = Math.floor(Number(v));
|
|
29
|
+
return Number.isFinite(n) && n >= 최소 && n <= 최대허용 ? n : null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 거절 문장에서 배울 것이 있나.
|
|
34
|
+
*
|
|
35
|
+
* 여러 규격의 문장을 받는다. 영어가 대부분이지만 사내 게이트웨이가 한국어로
|
|
36
|
+
* 옮겨 놓은 것도 있어서 그쪽도 같이 본다.
|
|
37
|
+
*
|
|
38
|
+
* @returns {{kind:'ctx'|'out', limit:number, asked:number|null, text:string}|null}
|
|
39
|
+
* kind 'ctx' 는 컨텍스트 전체, 'out' 은 한 번에 낼 답 길이
|
|
40
|
+
* limit 서버가 말한 한계
|
|
41
|
+
* asked 우리가 요청했던 값 (알 수 있으면)
|
|
42
|
+
*/
|
|
43
|
+
export function 배울것(message) {
|
|
44
|
+
const s = String(message ?? '');
|
|
45
|
+
if (!s) return null;
|
|
46
|
+
|
|
47
|
+
// ── 1) 컨텍스트 한계 ──────────────────────────────────────────────────
|
|
48
|
+
// OpenAI·vLLM·LiteLLM·대부분의 게이트웨이가 이 모양으로 말한다.
|
|
49
|
+
const ctx표 = [
|
|
50
|
+
// "maximum context length is 8192 tokens, however you requested 41003"
|
|
51
|
+
// "…is 4096 tokens. However, you requested 5000 tokens." ← 마침표가 중간에 낀다.
|
|
52
|
+
// 그래서 [^.] 로 막으면 안 된다. 대신 사이를 80자로만 묶어 엉뚱한 숫자를 안 집게 한다.
|
|
53
|
+
/maximum context length is\s+(\d+)\s*tokens?(?:[\s\S]{0,80}?requested\s+(\d+))?/i,
|
|
54
|
+
/context length[^.\d]{0,40}(\d{3,})[\s\S]{0,80}?requested\s+(\d+)/i,
|
|
55
|
+
// llama.cpp: "the request exceeds the available context size. try increasing the context size or enable context shift"
|
|
56
|
+
// 숫자를 안 주는 경우다. n_ctx = 8192 처럼 따로 적어 주기도 한다.
|
|
57
|
+
/n_ctx\s*[:=]?\s*(\d{3,})/i,
|
|
58
|
+
// 한국어로 옮겨 놓은 사내 게이트웨이
|
|
59
|
+
/(?:최대\s*)?컨텍스트[^\d]{0,20}(\d{3,})/,
|
|
60
|
+
];
|
|
61
|
+
for (const re of ctx표) {
|
|
62
|
+
const m = re.exec(s);
|
|
63
|
+
if (!m) continue;
|
|
64
|
+
const limit = 성한수(m[1]);
|
|
65
|
+
if (!limit) continue;
|
|
66
|
+
return { kind: 'ctx', limit, asked: 성한수(m[2]) ?? null, text: 짧게(s) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── 2) 답 길이 한계 ───────────────────────────────────────────────────
|
|
70
|
+
const out표 = [
|
|
71
|
+
// "max_tokens is too large: 200000. This model supports at most 16384 completion tokens"
|
|
72
|
+
/supports? at most\s+(\d+)\s*(?:completion\s*)?tokens?/i,
|
|
73
|
+
// "max_tokens must be less than or equal to 8192"
|
|
74
|
+
/max_(?:completion_)?tokens[^\d]{0,40}(\d{3,})/i,
|
|
75
|
+
/num_predict[^\d]{0,20}(\d{3,})/i,
|
|
76
|
+
/(?:최대\s*)?(?:출력|답)[^\d]{0,20}(\d{3,})/,
|
|
77
|
+
];
|
|
78
|
+
for (const re of out표) {
|
|
79
|
+
const m = re.exec(s);
|
|
80
|
+
if (!m) continue;
|
|
81
|
+
const limit = 성한수(m[1]);
|
|
82
|
+
if (!limit) continue;
|
|
83
|
+
return { kind: 'out', limit, asked: null, text: 짧게(s) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 이 오류가 '너무 길어서' 인가.
|
|
91
|
+
*
|
|
92
|
+
* 숫자를 못 뽑았어도 이건 알 수 있을 때가 있다. 그러면 값을 배우지는 못해도
|
|
93
|
+
* **줄여서 다시 해 볼 수는 있다.** 사용자에게는 실패가 안 보이는 편이 낫다.
|
|
94
|
+
*/
|
|
95
|
+
export function 길이문제인가(message) {
|
|
96
|
+
const s = String(message ?? '');
|
|
97
|
+
return /context (?:length|size|window)|too (?:long|large|many tokens)|exceeds?[^.]{0,30}(?:context|limit|token)|max_tokens|token limit|컨텍스트|너무 (?:깁|많|큽)/i.test(s);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function 짧게(s) {
|
|
101
|
+
return String(s).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
102
|
+
}
|