deel-local-cli 1.20.6 → 1.20.8
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.ko.md +4 -2
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/agent/compact.js +103 -8
- package/src/agent/evolve.js +18 -1
- package/src/backend/adapter.js +93 -9
- package/src/backend/http.js +52 -8
- package/src/backend/mcp.js +108 -13
- package/src/backend/probe.js +108 -14
- package/src/backend/wire.js +323 -18
- package/src/commands.js +49 -3
- package/src/i18n/en.js +1 -0
- package/src/i18n/ko.js +1 -0
- package/src/repl.js +3 -2
- package/src/tools/jobs.js +85 -7
package/README.ko.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/ko/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -301,7 +301,7 @@ deel --offline
|
|
|
301
301
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
302
302
|
|
|
303
303
|
```
|
|
304
|
-
deel 1.20.
|
|
304
|
+
deel 1.20.8 ⌂ 이 안
|
|
305
305
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -1289,6 +1289,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1289
1289
|
|
|
1290
1290
|
| 판 | 무엇이 바뀌었나 |
|
|
1291
1291
|
|---|---|
|
|
1292
|
+
| [1.20.8](docs/ko/releases/1.20.md#1208) | 못 쟀다는 것을 안 된다고 적고 있었습니다 |
|
|
1293
|
+
| [1.20.7](docs/ko/releases/1.20.md#1207) | 서버가 말해 준 것과 반대를 배우고 있었습니다 |
|
|
1292
1294
|
| [1.20.6](docs/ko/releases/1.20.md#1206) | 2차 리뷰가 판을 조용히 잃고 있었습니다 |
|
|
1293
1295
|
| [1.20.5](docs/ko/releases/1.20.md#1205) | `aws_access_token` 이 안 잡히고 있었습니다 |
|
|
1294
1296
|
| [1.20.4](docs/ko/releases/1.20.md#1204) | 붙임표 마디를 하나만 세고 있었습니다 |
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
22
22
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
23
|
-
[](docs/en/develop.md)
|
|
24
24
|
|
|
25
25
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
26
26
|
[](package.json)
|
|
@@ -374,7 +374,7 @@ deel --offline
|
|
|
374
374
|
The destination is printed at the top of every session:
|
|
375
375
|
|
|
376
376
|
```
|
|
377
|
-
deel 1.20.
|
|
377
|
+
deel 1.20.8 ⌂ inside
|
|
378
378
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
379
379
|
```
|
|
380
380
|
|
|
@@ -1350,6 +1350,8 @@ so one run tells you everything.
|
|
|
1350
1350
|
|
|
1351
1351
|
| Version | What changed |
|
|
1352
1352
|
|---|---|
|
|
1353
|
+
| [1.20.8](docs/en/releases/1.20.md#1208) | We were writing down "can't" where the truth was "couldn't measure" |
|
|
1354
|
+
| [1.20.7](docs/en/releases/1.20.md#1207) | We were learning the opposite of what the server said |
|
|
1353
1355
|
| [1.20.6](docs/en/releases/1.20.md#1206) | The second review was losing rounds silently |
|
|
1354
1356
|
| [1.20.5](docs/en/releases/1.20.md#1205) | `aws_access_token` was not being caught |
|
|
1355
1357
|
| [1.20.4](docs/en/releases/1.20.md#1204) | Only one hyphen segment was being counted |
|
package/package.json
CHANGED
package/src/agent/compact.js
CHANGED
|
@@ -105,7 +105,9 @@ export const 캐시일때비울몫 = 0.10;
|
|
|
105
105
|
* 늦어질 뿐이고, 있는데 없다고 치면 방금 만든 캐시를 깬다. 그래서 모르는
|
|
106
106
|
* 쪽은 덜 잃는 쪽으로 기울인다.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* **두 번** 불렀는데도 읽힌 것이 하나도 없으면, 그때는 정말 없는 것이다.
|
|
109
|
+
* (여기가 「두 번 넘게」 라고 적혀 있었다. 아래 셈은 두 번째에 이미 단정한다 —
|
|
110
|
+
* 한 번의 차이지만, 적힌 것과 하는 것이 다르면 다음 사람이 그 값을 못 믿는다.)
|
|
109
111
|
*/
|
|
110
112
|
export const 재볼기회 = 2;
|
|
111
113
|
|
|
@@ -163,7 +165,37 @@ export function foldImages(session, { keep = KEEP_IMAGES } = {}) {
|
|
|
163
165
|
const 자리 = [];
|
|
164
166
|
ms.forEach((m, i) => { if (그림장수(m)) 자리.push(i); });
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
/*
|
|
169
|
+
* ── 남기는 것은 **장수**다, 메시지 수가 아니다 ─────────────────────────
|
|
170
|
+
*
|
|
171
|
+
* `KEEP_IMAGES` 는 위 머리말대로 「최근 몇 **장**」 이다. 그런데 자리를
|
|
172
|
+
* 메시지 단위로 세고 거기서 keep 을 빼고 있었다. 그래서 한 메시지에 여러
|
|
173
|
+
* 장이 실리면(`@a.png @b.png @c.png` 를 한 줄로 붙이면 그렇게 된다)
|
|
174
|
+
* **남는 장수가 keep 을 훌쩍 넘었다.**
|
|
175
|
+
*
|
|
176
|
+
* 석 장 실린 메시지 + 한 장 실린 메시지 → 자리가 둘이라 한 장도 안 뺀다
|
|
177
|
+
* (남는 것은 넉 장. keep 은 둘)
|
|
178
|
+
*
|
|
179
|
+
* 몇 MB 짜리 사진 셋이 「최근 둘만 남긴다」 는 이름 아래 그대로 남는다.
|
|
180
|
+
*
|
|
181
|
+
* ── 그러면서도 한 덩이는 쪼개지 않는다 ────────────────────────────────
|
|
182
|
+
*
|
|
183
|
+
* 사람이 한 번에 붙인 사진들은 한 이야기다. 그 안에서 어느 장이 더 오래된
|
|
184
|
+
* 것인지 알 길도 없다. 그래서 **마지막 덩이는 몇 장이든 남기고**, 그보다
|
|
185
|
+
* 앞엣것은 장수를 더해 keep 을 안 넘을 때만 남긴다. 마지막 덩이를 빼지
|
|
186
|
+
* 않는 까닭: 방금 보여 준 사진을 그 자리에서 없애면 그 턴이 헛턴이 된다.
|
|
187
|
+
*/
|
|
188
|
+
let 남긴장수 = 0;
|
|
189
|
+
let 첫남길자리 = 자리.length;
|
|
190
|
+
for (let k = 자리.length - 1; k >= 0; k--) {
|
|
191
|
+
const 이번장수 = 그림장수(ms[자리[k]]);
|
|
192
|
+
// 마지막 덩이는 무조건 남긴다. 그 뒤로는 넘치면 거기서 끊는다.
|
|
193
|
+
if (k < 자리.length - 1 && 남긴장수 + 이번장수 > keep) break;
|
|
194
|
+
남긴장수 += 이번장수;
|
|
195
|
+
첫남길자리 = k;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const 뺄것 = 자리.slice(0, 첫남길자리);
|
|
167
199
|
const 뺀것들 = [];
|
|
168
200
|
for (const i of 뺄것) {
|
|
169
201
|
const m = ms[i];
|
|
@@ -174,7 +206,14 @@ ${접힘표} 그림 ${장수}장은 자리를 비우려고 뺐습니다. 필요
|
|
|
174
206
|
delete ms[i].images;
|
|
175
207
|
뺀것들.push({ 장수 });
|
|
176
208
|
}
|
|
177
|
-
|
|
209
|
+
/*
|
|
210
|
+
* `뺀것` 은 **장수**다.
|
|
211
|
+
*
|
|
212
|
+
* 여기가 `뺄것.length` — 곧 **메시지 수**였다. 화면은 이 값을 「그림 N장을
|
|
213
|
+
* 뺐습니다」 로 적으므로, 한 메시지에서 넉 장을 뺀 판에 「1장」 이라고
|
|
214
|
+
* 떴다. 사람이 세어 볼 수 있는 숫자라 틀리면 바로 거짓이 된다.
|
|
215
|
+
*/
|
|
216
|
+
return { 뺀것: 뺀것들.reduce((a, x) => a + x.장수, 0), 뺀메시지: 뺄것.length, 뺀것들 };
|
|
178
217
|
}
|
|
179
218
|
|
|
180
219
|
export function shouldFold(session, at = null) {
|
|
@@ -241,8 +280,21 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
|
|
|
241
280
|
* 같은 것을 두 번 뽑을 까닭이 없다 — 한 번 뽑아서 같이 들고 간다.
|
|
242
281
|
*/
|
|
243
282
|
const 아는것 = 이름표.get(첫?.id) ?? null;
|
|
283
|
+
/*
|
|
284
|
+
* 이름은 **한 메시지에 실린 것 전부**를 들고 간다.
|
|
285
|
+
*
|
|
286
|
+
* 아래 「마지막 할 일 목록은 안 접는다」 가 이 이름을 본다. 그런데
|
|
287
|
+
* 첫 것만 들고 가면, 모델이 한 턴에 `Read` 와 `TodoWrite` 를 같이 부른
|
|
288
|
+
* 판에서 — Anthropic 꼴은 그 결과 둘을 **한 메시지**에 싣는다 —
|
|
289
|
+
* 이름이 'Read' 로 잡혀 할 일 목록이 지킴 목록에서 빠진다. 그러면
|
|
290
|
+
* 그 자리가 접히면서 남은 항목이 어디에도 없어진다.
|
|
291
|
+
*
|
|
292
|
+
* 아래 머리말이 「접는 순간 남은 항목이 어디에도 없어진다」 고 적어 둔
|
|
293
|
+
* 바로 그 일이, 도구를 둘 같이 부른 판에서만 조용히 일어났다 (35차 리뷰).
|
|
294
|
+
*/
|
|
295
|
+
const 이름들 = 것들.map((x) => 이름표.get(x?.id)?.name ?? x?.name ?? '').filter(Boolean);
|
|
244
296
|
자리.push({
|
|
245
|
-
i, 글,
|
|
297
|
+
i, 글, 이름들,
|
|
246
298
|
이름: 아는것?.name ?? 첫?.name ?? '',
|
|
247
299
|
args: 아는것?.args ?? {},
|
|
248
300
|
});
|
|
@@ -262,7 +314,7 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
|
|
|
262
314
|
* 남겨 둘수록 해롭다. 지키는 것은 **마지막 하나**뿐이고, 목록 하나는
|
|
263
315
|
* 몇백 글자라 자리도 거의 안 먹는다.
|
|
264
316
|
*/
|
|
265
|
-
const 지킬할일 = 자리.filter((x) => x
|
|
317
|
+
const 지킬할일 = 자리.filter((x) => x.이름들.includes('TodoWrite')).at(-1)?.i ?? -1;
|
|
266
318
|
|
|
267
319
|
const 접을것 = 자리
|
|
268
320
|
.slice(0, Math.max(0, 자리.length - keep))
|
|
@@ -296,7 +348,31 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
|
|
|
296
348
|
* 100자도 안 된다). 그래서 이 수는 언제나 **새로 접을 것**의 수다.
|
|
297
349
|
*/
|
|
298
350
|
const 예상이득 = 접을것.reduce((a, x) => a + estimateTokens(x.글), 0);
|
|
299
|
-
|
|
351
|
+
/*
|
|
352
|
+
* ── 개수를 기다리는 것이 손해가 되는 판 ────────────────────────────────
|
|
353
|
+
*
|
|
354
|
+
* 위 개수 문턱은 「3천 토큰 아끼려고 앞머리를 깨지 말라」 는 말이다. 맞는
|
|
355
|
+
* 말인데, **아낄 것이 3만 토큰일 때도** 똑같이 기다렸다. 큰 파일 읽기 셋이
|
|
356
|
+
* 쌓인 판에서 접기를 미루면 그 다음 안전망은 80% 요약 압축이고, 그쪽은
|
|
357
|
+
* 대화를 **통째로 요약으로 갈아 치우는** 훨씬 비싼 일이다. 개수를 기다리다
|
|
358
|
+
* 더 큰 것을 치르는 셈이다.
|
|
359
|
+
*
|
|
360
|
+
* 그래서 기다림을 **값으로** 판단한다. 앞머리를 깨는 값은 **접은 뒤에**
|
|
361
|
+
* 그 자리부터 다시 보내야 하는 토큰이다 — 캐시에서 싸게 읽던 것을 정가로
|
|
362
|
+
* 다시 쓴다. 아끼는 것이 그보다 크면 지금 접는 것이 이득이라, 개수를 더
|
|
363
|
+
* 기다리지 않는다.
|
|
364
|
+
*
|
|
365
|
+
* 접기 전 양에서 아끼는 양을 뺀다. 접을 글 자체를 대가로 세면 안 된다 —
|
|
366
|
+
* 그건 접은 뒤에 사라지는 것이라, 세는 순간 늘 「기다려라」 가 나온다.
|
|
367
|
+
*/
|
|
368
|
+
const 깰자리 = 접을것[0]?.i ?? -1;
|
|
369
|
+
const 깨는값 = 깰자리 < 0 ? Infinity
|
|
370
|
+
: Math.max(0, ms.slice(깰자리).reduce((a, m) => a + estimateTokens(
|
|
371
|
+
도구결과인가(m) ? 결과들(m).map((x) => x.글).join('\n')
|
|
372
|
+
: (typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '')),
|
|
373
|
+
), 0) - 예상이득);
|
|
374
|
+
const 기다릴까 = 접을것.length < keep && 예상이득 <= 깨는값;
|
|
375
|
+
if (예상이득 < 넘을것 || 기다릴까) {
|
|
300
376
|
return {
|
|
301
377
|
접은것: 0, 아낀토큰: 0, 접은것들: [], 미룸: true,
|
|
302
378
|
모인것: 예상이득, 모인개수: 접을것.length, 넘을것,
|
|
@@ -352,7 +428,18 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
|
|
|
352
428
|
*/
|
|
353
429
|
return {
|
|
354
430
|
접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰), 접은것들,
|
|
355
|
-
|
|
431
|
+
/*
|
|
432
|
+
* 화면에 「캐시를 다시 씁니다」 를 적을까.
|
|
433
|
+
*
|
|
434
|
+
* `캐시가도나()` 를 그대로 쓰고 있었다. 그 함수는 **결정**을 위해 모르는
|
|
435
|
+
* 쪽을 「있다」 로 기울인다(그 머리말) — 그래서 첫 턴에는, 캐시가 아예
|
|
436
|
+
* 없는 창구에서도 늘 참이었다. 그러면 화면은 「값과 지연이 한 번 튑니다」
|
|
437
|
+
* 라고 겁을 주는데 잃을 캐시가 애초에 없다.
|
|
438
|
+
*
|
|
439
|
+
* 결정은 기울여도 되지만 **화면은 아는 것만 적는다.** 캐시를 정말 읽어
|
|
440
|
+
* 본 적이 있을 때만 말한다 (35차 리뷰).
|
|
441
|
+
*/
|
|
442
|
+
캐시다시씀: (session?.usage?.cacheRead ?? 0) > 0,
|
|
356
443
|
};
|
|
357
444
|
}
|
|
358
445
|
|
|
@@ -525,7 +612,15 @@ export async function compact(session, { auto = false, signal = null, onBackoff
|
|
|
525
612
|
* 「앞에서 읽은 그대로입니다」 를 내밀면 모델은 대화에 없는 글을 가리키는
|
|
526
613
|
* 쪽지만 받는다. 통째로 다시 싣는 것보다 훨씬 나쁘다.
|
|
527
614
|
*/
|
|
528
|
-
|
|
615
|
+
/*
|
|
616
|
+
* 읽은 파일 수도 같이 내린다.
|
|
617
|
+
*
|
|
618
|
+
* 아래 성공한 길은 `filesRead.clear()` 도 하는데 이쪽은 안 했다. 그 표는
|
|
619
|
+
* `/context` 가 「도구 결과 N개」 로 세는 자리라(agent/session.js), 물러선
|
|
620
|
+
* 뒤에는 **대화에 없는 파일까지 세어 보여 줬다.** 사람이 자리를 왜 이만큼
|
|
621
|
+
* 쓰는지 그 화면으로 판단하는데, 거기 없는 것이 섞여 있으면 안 된다.
|
|
622
|
+
*/
|
|
623
|
+
if (folded > 0) { session.파일기억?.잊기(); session.filesRead?.clear?.(); }
|
|
529
624
|
const after = session.breakdown().used;
|
|
530
625
|
return {
|
|
531
626
|
ok: folded > 0, folded, before, after, fallback: true,
|
package/src/agent/evolve.js
CHANGED
|
@@ -298,10 +298,27 @@ export class 배움 {
|
|
|
298
298
|
return out.length > 1 ? out.join('\n') : null;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* 사람이 지우라고 한 것. 파일째 비운다.
|
|
303
|
+
*
|
|
304
|
+
* ── `'전선'` 은 왜 따로 있나 ───────────────────────────────────────────
|
|
305
|
+
*
|
|
306
|
+
* 전선 모양은 **틀리게 배울 수 있는 유일한 것**이다 (backend/wire.js 의
|
|
307
|
+
* 배울전선: "잘못 배우는 것은 못 배우는 것보다 나쁘다"). 게이트웨이가
|
|
308
|
+
* 한동안 잘못 답했거나 우리가 문구를 잘못 읽으면, 멀쩡한 기능이 꺼진 채
|
|
309
|
+
* 디스크에 굳는다 — 그리고 그 창구에서는 영영 그대로다.
|
|
310
|
+
*
|
|
311
|
+
* 그런데 되돌릴 길이 **「쌓아 둔 것 전부 비우기」 하나**였다. 그러면 몇 주
|
|
312
|
+
* 걸려 쌓은 명령 겪음과 토큰 보정까지 같이 날아간다. 그 값이 아까워서
|
|
313
|
+
* 사람은 안 비우고, 안 비우니 꺼진 기능을 그냥 안고 쓴다.
|
|
314
|
+
*
|
|
315
|
+
* 그래서 전선만 지운다. 다음 요청에서 짐작으로 다시 시작하고, 서버가
|
|
316
|
+
* 여전히 거절하면 같은 것을 다시 배운다 — 잃을 것이 없다.
|
|
317
|
+
*/
|
|
302
318
|
지우기(어디 = '전부') {
|
|
303
319
|
if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
|
|
304
320
|
if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
|
|
321
|
+
if (어디 === '전선') { this.집.전선 = {}; this.#저장집(); }
|
|
305
322
|
return this;
|
|
306
323
|
}
|
|
307
324
|
|
package/src/backend/adapter.js
CHANGED
|
@@ -120,7 +120,16 @@ export function 규격이름(shape) {
|
|
|
120
120
|
export function 주소붙이기(base, 길) {
|
|
121
121
|
const b2 = String(base ?? '');
|
|
122
122
|
const i = b2.indexOf('?');
|
|
123
|
-
|
|
123
|
+
/*
|
|
124
|
+
* 끝 빗금은 **물음표가 있을 때만** 떼고 있었다.
|
|
125
|
+
*
|
|
126
|
+
* 사람이 주소를 적을 때 `…/v1/` 처럼 끝에 빗금을 하나 붙이는 것은 흔한
|
|
127
|
+
* 일이고, 그러면 `…/v1//chat/completions` 가 나갔다.
|
|
128
|
+
* `//` 는 게이트웨이마다 **다른 길**이라 404 다 — 그리고 그 404 는 화면에서
|
|
129
|
+
* 모델 이름을 잘못 적은 것과 구별이 안 된다. 물음표가 있는 쪽에서는 이미
|
|
130
|
+
* 떼고 있었으니, 같은 주소를 물음표 있고 없고에 따라 다르게 다듬던 셈이다.
|
|
131
|
+
*/
|
|
132
|
+
if (i < 0) return b2.replace(/\/+$/, '') + 길;
|
|
124
133
|
return b2.slice(0, i).replace(/\/+$/, '') + 길 + b2.slice(i);
|
|
125
134
|
}
|
|
126
135
|
|
|
@@ -174,7 +183,7 @@ export function buildBody(shape, { model, messages, tools, stream, json, think,
|
|
|
174
183
|
*/
|
|
175
184
|
else if (think === 'off') body.think = false;
|
|
176
185
|
else if (think !== undefined) {
|
|
177
|
-
const 눈금 =
|
|
186
|
+
const 눈금 = 나가는눈금(shape, 카드, think);
|
|
178
187
|
if (눈금) body.think = 눈금;
|
|
179
188
|
}
|
|
180
189
|
return body;
|
|
@@ -272,7 +281,7 @@ export function buildBody(shape, { model, messages, tools, stream, json, think,
|
|
|
272
281
|
* 전선에서도 high 밖에 못 나간다 — 화면에는 max 라고 떠 있는 채로.
|
|
273
282
|
* 카드가 있으면 카드가 아는 눈금으로, 없으면 여태처럼 좁은 쪽으로 맞춘다.
|
|
274
283
|
*/
|
|
275
|
-
const 눈금 =
|
|
284
|
+
const 눈금 = 나가는눈금(shape, 카드, think);
|
|
276
285
|
if (눈금) body.reasoning_effort = 눈금;
|
|
277
286
|
}
|
|
278
287
|
/*
|
|
@@ -369,6 +378,52 @@ export function 강도말(강도) {
|
|
|
369
378
|
return 전선눈금[String(강도)] ?? null;
|
|
370
379
|
}
|
|
371
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 이 전선에 **눈금 글자가 정말로 실려 나가나** — 나가면 그 글자, 안 나가면 null.
|
|
383
|
+
*
|
|
384
|
+
* ── 왜 따로 함수인가 ────────────────────────────────────────────────────
|
|
385
|
+
*
|
|
386
|
+
* `/think` 화면이 「지금 나가는 값 high」 를 적으려면 이것을 알아야 하는데,
|
|
387
|
+
* 여태 화면은 `눈금맞추기(카드, 강도)` 를 직접 불렀다. 그 함수는 **카드가 아는
|
|
388
|
+
* 눈금으로 낮추는 일**만 한다 — 그 눈금이 실릴 칸이 있는지는 모른다. 그래서
|
|
389
|
+
* 화면은 이런 자리에서 거짓을 적었다.
|
|
390
|
+
*
|
|
391
|
+
* budget (Anthropic 옛 판) 눈금이 아니라 **토큰 수**가 나간다
|
|
392
|
+
* boolean (ollama) 카드 눈금 표를 안 쓴다 — 이 규격이 아는 말로 옮긴다
|
|
393
|
+
* none 실을 칸이 아예 없다
|
|
394
|
+
* adaptive + 효력칸 null 서버가 output_config 를 안 받는다고 말해 준 판
|
|
395
|
+
*
|
|
396
|
+
* 앞의 세 자리에서 화면은 「지금 나가는 값 high」 라고 적고 전선에는 아무
|
|
397
|
+
* 눈금도 안 나갔다. `/think high` 를 치고 화면이 받아 준 것을 본 사람은 그
|
|
398
|
+
* 값이 먹었다고 믿는다 — 안 먹은 것보다 나쁘다.
|
|
399
|
+
*
|
|
400
|
+
* 그래서 **몸을 만드는 세 갈래가 모두 이 함수를 부른다.** 화면도 이 함수를
|
|
401
|
+
* 부른다. 둘이 갈라질 수 없는 모양으로 묶어 두는 것이 이 함수의 목적이라,
|
|
402
|
+
* 여기 갈래를 늘릴 때는 buildBody 쪽에 같은 갈래를 또 적지 않는다.
|
|
403
|
+
*
|
|
404
|
+
* @param {string|null} 규격 conn.kind — 눈금이 실릴 칸은 규격이 정한다
|
|
405
|
+
* @param {object|null} 카드 backend/wire.js 의 전선 카드 (없으면 짐작)
|
|
406
|
+
* @param {*} 강도 사람이 정한 세기 ('off' · 'low' … 'max' · 참거짓 · undefined)
|
|
407
|
+
* @returns {string|null}
|
|
408
|
+
*/
|
|
409
|
+
export function 나가는눈금(규격, 카드, 강도) {
|
|
410
|
+
// 안 정했거나 참거짓이면 눈금 글자가 나갈 일이 없다 (참거짓은 켜고 끄는 말이다).
|
|
411
|
+
if (강도 === undefined || typeof 강도 === 'boolean') return null;
|
|
412
|
+
// 이 규격은 카드 눈금 표를 안 쓴다 — 위 ollama 갈래가 강도말() 로 옮겨 싣는다.
|
|
413
|
+
if (규격 === 'ollama') return 강도 === 'off' ? null : 강도말(강도);
|
|
414
|
+
if (규격 === 'anthropic') {
|
|
415
|
+
// 끄라는 말에는 thinking 칸 자체를 안 싣는다 — 눈금도 같이 안 나간다.
|
|
416
|
+
if (강도 === 'off') return null;
|
|
417
|
+
// budget 은 눈금이 아니라 숫자로 준다(생각예산). 눈금은 그 숫자를 고르는 데만 쓴다.
|
|
418
|
+
if ((카드?.생각형식 ?? 'budget') !== 'adaptive') return null;
|
|
419
|
+
if (카드?.효력칸 !== 'output_config') return null;
|
|
420
|
+
return 눈금맞추기(카드, 강도);
|
|
421
|
+
}
|
|
422
|
+
// openai 꼴. 카드가 없으면(검사·진단) 여태처럼 좁은 쪽으로 맞춘다.
|
|
423
|
+
if (!카드) return 강도말(강도);
|
|
424
|
+
return 카드.생각형식 === 'effort' ? 눈금맞추기(카드, 강도) : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
372
427
|
export function 생각예산(강도, maxTokens) {
|
|
373
428
|
const 바라는것 = 강도별예산[String(강도)];
|
|
374
429
|
if (!바라는것) return 0;
|
|
@@ -434,8 +489,9 @@ function anthropic몸({ model, messages, tools, stream, json, think, maxTokens,
|
|
|
434
489
|
if (형식 === 'adaptive') {
|
|
435
490
|
if (think !== undefined && think !== false && think !== 'off') {
|
|
436
491
|
body.thinking = { type: 'adaptive' };
|
|
437
|
-
|
|
438
|
-
|
|
492
|
+
// 효력칸을 볼지 말지도 나가는눈금() 이 안다 — 여기서 또 보면 둘이 갈라진다.
|
|
493
|
+
const 눈금 = 나가는눈금('anthropic', 카드, think);
|
|
494
|
+
if (눈금) body.output_config = { effort: 눈금 };
|
|
439
495
|
}
|
|
440
496
|
} else if (형식 === 'budget') {
|
|
441
497
|
const 예산 = 생각예산(think, maxTokens);
|
|
@@ -746,7 +802,18 @@ export function assistantMessage(shape, { content = '', thinking = '', toolCalls
|
|
|
746
802
|
if (toolCalls.length) m.tool_calls = toolCalls.map((t) => ({ function: { name: t.name, arguments: t.args } }));
|
|
747
803
|
return m;
|
|
748
804
|
}
|
|
749
|
-
|
|
805
|
+
/*
|
|
806
|
+
* `content: null` 은 **도구를 부를 때만** 옳다.
|
|
807
|
+
*
|
|
808
|
+
* 이 규격에서 assistant 의 content 는 「tool_calls 가 없으면 있어야 하는」
|
|
809
|
+
* 칸이다. 그런데 빈 답에 도구도 없는 턴(거절·상한에 걸려 아무 말도 못 한
|
|
810
|
+
* 턴)에서 `content: null` 만 실어 이력에 넣었고, 그 이력을 다음 요청에
|
|
811
|
+
* 그대로 보내면 그 턴이 거절된다. 옆 갈래(anthropic)는 같은 자리에서
|
|
812
|
+
* 자리표시를 넣어 막아 뒀는데 이쪽만 안 막혀 있었다.
|
|
813
|
+
*
|
|
814
|
+
* 없는 말을 지어 넣지는 않는다 — 빈 글자로 칸만 채운다.
|
|
815
|
+
*/
|
|
816
|
+
const m = { role: 'assistant', content: content || (toolCalls.length ? null : '') };
|
|
750
817
|
if (toolCalls.length) {
|
|
751
818
|
m.tool_calls = toolCalls.map((t) => ({
|
|
752
819
|
id: t.id, type: 'function',
|
|
@@ -1490,7 +1557,14 @@ function anthropic흡수(obj, acc, out) {
|
|
|
1490
1557
|
// 캐시 수치는 여기 한 번만 온다. message_delta 에는 안 실린다 —
|
|
1491
1558
|
// 여기서 안 챙기면 흘려받기에서는 캐시가 영영 0 으로 보인다.
|
|
1492
1559
|
const c = 캐시읽기(u);
|
|
1493
|
-
|
|
1560
|
+
/*
|
|
1561
|
+
* `잰것` 을 여기만 안 적고 있었다 — ollama·openai 갈래는 둘 다 적는다.
|
|
1562
|
+
*
|
|
1563
|
+
* 이 값은 **usage 가 진짜로 왔나**를 뜻한다. 잠잠한지 보는 자(자란만큼)가
|
|
1564
|
+
* 그걸 「살아 있다」 의 한 표로 세는데, Anthropic 흘려받기에서는 usage 가
|
|
1565
|
+
* 제일 먼저 오는 소식이라 하필 그 한 표를 못 셌다.
|
|
1566
|
+
*/
|
|
1567
|
+
acc.usage = { 잰것: true, in: u.input_tokens ?? 0, out: u.output_tokens ?? 0, cacheRead: c.읽음, cacheWrite: c.씀 };
|
|
1494
1568
|
}
|
|
1495
1569
|
return out;
|
|
1496
1570
|
}
|
|
@@ -1586,8 +1660,18 @@ function 도구마무리(acc) {
|
|
|
1586
1660
|
// 스트리밍은 마지막 조각이 안 오면 여기서 늘 깨진 채로 끝난다.
|
|
1587
1661
|
acc.toolCalls = acc._raw.filter(Boolean).map((c, i) => {
|
|
1588
1662
|
const call = { id: c.id ?? `call_${i + 1}`, name: c.name, args: {} };
|
|
1589
|
-
|
|
1590
|
-
|
|
1663
|
+
/*
|
|
1664
|
+
* 빈 것은 깨진 것이 아니다 — 인자가 아예 없는 도구가 있다.
|
|
1665
|
+
*
|
|
1666
|
+
* 한 번에 받는 쪽(normalizeCalls)은 `trim()` 을 하고 나서 비었나 보는데
|
|
1667
|
+
* 여기는 안 했다. 그래서 창구가 인자 자리에 공백 한 칸(`" "` · `"\n"`)을
|
|
1668
|
+
* 흘려 주면 흘려받을 때만 **깨진 도구 부름**이 됐다 — 같은 모델, 같은
|
|
1669
|
+
* 도구인데 흘려받나 아니냐로 갈렸다. 같은 일을 하는 두 자가 같은 것을
|
|
1670
|
+
* 빈 것으로 봐야 한다.
|
|
1671
|
+
*/
|
|
1672
|
+
const 원문 = typeof c.args === 'string' ? c.args.trim() : c.args;
|
|
1673
|
+
if (!원문) return call;
|
|
1674
|
+
try { call.args = JSON.parse(원문); }
|
|
1591
1675
|
catch { call.argsBroken = true; call.rawArgs = c.args; }
|
|
1592
1676
|
return call;
|
|
1593
1677
|
});
|
package/src/backend/http.js
CHANGED
|
@@ -115,6 +115,8 @@ const 되돌림상태 = new Set([301, 302, 303, 307, 308]);
|
|
|
115
115
|
* 되돌림(다음URL) — 되돌림을 따라가기 **전에** 부른다. 던지면 안 따라간다.
|
|
116
116
|
* 부르는 쪽이 제 규칙(사내망 거절 등)을 여기서 건다.
|
|
117
117
|
* @returns 흘려 받기면 { ok, status, headers, res: { body(getReader), headers, text() }, ms }
|
|
118
|
+
* — 다만 **실패한 응답에는 body 가 없다** (읽을 것이 흐름이 아니라 거절 글이다).
|
|
119
|
+
* 부르는 쪽은 `ok` 를 먼저 보고 body 를 만져야 한다 (adapter 의 거절읽기 참고).
|
|
118
120
|
* 아니면 { ok, status, headers, bytes, text, json, ms }
|
|
119
121
|
* 프록시가 407 로 막으면 { ok:false, status:407, error } (몸은 없다)
|
|
120
122
|
*/
|
|
@@ -617,13 +619,28 @@ function 터널(hostname, port, 프록시, sig, 왜끊겼나, 인증서 = null)
|
|
|
617
619
|
host: 프록시.host, port: 프록시.port, agent: false,
|
|
618
620
|
method: 'CONNECT', path: `${hostname}:${port}`, headers: 머리,
|
|
619
621
|
});
|
|
620
|
-
|
|
622
|
+
/*
|
|
623
|
+
* 중단이 닿아야 할 자리가 판마다 다르다.
|
|
624
|
+
*
|
|
625
|
+
* · CONNECT 를 기다리는 동안 → 요청
|
|
626
|
+
* · 그 위에 TLS 를 올리는 동안 → 터널 소켓
|
|
627
|
+
*
|
|
628
|
+
* 예전에는 CONNECT 답을 받은 **그 자리에서** 귀를 떼 버렸다. 그래서 200 만
|
|
629
|
+
* 주고 입을 다무는 프록시를 만나면(뒷단이 죽은 프록시·들여다보는 프록시)
|
|
630
|
+
* 중단도 시계도 그 소켓에 안 닿았다. 부르는 쪽은 제때 깨지는데 소켓이 남아
|
|
631
|
+
* — 답을 찍고도 프로세스가 안 끝나는 모양이 됐다(아래 close 머리말과 같은 탈).
|
|
632
|
+
* 귀는 그대로 두고 **끊을 자리만 바꿔** 쥔다.
|
|
633
|
+
*/
|
|
634
|
+
let 끊을것 = () => rq.destroy(왜끊겼나());
|
|
635
|
+
const 끊기 = () => 끊을것();
|
|
621
636
|
sig.addEventListener('abort', 끊기, { once: true });
|
|
622
637
|
const 정리 = () => sig.removeEventListener('abort', 끊기);
|
|
623
638
|
|
|
624
639
|
rq.once('connect', (res, socket, head) => {
|
|
625
|
-
|
|
640
|
+
// 끊긴 뒤에 답이 도착한 판 — 소켓을 받아 놓고 아무도 안 쥐면 그대로 남는다.
|
|
641
|
+
if (sig.aborted) { 정리(); socket.destroy(); return reject(왜끊겼나()); }
|
|
626
642
|
if (res.statusCode !== 200) {
|
|
643
|
+
정리();
|
|
627
644
|
socket.destroy();
|
|
628
645
|
const e = new Error(res.statusCode === 407
|
|
629
646
|
? 인증말(res.headers['proxy-authenticate'], 프록시)
|
|
@@ -636,8 +653,10 @@ function 터널(hostname, port, 프록시, sig, 왜끊겼나, 인증서 = null)
|
|
|
636
653
|
const 이름 = /^[\d.]+$|:/.test(hostname) ? undefined : hostname;
|
|
637
654
|
// 우리 인증서도 이 위에서 낸다 — 프록시 뒤에서 mTLS 가 안 되면
|
|
638
655
|
// 사내(프록시 필수 + 인증서 필수)에서는 붙는 방법이 아예 없다.
|
|
639
|
-
const tls = tlsConnect({ socket, host: hostname, servername: 이름, ...(인증서 ?? {}) }, () => resolve(tls));
|
|
640
|
-
|
|
656
|
+
const tls = tlsConnect({ socket, host: hostname, servername: 이름, ...(인증서 ?? {}) }, () => { 정리(); resolve(tls); });
|
|
657
|
+
// 이제부터 중단은 **이 소켓**을 끊어야 한다 (위 머리말).
|
|
658
|
+
끊을것 = () => { const e = 왜끊겼나(); tls.destroy(e); socket.destroy(); reject(e); };
|
|
659
|
+
tls.once('error', (e) => { 정리(); reject(e); }); // 대상 인증서 문제 — 프록시 탓이 아니라 표시를 안 붙인다
|
|
641
660
|
// TLS 층이 닫혀도 밑의 프록시 소켓은 저절로 안 닫힌다. 그대로 두면 요청마다 소켓이
|
|
642
661
|
// 하나씩 남아 프로세스가 안 끝난다 (deel run 이 답을 찍고도 안 나가는 모양이 된다).
|
|
643
662
|
tls.once('close', () => socket.destroy());
|
|
@@ -661,8 +680,22 @@ function 다읽기(res) {
|
|
|
661
680
|
// 고치라고 하면 고쳐도 안 바뀐다 (평가에서 잡혔다).
|
|
662
681
|
function 인증말(도전, 프록시) {
|
|
663
682
|
const s = String(도전 ?? '');
|
|
664
|
-
|
|
665
|
-
|
|
683
|
+
/*
|
|
684
|
+
* 사내 프록시는 흔히 **여러 방식을 한꺼번에** 내민다 —
|
|
685
|
+
* Proxy-Authenticate: Negotiate, Basic realm="corp"
|
|
686
|
+
* 앞엣것만 보고 "이 방식은 지원하지 않습니다, 담당자에게 문의하세요" 로 끝내면,
|
|
687
|
+
* 바로 옆에 우리가 갈 수 있는 Basic 이 열려 있는데도 사람을 돌려보내는 셈이다.
|
|
688
|
+
* 이 파일이 애초에 생긴 까닭이 그거다 — 적힌 탈출구가 막혀 있는 것이 제일 나쁘다.
|
|
689
|
+
*
|
|
690
|
+
* 방식 이름은 쉼표로 나뉜 **앞자리**에만 온다. realm="basic-corp" 같은 글에
|
|
691
|
+
* 걸리면 안 되니 자리까지 본다. 그리고 자리를 보려면 **따옴표 속을 먼저
|
|
692
|
+
* 지워야** 한다 — realm="internal, basic auth" 의 쉼표는 방식을 나누는
|
|
693
|
+
* 쉼표가 아닌데, 안 지우면 그것만으로 「Basic 도 준다」 로 읽힌다.
|
|
694
|
+
*/
|
|
695
|
+
const 베이직 = /(?:^|,)\s*basic\b/i.test(s.replace(/"[^"]*"/g, '""'));
|
|
696
|
+
const 못하는것 = /ntlm/i.test(s) ? 'NTLM' : (/negotiate/i.test(s) ? 'Negotiate' : null);
|
|
697
|
+
if (못하는것 && !베이직) {
|
|
698
|
+
return `프록시(${프록시.url})가 ${못하는것} 인증을 요구합니다 — 이 방식은 지원하지 않습니다.`
|
|
666
699
|
+ ' 사내 담당자에게 Basic 인증이나 인증 없는 프록시 주소를 문의하세요.';
|
|
667
700
|
}
|
|
668
701
|
const 주소 = `http://user:pw@${프록시.host}:${프록시.port}`;
|
|
@@ -670,7 +703,8 @@ function 인증말(도전, 프록시) {
|
|
|
670
703
|
? `설정 파일의 "proxy": "${주소}"`
|
|
671
704
|
: `${프록시.출처 ?? 'HTTPS_PROXY'}=${주소}`;
|
|
672
705
|
return `프록시(${프록시.url})가 인증을 요구합니다 (407${s ? ` · ${s.slice(0, 60)}` : ''})`
|
|
673
|
-
+ ` — 프록시 주소에 user:pw@ 를 넣으세요: ${자리}
|
|
706
|
+
+ ` — 프록시 주소에 user:pw@ 를 넣으세요: ${자리}`
|
|
707
|
+
+ (못하는것 ? ` (${못하는것} 도 같이 요구하지만 그건 못 합니다 — Basic 으로 가세요)` : '');
|
|
674
708
|
}
|
|
675
709
|
|
|
676
710
|
// fetch 가 던진 것에서 코드 하나를 뽑는다. undici 는 원인을 cause 에 싸서 준다.
|
|
@@ -739,7 +773,17 @@ export function normalizeError(err) {
|
|
|
739
773
|
* 그래서 두 가지를 한 자리에서 잡아 **우리 문장으로 갈아 끼운다.** 원문을
|
|
740
774
|
* 안 보여 주는 것이 여기서는 친절이 아니라 안전이다.
|
|
741
775
|
*/
|
|
742
|
-
|
|
776
|
+
/*
|
|
777
|
+
* 셋째 말투 — **프록시를 켠 사람만 다른 답을 받고 있었다.**
|
|
778
|
+
*
|
|
779
|
+
* 곧장 가는 길은 fetch(undici)라 위의 두 말투가 나온다. 그런데 프록시를 켜면
|
|
780
|
+
* node:http 로 가고, 거기서는 말투가 아예 다르다:
|
|
781
|
+
* Invalid character in header content ["Authorization"] (code ERR_INVALID_CHAR)
|
|
782
|
+
* 열쇠 값은 안 들어 있어 새지는 않는다. 대신 **안내가 안 뜬다** — 같은 열쇠,
|
|
783
|
+
* 같은 실수인데 프록시가 있고 없고에 따라 한 쪽만 한국어 안내를 받았다.
|
|
784
|
+
*/
|
|
785
|
+
if (/ByteString|character at index|invalid header (value|name)|invalid character in header|must be a valid HTTP token/i.test(m)
|
|
786
|
+
|| /^ERR_INVALID_(CHAR|HTTP_TOKEN)$/.test(코드)) return '열쇠(또는 헤더)에 한글·특수문자가 섞여 있습니다 — API 키는 영문·숫자만 실립니다. 붙여넣을 때 따옴표나 줄바꿈이 딸려 오지 않았는지 보세요';
|
|
743
787
|
if (/fetch failed/i.test(m)) return '연결 실패 — 주소·포트·프록시를 확인하세요';
|
|
744
788
|
return m;
|
|
745
789
|
})();
|