deel-local-cli 1.17.8 → 1.17.9
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 -3
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/acp/serve.js +2 -2
- package/src/backend/adapter.js +10 -7
- package/src/backend/http.js +37 -1
- package/src/backend/quota.js +79 -9
- package/src/backend/retry.js +61 -6
- package/src/commands.js +10 -4
- package/src/i18n/en.js +3 -0
- package/src/i18n/ja.js +3 -0
- package/src/i18n/ko.js +3 -0
- package/src/i18n/zh.js +3 -0
- package/src/oneshot.js +2 -2
- package/src/repl.js +2 -2
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.17.
|
|
304
|
+
deel 1.17.9 ⌂ 이 안
|
|
305
305
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -1218,7 +1218,7 @@ deel stats # 이 폴더에서 실제로 무엇을 했나 (.de
|
|
|
1218
1218
|
## 개발
|
|
1219
1219
|
|
|
1220
1220
|
```bash
|
|
1221
|
-
npm test 전체 검증 (8,
|
|
1221
|
+
npm test 전체 검증 (8,140항목 — 몇몇은 터미널에 따라 갈립니다)
|
|
1222
1222
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1223
1223
|
npm run verify 반입·통신 검증만
|
|
1224
1224
|
npm run bench 편집 성공률 측정
|
|
@@ -1287,6 +1287,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1287
1287
|
|
|
1288
1288
|
| 판 | 무엇이 바뀌었나 |
|
|
1289
1289
|
|---|---|
|
|
1290
|
+
| [1.17.9](docs/ko/releases/1.17.md#1179) | 한도에 걸렸는데 1초 뒤에 또 두드리고 있었습니다 |
|
|
1290
1291
|
| [1.17.8](docs/ko/releases/1.17.md#1178) | 긴 지시문을 통째로 붙여 넣으면 시킨 것과 반대로 갔습니다 — 그리고 망이 한 번 끊기면 턴이 사라졌습니다 |
|
|
1291
1292
|
| [1.17.7](docs/ko/releases/1.17.md#1177) | 나란히 세워 재 봤고, 진 항목을 고쳤습니다 |
|
|
1292
1293
|
| [1.17.6](docs/ko/releases/1.17.md#1176) | 살아 있다는 신호만 오고 내용이 안 오면, 여태 영원히 기다렸습니다 |
|
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.17.
|
|
377
|
+
deel 1.17.9 ⌂ inside
|
|
378
378
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
379
379
|
```
|
|
380
380
|
|
|
@@ -1293,7 +1293,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
|
|
|
1293
1293
|
## Development
|
|
1294
1294
|
|
|
1295
1295
|
```bash
|
|
1296
|
-
npm test Full suite (8,
|
|
1296
|
+
npm test Full suite (8,140 checks; a few are TTY-dependent)
|
|
1297
1297
|
npm run coverage Which lines the tests actually execute
|
|
1298
1298
|
npm run verify Import + network checks only
|
|
1299
1299
|
npm run bench Edit success rate
|
|
@@ -1348,6 +1348,7 @@ so one run tells you everything.
|
|
|
1348
1348
|
|
|
1349
1349
|
| Version | What changed |
|
|
1350
1350
|
|---|---|
|
|
1351
|
+
| [1.17.9](docs/en/releases/1.17.md#1179) | Rate-limited, and knocking again one second later |
|
|
1351
1352
|
| [1.17.8](docs/en/releases/1.17.md#1178) | A long brief got the opposite of what it asked for — and one network blip threw the turn away |
|
|
1352
1353
|
| [1.17.7](docs/en/releases/1.17.md#1177) | We measured it side by side, and fixed the row we lost |
|
|
1353
1354
|
| [1.17.6](docs/en/releases/1.17.md#1176) | Only keep-alive and no content used to mean waiting forever |
|
package/package.json
CHANGED
package/src/acp/serve.js
CHANGED
|
@@ -43,7 +43,7 @@ import { History } from '../safety/undo.js';
|
|
|
43
43
|
import { Audit, 열쇠묻기 } from '../safety/audit.js';
|
|
44
44
|
import { activeProfile, load, resolveKey, homeDir, save as saveCfg } from '../config.js';
|
|
45
45
|
import { 말 as 옮긴말 } from '../i18n/index.js';
|
|
46
|
-
import {
|
|
46
|
+
import { 알림채움, 알림말 } from '../backend/retry.js';
|
|
47
47
|
import { 전선붙이기, 세션이름짓기 } from '../backend/wire.js';
|
|
48
48
|
import { discover } from '../skills/discover.js';
|
|
49
49
|
import { allowEndpoint, setOffline } from '../safety/network.js';
|
|
@@ -631,7 +631,7 @@ export async function acp(opts = {}) {
|
|
|
631
631
|
// 서버가 잠깐 막아 기다리는 중. 아직 흘러간 글이 없으니 답을 새로 시작하지는 않는다.
|
|
632
632
|
case 'backoff':
|
|
633
633
|
말하기(`\n\n_(${ev.미리
|
|
634
|
-
? 옮긴말(
|
|
634
|
+
? 옮긴말(알림말(ev), { 초: 알림채움(ev).초 })
|
|
635
635
|
: 옮긴말('loop.backoff', 알림채움(ev))})_\n\n`);
|
|
636
636
|
break;
|
|
637
637
|
|
package/src/backend/adapter.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
req, headersFor, serverMessage, Aborted, 잠잠기본, 연결기본, 초로,
|
|
7
7
|
무소식기본, 무소식알림기본, 소식없음오류,
|
|
8
8
|
} from './http.js';
|
|
9
|
-
import { 할당량기억, 미리기다릴까, 마지막할당량, 할당량자리 } from './quota.js';
|
|
9
|
+
import { 할당량기억, 미리기다릴까, 왜띄우나, 마지막할당량, 할당량자리 } from './quota.js';
|
|
10
10
|
import { 열쇠 as 열쇠받아오기, 쓸수있나 } from '../safety/authcmd.js';
|
|
11
11
|
import { 말 } from '../i18n/index.js';
|
|
12
12
|
import { 다시부를지, 기다리기, 정책고르기 } from './retry.js';
|
|
@@ -1011,9 +1011,10 @@ function 몸덤프(body) {
|
|
|
1011
1011
|
*/
|
|
1012
1012
|
async function 미리비키기(opts, conn) {
|
|
1013
1013
|
// **이 창구** 것만 본다. 옆 창구가 바닥났다고 이쪽이 기다리면 안 된다.
|
|
1014
|
-
const
|
|
1014
|
+
const 것 = 마지막할당량(할당량자리(conn));
|
|
1015
|
+
const ms = 미리기다릴까(것);
|
|
1015
1016
|
if (!ms) return 0;
|
|
1016
|
-
opts.onBackoff?.({ type: 'backoff', status: 429, code: null, wait: ms, attempt: 0, max: 0, 미리: true });
|
|
1017
|
+
opts.onBackoff?.({ type: 'backoff', status: 429, code: null, wait: ms, attempt: 0, max: 0, 미리: true, 왜: 왜띄우나(것) });
|
|
1017
1018
|
await 기다리기(ms, opts.signal ?? null);
|
|
1018
1019
|
return ms;
|
|
1019
1020
|
}
|
|
@@ -1091,7 +1092,8 @@ export async function chat(conn, opts) {
|
|
|
1091
1092
|
});
|
|
1092
1093
|
// 서버가 남았다고 말해 준 할당량을 적어 둔다 (backend/quota.js).
|
|
1093
1094
|
// 429 를 맞고 나서야 아는 것과, 맞기 전에 아는 것은 사람이 할 일이 다르다.
|
|
1094
|
-
|
|
1095
|
+
// 막힌 것이면 그 사실도 적는다 — 다음 부름이 이걸 보고 띄운다 (backend/quota.js).
|
|
1096
|
+
할당량기억(r.headers, 할당량자리(conn), { 막힘: r.status === 429 });
|
|
1095
1097
|
// 다듬느라 이름을 고쳤으면 여기서 되돌린다. 밖에서는 그런 일이 있었는지
|
|
1096
1098
|
// 모른 채로 원래 이름을 받는다.
|
|
1097
1099
|
if (r.ok) {
|
|
@@ -1175,9 +1177,10 @@ export async function* chatStream(conn, opts) {
|
|
|
1175
1177
|
let 열쇠다시받음 = false;
|
|
1176
1178
|
let 쌓인대기 = 0;
|
|
1177
1179
|
{
|
|
1178
|
-
const
|
|
1180
|
+
const 잰것 = 마지막할당량(할당량자리(conn));
|
|
1181
|
+
const 미리 = 미리기다릴까(잰것);
|
|
1179
1182
|
if (미리) {
|
|
1180
|
-
yield { type: 'backoff', status: 429, code: null, wait: 미리, attempt: 0, max: 0, 미리: true };
|
|
1183
|
+
yield { type: 'backoff', status: 429, code: null, wait: 미리, attempt: 0, max: 0, 미리: true, 왜: 왜띄우나(잰것) };
|
|
1181
1184
|
await 기다리기(미리, opts.signal ?? null);
|
|
1182
1185
|
쌓인대기 += 미리;
|
|
1183
1186
|
}
|
|
@@ -1199,7 +1202,7 @@ export async function* chatStream(conn, opts) {
|
|
|
1199
1202
|
stream: true,
|
|
1200
1203
|
signal: opts.signal ?? null,
|
|
1201
1204
|
});
|
|
1202
|
-
할당량기억(r.headers ?? r.res?.headers, 할당량자리(conn));
|
|
1205
|
+
할당량기억(r.headers ?? r.res?.headers, 할당량자리(conn), { 막힘: r.status === 429 });
|
|
1203
1206
|
if (r.ok && r.res?.body) break;
|
|
1204
1207
|
const 거절 = await 거절읽기(r);
|
|
1205
1208
|
// 위 chat() 과 같은 규칙. 몸을 먼저 읽고(거절읽기) 나서 다시 부른다 —
|
package/src/backend/http.js
CHANGED
|
@@ -25,6 +25,7 @@ import { Readable } from 'node:stream';
|
|
|
25
25
|
import { checkUrl, NetBlocked } from '../safety/network.js';
|
|
26
26
|
import { 프록시고르기 } from './proxy.js';
|
|
27
27
|
import { 인증서찾기 } from './clientcert.js';
|
|
28
|
+
import { 말 as 옮긴말 } from '../i18n/index.js';
|
|
28
29
|
|
|
29
30
|
export const AUTH_STYLES = [
|
|
30
31
|
{ id: 'bearer', label: 'Authorization: Bearer', apply: (h, k) => { h['Authorization'] = `Bearer ${k}`; } },
|
|
@@ -768,6 +769,41 @@ export function 프록시힌트(말) {
|
|
|
768
769
|
return `게이트웨이 앞단 프록시가 '${도구}' 를 돌리다 실패했습니다 — 모델이 아니라 프록시 쪽 문제입니다.`;
|
|
769
770
|
}
|
|
770
771
|
|
|
772
|
+
/**
|
|
773
|
+
* 429 본문을 보고 **무슨 한도인지** 알아본다.
|
|
774
|
+
*
|
|
775
|
+
* ── 왜 이게 필요한가 ────────────────────────────────────────────────────
|
|
776
|
+
*
|
|
777
|
+
* 화면에 「서버가 잠시 막았습니다 (HTTP 429)」 만 뜨면, 사람이 할 수 있는 것이
|
|
778
|
+
* 기다리는 것밖에 없다. 그런데 429 는 **한 가지가 아니다.** 어떤 것은 1분 뒤에
|
|
779
|
+
* 저절로 풀리고, 어떤 것은 관리자가 할당량을 올려 주기 전까지 안 풀린다. 그
|
|
780
|
+
* 둘에 같은 말을 하면 사람은 30분을 기다리다 포기한다.
|
|
781
|
+
*
|
|
782
|
+
* 실제로 이런 것이 왔다.
|
|
783
|
+
*
|
|
784
|
+
* Too many requests sent to ApplyGuardrail: On-demand ApplyGuardrail
|
|
785
|
+
* sensitive information policy text units per second limit exceeded.
|
|
786
|
+
*
|
|
787
|
+
* 이건 모델 토큰 한도가 아니다. **가드레일이 훑는 글의 양**이고, 단위가 초당
|
|
788
|
+
* text unit(1 TU 는 1,000자쯤)이다. 그래서 대화가 자랄수록 매 요청이 커지고,
|
|
789
|
+
* 어느 순간부터 **매번** 걸린다 — 「잘하다가 갑자기」 가 이 모양이다.
|
|
790
|
+
*
|
|
791
|
+
* 그리고 프리픽스 캐시가 여기서는 하나도 안 듣는다. 캐시는 모델 쪽 이야기고,
|
|
792
|
+
* 가드레일은 캐시와 무관하게 매번 전체 글을 훑는다. 이걸 모르면 「캐시가 걸리는데
|
|
793
|
+
* 왜 한도에 걸리지」 에서 막힌다.
|
|
794
|
+
*
|
|
795
|
+
* 원문은 지우지 않고 뒤에 남긴다 — 사내 담당자에게 그대로 보여 줘야 할 때가
|
|
796
|
+
* 있다. 프록시힌트 와 같은 자세다.
|
|
797
|
+
*/
|
|
798
|
+
export function 막힘힌트(글) {
|
|
799
|
+
const s = String(글 ?? '');
|
|
800
|
+
// 가드레일: 초당 **글의 양**. 기다린다고 안 풀리고, 프롬프트를 줄이거나 한도를 올려야 한다.
|
|
801
|
+
if (/guardrail/i.test(s) && /text\s*units?/i.test(s)) return 옮긴말('net.limit.guardrail');
|
|
802
|
+
// 분당 한도: 창이 새로 열린다. 여기서는 기다리는 것이 맞는 답이다.
|
|
803
|
+
if (/per\s*minute|\bTPM\b|\bRPM\b/i.test(s)) return 옮긴말('net.limit.perMinute');
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
|
|
771
807
|
// 서버가 준 오류 본문에서 사람이 읽을 문장만 뽑는다.
|
|
772
808
|
export function serverMessage(r) {
|
|
773
809
|
if (r.error) return r.error;
|
|
@@ -780,6 +816,6 @@ export function serverMessage(r) {
|
|
|
780
816
|
else return `HTTP ${r.status}`;
|
|
781
817
|
// 알아본 것이 있으면 원문 대신 그것을 앞에 세운다. 원문은 뒤에 한 줄로 남긴다 —
|
|
782
818
|
// 사내 담당자에게 그대로 보여 줘야 할 때가 있다.
|
|
783
|
-
const 힌트 = 프록시힌트(말);
|
|
819
|
+
const 힌트 = 프록시힌트(말) ?? (Number(r.status) === 429 ? 막힘힌트(말) : null);
|
|
784
820
|
return 힌트 ? `${힌트}\n원문: ${말.slice(0, 160)}` : 말;
|
|
785
821
|
}
|
package/src/backend/quota.js
CHANGED
|
@@ -201,9 +201,27 @@ export function 할당량자리(conn) {
|
|
|
201
201
|
return `${host}|${String(conn.model ?? '').trim()}${꼴 ? `#${꼴}` : ''}`;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
/**
|
|
205
|
+
* 응답 머리에서 할당량을 읽어 적어 둔다.
|
|
206
|
+
*
|
|
207
|
+
* `막힘` 은 **그 응답이 429 였나**다. 머리에 남은 수가 안 실려 와도 이 한
|
|
208
|
+
* 글자는 안다 — 그리고 그것만으로도 다음 요청을 띄울 까닭이 된다
|
|
209
|
+
* (아래 미리기다릴까).
|
|
210
|
+
*
|
|
211
|
+
* @param {boolean} [옵션.막힘] 이 응답이 429 였나
|
|
212
|
+
*/
|
|
213
|
+
export function 할당량기억(머리, 어디 = '', { 막힘 = false } = {}) {
|
|
205
214
|
const 것 = 할당량읽기(머리);
|
|
206
|
-
|
|
215
|
+
것.막힘 = !!막힘;
|
|
216
|
+
/*
|
|
217
|
+
* 429 는 머리가 비어 있어도 적어 둔다.
|
|
218
|
+
*
|
|
219
|
+
* 게이트웨이 상당수가 막을 때 남은 수도 `Retry-After` 도 안 준다. 그래도
|
|
220
|
+
* **방금 막혔다는 사실**은 우리가 안다. 그 하나로 다음 요청을 조금 띄울 수
|
|
221
|
+
* 있고, 그게 이 루프에서 제일 크게 듣는다 — 200걸음짜리 턴에서 41번째가
|
|
222
|
+
* 바로 다시 두드리면 한도는 영영 안 풀린다.
|
|
223
|
+
*/
|
|
224
|
+
if (것.있나 || 막힘) {
|
|
207
225
|
const 적을것 = { ...것, 때: Date.now() };
|
|
208
226
|
마지막 = 적을것;
|
|
209
227
|
if (어디) {
|
|
@@ -212,6 +230,15 @@ export function 할당량기억(머리, 어디 = '') {
|
|
|
212
230
|
// 오래 안 쓴 자리부터 버린다. 창구를 옮겨 다녀도 표가 안 자란다.
|
|
213
231
|
while (자리들.size > 자리최대) 자리들.delete(자리들.keys().next().value);
|
|
214
232
|
}
|
|
233
|
+
} else if (어디) {
|
|
234
|
+
/*
|
|
235
|
+
* 머리가 없는 **성공** 응답. 적을 것은 없지만, 앞서 적어 둔 「막혔다」 는
|
|
236
|
+
* 지워야 한다. 안 지우면 한 번 막힌 뒤로 낡을 때까지(낡은값) 멀쩡한
|
|
237
|
+
* 요청마다 띄운다 — 고친 것이 아니라 새 고장이다.
|
|
238
|
+
*/
|
|
239
|
+
const 옛 = 자리들.get(어디);
|
|
240
|
+
if (옛?.막힘) 자리들.set(어디, { ...옛, 막힘: false });
|
|
241
|
+
if (마지막?.막힘) 마지막 = { ...마지막, 막힘: false };
|
|
215
242
|
}
|
|
216
243
|
return 것;
|
|
217
244
|
}
|
|
@@ -253,6 +280,23 @@ export function 할당량잊기(어디 = '') {
|
|
|
253
280
|
export const 낡은값 = 60000; // 이보다 오래된 할당량으로는 안 정한다
|
|
254
281
|
export const 미리기다림상한 = 60000;
|
|
255
282
|
|
|
283
|
+
/*
|
|
284
|
+
* 방금 막혔으면 다음 요청을 이만큼은 띄운다.
|
|
285
|
+
*
|
|
286
|
+
* 서버가 「언제 오라」 고 말해 줬으면 그 말을 쓴다(풀림). 이건 **아무 말도 안
|
|
287
|
+
* 해 줬을 때**의 바닥이다. 게이트웨이 상당수가 막을 때 남은 수도 `Retry-After`
|
|
288
|
+
* 도 안 준다 — 그때 우리가 아는 것은 「방금 막혔다」 하나뿐이다.
|
|
289
|
+
*
|
|
290
|
+
* 이건 서버의 창을 짐작하는 것이 아니다. **우리 박자를 늦추는 것**이다. 그
|
|
291
|
+
* 둘은 다르다 — 앞엣것은 없는 것을 지어내는 짓이고, 뒤엣것은 우리가 얼마나
|
|
292
|
+
* 빨리 두드릴지 우리가 정하는 것이다.
|
|
293
|
+
*
|
|
294
|
+
* 5초로 둔다. 실제로 본 한도가 **초당**이라(Bedrock 의 ApplyGuardrail text
|
|
295
|
+
* units per second) 몇 초만 띄워도 듣는다. 그러면서 한 번 막혔다고 사람이
|
|
296
|
+
* 체감할 만큼 느려지지도 않는다.
|
|
297
|
+
*/
|
|
298
|
+
export const 막힘띄움 = 5000;
|
|
299
|
+
|
|
256
300
|
/*
|
|
257
301
|
* 어느 창구 것인지는 **부르는 쪽이 정해서 준다.** 기본값을 두지 않는다.
|
|
258
302
|
*
|
|
@@ -261,14 +305,40 @@ export const 미리기다림상한 = 60000;
|
|
|
261
305
|
* 방금 고친 그 고장이다. 기본값을 없애면 다음에 빠뜨렸을 때 조용히 옛
|
|
262
306
|
* 동작으로 돌아가는 대신 눈에 보이게 어긋난다.
|
|
263
307
|
*/
|
|
308
|
+
/**
|
|
309
|
+
* 왜 띄우나 — `'바닥'`(남은 것이 0 이라고 서버가 말해 줌) 또는 `'막힘'`(방금 429).
|
|
310
|
+
*
|
|
311
|
+
* 화면 문구가 여기서 갈린다. 「할당량이 바닥났다」 는 **서버가 그렇게 말해 줬을
|
|
312
|
+
* 때만** 할 수 있는 말이다. 429 만 맞고 남은 수는 못 받은 자리에서 그 말을 하면
|
|
313
|
+
* 화면이 모르는 것을 아는 척하는 것이다 — 이 프로그램이 안 하기로 한 바로 그것.
|
|
314
|
+
*/
|
|
315
|
+
export function 왜띄우나(것) {
|
|
316
|
+
if (!것) return null;
|
|
317
|
+
const 바닥난것 = (것.요청 !== null && 것.요청 <= 0) || (것.토큰 !== null && 것.토큰 <= 0);
|
|
318
|
+
if (바닥난것) return '바닥';
|
|
319
|
+
return 것.막힘 ? '막힘' : null;
|
|
320
|
+
}
|
|
321
|
+
|
|
264
322
|
export function 미리기다릴까(것, 지금 = Date.now()) {
|
|
265
|
-
if (!것?.있나) return null;
|
|
323
|
+
if (!것?.있나 && !것?.막힘) return null;
|
|
266
324
|
if (!(것.때 > 0) || 지금 - 것.때 > 낡은값) return null;
|
|
325
|
+
|
|
267
326
|
const 바닥난것 = (것.요청 !== null && 것.요청 <= 0) || (것.토큰 !== null && 것.토큰 <= 0);
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
327
|
+
// 서버가 언제 오라고 말해 줬으면 그 말이 먼저다. 바닥났다고 했거나 방금 막혔거나.
|
|
328
|
+
if ((바닥난것 || 것.막힘) && 것.풀림 > 0) {
|
|
329
|
+
// 그 응답을 받은 뒤로 흐른 만큼은 빼 준다.
|
|
330
|
+
const 남은초 = 것.풀림 - Math.floor((지금 - 것.때) / 1000);
|
|
331
|
+
if (남은초 > 0) return Math.min(남은초 * 1000, 미리기다림상한);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* 아무 말도 없이 막기만 한 자리. 그래도 방금 막혔다는 것은 안다 —
|
|
336
|
+
* 그만큼은 띄운다 (막힘띄움). 바닥났다고만 하고 풀림 시각이 없는 것은
|
|
337
|
+
* 여기 안 들어온다. 언제 풀릴지 모르는 것과 방금 맞은 것은 다르다.
|
|
338
|
+
*/
|
|
339
|
+
if (것.막힘) {
|
|
340
|
+
const 남은 = 막힘띄움 - (지금 - 것.때);
|
|
341
|
+
if (남은 > 0) return Math.min(남은, 미리기다림상한);
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
274
344
|
}
|
package/src/backend/retry.js
CHANGED
|
@@ -47,9 +47,40 @@ import { Aborted } from './http.js';
|
|
|
47
47
|
* 여기서 멎는다 — 얼마나 기다릴지 모르는 채로 붙드는 일이 없어야 한다.
|
|
48
48
|
*/
|
|
49
49
|
export function 기본정책() {
|
|
50
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
최대: 3, 막힘최대: null, base: [1000, 2000, 4000],
|
|
52
|
+
막힘base: [5000, 15000, 30000],
|
|
53
|
+
흔들림: 0.3, 상한: 60000, 총상한: 300000,
|
|
54
|
+
};
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/*
|
|
58
|
+
* ── 429 는 사다리가 따로여야 한다 ───────────────────────────────────────
|
|
59
|
+
*
|
|
60
|
+
* 502·503 은 「서버가 딸꾹질했다」 다. 1초 뒤에 다시 보내면 대개 된다.
|
|
61
|
+
* 429 는 **「네 몫을 다 썼다」** 다. 1초 뒤에 보내면 거의 확실히 또 막히고,
|
|
62
|
+
* 게이트웨이 상당수는 **막아서 돌려보낸 요청도 한도에 센다** — 빨리 두드릴수록
|
|
63
|
+
* 구덩이가 깊어진다. 같은 사다리를 쓰면 안 되는 까닭이 이것이다.
|
|
64
|
+
*
|
|
65
|
+
* 실제로 본 것(사내 LiteLLM → Bedrock):
|
|
66
|
+
*
|
|
67
|
+
* litellm.RateLimitError: BedrockException - Too many requests sent to
|
|
68
|
+
* ApplyGuardrail: On-demand ApplyGuardrail sensitive information policy
|
|
69
|
+
* text units per second limit exceeded.
|
|
70
|
+
* ... LiteLLM Retried: 2 times, LiteLLM Max Retries: 2
|
|
71
|
+
*
|
|
72
|
+
* 두 가지가 보인다.
|
|
73
|
+
*
|
|
74
|
+
* · 한도가 **초당**이다. 분당이 아니라 초당이라, 프롬프트가 크면 한 번의
|
|
75
|
+
* 요청만으로도 그 초의 몫을 다 쓴다.
|
|
76
|
+
* · **게이트웨이가 이미 두 번 다시 불렀다.** 우리가 그 답을 보기 전에 벌써
|
|
77
|
+
* 세 번 나간 것이다. 거기에 1초·2초·4초를 얹으면 7초 안에 여섯 번이다.
|
|
78
|
+
*
|
|
79
|
+
* 그래서 5초 · 15초 · 30초로 시작한다. 흔한 한도 창이 1분이라, 세 번째면
|
|
80
|
+
* 50초를 기다린 셈이 되어 창 하나를 거의 넘긴다. 서버가 `Retry-After` 로
|
|
81
|
+
* 말해 주면 **그 말이 언제나 이긴다** — 아래 기다릴시간 의 첫 줄이다.
|
|
82
|
+
*/
|
|
83
|
+
|
|
53
84
|
// 잠깐 막힌 것으로 보는 상태 코드. 529 는 Anthropic 계열 게이트웨이의 '과부하' 다.
|
|
54
85
|
const 다시부를상태 = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
55
86
|
// 머리말도 못 받고 끊긴 것. undici 는 상대가 닫으면 UND_ERR_SOCKET 으로 온다.
|
|
@@ -113,13 +144,17 @@ export function 못붙은것인가(code) {
|
|
|
113
144
|
* 몇 ms 기다릴까. 서버가 말해 준 것이 있으면 그것, 없으면 사다리.
|
|
114
145
|
* @param {{attempt?: number, retryAfter?: string|number|null}} 자리
|
|
115
146
|
*/
|
|
116
|
-
export function 기다릴시간({ attempt = 1, retryAfter = null } = {}, 정책 = 기본정책()) {
|
|
147
|
+
export function 기다릴시간({ attempt = 1, retryAfter = null, status = 0 } = {}, 정책 = 기본정책()) {
|
|
117
148
|
const 서버말 = retryAfter읽기(retryAfter);
|
|
118
149
|
if (서버말 !== null) return Math.min(서버말, 정책.상한);
|
|
150
|
+
// 429 는 제 사다리를 쓴다. 안 정해 뒀으면 기본 사다리로 떨어진다 — 예전
|
|
151
|
+
// 설정을 그대로 쓰는 사람에게 갑자기 다른 박자가 생기지는 않는다.
|
|
152
|
+
const 막힘인가 = Number(status) === 429;
|
|
153
|
+
const 고른것 = 막힘인가 ? (정책.막힘base ?? 정책.base) : 정책.base;
|
|
119
154
|
// 사다리가 비었으면(설정이 이상하면) 기본 사다리로 — NaN 초를 기다릴 수는 없다.
|
|
120
|
-
const 사다리 = Array.isArray(
|
|
121
|
-
?
|
|
122
|
-
: 기본정책().base;
|
|
155
|
+
const 사다리 = Array.isArray(고른것) && 고른것.some(Number.isFinite)
|
|
156
|
+
? 고른것.filter(Number.isFinite)
|
|
157
|
+
: (막힘인가 ? 기본정책().막힘base : 기본정책().base);
|
|
123
158
|
const 칸 = 사다리[Math.min(attempt, 사다리.length) - 1] ?? 사다리[사다리.length - 1];
|
|
124
159
|
return Math.min(정책.상한, Math.round(칸 * (1 + Math.random() * 정책.흔들림)));
|
|
125
160
|
}
|
|
@@ -164,7 +199,7 @@ export function 다시부를지(r, attempt, 정책 = 기본정책(), 쌓인 = 0)
|
|
|
164
199
|
const code = r?.code ?? null;
|
|
165
200
|
if (!다시부를까({ status, code, attempt }, 정책)) return null;
|
|
166
201
|
const retryAfter = r?.headers?.get?.('retry-after') ?? r?.res?.headers?.get?.('retry-after') ?? null;
|
|
167
|
-
const wait = 기다릴시간({ attempt, retryAfter }, 정책);
|
|
202
|
+
const wait = 기다릴시간({ attempt, retryAfter, status }, 정책);
|
|
168
203
|
/*
|
|
169
204
|
* 한 요청에서 기다린 것을 다 더해 울타리를 친다.
|
|
170
205
|
*
|
|
@@ -189,6 +224,26 @@ export function 다시부를지(r, attempt, 정책 = 기본정책(), 쌓인 = 0)
|
|
|
189
224
|
* 알림 한 덩이를 화면 말(i18n 의 loop.backoff)에 끼울 자리로 바꾼다.
|
|
190
225
|
* 세 화면(repl · deel run · acp)이 같은 것을 본다 — 한 군데만 고치면 셋이 어긋난다.
|
|
191
226
|
*/
|
|
227
|
+
/**
|
|
228
|
+
* 이 알림에 쓸 화면 말 열쇠.
|
|
229
|
+
*
|
|
230
|
+
* 세 가지가 다른 말이라 세 열쇠다.
|
|
231
|
+
*
|
|
232
|
+
* loop.backoff 서버가 밀어냈다 — 기다렸다 **다시** 부른다 (n/max 가 있다)
|
|
233
|
+
* loop.quotaAhead 남은 것이 0 이라고 **서버가 말해 줬다** — 보내기 전에 비킨다
|
|
234
|
+
* loop.limitAhead 방금 429 를 맞았다. 남은 수는 모른다 — 우리 박자를 늦춘다
|
|
235
|
+
*
|
|
236
|
+
* 뒤의 둘을 한 말로 합치면 화면이 거짓말을 한다. 「할당량이 바닥났다」 는 서버가
|
|
237
|
+
* 그렇게 말해 줬을 때만 할 수 있는 말이다.
|
|
238
|
+
*
|
|
239
|
+
* 다섯 자리(repl · deel run · acp · consult 둘)가 이걸 같이 쓴다. 각자 갈라 놓으면
|
|
240
|
+
* 언젠가 하나가 빠지고, 빠진 자리는 `(0/0)` 을 찍는다 — 실제로 그랬다.
|
|
241
|
+
*/
|
|
242
|
+
export function 알림말(ev) {
|
|
243
|
+
if (!ev?.미리) return 'loop.backoff';
|
|
244
|
+
return ev.왜 === '막힘' ? 'loop.limitAhead' : 'loop.quotaAhead';
|
|
245
|
+
}
|
|
246
|
+
|
|
192
247
|
export function 알림채움(ev) {
|
|
193
248
|
const 초 = (ev?.wait ?? 0) / 1000;
|
|
194
249
|
return {
|
package/src/commands.js
CHANGED
|
@@ -32,7 +32,7 @@ import { allowTemporarily } from './safety/network.js';
|
|
|
32
32
|
import { chat, 규격이름, 더할머리 } from './backend/adapter.js';
|
|
33
33
|
import { 잠잠기본, 무소식기본 } from './backend/http.js';
|
|
34
34
|
import { 인증서설정, 인증서말 } from './backend/clientcert.js';
|
|
35
|
-
import {
|
|
35
|
+
import { 알림채움, 알림말 } from './backend/retry.js';
|
|
36
36
|
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
37
37
|
import { 정한셸 } from './tools/shell.js';
|
|
38
38
|
import { TOOLS, 영어설명 } from './tools/index.js';
|
|
@@ -277,7 +277,9 @@ export async function handle(line, session, ctx) {
|
|
|
277
277
|
temperature: 0.3,
|
|
278
278
|
// 서버가 잠깐 막으면 돌림표 뒤에 숨기지 않고 말한다 — 왜 오래 걸리는지 보여야 한다.
|
|
279
279
|
onBackoff: (알림) => {
|
|
280
|
-
돌림.stop(
|
|
280
|
+
돌림.stop((알림.미리
|
|
281
|
+
? ` ${c.yellow('⏸')} ${c.gray(말(알림말(알림), { 초: 알림채움(알림).초 }))}`
|
|
282
|
+
: ` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`));
|
|
281
283
|
돌림 = spin(말('consult.asking', { 모델: 새conn.model }));
|
|
282
284
|
},
|
|
283
285
|
});
|
|
@@ -644,7 +646,9 @@ export async function handle(line, session, ctx) {
|
|
|
644
646
|
let s = spin('앞선 대화를 요약해 접는 중…');
|
|
645
647
|
const r = await compact(session, {
|
|
646
648
|
onBackoff: (알림) => {
|
|
647
|
-
s.stop(
|
|
649
|
+
s.stop((알림.미리
|
|
650
|
+
? ` ${c.yellow('⏸')} ${c.gray(말(알림말(알림), { 초: 알림채움(알림).초 }))}`
|
|
651
|
+
: ` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`));
|
|
648
652
|
s = spin('앞선 대화를 요약해 접는 중…');
|
|
649
653
|
},
|
|
650
654
|
});
|
|
@@ -2289,7 +2293,9 @@ async function 커밋명령(session, ctx, arg = '') {
|
|
|
2289
2293
|
전부,
|
|
2290
2294
|
제목: 준제목,
|
|
2291
2295
|
onBackoff: (알림) => {
|
|
2292
|
-
돌림.stop(
|
|
2296
|
+
돌림.stop((알림.미리
|
|
2297
|
+
? ` ${c.yellow('⏸')} ${c.gray(말(알림말(알림), { 초: 알림채움(알림).초 }))}`
|
|
2298
|
+
: ` ${c.yellow('↻')} ${c.gray(말('loop.backoff', 알림채움(알림)))}`));
|
|
2293
2299
|
돌림 = spin(도는말);
|
|
2294
2300
|
},
|
|
2295
2301
|
});
|
package/src/i18n/en.js
CHANGED
|
@@ -558,6 +558,9 @@ export const en = {
|
|
|
558
558
|
'think.wireNoOff': 'this endpoint never told us its off value — the field is omitted, so the server default applies',
|
|
559
559
|
'loop.wire': 'The server does not take that field — retrying with it adjusted ({무엇})',
|
|
560
560
|
'loop.quotaAhead': 'Quota is empty — waiting {초}s before sending',
|
|
561
|
+
'loop.limitAhead': 'Just rate-limited — spacing this out by {초}s',
|
|
562
|
+
'net.limit.guardrail': 'This is a guardrail limit on the gateway, not a model token limit. It counts text scanned per second (text units, roughly 1,000 characters each), so as the conversation grows every request gets bigger and past some point it fails every time. Prompt caching does not help here — the guardrail scans the full text every time regardless. Raising the model per-minute request/token limits (RPM/TPM) does nothing to it: different API, different quota. /compact shrinks the conversation for immediate relief; the real fix is for whoever runs the gateway to raise the guardrail quota or stop applying it to input.',
|
|
563
|
+
'net.limit.perMinute': 'This is a per-minute limit — it clears itself once the window rolls over. Waiting is the right answer here.',
|
|
561
564
|
'cost.quota': 'Server quota',
|
|
562
565
|
'cost.quotaAge': 'as of {초}s ago',
|
|
563
566
|
|
package/src/i18n/ja.js
CHANGED
|
@@ -515,6 +515,9 @@ export const ja = {
|
|
|
515
515
|
'think.wireNoOff': 'この窓口は切る値を教えてくれませんでした — 項目を送らないので、サーバー既定値で動きます',
|
|
516
516
|
'loop.wire': 'サーバーが受け取らない項目があるため、直して呼び直します ({무엇})',
|
|
517
517
|
'loop.quotaAhead': '残量が尽きたので {초} 秒待ってから送ります',
|
|
518
|
+
'loop.limitAhead': '直前に制限に当たったので {초} 秒あけてから送ります',
|
|
519
|
+
'net.limit.guardrail': 'ゲートウェイに付いたガードレールの上限です — モデルのトークン上限ではありません。数えているのは毎秒走査する文字量(text unit・1 TU はおよそ 1,000 文字)なので、会話が伸びるほど一回の要求が大きくなり、ある時点から毎回引っかかります。プレフィックスキャッシュはここでは効きません — ガードレールはキャッシュと無関係に毎回全文を走査します。モデルの毎分の要求・トークン上限(RPM・TPM)を上げても動きません。別の API の別の割当です。/compact で会話を縮めると当座はしのげます。根本はゲートウェイ担当者がガードレールの割当を上げるか、入力には掛けないようにすることです。',
|
|
520
|
+
'net.limit.perMinute': '毎分の上限です — 窓が変われば自然に解けます。ここでは待つのが正解です。',
|
|
518
521
|
'cost.quota': 'サーバー残量',
|
|
519
522
|
'cost.quotaAge': '{초}秒前の応答基準',
|
|
520
523
|
|
package/src/i18n/ko.js
CHANGED
|
@@ -605,6 +605,9 @@ export const ko = {
|
|
|
605
605
|
'think.wireNoOff': '이 창구는 끄는 말을 안 알려 줬습니다 — 칸을 안 싣고, 서버 기본값으로 돕니다',
|
|
606
606
|
'loop.wire': '서버가 안 받는 칸이 있어 맞춰 다시 부릅니다 ({무엇})',
|
|
607
607
|
'loop.quotaAhead': '할당량이 바닥나서 {초}초 기다렸다 보냅니다',
|
|
608
|
+
'loop.limitAhead': '방금 한도에 걸려서 {초}초 띄웠다 보냅니다',
|
|
609
|
+
'net.limit.guardrail': '게이트웨이에 붙은 가드레일 한도입니다 — 모델 토큰 한도가 아닙니다. 재는 것이 초당 훑는 글의 양(text unit, 1 TU 가 1,000자쯤)이라, 대화가 자랄수록 매 요청이 커져 어느 순간부터 계속 걸립니다. 프리픽스 캐시는 여기서 안 듣습니다 — 가드레일은 캐시와 무관하게 매번 전체 글을 훑습니다. 모델의 분당 요청·토큰 한도(RPM·TPM)를 올려도 이건 안 움직입니다: 다른 API 의 다른 할당량입니다. /compact 로 대화를 줄이면 당장 낫고, 근본은 게이트웨이 담당자가 가드레일 할당량을 올리거나 입력에는 안 걸도록 바꾸는 것입니다.',
|
|
610
|
+
'net.limit.perMinute': '분당 한도입니다 — 창이 새로 열리면 저절로 풀립니다. 여기서는 기다리는 것이 맞습니다.',
|
|
608
611
|
'cost.quota': '서버 할당량',
|
|
609
612
|
'cost.quotaAge': '{초}초 전 응답 기준',
|
|
610
613
|
|
package/src/i18n/zh.js
CHANGED
|
@@ -515,6 +515,9 @@ export const zh = {
|
|
|
515
515
|
'think.wireNoOff': '该端点未告知关闭值 — 不发送该字段,因此按服务器默认值运行',
|
|
516
516
|
'loop.wire': '服务端不接受该字段,已调整后重试 ({무엇})',
|
|
517
517
|
'loop.quotaAhead': '额度用尽,等待 {초} 秒后再发送',
|
|
518
|
+
'loop.limitAhead': '刚被限流,间隔 {초} 秒后再发送',
|
|
519
|
+
'net.limit.guardrail': '这是网关上护栏的限额,不是模型的令牌限额。它按每秒扫描的文本量计数(text unit,1 TU 约 1,000 个字符),所以对话越长每次请求越大,过了某个点就会每次都被拦。前缀缓存在这里没有用 — 护栏与缓存无关,每次都会扫描全文。提高模型的每分钟请求/令牌限额(RPM/TPM)不会有任何作用:那是另一个 API 的另一份配额。用 /compact 缩短对话可以先缓解;根本办法是让网关维护者提高护栏配额,或不要对输入应用它。',
|
|
520
|
+
'net.limit.perMinute': '这是每分钟限额 — 窗口滚动后会自行解除,这里等待就是正确做法。',
|
|
518
521
|
'cost.quota': '服务器额度',
|
|
519
522
|
'cost.quotaAge': '基于{초}秒前的响应',
|
|
520
523
|
|
package/src/oneshot.js
CHANGED
|
@@ -26,7 +26,7 @@ import { History } from './safety/undo.js';
|
|
|
26
26
|
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
27
27
|
import { activeProfile, load, resolveKey, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
|
|
28
28
|
import { 말 as 옮긴말 } from './i18n/index.js';
|
|
29
|
-
import {
|
|
29
|
+
import { 알림채움, 알림말 } from './backend/retry.js';
|
|
30
30
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
31
31
|
import { newId } from './agent/store.js';
|
|
32
32
|
import { discover, loadCommand } from './skills/discover.js';
|
|
@@ -605,7 +605,7 @@ export async function runOnce(opts = {}) {
|
|
|
605
605
|
// 맞고 물러난 것과 맞기 전에 비킨 것은 다른 일이다. 같은 말로 적으면
|
|
606
606
|
// 있지도 않은 429 가 배치 기록에 남는다.
|
|
607
607
|
곁(ev.미리
|
|
608
|
-
? ` ${c.yellow('⏸')} ${c.gray(옮긴말(
|
|
608
|
+
? ` ${c.yellow('⏸')} ${c.gray(옮긴말(알림말(ev), { 초: 알림채움(ev).초 }))}`
|
|
609
609
|
: ` ${c.yellow('↻')} ${c.gray(옮긴말('loop.backoff', 알림채움(ev)))}`);
|
|
610
610
|
break;
|
|
611
611
|
|
package/src/repl.js
CHANGED
|
@@ -12,7 +12,7 @@ import { headerLines } from './ui/status.js';
|
|
|
12
12
|
import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
|
|
13
13
|
import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
14
14
|
import { 언어잡기, 말 as 옮긴말, 세말 } from './i18n/index.js';
|
|
15
|
-
import {
|
|
15
|
+
import { 알림채움, 알림말 } from './backend/retry.js';
|
|
16
16
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
17
17
|
import { 화면고르기 } from './ui/screen.js';
|
|
18
18
|
import { STAGES } from './agent/effort.js';
|
|
@@ -1877,7 +1877,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1877
1877
|
case 'backoff':
|
|
1878
1878
|
clearThinking();
|
|
1879
1879
|
say(ev.미리
|
|
1880
|
-
? ` ${c.yellow('⏸')} ${c.gray(옮긴말(
|
|
1880
|
+
? ` ${c.yellow('⏸')} ${c.gray(옮긴말(알림말(ev), { 초: 알림채움(ev).초 }))}`
|
|
1881
1881
|
: ` ${c.yellow('↻')} ${c.gray(옮긴말('loop.backoff', 알림채움(ev)))}`);
|
|
1882
1882
|
break;
|
|
1883
1883
|
|