deel-local-cli 1.6.3 → 1.8.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 (53) hide show
  1. package/README.ko.md +1182 -0
  2. package/README.md +850 -636
  3. package/bin/deel.js +44 -3
  4. package/package.json +3 -3
  5. package/src/acp/map.js +1 -1
  6. package/src/acp/serve.js +25 -2
  7. package/src/agent/loop.js +41 -9
  8. package/src/agent/session.js +15 -9
  9. package/src/backend/adapter.js +277 -4
  10. package/src/backend/ctxsize.js +16 -2
  11. package/src/backend/detect.js +104 -6
  12. package/src/backend/price.js +197 -0
  13. package/src/backend/retry.js +3 -0
  14. package/src/backend/scanui.js +2 -1
  15. package/src/backend/vision.js +26 -4
  16. package/src/commands.js +141 -38
  17. package/src/completion.js +21 -2
  18. package/src/i18n/en.js +160 -1
  19. package/src/i18n/index.js +22 -0
  20. package/src/i18n/ja.js +152 -1
  21. package/src/i18n/ko.js +173 -1
  22. package/src/i18n/zh.js +152 -1
  23. package/src/oneshot.js +39 -6
  24. package/src/pack/selfpack.js +1 -1
  25. package/src/providers/anthropic.js +48 -0
  26. package/src/providers/bedrock.js +102 -0
  27. package/src/providers/custom.js +65 -0
  28. package/src/providers/gemini.js +64 -0
  29. package/src/providers/index.js +121 -0
  30. package/src/providers/openai.js +68 -0
  31. package/src/repl.js +198 -33
  32. package/src/report.js +3 -2
  33. package/src/reset.js +397 -0
  34. package/src/safety/audit.js +5 -2
  35. package/src/safety/authcmd.js +305 -0
  36. package/src/safety/keystore.js +53 -0
  37. package/src/safety/runmode.js +123 -0
  38. package/src/safety/secrets.js +61 -0
  39. package/src/safety/undo.js +3 -2
  40. package/src/setup.js +215 -12
  41. package/src/tools/desc.en.js +19 -14
  42. package/src/tools/index.js +141 -43
  43. package/src/tools/jobs.js +17 -13
  44. package/src/tools/lsp.js +5 -4
  45. package/src/tools/outline.js +7 -3
  46. package/src/tools/task.js +10 -6
  47. package/src/tools/todo.js +9 -2
  48. package/src/tools/verify.js +9 -3
  49. package/src/tools/webfetch.js +2 -1
  50. package/src/ui/export.js +1 -1
  51. package/src/ui/histline.js +67 -0
  52. package/src/ui/status.js +88 -3
  53. package/README.en.md +0 -1025
package/src/i18n/zh.js CHANGED
@@ -94,8 +94,12 @@ export const zh = {
94
94
  'consult.failed': '问不成 — {왜}',
95
95
  'consult.empty': '返回是空的。那个模型可能不带工具就不回答。',
96
96
  'consult.added': '已把那个回答加进对话,并标明是谁说的。',
97
- 'head.spec.ollama': 'Ollama API',
97
+ 'head.spec.ollama': 'Ollama 自有规格',
98
98
  'head.spec.openai': 'OpenAI 兼容 API',
99
+ 'head.spec.anthropic': 'Anthropic API',
100
+ 'head.mode.default': '本机内(默认)',
101
+ 'head.mode.online': '外部(online)',
102
+ 'head.mode.offline': '封锁(offline)',
99
103
  'head.model': '模型',
100
104
  'head.tokens': '词元',
101
105
  'head.grade': '等级',
@@ -131,6 +135,7 @@ export const zh = {
131
135
  'grade.large': '大',
132
136
  'head.gradeSuffix': ' · {급} {급이름}{짐작}',
133
137
  'seg.dir': '工作文件夹 (前面的 ⌂/↗ 表示你写的东西会去哪里)',
138
+ 'seg.ver': '版本号',
134
139
  'seg.thread': '分支',
135
140
  'seg.model': '模型名',
136
141
  'seg.grade': '模型等级 (能独立做到哪一步)',
@@ -142,6 +147,7 @@ export const zh = {
142
147
  'seg.verify': '真正验证过的东西',
143
148
  'seg.undoable': '还能撤销的轮数',
144
149
  'seg.tok': '输入/输出词元',
150
+ 'seg.cost': '已花费金额(仅在已知价格时)',
145
151
  'seg.tools': '工具调用',
146
152
  'seg.skills': '本机上找到的技能',
147
153
  'seg.time': '开始以来的时间',
@@ -259,6 +265,151 @@ export const zh = {
259
265
  'ask.freePrompt': '回答',
260
266
  'loop.backoff': '服务端把请求推回来了 ({무엇}) — {초} 秒后再试 ({n}/{max})',
261
267
  'cost.retries': '重试',
268
+ // ── 计数词 ────────────────────────────────────────────────────────────
269
+ 'unit.files': '{n}个文件',
270
+ 'unit.files1': '1个文件',
271
+ 'unit.hits': '{n}处匹配',
272
+ 'unit.hits1': '1处匹配',
273
+ 'unit.lines': '{n}行',
274
+ 'unit.lines1': '1行',
275
+ 'unit.spots': '{n}处',
276
+ 'unit.spots1': '1处',
277
+ 'unit.places': '{n}处',
278
+ 'unit.places1': '1处',
279
+ 'unit.count': '{n}个',
280
+ 'unit.count1': '1个',
281
+ 'unit.chars': '{n}字',
282
+ 'unit.kchars': '{n}k字',
283
+ 'unit.calls': '{n}次',
284
+ 'unit.sec': '{n}秒',
285
+ 'unit.min': '{n}分钟',
286
+
287
+ 'sum.done': '完成',
288
+ 'sum.ok': '成功',
289
+ 'sum.exitCode': '退出码 {code}',
290
+ 'sum.killedBy': '被 {시그널} 结束',
291
+ 'sum.partial': '仅部分',
292
+ 'sum.cut': '已截断',
293
+ 'sum.notAllSeen': '未全部扫描',
294
+ 'sum.filesFailed': '{n}个失败',
295
+ 'sum.spotsFailed': '{n}处失败',
296
+ 'sum.moved': '移动{n}个',
297
+ 'sum.total': '共{줄}',
298
+ 'sum.addedLines': '+{줄}',
299
+ 'sum.noVision': '当前模型看不了图片',
300
+ 'sum.emptyAnswer': '空回答',
301
+ 'sum.unread': '读不了{n}个',
302
+ 'sum.doneOf': '{끝}/{전체} 完成',
303
+ 'sum.justDone': '刚完成{n}个',
304
+ 'sum.noChange': '没有变化',
305
+ 'sum.verified': '核对{n}项',
306
+ 'sum.readAs': '用 {쓴것} 转换后读取',
307
+ 'sum.linesOf': '{준}/{전체}行',
308
+ 'sum.countOf': '{n}/{전체}个',
309
+ 'sum.remembered': '已记住({n}行)',
310
+ 'sum.rememberFull': '位置满了,已移除最旧的',
311
+ 'sum.recall': '过往对话 {전체}条中 {맞음}条',
312
+ 'sum.jobStarted': '以{n}号启动',
313
+ 'sum.jobRunning': '运行中',
314
+ 'sum.jobEndedAlready': '已经结束',
315
+ 'sum.jobStopped': '已结束',
316
+
317
+ 'tier.exact': '完全匹配',
318
+ 'tier.trail': '忽略行尾空白和换行差异',
319
+ 'tier.indent': '忽略缩进差异',
320
+ 'tier.space': '忽略所有空白差异',
321
+
322
+ 'sum.image': '图片',
323
+ 'sum.imageDropped': '省略图片 {크기}',
324
+
325
+ 'sum.broken': '{n}处问题',
326
+ 'sum.unverified': '{n}项未核对',
327
+ 'lsp.noDef': '找不到定义。用 Grep 试试。',
328
+ 'lsp.defs': '定义 {n}',
329
+ 'lsp.noRefs': '没有任何地方用到。',
330
+ 'lsp.refs': '使用 {자리} · 文件 {파일}',
331
+
332
+ 'sum.nothingToCheck': '没有可核对的东西',
333
+ 'job.no': '{n}号',
334
+ 'job.stop': '结束',
335
+ 'more.files': '另外{n}个',
336
+ 'more.spots': '另外{n}处',
337
+
338
+ 'err.noSuchFile': '没有这个文件: {경로}',
339
+ 'err.alreadyThere': '已经存在: {경로}',
340
+ 'err.overwriteHint': '要覆盖请传 overwrite: true。',
341
+
342
+ 'run.ctxProbe': '正在确认模型启动时的上下文长度…',
343
+ 'run.noStream': '没有流式输出,回答会一次性出现',
344
+ 'run.hintHelp': '/help 命令列表',
345
+ 'run.hintEsc': 'ESC 中断',
346
+ 'run.hintQuit': 'Ctrl+C 中断·退出',
347
+ 'run.thinking': '思考中…',
348
+ 'run.thinkingChars': '思考中… {n}字',
349
+ 'run.turnFoot': '{초}秒 · 工具{n}次 · ↑{입력} ↓{출력}',
350
+ 'run.bye': '结束。',
351
+ 'run.byeStats': '模型调用{n}次 · 工具耗时{초}秒 · ↑{입력} ↓{출력}',
352
+ 'run.notMade': '— 没有被创建',
353
+
354
+ 'stage.plan': '先做判断',
355
+ 'stage.work': '接着做',
356
+ 'stage.fix': '卡住时',
357
+
358
+ 'scr.tools': '工具',
359
+ 'scr.conn': '连接',
360
+ 'scr.session': '本次会话',
361
+ 'status.kind': '规格',
362
+ 'status.url': '地址',
363
+ 'status.proxy': '代理',
364
+ 'status.proxyOff': '用不了',
365
+ 'status.model': '模型',
366
+ 'status.root': '工作目录',
367
+ 'status.shell': 'Shell',
368
+ 'status.keyStore': '密钥存放',
369
+ 'status.rules': '规则',
370
+ 'status.noRules': '无(可用 /init 建一个)',
371
+ 'status.caps': '支持',
372
+ 'status.capTools': '工具',
373
+ 'status.capStream': '流式',
374
+ 'status.capSchema': '结构化',
375
+ 'status.capThink': '推理',
376
+ 'cost.calls': '模型调用',
377
+ 'cost.tokIn': '输入 token',
378
+ 'cost.tokOut': '输出 token',
379
+ 'cost.toolMs': '工具耗时',
380
+ 'cost.elapsed': '已用时',
381
+ 'cost.money': '费用',
382
+ 'cost.quota': '服务器额度',
383
+ 'cost.quotaAge': '基于{초}秒前的响应',
384
+
385
+ 'scr.context': '上下文',
386
+ 'unit.tokens': '{n} token',
387
+ 'ctx.system': '系统提示',
388
+ 'ctx.rules': '规则 ({이름})',
389
+ 'ctx.rulesNone': '规则 (无)',
390
+ 'ctx.memory': '记忆 ({n}行)',
391
+ 'ctx.learned': '在这里学到的',
392
+ 'ctx.skills': '技能列表 ({실림}/{전체})',
393
+ 'ctx.tools': '工具定义',
394
+ 'ctx.history': '对话历史',
395
+ 'ctx.toolResults': '工具结果 (文件{n}个)',
396
+ 'ctx.left': '剩余',
397
+ 'ctx.compactHint': '折叠对话',
398
+ 'ctx.clearHint': '全部清空',
399
+ 'ctx.calibrated': '已按服务器实测值校正 {부호}{퍼센트}%(采样{번}次)。',
400
+ 'ctx.estimate': '数字为估算 — 没有使用精确的分词器。',
401
+
402
+ 'auth.waiting': '正在等待公司登录…',
403
+ 'auth.got': '已取得密钥 — 大约可用{분}分钟',
404
+ 'auth.failed': '没能取得密钥 — {왜}',
405
+ 'auth.from': '取得',
406
+ 'auth.fromPolicy': '取得(管理策略)',
407
+ 'auth.leftMin': '剩余{분}分钟',
408
+ 'auth.notYet': '尚未取得',
409
+
410
+ 'undo.wayRestored': '已还原',
411
+ 'undo.wayDeleted': '已删除(原本不存在的文件)',
412
+
262
413
  'common.unknownCommand': '不认识的命令',
263
414
  'common.seeHelp': '用 /help 看命令列表',
264
415
  'common.yes': '是',
package/src/oneshot.js CHANGED
@@ -11,7 +11,8 @@
11
11
  // 그래서 '묻는 자리' 를 전부 없앤 길을 따로 낸다. 에이전트 루프는 그대로 쓴다 —
12
12
  // 여기서 루프를 다시 짜면 두 벌이 되고, 언젠가 한쪽만 고쳐진다.
13
13
  import { c, mark, clip } from './ui/ansi.js';
14
- import { 규칙모으기 } from './safety/policy.js';
14
+ import { 규칙모으기, 정책읽기 } from './safety/policy.js';
15
+ import { 받기설정 } from './safety/authcmd.js';
15
16
  import { run } from './agent/loop.js';
16
17
  import { Session } from './agent/session.js';
17
18
  import { makeScope } from './safety/guard.js';
@@ -23,6 +24,8 @@ import { 말 as 옮긴말 } from './i18n/index.js';
23
24
  import { 알림채움 } from './backend/retry.js';
24
25
  import { discover } from './skills/discover.js';
25
26
  import { allowEndpoint, setOffline } from './safety/network.js';
27
+ import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
28
+ import { 주소가리기 } from './safety/secrets.js';
26
29
  import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
27
30
  import { route } from './agent/route.js';
28
31
  import { get as getWork } from './agent/modes.js';
@@ -61,7 +64,7 @@ async function 표준입력읽기() {
61
64
  // 도구 한 줄 요약. 화면 그림이 아니라 로그에 남을 글이라 색을 아낀다.
62
65
  function 도구줄(name, args) {
63
66
  const a = args ?? {};
64
- const 첫 = a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.목적 ??
67
+ const 첫 = a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.purpose ?? a.목적 ??
65
68
  (a.command ? String(a.command).replace(/\s+/g, ' ') : null) ??
66
69
  // 뒤에서 도는 명령. 번호가 곧 그 일감의 이름이라, 이게 없으면 기록에
67
70
  // `Jobs()` 만 여러 줄 남아 나중에 무엇을 본 것인지 알 수 없다.
@@ -152,6 +155,14 @@ export async function runOnce(opts = {}) {
152
155
  const conn = {
153
156
  kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
154
157
  key: resolveKey(prof), model: prof.model,
158
+ /*
159
+ * 열쇠를 갖고 있는 대신 **받아 오는** 설정 (safety/authcmd.js).
160
+ *
161
+ * 여기서는 설정만 싣는다. 명령은 요청 직전에 부른다 — 켤 때 한 번
162
+ * 받아 두면 한 시간짜리 토큰이 세 시간짜리 대화 한복판에서 죽고,
163
+ * 그 401 은 화면에서 「열쇠가 틀렸다」 와 구별이 안 된다.
164
+ */
165
+ 열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
155
166
  ctx: opts.ctx ?? prof.ctx ?? CTX_DEFAULT,
156
167
  // 답 길이 상한 — deel --max-tokens 32k 로 높일 수 있다(대화 화면의 /out 과 같은 값).
157
168
  maxTokens: opts.maxTokens ?? prof.maxTokens ?? null,
@@ -160,11 +171,30 @@ export async function runOnce(opts = {}) {
160
171
  vision: prof.vision ?? false,
161
172
  };
162
173
 
163
- // 자물쇠는 대화 화면과 똑같이 건다. 비대화라고 느슨해질 이유가 없다 —
164
- // 오히려 배치는 아무도 보는 자리라 단단해야 한다.
174
+ /*
175
+ * 자물쇠는 대화 화면과 똑같이 건다. 비대화라고 느슨해질 이유가 없다 —
176
+ * 오히려 배치는 아무도 안 보는 자리라 더 단단해야 한다.
177
+ *
178
+ * 다른 것은 딱 하나, **물을 수가 없다**는 것이다. 파이프 뒤에는 답할 사람이
179
+ * 없다. 그래서 여기서는 묻는 대신 멈추고, 무엇을 붙이면 되는지를 말한다.
180
+ * 조용히 나가 버리면 스크립트가 바깥으로 나가고 있는 줄 아무도 모른다.
181
+ */
182
+ const 바깥연결 = 바깥인가(conn.base);
183
+ const 실행모드 = 지금모드({
184
+ online: opts.online === true,
185
+ // 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
186
+ offline: !!(opts.offline ?? prof.offline ?? cfg?.offline),
187
+ });
188
+ const 나감 = 나갈수있나(실행모드, { 바깥: 바깥연결, 허가: prof.online === true });
189
+ if (나감.물어볼까) {
190
+ const 어디 = (() => { try { return new URL(conn.base).host; } catch { return conn.base; } })();
191
+ return 못함('needs-online',
192
+ `이 연결은 이 컴퓨터 밖으로 나갑니다 (${주소가리기(어디)}). 여기서는 물어볼 수 없습니다.\n`
193
+ + ' 나가도 된다면 --online 을 붙이거나, 대화 화면(deel)에서 한 번 허락해 두세요.\n'
194
+ + ' 이 컴퓨터 안의 모델을 쓰려면 deel scan --save 로 붙이세요.');
195
+ }
165
196
  allowEndpoint(conn.base);
166
- // 관리 정책이 offline 을 못박아 뒀으면 옵션과 상관없이 켠다 (safety/policy.js).
167
- if (opts.offline ?? prof.offline ?? cfg?.offline) setOffline(true);
197
+ if (실행모드.허가무시) setOffline(true);
168
198
 
169
199
  const session = new Session(conn, {
170
200
  root,
@@ -174,6 +204,9 @@ export async function runOnce(opts = {}) {
174
204
  effort: opts.effort ?? 'save',
175
205
  maxSteps: opts.maxSteps ?? null, // null 이면 작업 모드가 정한다
176
206
  });
207
+ // 지금 어느 실행 모드인가. 화면이 첫 줄에 이걸 그린다(ui/status.js).
208
+ // session 에 실어 두는 까닭은, 대화 도중 /model 로 옮겨도 같은 자리를 보게 하려는 것이다.
209
+ session.실행모드 = 실행모드;
177
210
 
178
211
  const found = discover(root);
179
212
  session.skills = found.skills;
@@ -17,7 +17,7 @@ export const repoRoot = () => join(dirname(fileURLToPath(import.meta.url)), '..'
17
17
 
18
18
  const SKIP_DIRS = new Set(['node_modules', '.git', '.github', 'test', '.deel']);
19
19
  const SHIP = ['bin', 'src'];
20
- const SHIP_FILES = ['package.json', 'README.md', 'README.en.md', 'LICENSE'];
20
+ const SHIP_FILES = ['package.json', 'README.md', 'README.ko.md', 'LICENSE'];
21
21
 
22
22
  function walk(dir, base, out) {
23
23
  for (const e of readdirSync(dir, { withFileTypes: true })) {
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Anthropic 직접 (Claude).
3
+ *
4
+ * ── 여기만 규격이 하나 더 필요했다 ─────────────────────────────────────
5
+ *
6
+ * 다섯 중에 유일하게 **몸통 모양이 다르다.** OpenAI 호환 창구가 없어서,
7
+ * backend/adapter.js 에 `anthropic` 갈래를 하나 더해야 말이 통한다. 그
8
+ * 갈래를 붙였다 — 문 이름 · 몸통 · 답 읽기 · 이력 되돌리기 · 도구 결과 ·
9
+ * 흘려받기 여섯 자리, 그리고 그림과 창 크기.
10
+ *
11
+ * 무엇이 다른지는 backend/adapter.js 의 anthropic몸() 머리말에 있다.
12
+ */
13
+ export const 제공자 = {
14
+ id: 'anthropic',
15
+ 이름: 'Anthropic (Claude)',
16
+ 한줄: '벤더에서 바로. 열쇠 하나만 있으면 됩니다.',
17
+
18
+ 규격: 'anthropic',
19
+ // 인증 방식이 다르다. Bearer 가 아니라 x-api-key 다 — 축이 갈라져 있다는
20
+ // 것이 여기서 그대로 보인다 (계획표 05절).
21
+ 인증: 'x-api-key',
22
+ 주소들: () => ['https://api.anthropic.com'],
23
+ 리전들: null,
24
+
25
+ 규격됐나: true,
26
+
27
+ 빈칸: ['열쇠'],
28
+ 열쇠받는곳: 'https://console.anthropic.com/settings/keys',
29
+ 키앞머리: [/^sk-ant-/],
30
+
31
+ // 왜 비워 두는지는 providers/openai.js 의 같은 자리에 적어 두었다.
32
+ 요금: {},
33
+ 요금기준: null,
34
+ 요금표주소: 'https://www.anthropic.com/pricing',
35
+
36
+ 오류읽기({ status, 서버말 }) {
37
+ if (status === 400 && /anthropic-version/i.test(서버말)) {
38
+ return 'anthropic-version 헤더가 없거나 틀렸습니다 — deel 의 규격 붙임이 아직 안 끝난 자리입니다.';
39
+ }
40
+ if (status === 404 && /model/i.test(서버말)) {
41
+ return '그 모델 이름을 이 열쇠로는 못 씁니다 — 이름이 틀렸거나, 아직 안 열린 모델입니다.';
42
+ }
43
+ if (status === 400 && /credit|balance/i.test(서버말)) {
44
+ return '잔액이 모자랍니다 — 콘솔에서 결제·크레딧을 확인하세요.';
45
+ }
46
+ return null;
47
+ },
48
+ };
@@ -0,0 +1,102 @@
1
+ /*
2
+ * AWS Bedrock.
3
+ *
4
+ * ── SigV4 서명이 필요 없다 ─────────────────────────────────────────────
5
+ *
6
+ * 이게 이 파일에서 제일 중요한 사실이고, 계획을 세울 때 내가 틀렸던 자리다.
7
+ * AWS 를 붙인다고 하면 보통 SigV4 서명(해시 체인 · 날짜 키 유도)을 떠올리고,
8
+ * 그건 의존성 0개로는 꽤 큰 일이다. 그런데 문서를 보면 —
9
+ * (docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions-mantle.md)
10
+ *
11
+ * curl -X POST "https://bedrock-runtime.us-east-1.amazonaws.com/v1/chat/completions" \
12
+ * -H "Authorization: Bearer $AWS_BEARER_TOKEN_BEDROCK"
13
+ *
14
+ * **Bedrock API 키를 그냥 Bearer 로 받는다.** 그리고 OpenAI Chat Completions
15
+ * 창구로 부를 때는 인증이 API 키 하나로 제한된다고 문서가 못 박아 두었다.
16
+ * 그러면 Bedrock 은 새 규격이 아니라 **이미 있는 축들의 새 조합**일 뿐이다 —
17
+ * 규격 openai · 인증 bearer · 주소가 리전을 탄다.
18
+ *
19
+ * ── 주소가 셋이다 ──────────────────────────────────────────────────────
20
+ *
21
+ * bedrock-runtime.{리전}.amazonaws.com/v1 API 키를 Bearer 로 (문서)
22
+ * bedrock-runtime.{리전}.amazonaws.com/openai/v1 Bedrock 위의 OpenAI 모델 (문서)
23
+ * bedrock-mantle.{리전}.api.aws/v1 mantle 창구 (문서)
24
+ *
25
+ * 셋 다 후보로 주고 **어느 것이 되는지는 물어본다**(backend/detect.js).
26
+ * 표를 믿고 하나만 쓰면 그 표가 낡은 날 사람은 「연결 실패」 만 본다.
27
+ *
28
+ * ── 리전 목록은 짧게, 진실은 엔드포인트에 묻는다 ───────────────────────
29
+ *
30
+ * 리전마다 열려 있는 모델이 다르다. 「서울에 최신 Claude 가 있다」 를 표에
31
+ * 박으면 그 표가 낡는다. 리전을 고르면 그 자리에서 실제로 물어보고, 없으면
32
+ * 없다고 말한다. 그래서 목록에는 「직접 입력」 이 늘 있다 — 목록은 빈칸을
33
+ * 줄여 주는 것이지 울타리가 아니다.
34
+ */
35
+ export const 리전들 = [
36
+ { id: 'us-east-1', 어디: '버지니아', 왜: '새 모델이 제일 먼저 열립니다' },
37
+ { id: 'us-west-2', 어디: '오리건', 왜: '두 번째로 넓습니다' },
38
+ { id: 'ap-northeast-2', 어디: '서울', 왜: '자료를 국내에 남깁니다 — 사내 도입엔 이게 관건입니다' },
39
+ { id: 'ap-northeast-1', 어디: '도쿄', 왜: '서울에 없을 때 제일 가까운 대안입니다' },
40
+ { id: 'eu-central-1', 어디: '프랑크푸르트', 왜: 'EU' },
41
+ ];
42
+
43
+ export const 제공자 = {
44
+ id: 'bedrock',
45
+ 이름: 'AWS Bedrock',
46
+ 한줄: '리전과 열쇠 둘만 있으면 됩니다. AWS 서명(SigV4)은 필요 없습니다.',
47
+
48
+ 규격: 'openai',
49
+ 인증: 'bearer',
50
+ 기본리전: 'us-east-1',
51
+ 리전들,
52
+
53
+ 주소들: ({ 리전 } = {}) => {
54
+ const r = String(리전 ?? 'us-east-1').trim();
55
+ if (!/^[a-z]{2}-[a-z]+-\d$/.test(r)) return [];
56
+ return [
57
+ `https://bedrock-runtime.${r}.amazonaws.com/v1`,
58
+ `https://bedrock-runtime.${r}.amazonaws.com/openai/v1`,
59
+ `https://bedrock-mantle.${r}.api.aws/v1`,
60
+ ];
61
+ },
62
+
63
+ 규격됐나: true,
64
+ 빈칸: ['리전', '열쇠'],
65
+ 열쇠받는곳: 'https://console.aws.amazon.com/bedrock/home#/api-keys',
66
+
67
+ /*
68
+ * 앞머리를 안 둔다.
69
+ *
70
+ * Bedrock API 키에는 알아볼 만한 고정 앞머리가 없다. 「모르면 뿌리지 말고
71
+ * 물어본다」 가 이 도구의 규칙이라(providers/index.js), 짐작이 안 서면
72
+ * 짐작하지 않는다. 빈 목록이 곧 그 뜻이다.
73
+ */
74
+ 키앞머리: [],
75
+
76
+ // 왜 비워 두는지는 providers/openai.js 의 같은 자리에 적어 두었다.
77
+ 요금: {},
78
+ 요금기준: null,
79
+ 요금표주소: 'https://aws.amazon.com/bedrock/pricing/',
80
+
81
+ 오류읽기({ status, 서버말 }) {
82
+ /*
83
+ * Bedrock 에서 제일 흔한 첫 실패는 **모델 접근 신청을 안 한 것**이다.
84
+ * 열쇠도 맞고 주소도 맞는데 그 리전의 그 모델을 아직 안 열어 둔 상태다.
85
+ * 이걸 「연결 실패」로 뭉개면 사람은 주소와 열쇠를 몇 번이고 다시 본다 —
86
+ * 정작 해야 할 일은 콘솔에서 신청 버튼 한 번 누르는 것인데.
87
+ */
88
+ if (/AccessDenied|not authorized|access to the model/i.test(서버말)) {
89
+ return '이 리전에서 그 모델에 접근 권한이 없습니다 — Bedrock 콘솔의 모델 접근(Model access)에서 신청하세요.';
90
+ }
91
+ if (/ValidationException/i.test(서버말) && /model/i.test(서버말)) {
92
+ return '그 모델 이름을 이 리전에서 못 씁니다 — 리전을 바꾸거나 이름을 다시 보세요.';
93
+ }
94
+ if (status === 403 && /security token|ExpiredToken/i.test(서버말)) {
95
+ return '열쇠가 만료됐습니다 — Bedrock 콘솔에서 새로 발급받으세요 (단기 키는 만료됩니다).';
96
+ }
97
+ if (status === 404 && /^$|Not Found/i.test(서버말)) {
98
+ return '그 리전에 이 창구가 없습니다 — 리전 이름을 다시 보세요.';
99
+ }
100
+ return null;
101
+ },
102
+ };
@@ -0,0 +1,65 @@
1
+ /*
2
+ * 주소를 직접 넣기 — 사내 게이트웨이 · 이름 없는 창구 · 아직 안 적은 벤더.
3
+ *
4
+ * ── 이게 목록의 주인공이다 ─────────────────────────────────────────────
5
+ *
6
+ * 앞의 넷은 「못 외우는 주소를 대신 적어 둔 메모」 다. 이 다섯째가 진짜로
7
+ * 하는 일이고, 화면에서도 위쪽에 둔다.
8
+ *
9
+ * 회사마다 규격도 도메인도 다르다. 명단에 없는 회사가 안 되는 것처럼 보이면
10
+ * 그 명단은 도움이 아니라 울타리다. 그래서 목록의 어느 자리에서든 「주소를
11
+ * 직접」 으로 빠져나갈 수 있어야 한다.
12
+ *
13
+ * ── 빈칸이 하나도 안 채워져 있다 ───────────────────────────────────────
14
+ *
15
+ * 규격도 인증도 안 적는다. **스캐너가 알아낸다**(backend/detect.js) —
16
+ * 후보 주소를 넓혀 보고, 인증 방식을 다 해 보고, 첫 401 에서 안 멈추고,
17
+ * 401/403(규격은 맞고 인증이 틀림)과 404(규격이 아님)를 구분하고, 200 인데
18
+ * JSON 이 아니면(사내 프록시의 로그인 페이지) 성공으로 안 친다.
19
+ *
20
+ * 그 힘이 이미 있는데 여기에 규격을 적어 두면, 적어 둔 것이 틀린 날 스캐너가
21
+ * 알아낼 수 있는 것까지 못 붙게 만든다.
22
+ *
23
+ * ── 사내는 빈칸이 아예 0개가 될 수 있다 ────────────────────────────────
24
+ *
25
+ * 관리자가 policy.json (%ProgramData%\deel · /etc/deel)으로 baseUrl 을 못박아
26
+ * 두면 직원은 열쇠만 넣는다. 정책은 설정을 이기지만 넓히지는 못한다 — 금지를
27
+ * 더할 수는 있어도 사용자가 걸어 둔 금지를 풀지는 못한다(safety/policy.js).
28
+ */
29
+ export const 제공자 = {
30
+ id: 'custom',
31
+ 이름: '주소를 직접 넣기',
32
+ 한줄: '사내 게이트웨이·목록에 없는 곳. 규격과 인증은 알아서 알아냅니다.',
33
+
34
+ // 셋 다 null 이다. 「모른다」 를 기본값으로 덮어쓰지 않는다.
35
+ 규격: null,
36
+ 인증: null,
37
+ 주소들: () => [],
38
+ 리전들: null,
39
+
40
+ 규격됐나: true,
41
+ 빈칸: ['주소', '열쇠(있으면)'],
42
+ 열쇠받는곳: null,
43
+ 키앞머리: [],
44
+
45
+ 요금: {},
46
+ 요금기준: null,
47
+ 요금표주소: null,
48
+
49
+ 오류읽기({ status, 서버말 }) {
50
+ /*
51
+ * 사내에서 제일 흔한 두 가지. 둘 다 「연결 실패」로 보이지만 고칠 자리가
52
+ * 전혀 다르다 — 하나는 프록시 설정, 하나는 인증서다.
53
+ */
54
+ if (/ENOTFOUND|EAI_AGAIN/i.test(서버말)) {
55
+ return '주소를 못 찾았습니다 — 사내망이라면 VPN 이 올라와 있는지, 프록시가 필요한지 보세요 (HTTPS_PROXY).';
56
+ }
57
+ if (/self.signed|unable to verify|CERT_|certificate/i.test(서버말)) {
58
+ return '사내 인증서를 못 믿습니다 — NODE_EXTRA_CA_CERTS 에 회사 인증서 파일을 걸어 주세요.';
59
+ }
60
+ if (status === 200 && /<html|<!DOCTYPE/i.test(서버말)) {
61
+ return '서버가 JSON 대신 웹 페이지를 줬습니다 — 앞단 프록시의 로그인 화면일 가능성이 큽니다.';
62
+ }
63
+ return null;
64
+ },
65
+ };
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Google Gemini.
3
+ *
4
+ * ── 규격을 안 늘려도 된다 ──────────────────────────────────────────────
5
+ *
6
+ * 구글이 OpenAI 호환 창구를 낸다. 문서로 확인한 것 (ai.google.dev/gemini-api/docs/openai):
7
+ *
8
+ * base_url https://generativelanguage.googleapis.com/v1beta/openai/
9
+ * 목록 GET {base}/models Authorization: Bearer <열쇠>
10
+ * 대화 POST {base}/chat/completions · tools · tool_choice 그대로
11
+ *
12
+ * 그래서 Gemini 는 **주소 한 줄**이면 끝난다. deel 의 기본 규격이 이미 이
13
+ * 모양이라 adapter 를 안 건드린다.
14
+ *
15
+ * ── 다만 도구 스키마에 걸릴 수 있다 ────────────────────────────────────
16
+ *
17
+ * 구글 문서가 「함수 호출은 OpenAPI 스키마의 일부만 지원하고, 크거나 깊이
18
+ * 중첩된 스키마는 거절될 수 있다」 고 적어 두었다. deel 의 Write(files[]) ·
19
+ * Edit(edits[]) · Move(moves[]) 는 객체 배열이라 여기 걸릴 수 있다.
20
+ *
21
+ * 추측하지 않는다 — `deel diagnose` 가 그 자리에서 진짜로 찔러 본다. 걸리면
22
+ * 그때 배열 도구를 한 개씩 쓰도록 낮춘다. 미리 낮춰 두면 멀쩡한 자리에서까지
23
+ * 한 번에 한 파일씩 고치게 되고, 그건 몇 분을 그냥 버리는 일이다.
24
+ */
25
+ export const 제공자 = {
26
+ id: 'gemini',
27
+ 이름: 'Google Gemini',
28
+ 한줄: 'OpenAI 호환 창구가 있어 주소 한 줄이면 됩니다.',
29
+
30
+ 규격: 'openai',
31
+ 인증: 'bearer',
32
+ 주소들: () => ['https://generativelanguage.googleapis.com/v1beta/openai/'],
33
+ 리전들: null,
34
+
35
+ 규격됐나: true,
36
+ 빈칸: ['열쇠'],
37
+ 열쇠받는곳: 'https://aistudio.google.com/apikey',
38
+ 키앞머리: [/^AIza/],
39
+
40
+ // 왜 비워 두는지는 providers/openai.js 의 같은 자리에 적어 두었다.
41
+ 요금: {},
42
+ 요금기준: null,
43
+ 요금표주소: 'https://ai.google.dev/gemini-api/docs/pricing',
44
+
45
+ /*
46
+ * 도구 스키마가 걸릴 수 있는 곳이라고 표를 달아 둔다. 화면이 이걸 보고
47
+ * 「걸리면 deel diagnose 로 확인하세요」 한 줄을 붙인다.
48
+ */
49
+ 도구스키마조심: '함수 호출이 OpenAPI 스키마의 일부만 지원됩니다 — 깊이 중첩된 스키마는 거절될 수 있습니다.',
50
+
51
+ 오류읽기({ status, 서버말 }) {
52
+ if (status === 400 && /API key not valid/i.test(서버말)) {
53
+ return '열쇠를 못 알아봅니다 — AI Studio 에서 받은 열쇠(AIza…)가 맞는지 보세요.';
54
+ }
55
+ if (status === 400 && /schema|function|tool/i.test(서버말)) {
56
+ return '도구 스키마를 거절했습니다 — Gemini 는 스키마의 일부만 받습니다.'
57
+ + ' deel diagnose 로 어느 도구가 걸리는지 확인하세요.';
58
+ }
59
+ if (status === 403 && /SERVICE_DISABLED|PERMISSION_DENIED/i.test(서버말)) {
60
+ return '이 프로젝트에서 Generative Language API 가 꺼져 있습니다 — 콘솔에서 켜세요.';
61
+ }
62
+ return null;
63
+ },
64
+ };