deel-local-cli 1.13.0 → 1.14.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.
Files changed (54) hide show
  1. package/README.ko.md +32 -26
  2. package/README.md +32 -26
  3. package/bin/deel.js +6 -1
  4. package/package.json +3 -2
  5. package/src/acp/map.js +367 -367
  6. package/src/acp/serve.js +830 -810
  7. package/src/agent/budget.js +156 -156
  8. package/src/agent/commit.js +534 -534
  9. package/src/agent/compact.js +441 -441
  10. package/src/agent/effort.js +308 -308
  11. package/src/agent/evolve.js +263 -263
  12. package/src/agent/loop.js +1755 -1728
  13. package/src/agent/memory.js +156 -156
  14. package/src/agent/mention.js +210 -210
  15. package/src/agent/recall.js +222 -222
  16. package/src/agent/session.js +1056 -1056
  17. package/src/backend/adapter.js +1355 -1355
  18. package/src/backend/cachemark.js +223 -223
  19. package/src/backend/detect.js +327 -327
  20. package/src/backend/http.js +460 -460
  21. package/src/backend/mcp.js +407 -407
  22. package/src/backend/price.js +260 -260
  23. package/src/backend/probe.js +487 -487
  24. package/src/backend/quota.js +274 -274
  25. package/src/backend/tokens.js +59 -59
  26. package/src/backend/toolfit.js +352 -352
  27. package/src/backend/wire.js +715 -715
  28. package/src/cmdnames.js +68 -0
  29. package/src/commands.js +3116 -3184
  30. package/src/config.js +283 -283
  31. package/src/i18n/en.js +545 -545
  32. package/src/i18n/ja.js +498 -498
  33. package/src/i18n/ko.js +592 -592
  34. package/src/i18n/zh.js +498 -498
  35. package/src/oneshot.js +685 -625
  36. package/src/pack/selfpack.js +15 -2
  37. package/src/pack/sheet.en.js +16 -2
  38. package/src/pack/tar.js +154 -154
  39. package/src/pack/zip.js +235 -235
  40. package/src/plugins/manage.js +416 -416
  41. package/src/repl.js +2616 -2616
  42. package/src/safety/audit.js +148 -148
  43. package/src/safety/guard.js +714 -641
  44. package/src/safety/network.js +200 -200
  45. package/src/tools/fastgrep.js +229 -229
  46. package/src/tools/fsutil.js +312 -291
  47. package/src/tools/index.js +2590 -2560
  48. package/src/tools/jobs.js +876 -876
  49. package/src/tools/spawn.js +213 -213
  50. package/src/tools/verify.js +358 -358
  51. package/src/tools/webfetch.js +418 -418
  52. package/src/ui/export.js +233 -233
  53. package/src/ui/pick.js +115 -115
  54. package/src/ui/status.js +617 -617
@@ -1,274 +1,274 @@
1
- // 게이트웨이가 남았다고 알려 주는 할당량.
2
- //
3
- // ── 왜 필요한가 ────────────────────────────────────────────────────────
4
- //
5
- // 사내 게이트웨이는 사람마다 할당량을 건다. 그런데 지금까지 그걸 아는 방법은
6
- // **429 를 맞는 것뿐**이었다. 일하는 도중에 갑자기 막히고, 화면에는
7
- // "잠깐 막혔습니다" 가 뜨고, 사람은 언제 풀리는지 모른 채 기다린다.
8
- //
9
- // 그런데 서버는 매 응답에 남은 양을 실어 보내고 있었다. 우리가 안 읽었을
10
- // 뿐이다. 읽어서 보여 주면 사람은 막히기 전에 안다 — 큰 작업을 시작할지,
11
- // 오늘은 여기까지 할지 스스로 정할 수 있다.
12
- //
13
- // ── 이름이 제각각이다 ──────────────────────────────────────────────────
14
- //
15
- // 표준이 없다. OpenAI 계열은 `x-ratelimit-remaining-requests`, Azure 는
16
- // `x-ratelimit-remaining-tokens` 를 쓰기도 하고 아예 안 주기도 한다.
17
- // 그래서 **아는 이름만 읽고, 없으면 없다고 한다.** 없는 것을 0 으로 치면
18
- // 화면에 "0 남음" 이 떠서, 멀쩡한데 다 썼다고 믿게 된다.
19
- //
20
- // Anthropic 은 `anthropic-` 을 앞에 달고, 남은 것을 **뒤에** 적는다
21
- // (`anthropic-ratelimit-requests-remaining`). 그 이름을 몰라서, Claude 직통과
22
- // Bedrock 게이트웨이에서는 이 줄이 **언제나 비어 있었다** — 서버는 매 응답에
23
- // 남은 양을 실어 보내고 있었는데 우리만 못 읽었다. 그래서 그 두 자리에서는
24
- // 아직도 429 를 맞아야만 알 수 있었다. 이 파일이 없애려던 바로 그 상황이다.
25
-
26
- // 읽을 이름들. 앞에서부터 처음 있는 것 하나를 쓴다.
27
- const 요청남음 = [
28
- 'x-ratelimit-remaining-requests', 'ratelimit-remaining-requests', 'x-ratelimit-remaining',
29
- 'anthropic-ratelimit-requests-remaining',
30
- ];
31
- /*
32
- * 토큰 통은 **여러 개일 수 있다.**
33
- *
34
- * Anthropic 은 입력과 출력을 따로 센다. 둘은 서로 다른 통이고, 둘 중
35
- * **먼저 바닥나는 쪽**이 곧 막히는 쪽이다.
36
- *
37
- * 여태 이걸 한 줄짜리 이름 목록으로 두고 골라() 로 「처음 있는 것」 을
38
- * 집었다. 목록 차례상 입력이 앞이라, 입력 100,000 · 출력 0 인 응답에서
39
- * 남은 양을 **100,000 으로 적었다.** 그러면 미리기다릴까() 가 「아직
40
- * 넉넉하다」 고 보고 곧장 다음 요청을 보내 429 를 맞는다. 주석에는
41
- * 「먼저 바닥나는 쪽」 이라고 적혀 있었는데 코드는 그 반대를 했다.
42
- *
43
- * 남음과 한도를 **짝으로** 묶어 두고, 남은 것이 가장 적은 짝을 쓴다.
44
- * 짝으로 안 묶으면 화면이 「출력 0 / 입력 한도 100만」 같은 소리를 한다.
45
- */
46
- const 토큰통 = [
47
- ['x-ratelimit-remaining-tokens', 'x-ratelimit-limit-tokens'],
48
- ['ratelimit-remaining-tokens', 'ratelimit-limit-tokens'],
49
- ['anthropic-ratelimit-tokens-remaining', 'anthropic-ratelimit-tokens-limit'],
50
- ['anthropic-ratelimit-input-tokens-remaining', 'anthropic-ratelimit-input-tokens-limit'],
51
- ['anthropic-ratelimit-output-tokens-remaining', 'anthropic-ratelimit-output-tokens-limit'],
52
- ];
53
- const 요청한도 = ['x-ratelimit-limit-requests', 'ratelimit-limit-requests', 'anthropic-ratelimit-requests-limit'];
54
- /*
55
- * 풀리는 때. Anthropic 은 초가 아니라 **날짜(RFC 3339)** 로 준다 —
56
- * `언제풀리나` 가 Date.parse 로 받아 지금과의 차이를 초로 바꾼다.
57
- */
58
- const 다시언제 = [
59
- 'retry-after', 'x-ratelimit-reset-requests', 'x-ratelimit-reset-tokens', 'ratelimit-reset',
60
- 'anthropic-ratelimit-requests-reset', 'anthropic-ratelimit-tokens-reset',
61
- ];
62
-
63
- function 골라(머리, 이름들) {
64
- if (!머리) return null;
65
- const 보기 = (k) => (typeof 머리.get === 'function' ? 머리.get(k) : 머리[k] ?? 머리[k.toLowerCase()]);
66
- for (const k of 이름들) {
67
- const v = 보기(k);
68
- if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
69
- }
70
- return null;
71
- }
72
-
73
- /*
74
- * 숫자로 읽는다. 못 읽으면 null — 0 이 아니다.
75
- *
76
- * 이 구분이 여기서 제일 중요하다. 못 읽은 것을 0 으로 치면 화면에
77
- * "남은 요청 0" 이 뜨고, 사람은 멀쩡한 할당량을 다 썼다고 믿는다.
78
- */
79
- function 숫자(v) {
80
- if (v === null) return null;
81
- const n = Number(String(v).replace(/[,_\s]/g, ''));
82
- return Number.isFinite(n) ? n : null;
83
- }
84
-
85
- /*
86
- * `retry-after` 는 초일 수도 날짜일 수도 있다 (RFC 9110).
87
- * 날짜면 지금과의 차이를 초로 바꾼다. 못 읽으면 null.
88
- */
89
- export function 언제풀리나(v) {
90
- if (v === null || v === undefined) return null;
91
- const s = String(v).trim();
92
- const n = Number(s);
93
- if (Number.isFinite(n)) return Math.max(0, Math.round(n));
94
- const t = Date.parse(s);
95
- if (Number.isFinite(t)) return Math.max(0, Math.round((t - Date.now()) / 1000));
96
- // `1m30s` 같은 꼴을 주는 게이트웨이가 있다.
97
- const m = /^(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?$/.exec(s);
98
- if (m && (m[1] || m[2])) return Math.round((Number(m[1] ?? 0) * 60) + Number(m[2] ?? 0));
99
- return null;
100
- }
101
-
102
- /** 토큰 통 중 **가장 적게 남은** 짝. 없으면 둘 다 null. */
103
- function 가장빠듯한통(머리) {
104
- let 고른것 = { 토큰: null, 토큰한도: null };
105
- for (const [남음이름, 한도이름] of 토큰통) {
106
- const 남 = 숫자(골라(머리, [남음이름]));
107
- if (남 === null) continue;
108
- if (고른것.토큰 === null || 남 < 고른것.토큰) {
109
- 고른것 = { 토큰: 남, 토큰한도: 숫자(골라(머리, [한도이름])) };
110
- }
111
- }
112
- return 고른것;
113
- }
114
-
115
- /**
116
- * 응답 머리에서 할당량을 읽는다.
117
- *
118
- * @returns {{요청:number|null, 요청한도:number|null, 토큰:number|null, 토큰한도:number|null, 풀림:number|null, 있나:boolean}}
119
- */
120
- export function 할당량읽기(머리) {
121
- const 것 = {
122
- 요청: 숫자(골라(머리, 요청남음)),
123
- 요청한도: 숫자(골라(머리, 요청한도)),
124
- ...가장빠듯한통(머리),
125
- 풀림: 언제풀리나(골라(머리, 다시언제)),
126
- };
127
- 것.있나 = 것.요청 !== null || 것.토큰 !== null || 것.풀림 !== null;
128
- return 것;
129
- }
130
-
131
- /*
132
- * 얼마나 남았을 때 화면에 띄울까.
133
- *
134
- * 한도를 알면 비율로 본다(10% 아래). 한도를 안 알려주는 서버가 많아서,
135
- * 그때는 남은 수 자체로 본다 — 요청 20회 아래, 토큰 20,000 아래.
136
- * 넉넉할 때 자꾸 띄우면 사람이 그 줄을 안 읽게 된다.
137
- */
138
- export const 요청바닥 = 20;
139
- export const 토큰바닥 = 20000;
140
-
141
- export function 아슬아슬한가(것) {
142
- if (!것?.있나) return false;
143
- if (것.풀림 !== null && 것.풀림 > 0) return true;
144
- if (것.요청 !== null) {
145
- if (것.요청한도) { if (것.요청 / 것.요청한도 <= 0.1) return true; }
146
- else if (것.요청 <= 요청바닥) return true;
147
- }
148
- if (것.토큰 !== null) {
149
- if (것.토큰한도) { if (것.토큰 / 것.토큰한도 <= 0.1) return true; }
150
- else if (것.토큰 <= 토큰바닥) return true;
151
- }
152
- return false;
153
- }
154
-
155
- /** 화면 한 줄. 아는 것만 적는다 — 모르는 자리는 아예 안 적는다. */
156
- export function 할당량말(것) {
157
- if (!것?.있나) return '';
158
- const 조각 = [];
159
- if (것.요청 !== null) 조각.push(`요청 ${것.요청.toLocaleString()}${것.요청한도 ? `/${것.요청한도.toLocaleString()}` : ''}`);
160
- if (것.토큰 !== null) 조각.push(`토큰 ${것.토큰.toLocaleString()}${것.토큰한도 ? `/${것.토큰한도.toLocaleString()}` : ''}`);
161
- if (것.풀림 !== null) 조각.push(`${것.풀림}초 뒤 풀림`);
162
- return 조각.join(' · ');
163
- }
164
-
165
- /*
166
- * 마지막으로 본 할당량. 응답마다 덮어쓴다.
167
- *
168
- * 세션에 안 두고 모듈에 두는 까닭: 이걸 읽는 자리가 여럿인데(상태줄, /cost,
169
- * 시작 화면) 그 자리들이 세션을 다 들고 있지는 않다. 그리고 값 자체가
170
- * '지금 서버가 말한 것' 이라 한 벌이면 충분하다.
171
- */
172
- let 마지막 = null;
173
-
174
- /*
175
- * ── 창구마다 따로 센다 ──────────────────────────────────────────────────
176
- *
177
- * 값이 한 벌이면 **누구 것인지**가 없다. 그런데 이 프로그램은 한 번에 여러
178
- * 창구를 부른다 — 본 모델, 하위 작업이 고른 모델(agent/loop.js), `/model` 로
179
- * 물어보는 자리, 요약을 짓는 자리. 사내 게이트웨이가 「남은 것 0, 55초 뒤」
180
- * 라고 답하면, 그 다음에 **전혀 다른 주소**로 나가는 요청까지 55초를 기다렸다.
181
- * 옆에 켜 둔 로컬 모델이 남의 할당량 때문에 멎는 것이다.
182
- *
183
- * 그래서 창구별로 담아 두고, 보내기 전에 비킬 때는 **그 창구 것만** 본다.
184
- * 화면(상태줄·/cost)은 여전히 마지막 것을 쓴다 — 사람이 보는 것은 지금 쓰는
185
- * 창구 하나라, 그 자리에는 한 벌로 충분하다.
186
- */
187
- const 자리들 = new Map();
188
- const 자리최대 = 24;
189
-
190
- /** 이 연결을 가리키는 이름. 주소의 호스트와 모델까지만 쓴다 — 열쇠는 안 넣는다. */
191
- export function 할당량자리(conn) {
192
- if (!conn) return '';
193
- let host = '';
194
- try { host = new URL(String(conn.base ?? '')).host; } catch { host = ''; }
195
- /*
196
- * 규격까지 넣는다. 전선 카드 열쇠와 같은 까닭이다(agent/evolve.js) —
197
- * mantle 은 `/openai/v1` 과 `/anthropic/v1` 이 **같은 호스트에 같은 모델
198
- * 이름**으로 서 있어서, 규격을 빼면 한쪽이 바닥났다고 다른 쪽까지 기다린다.
199
- */
200
- const 꼴 = String(conn.kind ?? '').trim();
201
- return `${host}|${String(conn.model ?? '').trim()}${꼴 ? `#${꼴}` : ''}`;
202
- }
203
-
204
- export function 할당량기억(머리, 어디 = '') {
205
- const 것 = 할당량읽기(머리);
206
- if (것.있나) {
207
- const 적을것 = { ...것, 때: Date.now() };
208
- 마지막 = 적을것;
209
- if (어디) {
210
- 자리들.delete(어디);
211
- 자리들.set(어디, 적을것);
212
- // 오래 안 쓴 자리부터 버린다. 창구를 옮겨 다녀도 표가 안 자란다.
213
- while (자리들.size > 자리최대) 자리들.delete(자리들.keys().next().value);
214
- }
215
- }
216
- return 것;
217
- }
218
-
219
- /** 마지막으로 본 할당량. 자리를 주면 **그 창구 것**, 안 주면 가장 최근 것. */
220
- export function 마지막할당량(어디 = '') {
221
- if (어디) return 자리들.get(어디) ?? null;
222
- return 마지막;
223
- }
224
-
225
- export function 할당량잊기(어디 = '') {
226
- if (어디) { 자리들.delete(어디); return null; }
227
- 마지막 = null;
228
- 자리들.clear();
229
- return null;
230
- }
231
-
232
- /*
233
- * ── 맞기 전에 비킨다 ────────────────────────────────────────────────────
234
- *
235
- * 서버는 매 응답에 「남은 것이 없다, 언제 풀린다」 를 실어 보낸다. 그런데
236
- * 여태 그 값은 **화면에만** 썼다. 그래서 남은 것이 0 인 줄 알면서도 그대로
237
- * 다음 요청을 보내고, 429 를 맞고, 다시 부르기 사다리를 태우고, 세 번째에
238
- * 턴이 죽었다. 사람이 본 것은 「호출 한도 초과」 한 줄이다.
239
- *
240
- * 맞기 전에 아는 것과 맞고 나서 아는 것은 사람이 할 일이 다르다. 알고 있으면
241
- * 그냥 기다렸다 보내면 된다 — 기다리는 것은 실패가 아니다.
242
- *
243
- * 조심한 것:
244
- * · **모르면 안 기다린다.** 헤더를 안 주는 서버가 많고, 모르는 것을 0 으로
245
- * 치면 멀쩡한 연결이 영영 기다린다.
246
- * · 풀림 시각이 없으면 안 기다린다. 얼마나 기다릴지 모르는 채로 붙들면
247
- * 화면이 멈춘 것과 구별이 안 된다.
248
- * · 오래된 값으로는 안 정한다. 방금 응답이 아니면 그 사이 풀렸을 수 있다.
249
- * · 상한을 둔다. 그보다 길면 사람이 정할 일이다 — 사실대로 말하고 보낸다.
250
- *
251
- * @returns {number|null} 기다릴 밀리초. 안 기다려도 되면 null
252
- */
253
- export const 낡은값 = 60000; // 이보다 오래된 할당량으로는 안 정한다
254
- export const 미리기다림상한 = 60000;
255
-
256
- /*
257
- * 어느 창구 것인지는 **부르는 쪽이 정해서 준다.** 기본값을 두지 않는다.
258
- *
259
- * 전에는 `것 = 마지막` 이었다. 그 기본값이 곧 「아무 창구나 마지막에 답한
260
- * 것」 이라, 빠뜨리고 부르면 옆 창구의 바닥난 할당량으로 이쪽이 기다린다 —
261
- * 방금 고친 그 고장이다. 기본값을 없애면 다음에 빠뜨렸을 때 조용히 옛
262
- * 동작으로 돌아가는 대신 눈에 보이게 어긋난다.
263
- */
264
- export function 미리기다릴까(것, 지금 = Date.now()) {
265
- if (!것?.있나) return null;
266
- if (!(것.때 > 0) || 지금 - 것.때 > 낡은값) return null;
267
- const 바닥난것 = (것.요청 !== null && 것.요청 <= 0) || (것.토큰 !== null && 것.토큰 <= 0);
268
- if (!바닥난것) return null;
269
- if (!(것.풀림 > 0)) return null;
270
- // 그 응답을 받은 뒤로 흐른 만큼은 빼 준다.
271
- const 남은초 = 것.풀림 - Math.floor((지금 - 것.때) / 1000);
272
- if (!(남은초 > 0)) return null;
273
- return Math.min(남은초 * 1000, 미리기다림상한);
274
- }
1
+ // 게이트웨이가 남았다고 알려 주는 할당량.
2
+ //
3
+ // ── 왜 필요한가 ────────────────────────────────────────────────────────
4
+ //
5
+ // 사내 게이트웨이는 사람마다 할당량을 건다. 그런데 지금까지 그걸 아는 방법은
6
+ // **429 를 맞는 것뿐**이었다. 일하는 도중에 갑자기 막히고, 화면에는
7
+ // "잠깐 막혔습니다" 가 뜨고, 사람은 언제 풀리는지 모른 채 기다린다.
8
+ //
9
+ // 그런데 서버는 매 응답에 남은 양을 실어 보내고 있었다. 우리가 안 읽었을
10
+ // 뿐이다. 읽어서 보여 주면 사람은 막히기 전에 안다 — 큰 작업을 시작할지,
11
+ // 오늘은 여기까지 할지 스스로 정할 수 있다.
12
+ //
13
+ // ── 이름이 제각각이다 ──────────────────────────────────────────────────
14
+ //
15
+ // 표준이 없다. OpenAI 계열은 `x-ratelimit-remaining-requests`, Azure 는
16
+ // `x-ratelimit-remaining-tokens` 를 쓰기도 하고 아예 안 주기도 한다.
17
+ // 그래서 **아는 이름만 읽고, 없으면 없다고 한다.** 없는 것을 0 으로 치면
18
+ // 화면에 "0 남음" 이 떠서, 멀쩡한데 다 썼다고 믿게 된다.
19
+ //
20
+ // Anthropic 은 `anthropic-` 을 앞에 달고, 남은 것을 **뒤에** 적는다
21
+ // (`anthropic-ratelimit-requests-remaining`). 그 이름을 몰라서, Claude 직통과
22
+ // Bedrock 게이트웨이에서는 이 줄이 **언제나 비어 있었다** — 서버는 매 응답에
23
+ // 남은 양을 실어 보내고 있었는데 우리만 못 읽었다. 그래서 그 두 자리에서는
24
+ // 아직도 429 를 맞아야만 알 수 있었다. 이 파일이 없애려던 바로 그 상황이다.
25
+
26
+ // 읽을 이름들. 앞에서부터 처음 있는 것 하나를 쓴다.
27
+ const 요청남음 = [
28
+ 'x-ratelimit-remaining-requests', 'ratelimit-remaining-requests', 'x-ratelimit-remaining',
29
+ 'anthropic-ratelimit-requests-remaining',
30
+ ];
31
+ /*
32
+ * 토큰 통은 **여러 개일 수 있다.**
33
+ *
34
+ * Anthropic 은 입력과 출력을 따로 센다. 둘은 서로 다른 통이고, 둘 중
35
+ * **먼저 바닥나는 쪽**이 곧 막히는 쪽이다.
36
+ *
37
+ * 여태 이걸 한 줄짜리 이름 목록으로 두고 골라() 로 「처음 있는 것」 을
38
+ * 집었다. 목록 차례상 입력이 앞이라, 입력 100,000 · 출력 0 인 응답에서
39
+ * 남은 양을 **100,000 으로 적었다.** 그러면 미리기다릴까() 가 「아직
40
+ * 넉넉하다」 고 보고 곧장 다음 요청을 보내 429 를 맞는다. 주석에는
41
+ * 「먼저 바닥나는 쪽」 이라고 적혀 있었는데 코드는 그 반대를 했다.
42
+ *
43
+ * 남음과 한도를 **짝으로** 묶어 두고, 남은 것이 가장 적은 짝을 쓴다.
44
+ * 짝으로 안 묶으면 화면이 「출력 0 / 입력 한도 100만」 같은 소리를 한다.
45
+ */
46
+ const 토큰통 = [
47
+ ['x-ratelimit-remaining-tokens', 'x-ratelimit-limit-tokens'],
48
+ ['ratelimit-remaining-tokens', 'ratelimit-limit-tokens'],
49
+ ['anthropic-ratelimit-tokens-remaining', 'anthropic-ratelimit-tokens-limit'],
50
+ ['anthropic-ratelimit-input-tokens-remaining', 'anthropic-ratelimit-input-tokens-limit'],
51
+ ['anthropic-ratelimit-output-tokens-remaining', 'anthropic-ratelimit-output-tokens-limit'],
52
+ ];
53
+ const 요청한도 = ['x-ratelimit-limit-requests', 'ratelimit-limit-requests', 'anthropic-ratelimit-requests-limit'];
54
+ /*
55
+ * 풀리는 때. Anthropic 은 초가 아니라 **날짜(RFC 3339)** 로 준다 —
56
+ * `언제풀리나` 가 Date.parse 로 받아 지금과의 차이를 초로 바꾼다.
57
+ */
58
+ const 다시언제 = [
59
+ 'retry-after', 'x-ratelimit-reset-requests', 'x-ratelimit-reset-tokens', 'ratelimit-reset',
60
+ 'anthropic-ratelimit-requests-reset', 'anthropic-ratelimit-tokens-reset',
61
+ ];
62
+
63
+ function 골라(머리, 이름들) {
64
+ if (!머리) return null;
65
+ const 보기 = (k) => (typeof 머리.get === 'function' ? 머리.get(k) : 머리[k] ?? 머리[k.toLowerCase()]);
66
+ for (const k of 이름들) {
67
+ const v = 보기(k);
68
+ if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /*
74
+ * 숫자로 읽는다. 못 읽으면 null — 0 이 아니다.
75
+ *
76
+ * 이 구분이 여기서 제일 중요하다. 못 읽은 것을 0 으로 치면 화면에
77
+ * "남은 요청 0" 이 뜨고, 사람은 멀쩡한 할당량을 다 썼다고 믿는다.
78
+ */
79
+ function 숫자(v) {
80
+ if (v === null) return null;
81
+ const n = Number(String(v).replace(/[,_\s]/g, ''));
82
+ return Number.isFinite(n) ? n : null;
83
+ }
84
+
85
+ /*
86
+ * `retry-after` 는 초일 수도 날짜일 수도 있다 (RFC 9110).
87
+ * 날짜면 지금과의 차이를 초로 바꾼다. 못 읽으면 null.
88
+ */
89
+ export function 언제풀리나(v) {
90
+ if (v === null || v === undefined) return null;
91
+ const s = String(v).trim();
92
+ const n = Number(s);
93
+ if (Number.isFinite(n)) return Math.max(0, Math.round(n));
94
+ const t = Date.parse(s);
95
+ if (Number.isFinite(t)) return Math.max(0, Math.round((t - Date.now()) / 1000));
96
+ // `1m30s` 같은 꼴을 주는 게이트웨이가 있다.
97
+ const m = /^(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?$/.exec(s);
98
+ if (m && (m[1] || m[2])) return Math.round((Number(m[1] ?? 0) * 60) + Number(m[2] ?? 0));
99
+ return null;
100
+ }
101
+
102
+ /** 토큰 통 중 **가장 적게 남은** 짝. 없으면 둘 다 null. */
103
+ function 가장빠듯한통(머리) {
104
+ let 고른것 = { 토큰: null, 토큰한도: null };
105
+ for (const [남음이름, 한도이름] of 토큰통) {
106
+ const 남 = 숫자(골라(머리, [남음이름]));
107
+ if (남 === null) continue;
108
+ if (고른것.토큰 === null || 남 < 고른것.토큰) {
109
+ 고른것 = { 토큰: 남, 토큰한도: 숫자(골라(머리, [한도이름])) };
110
+ }
111
+ }
112
+ return 고른것;
113
+ }
114
+
115
+ /**
116
+ * 응답 머리에서 할당량을 읽는다.
117
+ *
118
+ * @returns {{요청:number|null, 요청한도:number|null, 토큰:number|null, 토큰한도:number|null, 풀림:number|null, 있나:boolean}}
119
+ */
120
+ export function 할당량읽기(머리) {
121
+ const 것 = {
122
+ 요청: 숫자(골라(머리, 요청남음)),
123
+ 요청한도: 숫자(골라(머리, 요청한도)),
124
+ ...가장빠듯한통(머리),
125
+ 풀림: 언제풀리나(골라(머리, 다시언제)),
126
+ };
127
+ 것.있나 = 것.요청 !== null || 것.토큰 !== null || 것.풀림 !== null;
128
+ return 것;
129
+ }
130
+
131
+ /*
132
+ * 얼마나 남았을 때 화면에 띄울까.
133
+ *
134
+ * 한도를 알면 비율로 본다(10% 아래). 한도를 안 알려주는 서버가 많아서,
135
+ * 그때는 남은 수 자체로 본다 — 요청 20회 아래, 토큰 20,000 아래.
136
+ * 넉넉할 때 자꾸 띄우면 사람이 그 줄을 안 읽게 된다.
137
+ */
138
+ export const 요청바닥 = 20;
139
+ export const 토큰바닥 = 20000;
140
+
141
+ export function 아슬아슬한가(것) {
142
+ if (!것?.있나) return false;
143
+ if (것.풀림 !== null && 것.풀림 > 0) return true;
144
+ if (것.요청 !== null) {
145
+ if (것.요청한도) { if (것.요청 / 것.요청한도 <= 0.1) return true; }
146
+ else if (것.요청 <= 요청바닥) return true;
147
+ }
148
+ if (것.토큰 !== null) {
149
+ if (것.토큰한도) { if (것.토큰 / 것.토큰한도 <= 0.1) return true; }
150
+ else if (것.토큰 <= 토큰바닥) return true;
151
+ }
152
+ return false;
153
+ }
154
+
155
+ /** 화면 한 줄. 아는 것만 적는다 — 모르는 자리는 아예 안 적는다. */
156
+ export function 할당량말(것) {
157
+ if (!것?.있나) return '';
158
+ const 조각 = [];
159
+ if (것.요청 !== null) 조각.push(`요청 ${것.요청.toLocaleString()}${것.요청한도 ? `/${것.요청한도.toLocaleString()}` : ''}`);
160
+ if (것.토큰 !== null) 조각.push(`토큰 ${것.토큰.toLocaleString()}${것.토큰한도 ? `/${것.토큰한도.toLocaleString()}` : ''}`);
161
+ if (것.풀림 !== null) 조각.push(`${것.풀림}초 뒤 풀림`);
162
+ return 조각.join(' · ');
163
+ }
164
+
165
+ /*
166
+ * 마지막으로 본 할당량. 응답마다 덮어쓴다.
167
+ *
168
+ * 세션에 안 두고 모듈에 두는 까닭: 이걸 읽는 자리가 여럿인데(상태줄, /cost,
169
+ * 시작 화면) 그 자리들이 세션을 다 들고 있지는 않다. 그리고 값 자체가
170
+ * '지금 서버가 말한 것' 이라 한 벌이면 충분하다.
171
+ */
172
+ let 마지막 = null;
173
+
174
+ /*
175
+ * ── 창구마다 따로 센다 ──────────────────────────────────────────────────
176
+ *
177
+ * 값이 한 벌이면 **누구 것인지**가 없다. 그런데 이 프로그램은 한 번에 여러
178
+ * 창구를 부른다 — 본 모델, 하위 작업이 고른 모델(agent/loop.js), `/model` 로
179
+ * 물어보는 자리, 요약을 짓는 자리. 사내 게이트웨이가 「남은 것 0, 55초 뒤」
180
+ * 라고 답하면, 그 다음에 **전혀 다른 주소**로 나가는 요청까지 55초를 기다렸다.
181
+ * 옆에 켜 둔 로컬 모델이 남의 할당량 때문에 멎는 것이다.
182
+ *
183
+ * 그래서 창구별로 담아 두고, 보내기 전에 비킬 때는 **그 창구 것만** 본다.
184
+ * 화면(상태줄·/cost)은 여전히 마지막 것을 쓴다 — 사람이 보는 것은 지금 쓰는
185
+ * 창구 하나라, 그 자리에는 한 벌로 충분하다.
186
+ */
187
+ const 자리들 = new Map();
188
+ const 자리최대 = 24;
189
+
190
+ /** 이 연결을 가리키는 이름. 주소의 호스트와 모델까지만 쓴다 — 열쇠는 안 넣는다. */
191
+ export function 할당량자리(conn) {
192
+ if (!conn) return '';
193
+ let host = '';
194
+ try { host = new URL(String(conn.base ?? '')).host; } catch { host = ''; }
195
+ /*
196
+ * 규격까지 넣는다. 전선 카드 열쇠와 같은 까닭이다(agent/evolve.js) —
197
+ * mantle 은 `/openai/v1` 과 `/anthropic/v1` 이 **같은 호스트에 같은 모델
198
+ * 이름**으로 서 있어서, 규격을 빼면 한쪽이 바닥났다고 다른 쪽까지 기다린다.
199
+ */
200
+ const 꼴 = String(conn.kind ?? '').trim();
201
+ return `${host}|${String(conn.model ?? '').trim()}${꼴 ? `#${꼴}` : ''}`;
202
+ }
203
+
204
+ export function 할당량기억(머리, 어디 = '') {
205
+ const 것 = 할당량읽기(머리);
206
+ if (것.있나) {
207
+ const 적을것 = { ...것, 때: Date.now() };
208
+ 마지막 = 적을것;
209
+ if (어디) {
210
+ 자리들.delete(어디);
211
+ 자리들.set(어디, 적을것);
212
+ // 오래 안 쓴 자리부터 버린다. 창구를 옮겨 다녀도 표가 안 자란다.
213
+ while (자리들.size > 자리최대) 자리들.delete(자리들.keys().next().value);
214
+ }
215
+ }
216
+ return 것;
217
+ }
218
+
219
+ /** 마지막으로 본 할당량. 자리를 주면 **그 창구 것**, 안 주면 가장 최근 것. */
220
+ export function 마지막할당량(어디 = '') {
221
+ if (어디) return 자리들.get(어디) ?? null;
222
+ return 마지막;
223
+ }
224
+
225
+ export function 할당량잊기(어디 = '') {
226
+ if (어디) { 자리들.delete(어디); return null; }
227
+ 마지막 = null;
228
+ 자리들.clear();
229
+ return null;
230
+ }
231
+
232
+ /*
233
+ * ── 맞기 전에 비킨다 ────────────────────────────────────────────────────
234
+ *
235
+ * 서버는 매 응답에 「남은 것이 없다, 언제 풀린다」 를 실어 보낸다. 그런데
236
+ * 여태 그 값은 **화면에만** 썼다. 그래서 남은 것이 0 인 줄 알면서도 그대로
237
+ * 다음 요청을 보내고, 429 를 맞고, 다시 부르기 사다리를 태우고, 세 번째에
238
+ * 턴이 죽었다. 사람이 본 것은 「호출 한도 초과」 한 줄이다.
239
+ *
240
+ * 맞기 전에 아는 것과 맞고 나서 아는 것은 사람이 할 일이 다르다. 알고 있으면
241
+ * 그냥 기다렸다 보내면 된다 — 기다리는 것은 실패가 아니다.
242
+ *
243
+ * 조심한 것:
244
+ * · **모르면 안 기다린다.** 헤더를 안 주는 서버가 많고, 모르는 것을 0 으로
245
+ * 치면 멀쩡한 연결이 영영 기다린다.
246
+ * · 풀림 시각이 없으면 안 기다린다. 얼마나 기다릴지 모르는 채로 붙들면
247
+ * 화면이 멈춘 것과 구별이 안 된다.
248
+ * · 오래된 값으로는 안 정한다. 방금 응답이 아니면 그 사이 풀렸을 수 있다.
249
+ * · 상한을 둔다. 그보다 길면 사람이 정할 일이다 — 사실대로 말하고 보낸다.
250
+ *
251
+ * @returns {number|null} 기다릴 밀리초. 안 기다려도 되면 null
252
+ */
253
+ export const 낡은값 = 60000; // 이보다 오래된 할당량으로는 안 정한다
254
+ export const 미리기다림상한 = 60000;
255
+
256
+ /*
257
+ * 어느 창구 것인지는 **부르는 쪽이 정해서 준다.** 기본값을 두지 않는다.
258
+ *
259
+ * 전에는 `것 = 마지막` 이었다. 그 기본값이 곧 「아무 창구나 마지막에 답한
260
+ * 것」 이라, 빠뜨리고 부르면 옆 창구의 바닥난 할당량으로 이쪽이 기다린다 —
261
+ * 방금 고친 그 고장이다. 기본값을 없애면 다음에 빠뜨렸을 때 조용히 옛
262
+ * 동작으로 돌아가는 대신 눈에 보이게 어긋난다.
263
+ */
264
+ export function 미리기다릴까(것, 지금 = Date.now()) {
265
+ if (!것?.있나) return null;
266
+ if (!(것.때 > 0) || 지금 - 것.때 > 낡은값) return null;
267
+ const 바닥난것 = (것.요청 !== null && 것.요청 <= 0) || (것.토큰 !== null && 것.토큰 <= 0);
268
+ if (!바닥난것) return null;
269
+ if (!(것.풀림 > 0)) return null;
270
+ // 그 응답을 받은 뒤로 흐른 만큼은 빼 준다.
271
+ const 남은초 = 것.풀림 - Math.floor((지금 - 것.때) / 1000);
272
+ if (!(남은초 > 0)) return null;
273
+ return Math.min(남은초 * 1000, 미리기다림상한);
274
+ }