deel-local-cli 1.10.0 → 1.13.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 (55) hide show
  1. package/README.ko.md +1167 -1166
  2. package/README.md +1224 -1223
  3. package/bin/deel.js +1 -1
  4. package/package.json +2 -2
  5. package/src/acp/map.js +367 -362
  6. package/src/acp/serve.js +810 -769
  7. package/src/agent/budget.js +156 -153
  8. package/src/agent/commit.js +534 -511
  9. package/src/agent/compact.js +158 -31
  10. package/src/agent/effort.js +308 -197
  11. package/src/agent/evolve.js +263 -213
  12. package/src/agent/filemem.js +16 -2
  13. package/src/agent/loop.js +1728 -1516
  14. package/src/agent/memory.js +156 -152
  15. package/src/agent/mention.js +210 -210
  16. package/src/agent/recall.js +222 -209
  17. package/src/agent/session.js +1056 -948
  18. package/src/backend/adapter.js +1355 -798
  19. package/src/backend/cachemark.js +223 -0
  20. package/src/backend/detect.js +327 -288
  21. package/src/backend/http.js +460 -458
  22. package/src/backend/mcp.js +407 -395
  23. package/src/backend/price.js +260 -197
  24. package/src/backend/probe.js +487 -473
  25. package/src/backend/quota.js +274 -157
  26. package/src/backend/retry.js +30 -6
  27. package/src/backend/tokens.js +59 -0
  28. package/src/backend/toolfit.js +352 -325
  29. package/src/backend/wire.js +715 -0
  30. package/src/commands.js +3184 -2998
  31. package/src/config.js +283 -244
  32. package/src/i18n/en.js +545 -520
  33. package/src/i18n/ja.js +498 -473
  34. package/src/i18n/ko.js +592 -567
  35. package/src/i18n/zh.js +498 -473
  36. package/src/oneshot.js +625 -549
  37. package/src/pack/tar.js +154 -132
  38. package/src/pack/zip.js +235 -217
  39. package/src/plugins/manage.js +416 -336
  40. package/src/providers/bedrock.js +17 -0
  41. package/src/repl.js +2616 -2515
  42. package/src/safety/audit.js +148 -129
  43. package/src/safety/guard.js +641 -526
  44. package/src/safety/network.js +200 -157
  45. package/src/tools/fastgrep.js +229 -214
  46. package/src/tools/fsutil.js +291 -250
  47. package/src/tools/index.js +2560 -2523
  48. package/src/tools/jobs.js +876 -795
  49. package/src/tools/label.js +52 -0
  50. package/src/tools/spawn.js +213 -99
  51. package/src/tools/verify.js +358 -357
  52. package/src/tools/webfetch.js +418 -393
  53. package/src/ui/export.js +233 -217
  54. package/src/ui/pick.js +115 -115
  55. package/src/ui/status.js +617 -610
@@ -1,197 +1,260 @@
1
- /**
2
- * 요금 셈 — 쓴 토큰을 돈으로 옮긴다.
3
- *
4
- * ── 이 파일에는 값이 박힌 요금표가 없다 ────────────────────────────────
5
- *
6
- * 일부러 안 넣었다. 요금은 회사가 아무 때나 바꾸는 값이고, 소스에 박아 두면
7
- * 고칠 사람이 없다. 그러면 이 도구는 **틀린 금액을 자신 있게 찍는 도구**가
8
- * 된다. 토큰 수를 안 보여 주는 것보다 그쪽이 훨씬 나쁘다 — 사람은 화면에
9
- * 뜬 숫자를 믿고 예산을 잡는다.
10
- *
11
- * 그래서 값의 출처는 둘뿐이다.
12
- *
13
- * 1. 사람이 설정에 적은 값 (.deel/config.json 의 `요금`)
14
- * 2. 제공자 서술 파일이 들고 있는 값 (src/providers/*.js 의 `요금`)
15
- *
16
- * 2번은 지금 전부 비어 있다. 확인한 값만 넣기로 했고, 확인하지 못했다.
17
- * 비어 있는 표는 「모른다」 고 말하지만, 지어낸 표는 거짓말을 한다.
18
- *
19
- * 모르면 돈을 안 찍는다. 토큰만 찍고 「요금 모름」 이라고 적는다.
20
- *
21
- * ── 단위 ───────────────────────────────────────────────────────────────
22
- *
23
- * 100만 토큰당 달러로 적는다. 회사들이 요금표를 그 단위로 내놓기 때문이다.
24
- * 사람이 요금표에서 본 숫자를 그대로 옮겨 적을 수 있어야 한다. 여기서
25
- * 1000토큰당으로 받으면 1000배 틀린 값이 조용히 들어온다.
26
- */
27
- import { 제공자고르기 } from '../providers/index.js';
28
-
29
- // 100만 토큰당. 요금표에 적힌 숫자를 그대로 받기 위한 단위다.
30
- export const 단위 = 1000000;
31
-
32
- /*
33
- * 기준 날짜가 이만큼 지나면 「오래됨」 이라고 덧붙인다.
34
- *
35
- * 값을 버리지는 않는다. 오래된 값도 자릿수는 맞으므로 안 보여 주는 것보다
36
- * 낫다. 다만 그대로 믿게 두면 안 된다. 반년이면 회사가 요금을 한 번쯤
37
- * 손대는 기간이라 이렇게 잡았다.
38
- */
39
- export const 낡는날 = 180;
40
-
41
- /**
42
- * 설정에 적힌 값이 쓸 수 있는 요금인가.
43
- *
44
- * 문자열로 적어도 받는다 — 사람이 JSON 에 "2.5" 라고 적는 일은 흔하고,
45
- * 그걸 튕기면 조용히 「요금 모름」 이 되어 왜 안 나오는지 알 길이 없다.
46
- * 대신 숫자가 아닌 것·음수는 받지 않고, 왜 못 받았는지 말로 남긴다.
47
- */
48
- function 값읽기(x) {
49
- if (x == null || x === '') return null;
50
- const v = typeof x === 'string' ? Number(x.trim()) : Number(x);
51
- if (!Number.isFinite(v) || v < 0) return null;
52
- return v;
53
- }
54
-
55
- /**
56
- * 모델 이름이 이 열쇠에 걸리나.
57
- *
58
- * 딱 맞는 이름이 먼저다. `*` 를 쓴 열쇠(`gpt-4o*`)는 사람이 **일부러**
59
- * 적었을 때만 걸린다 — 우리가 알아서 앞머리를 잘라 맞추면, 값이 다른
60
- * 형제 모델(`-mini`)에 비싼 요금이 붙어도 아무도 모른다.
61
- */
62
- function 걸리나(열쇠, 모델) {
63
- if (열쇠 === 모델) return { 맞나: true, 딱: true };
64
- if (!열쇠.includes('*')) return { 맞나: false };
65
- const 무늬 = new RegExp(`^${열쇠.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`);
66
- return { 맞나: 무늬.test(모델), 딱: false };
67
- }
68
-
69
- function 한칸읽기(열쇠, 값, 어디서) {
70
- const 입 = 값읽기(값?.입력 ?? 값?.in ?? 값?.input);
71
- const 출 = 값읽기(값?.출력 ?? 값?.out ?? 값?.output);
72
- if (입 == null || 출 == null) {
73
- return { 탈: `${어디서} 의 '${열쇠}' 요금이 숫자가 아닙니다 (입력·출력 둘 다 필요합니다)` };
74
- }
75
- const 기준 = 값?.기준 ?? 값?.asOf ?? null;
76
- return { 입력: 입, 출력: 출, 기준: 기준 ? String(기준) : null, 어디서, 열쇠 };
77
- }
78
-
79
- /**
80
- * 이 모델의 요금을 찾는다.
81
- *
82
- * 사람이 적은 값이 먼저다. 딸려 온 표가 낡았을 때 사람이 고칠 방법이
83
- * 그것뿐이라 그렇다. 찾으면 null 준다 0 아니다. 0 을 주면
84
- * 「공짜」 「모름」 화면에서 같아진다.
85
- *
86
- * @param {string} 모델
87
- * @param {{설정?: object|null, 제공자?: object|null, 이제?: number}} o
88
- */
89
- export function 요금찾기(모델, { 설정 = null, 제공자 = null, 이제 = Date.now() } = {}) {
90
- const 이름 = String(모델 ?? '').trim();
91
- if (!이름) return null;
92
- const 탈들 = [];
93
-
94
- for (const [표, 어디서] of [[설정, '설정'], [제공자?.요금, '딸려 온 표']]) {
95
- if (!표 || typeof 표 !== 'object') continue;
96
- // 딱 맞는 이름 먼저, 그다음에 사람이 `*` 로 적어 둔 것.
97
- for (const 딱먼저 of [true, false]) {
98
- for (const [열쇠, 값] of Object.entries(표)) {
99
- const g = 걸리나(열쇠, 이름);
100
- if (!g.맞나 || !!g.딱 !== 딱먼저) continue;
101
- const 읽은 = 한칸읽기(열쇠, 값, 어디서);
102
- if (읽은.탈) { 탈들.push(읽은.탈); continue; }
103
- const 기준 = 읽은.기준 ?? (어디서 === '딸려 온 표' ? 제공자?.요금기준 ?? null : null);
104
- return { ...읽은, 기준, 낡았나: 낡았나(기준, 이제), 탈들 };
105
- }
106
- }
107
- }
108
- return 탈들.length ? { 없음: true, 탈들 } : null;
109
- }
110
-
111
- /**
112
- * 대화에 걸린 요금값.
113
- *
114
- * 상태줄은 글자를 때마다 다시 그려진다. 그때마다 표를 훑으면 아까우니
115
- * 모델 이름이 그대로인 동안은 찾은 것을 들고 있는다. 모델을 갈아타면
116
- * (/model) 이름이 달라지므로 저절로 다시 찾는다 — 갈아탈 때 이걸 지워
117
- * 달라고 부탁하는 방식은 언젠가 한 군데를 빠뜨린다.
118
- */
119
- export function 세션요금(session, { 이제 = Date.now() } = {}) {
120
- const 모델 = String(session?.conn?.model ?? '');
121
- if (session?.요금캐시 && session.요금캐시.모델 === 모델) return session.요금캐시.값;
122
- const = 요금찾기(모델, {
123
- 설정: session?.요금표 ?? null,
124
- 제공자: session?.제공자 ? 제공자고르기(session.제공자) : null,
125
- 이제,
126
- });
127
- if (session) session.요금캐시 = { 모델, 값 };
128
- return 값;
129
- }
130
-
131
- /** 기준 날짜가 반년보다 오래됐나. 날짜가 없으면 「모른다」 — 오래됐다고 치지 않는다. */
132
- export function 낡았나(기준, 이제 = Date.now()) {
133
- if (!기준) return false;
134
- const t = Date.parse(기준);
135
- if (!Number.isFinite(t)) return false;
136
- return (이제 - t) / 86400000 > 낡는날;
137
- }
138
-
139
- /**
140
- * 토큰을 달러로.
141
- *
142
- * 요금을 모르면 null 이다. 여기서 0 을 돌려주면 부르는 쪽이 「$0.00」 을
143
- * 찍게 되고, 그건 공짜라는 뜻이 되어 버린다.
144
- */
145
- export function 돈셈({ in: = 0, out: = 0 } = {}, 값) {
146
- if (!값 || 값.없음 || 값.입력 == null || 값.출력 == null) return null;
147
- const 입돈 = (Number(입) || 0) / 단위 * 값.입력;
148
- const 출돈 = (Number(출) || 0) / 단위 * 값.출력;
149
- return { 달러: 입돈 + 출돈, 입력: 입돈, 출력: 출돈 };
150
- }
151
-
152
- /**
153
- * 돈을 사람이 읽는 글로.
154
- *
155
- * 아주 작은 값을 `$0.00` 으로 반올림하면 안 된다. 로컬에서 한두 턴 돌린
156
- * 뒤 화면에 `$0.00` 이 서 있으면 「공짜네」 로 읽히고, 그 상태로 백 턴을
157
- * 돌린다. 자릿수가 안 되면 반올림하지 말고 미만이라고 적는다.
158
- */
159
- export function 돈말(달러) {
160
- const v = Number(달러);
161
- if (!Number.isFinite(v) || v < 0) return null;
162
- if (v === 0) return '$0';
163
- if (v < 0.0001) return '<$0.0001';
164
- if (v < 1) return `$${v.toFixed(4)}`;
165
- if (v < 1000) return `$${v.toFixed(2)}`;
166
- return `$${Math.round(v).toLocaleString()}`;
167
- }
168
-
169
- /**
170
- * 이 금액이 어디서 온 값인지 한 줄로.
171
- *
172
- * 금액만 찍으면 사람은 그게 오늘 요금표에서 온 값이라고 믿는다. 어디서
173
- * 왔는지·언제 기준인지를 같이 적어야 의심할 거리가 생긴다.
174
- */
175
- export function 어디서온값(값) {
176
- if (!값 || 값.없음) return null;
177
- const 조각 = [값.어디서];
178
- if (값.열쇠 && 값.열쇠.includes('*')) 조각.push(`'${값.열쇠}' 로 맞춤`);
179
- if (값.기준) 조각.push(`${값.기준} 기준${값.낡았나 ? ' · 오래됨' : ''}`);
180
- else 조각.push('기준 날짜 없음');
181
- return 조각.join(' · ');
182
- }
183
-
184
- /**
185
- * 요금을 모를 사람에게 할 말.
186
- *
187
- * 「모릅니다」 로 끝내면 사람이 할 수 있는 일이 없다. 어디에 무엇을 적으면
188
- * 되는지까지 적는다. 요금표 주소를 아는 제공자면 그것도 같이 준다.
189
- */
190
- export function 요금적는법(모델, { 제공자 = null, 설정파일 = '.deel/config.json' } = {}) {
191
- const 줄 = [
192
- `요금을 모릅니다. ${설정파일} 100만 토큰당 달러로 적으면 셈합니다.`,
193
- ` "요금": { "${모델 || '모델이름'}": { "입력": 0, "출력": 0, "기준": "${new Date().toISOString().slice(0, 10)}" } }`,
194
- ];
195
- if (제공자?.요금표주소) 줄.push(` 요금표: ${제공자.요금표주소}`);
196
- return 줄;
197
- }
1
+ /**
2
+ * 요금 셈 — 쓴 토큰을 돈으로 옮긴다.
3
+ *
4
+ * ── 이 파일에는 값이 박힌 요금표가 없다 ────────────────────────────────
5
+ *
6
+ * 일부러 안 넣었다. 요금은 회사가 아무 때나 바꾸는 값이고, 소스에 박아 두면
7
+ * 고칠 사람이 없다. 그러면 이 도구는 **틀린 금액을 자신 있게 찍는 도구**가
8
+ * 된다. 토큰 수를 안 보여 주는 것보다 그쪽이 훨씬 나쁘다 — 사람은 화면에
9
+ * 뜬 숫자를 믿고 예산을 잡는다.
10
+ *
11
+ * 그래서 값의 출처는 둘뿐이다.
12
+ *
13
+ * 1. 사람이 설정에 적은 값 (.deel/config.json 의 `요금`)
14
+ * 2. 제공자 서술 파일이 들고 있는 값 (src/providers/*.js 의 `요금`)
15
+ *
16
+ * 2번은 지금 전부 비어 있다. 확인한 값만 넣기로 했고, 확인하지 못했다.
17
+ * 비어 있는 표는 「모른다」 고 말하지만, 지어낸 표는 거짓말을 한다.
18
+ *
19
+ * 모르면 돈을 안 찍는다. 토큰만 찍고 「요금 모름」 이라고 적는다.
20
+ *
21
+ * ── 단위 ───────────────────────────────────────────────────────────────
22
+ *
23
+ * 100만 토큰당 달러로 적는다. 회사들이 요금표를 그 단위로 내놓기 때문이다.
24
+ * 사람이 요금표에서 본 숫자를 그대로 옮겨 적을 수 있어야 한다. 여기서
25
+ * 1000토큰당으로 받으면 1000배 틀린 값이 조용히 들어온다.
26
+ */
27
+ import { 제공자고르기 } from '../providers/index.js';
28
+
29
+ // 100만 토큰당. 요금표에 적힌 숫자를 그대로 받기 위한 단위다.
30
+ export const 단위 = 1000000;
31
+
32
+ /*
33
+ * 기준 날짜가 이만큼 지나면 「오래됨」 이라고 덧붙인다.
34
+ *
35
+ * 값을 버리지는 않는다. 오래된 값도 자릿수는 맞으므로 안 보여 주는 것보다
36
+ * 낫다. 다만 그대로 믿게 두면 안 된다. 반년이면 회사가 요금을 한 번쯤
37
+ * 손대는 기간이라 이렇게 잡았다.
38
+ */
39
+ export const 낡는날 = 180;
40
+
41
+ /**
42
+ * 설정에 적힌 값이 쓸 수 있는 요금인가.
43
+ *
44
+ * 문자열로 적어도 받는다 — 사람이 JSON 에 "2.5" 라고 적는 일은 흔하고,
45
+ * 그걸 튕기면 조용히 「요금 모름」 이 되어 왜 안 나오는지 알 길이 없다.
46
+ * 대신 숫자가 아닌 것·음수는 받지 않고, 왜 못 받았는지 말로 남긴다.
47
+ */
48
+ function 값읽기(x) {
49
+ if (x == null || x === '') return null;
50
+ const v = typeof x === 'string' ? Number(x.trim()) : Number(x);
51
+ if (!Number.isFinite(v) || v < 0) return null;
52
+ return v;
53
+ }
54
+
55
+ /**
56
+ * 모델 이름이 이 열쇠에 걸리나.
57
+ *
58
+ * 딱 맞는 이름이 먼저다. `*` 를 쓴 열쇠(`gpt-4o*`)는 사람이 **일부러**
59
+ * 적었을 때만 걸린다 — 우리가 알아서 앞머리를 잘라 맞추면, 값이 다른
60
+ * 형제 모델(`-mini`)에 비싼 요금이 붙어도 아무도 모른다.
61
+ */
62
+ function 걸리나(열쇠, 모델) {
63
+ if (열쇠 === 모델) return { 맞나: true, 딱: true };
64
+ if (!열쇠.includes('*')) return { 맞나: false };
65
+ const 무늬 = new RegExp(`^${열쇠.split('*').map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`);
66
+ return { 맞나: 무늬.test(모델), 딱: false };
67
+ }
68
+
69
+ function 한칸읽기(열쇠, 값, 어디서) {
70
+ const 입 = 값읽기(값?.입력 ?? 값?.in ?? 값?.input);
71
+ const 출 = 값읽기(값?.출력 ?? 값?.out ?? 값?.output);
72
+ if (입 == null || 출 == null) {
73
+ return { 탈: `${어디서} 의 '${열쇠}' 요금이 숫자가 아닙니다 (입력·출력 둘 다 필요합니다)` };
74
+ }
75
+ /*
76
+ * ── 캐시 값은 **있으면 쓰고 없으면 지어낸다** ───────────────────
77
+ *
78
+ * 캐시로 읽힌 토큰은 정가로 안 받는다. 회사마다 다르지만 대개 정가의
79
+ * 한 자릿수 퍼센트고, 처음 써 넣을 때는 오히려 더 받는 곳도 있다.
80
+ * 그래서 둘을 정가로 세면 금액이 몇 배로 부풀고, 캐시를 켠 보람이
81
+ * 화면에서 사라진다 — 고쳐 놨는데 고친 표가 안 나는 셈이다.
82
+ *
83
+ * 그렇다고 배수를 여기 박지는 않는다. 파일이 요금을 박는 것과
84
+ * 같은 까닭이다(머리말). 모르면 정가로 세고 **모른다고 말한다** —
85
+ * 그 금액은 실제보다 **크다**. 큰 쪽으로 틀리는 것은 사람이 놀라고
86
+ * 끝나지만, 작은 쪽으로 틀리면 예산을 그 숫자로 잡는다.
87
+ */
88
+ const 캐시읽기 = 값읽기(값?.캐시읽기 ?? 값?.cacheRead ?? 값?.cache_read);
89
+ const 캐시쓰기 = 값읽기(값?.캐시쓰기 ?? 값?.cacheWrite ?? 값?.cache_write);
90
+ const 기준 = 값?.기준 ?? 값?.asOf ?? null;
91
+ return {
92
+ 입력: 입, 출력: 출, 캐시읽기, 캐시쓰기,
93
+ 기준: 기준 ? String(기준) : null, 어디서, 열쇠,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * 모델의 요금을 찾는다.
99
+ *
100
+ * 사람이 적은 값이 먼저다. 딸려 온 표가 낡았을 때 사람이 고칠 방법이
101
+ * 그것뿐이라 그렇다. 찾으면 null 을 준다 — 0 이 아니다. 0 을 주면
102
+ * 「공짜」 「모름」 이 화면에서 같아진다.
103
+ *
104
+ * @param {string} 모델
105
+ * @param {{설정?: object|null, 제공자?: object|null, 이제?: number}} o
106
+ */
107
+ export function 요금찾기(모델, { 설정 = null, 제공자 = null, 이제 = Date.now() } = {}) {
108
+ const 이름 = String(모델 ?? '').trim();
109
+ if (!이름) return null;
110
+ const 탈들 = [];
111
+
112
+ for (const [표, 어디서] of [[설정, '설정'], [제공자?.요금, '딸려 온 표']]) {
113
+ if (!표 || typeof 표 !== 'object') continue;
114
+ // 맞는 이름 먼저, 그다음에 사람이 `*` 적어 것.
115
+ for (const 딱먼저 of [true, false]) {
116
+ for (const [열쇠, 값] of Object.entries(표)) {
117
+ const g = 걸리나(열쇠, 이름);
118
+ if (!g.맞나 || !!g.딱 !== 딱먼저) continue;
119
+ const 읽은 = 한칸읽기(열쇠, 값, 어디서);
120
+ if (읽은.탈) { 탈들.push(읽은.탈); continue; }
121
+ const 기준 = 읽은.기준 ?? (어디서 === '딸려 표' ? 제공자?.요금기준 ?? null : null);
122
+ return { ...읽은, 기준, 낡았나: 낡았나(기준, 이제), 탈들 };
123
+ }
124
+ }
125
+ }
126
+ return 탈들.length ? { 없음: true, 탈들 } : null;
127
+ }
128
+
129
+ /**
130
+ * 이 대화에 걸린 요금값.
131
+ *
132
+ * 상태줄은 글자를 때마다 다시 그려진다. 그때마다 표를 훑으면 아까우니
133
+ * 모델 이름이 그대로인 동안은 찾은 것을 들고 있는다. 모델을 갈아타면
134
+ * (/model) 이름이 달라지므로 저절로 다시 찾는다 — 갈아탈 때 이걸 지워
135
+ * 달라고 부탁하는 방식은 언젠가 한 군데를 빠뜨린다.
136
+ */
137
+ export function 세션요금(session, { 이제 = Date.now() } = {}) {
138
+ const 모델 = String(session?.conn?.model ?? '');
139
+ if (session?.요금캐시 && session.요금캐시.모델 === 모델) return session.요금캐시.값;
140
+ const = 요금찾기(모델, {
141
+ 설정: session?.요금표 ?? null,
142
+ 제공자: session?.제공자 ? 제공자고르기(session.제공자) : null,
143
+ 이제,
144
+ });
145
+ if (session) session.요금캐시 = { 모델, };
146
+ return 값;
147
+ }
148
+
149
+ /** 기준 날짜가 반년보다 오래됐나. 날짜가 없으면 「모른다」 오래됐다고 치지 않는다. */
150
+ export function 낡았나(기준, 이제 = Date.now()) {
151
+ if (!기준) return false;
152
+ const t = Date.parse(기준);
153
+ if (!Number.isFinite(t)) return false;
154
+ return (이제 - t) / 86400000 > 낡는날;
155
+ }
156
+
157
+ /**
158
+ * 토큰을 달러로.
159
+ *
160
+ * 요금을 모르면 null 이다. 여기서 0 을 돌려주면 부르는 쪽이 「$0.00」 을
161
+ * 찍게 되고, 그건 공짜라는 뜻이 되어 버린다.
162
+ *
163
+ * ── 「보낸 토큰」 이 규격마다 다른 것을 여기서 안 틀리게 ─────────────
164
+ *
165
+ * Anthropic `input_tokens` 에 **캐시로 읽힌 것을 안 넣는다.** OpenAI 는
166
+ * `prompt_tokens` 가 캐시까지 **다 넣은 총계**다. 그래서 어느 한쪽에 맞춰
167
+ * 더하거나 빼면 다른 쪽에서 두 번 세거나 빠뜨린다. 이미 그 차이를 한 곳에서
168
+ * 흡수해 둔 값이 있다 — `usage.prompt` (backend/adapter.js 의 보낸토큰).
169
+ * 여기서는 그것을 총계로 보고, 캐시 몫을 빼서 정가로 낼 몫을 구한다.
170
+ *
171
+ * @param {{in?:number,out?:number,prompt?:number,cacheRead?:number,cacheWrite?:number}} 쓴것
172
+ * @param {object} 값 요금찾기()
173
+ * @returns {{달러:number,입력:number,출력:number,캐시:number,캐시모름:boolean}|null}
174
+ */
175
+ export function 돈셈({ in: 입 = 0, out: 출 = 0, prompt: 보냄 = 0, cacheRead: 읽음 = 0, cacheWrite: 씀 = 0 } = {}, 값) {
176
+ if (!값 || 값.없음 || 값.입력 == null || 값.출력 == null) return null;
177
+ const n = (v) => Math.max(0, Number(v) || 0);
178
+ const = n(읽음);
179
+ const = n();
180
+ // prompt 가 있으면 그것이 총계다. 없는 자리( 기록·검사)는 in 을 총계로 본다.
181
+ const 총계 = n(보냄) || n(입);
182
+ const 정가몫 = Math.max(0, 총계 - 읽 - 썼);
183
+
184
+ const 캐시값있나 = 값.캐시읽기 != null || 값.캐시쓰기 != null;
185
+ const 읽기값 = 값.캐시읽기 ?? 값.입력;
186
+ const 쓰기값 = 값.캐시쓰기 ?? 값.입력;
187
+
188
+ const 정가돈 = 정가몫 / 단위 * 값.입력;
189
+ const 캐시돈 = (읽 / 단위 * 읽기값) + (썼 / 단위 * 쓰기값);
190
+ const 출돈 = n(출) / 단위 * 값.출력;
191
+ return {
192
+ 달러: 정가돈 + 캐시돈 + 출돈,
193
+ 입력: 정가돈,
194
+ 출력: 출돈,
195
+ 캐시: 캐시돈,
196
+ // 캐시를 쓴 적이 있는데 그 값을 모르면, 이 금액은 실제보다 **크다**.
197
+ 캐시모름: (읽 + 썼) > 0 && !캐시값있나,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 돈을 사람이 읽는 글로.
203
+ *
204
+ * 아주 작은 값을 `$0.00` 으로 반올림하면 안 된다. 로컬에서 한두 턴 돌린
205
+ * 뒤 화면에 `$0.00` 이 서 있으면 「공짜네」 로 읽히고, 그 상태로 백 턴을
206
+ * 돌린다. 자릿수가 안 되면 반올림하지 말고 미만이라고 적는다.
207
+ */
208
+ export function 돈말(달러) {
209
+ const v = Number(달러);
210
+ if (!Number.isFinite(v) || v < 0) return null;
211
+ if (v === 0) return '$0';
212
+ if (v < 0.0001) return '<$0.0001';
213
+ if (v < 1) return `$${v.toFixed(4)}`;
214
+ if (v < 1000) return `$${v.toFixed(2)}`;
215
+ return `$${Math.round(v).toLocaleString()}`;
216
+ }
217
+
218
+ /**
219
+ * 이 금액이 어디서 온 값인지 한 줄로.
220
+ *
221
+ * 금액만 찍으면 사람은 그게 오늘 요금표에서 온 값이라고 믿는다. 어디서
222
+ * 왔는지·언제 기준인지를 같이 적어야 의심할 거리가 생긴다.
223
+ */
224
+ export function 어디서온값(값) {
225
+ if (!값 || 값.없음) return null;
226
+ const 조각 = [값.어디서];
227
+ if (값.열쇠 && 값.열쇠.includes('*')) 조각.push(`'${값.열쇠}' 로 맞춤`);
228
+ if (값.기준) 조각.push(`${값.기준} 기준${값.낡았나 ? ' · 오래됨' : ''}`);
229
+ else 조각.push('기준 날짜 없음');
230
+ return 조각.join(' · ');
231
+ }
232
+
233
+ /**
234
+ * 요금을 모를 때 사람에게 할 말.
235
+ *
236
+ * 「모릅니다」 로 끝내면 사람이 할 수 있는 일이 없다. 어디에 무엇을 적으면
237
+ * 되는지까지 적는다. 요금표 주소를 아는 제공자면 그것도 같이 준다.
238
+ */
239
+ export function 요금적는법(모델, { 제공자 = null, 설정파일 = '.deel/config.json' } = {}) {
240
+ const 줄 = [
241
+ `요금을 모릅니다. ${설정파일} 에 100만 토큰당 달러로 적으면 셈합니다.`,
242
+ /*
243
+ * 예시에 `0` 을 쓰면 안 된다.
244
+ *
245
+ * `0` 은 **유효한 요금**이다. 안내를 그대로 복사해 붙이고 모델 이름만
246
+ * 바꾸면 값읽기() 가 그것을 정상 요금으로 받아, 그 뒤로 쓴 돈이 전부
247
+ * `$0.00` 으로 뜬다. 「모릅니다」 라고 말하려다 「공짜입니다」 라고
248
+ * 말하게 되는 셈이다 — 이 프로그램에서 제일 나쁜 종류의 고장이다.
249
+ *
250
+ * 그래서 자리표는 **숫자가 아닌 것**으로 둔다. 그대로 붙이면 값읽기가
251
+ * 못 읽고 여전히 「모릅니다」 라고 말한다.
252
+ */
253
+ ` "요금": { "${모델 || '모델이름'}": { "입력": <100만 입력토큰당 달러>, "출력": <100만 출력토큰당 달러>, "기준": "${new Date().toISOString().slice(0, 10)}" } }`,
254
+ // 캐시 값은 안 적어도 된다 — 적으면 금액이 정확해지고, 안 적으면 정가로
255
+ // 세서 실제보다 크게 나온다. 있는 줄을 몰라서 못 적는 일이 없게 적어 둔다.
256
+ ' 캐시를 쓰는 창구면 `"캐시읽기": <달러>, "캐시쓰기": <달러>` 도 같이 적을 수 있습니다 (없으면 정가로 셉니다).',
257
+ ];
258
+ if (제공자?.요금표주소) 줄.push(` 요금표: ${제공자.요금표주소}`);
259
+ return 줄;
260
+ }