ethos-router 0.1.0 → 0.1.2
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.md +15 -0
- package/index.js +95 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ ethos codex # Codex 를 에토스 라우터로 실행
|
|
|
18
18
|
| `ethos login` | 브라우저에서 승인하고 키를 받습니다 |
|
|
19
19
|
| `ethos logout` | 키를 정지하고 설정을 지웁니다 |
|
|
20
20
|
| `ethos whoami` | 어느 워크스페이스로 붙어 있는지 봅니다 |
|
|
21
|
+
| `ethos use [코드]` | 어느 모델로 붙을지 정합니다 |
|
|
21
22
|
| `ethos claude [인자…]` | Claude Code 를 에토스 라우터로 실행합니다 |
|
|
22
23
|
| `ethos codex [인자…]` | Codex 를 에토스 라우터로 실행합니다 |
|
|
23
24
|
| `ethos env` | 다른 도구에 넣을 환경변수를 찍습니다 |
|
|
@@ -27,6 +28,20 @@ ethos codex # Codex 를 에토스 라우터로 실행
|
|
|
27
28
|
| `ethos usage` | 기간별 사용 내역을 봅니다 |
|
|
28
29
|
| `ethos doctor` | 어디가 막혔는지 점검합니다 |
|
|
29
30
|
|
|
31
|
+
## 모델
|
|
32
|
+
|
|
33
|
+
`ethos claude` 는 붙을 모델을 정해서 넘깁니다. **정해 주지 않으면 도구가 제
|
|
34
|
+
기본 이름을 보내는데, 저희 카탈로그 코드에는 공급사 접두사가 붙어 있어서
|
|
35
|
+
그 이름으로는 못 찾습니다.**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ethos use # 지금 무엇으로 붙는지
|
|
39
|
+
ethos models claude # 쓸 수 있는 코드
|
|
40
|
+
ethos use anthropic/claude-opus-5 # 바꾸기
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
고른 값은 설정에 남습니다. `ETHOS_MODEL` 로 한 번만 덮어쓸 수도 있습니다.
|
|
44
|
+
|
|
30
45
|
## 키
|
|
31
46
|
|
|
32
47
|
`ethos login` 은 키를 **두 개** 받아 `~/.ethos/config.json` 에 0600 으로
|
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
|
|
|
16
16
|
import { homedir, hostname } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
|
|
19
|
-
const VERSION = "0.1.
|
|
19
|
+
const VERSION = "0.1.2";
|
|
20
20
|
const DEFAULT_BASE = "https://router.ethos.ai.kr";
|
|
21
21
|
const CONFIG_PATH = join(homedir(), ".ethos", "config.json");
|
|
22
22
|
|
|
@@ -211,15 +211,29 @@ const TOOLS = {
|
|
|
211
211
|
bin: "claude",
|
|
212
212
|
install: "npm i -g @anthropic-ai/claude-code",
|
|
213
213
|
// 앤트로픽 형식 창구. 도구가 뒤에 /v1/messages 를 붙인다.
|
|
214
|
-
|
|
214
|
+
//
|
|
215
|
+
// **ANTHROPIC_API_KEY 는 넣지 않는다.** 둘 다 있으면 API_KEY 가 이기는데,
|
|
216
|
+
// 그 값이 있으면 Claude Code 가 "이 키를 쓸까요" 승인 절차를 따로 탄다.
|
|
217
|
+
// 게이트웨이 연결의 조합은 BASE_URL + AUTH_TOKEN 둘뿐이다.
|
|
218
|
+
env: (key, base, model) => ({
|
|
215
219
|
ANTHROPIC_BASE_URL: `${base}/claude`,
|
|
216
220
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
217
|
-
|
|
221
|
+
// **모델을 정해 주지 않으면 도구가 제 기본 이름을 보낸다.** 우리 카탈로그
|
|
222
|
+
// 코드에는 공급사 접두사가 붙어 있어서 그 이름으로는 못 찾고, 도구는
|
|
223
|
+
// 전체화면 화면이라 오류가 화면과 함께 지워진다 — "아무 일도 안 일어남" 이
|
|
224
|
+
// 된다. 실제로 그랬다(2026-09-09).
|
|
225
|
+
ANTHROPIC_MODEL: model.main,
|
|
226
|
+
// 요약·제목 같은 잔일에 쓰는 작은 모델. 이걸 안 정해 주면 주 모델은
|
|
227
|
+
// 붙는데 잔일에서만 조용히 실패한다 — 원인을 찾기 훨씬 어렵다.
|
|
228
|
+
ANTHROPIC_SMALL_FAST_MODEL: model.small,
|
|
218
229
|
}),
|
|
219
230
|
},
|
|
220
231
|
codex: {
|
|
221
232
|
bin: "codex",
|
|
222
233
|
install: "npm i -g @openai/codex",
|
|
234
|
+
// 모델은 정해 주지 않는다. 이 도구가 어떤 변수로 모델을 읽는지 확인한 적이
|
|
235
|
+
// 없고, 확인 안 한 값을 넣으면 붙는 것처럼 보이다가 호출에서만 틀린다.
|
|
236
|
+
// 모델은 도구의 제 방식(--model 등)으로 고르고, 코드는 ethos models 로 본다.
|
|
223
237
|
env: (key, base) => ({
|
|
224
238
|
OPENAI_BASE_URL: `${base}/v1`,
|
|
225
239
|
OPENAI_API_KEY: key,
|
|
@@ -227,16 +241,41 @@ const TOOLS = {
|
|
|
227
241
|
},
|
|
228
242
|
};
|
|
229
243
|
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
/**
|
|
245
|
+
* 어느 모델로 붙일지.
|
|
246
|
+
*
|
|
247
|
+
* 고른 값은 설정에 남는다 — 매번 정하게 하면 대부분 기본값으로 돌고, 기본값이
|
|
248
|
+
* 무엇인지 모른 채 쓴다. `ethos use <코드>` 로 바꾸고 `ethos whoami` 에서 확인한다.
|
|
249
|
+
* 코드는 `ethos models` 가 보여주는 그대로다(공급사 접두사 포함).
|
|
250
|
+
*/
|
|
251
|
+
const DEFAULT_MODEL = { main: "anthropic/claude-sonnet-5", small: "anthropic/claude-haiku-4.5" };
|
|
252
|
+
|
|
253
|
+
function modelFor(cfg) {
|
|
254
|
+
return {
|
|
255
|
+
main: process.env.ETHOS_MODEL || cfg.model || DEFAULT_MODEL.main,
|
|
256
|
+
small: process.env.ETHOS_SMALL_MODEL || cfg.smallModel || DEFAULT_MODEL.small,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function toolEnv(name, key, base, model) {
|
|
261
|
+
return TOOLS[name].env(key, base, model);
|
|
232
262
|
}
|
|
233
263
|
|
|
234
264
|
async function cmdRunTool(name, rest) {
|
|
235
265
|
const cfg = loadConfig();
|
|
236
266
|
const key = needKey(cfg);
|
|
237
267
|
const t = TOOLS[name];
|
|
238
|
-
const
|
|
268
|
+
const base = baseUrl(cfg);
|
|
269
|
+
const model = modelFor(cfg);
|
|
270
|
+
const env = { ...process.env, ...toolEnv(name, key, base, model) };
|
|
271
|
+
|
|
272
|
+
// 무엇을 어디로 띄우는지 한 줄 남긴다. 이 도구들은 전체화면 화면을 쓰기
|
|
273
|
+
// 때문에, 뜨자마자 죽으면 **화면이 복원되면서 오류 메시지까지 같이
|
|
274
|
+
// 사라진다.** 그러면 사용자에게는 아무 일도 안 일어난 것처럼 보인다.
|
|
275
|
+
// 이 줄이 있으면 적어도 "우리는 띄웠고 저쪽이 죽었다"는 것은 남는다.
|
|
276
|
+
err(dim(` ${t.bin} · ${base} · ${model.main}`));
|
|
239
277
|
|
|
278
|
+
const started = Date.now();
|
|
240
279
|
const child = spawn(t.bin, rest, { stdio: "inherit", env });
|
|
241
280
|
child.on("error", (e) => {
|
|
242
281
|
if (e.code === "ENOENT") {
|
|
@@ -245,6 +284,15 @@ async function cmdRunTool(name, rest) {
|
|
|
245
284
|
die(e.message);
|
|
246
285
|
});
|
|
247
286
|
child.on("exit", (code, signal) => {
|
|
287
|
+
// 곧바로 죽었으면 오류를 봤을 리가 없다 — 보는 방법을 알려준다.
|
|
288
|
+
// 인자를 준 실행(--version 등)은 빨리 끝나는 게 정상이므로 빼고,
|
|
289
|
+
// 대화형으로 띄웠는데 곧바로 끝났거나 실패로 끝난 경우만 말한다.
|
|
290
|
+
const tooFast = Date.now() - started < 3000;
|
|
291
|
+
if (!signal && tooFast && (rest.length === 0 || code !== 0)) {
|
|
292
|
+
err(dim(` ${t.bin} 이 바로 종료됐습니다(코드 ${code ?? 0}).`));
|
|
293
|
+
err(dim(` 화면이 지워져 메시지가 안 보일 수 있습니다. 아래로 직접 확인하세요:`));
|
|
294
|
+
err(dim(` eval "$(ethos env)" && ${t.bin}`));
|
|
295
|
+
}
|
|
248
296
|
// 신호로 죽은 것을 0 으로 보고하면 스크립트가 성공으로 읽는다
|
|
249
297
|
process.exit(signal ? 1 : (code ?? 0));
|
|
250
298
|
});
|
|
@@ -258,7 +306,8 @@ function cmdEnv(args) {
|
|
|
258
306
|
const cfg = loadConfig();
|
|
259
307
|
const key = needKey(cfg);
|
|
260
308
|
const base = baseUrl(cfg);
|
|
261
|
-
const
|
|
309
|
+
const model = modelFor(cfg);
|
|
310
|
+
const vars = { ...toolEnv("claude", key, base, model), ...toolEnv("codex", key, base, model) };
|
|
262
311
|
const shell = args.shell || (process.env.SHELL?.includes("fish") ? "fish" : "posix");
|
|
263
312
|
for (const [k, v] of Object.entries(vars)) {
|
|
264
313
|
out(shell === "fish" ? `set -gx ${k} ${v}`
|
|
@@ -431,6 +480,42 @@ function which(bin) {
|
|
|
431
480
|
});
|
|
432
481
|
}
|
|
433
482
|
|
|
483
|
+
// ── use ──────────────────────────────────────────────────
|
|
484
|
+
//
|
|
485
|
+
// 어느 모델로 붙을지 정한다. 코드가 카탈로그에 있는지 **정하는 자리에서**
|
|
486
|
+
// 확인한다 — 안 하면 오타가 설정에 그대로 저장되고, 그 다음 실행에서
|
|
487
|
+
// 도구가 뜨자마자 죽는다(화면이 걷히면서 오류까지 사라진다).
|
|
488
|
+
async function cmdUse(args) {
|
|
489
|
+
const cfg = loadConfig();
|
|
490
|
+
const code = args._[0];
|
|
491
|
+
if (!code) {
|
|
492
|
+
const m = modelFor(cfg);
|
|
493
|
+
out("");
|
|
494
|
+
out(` 지금 모델 ${bold(m.main)}`);
|
|
495
|
+
out(` 잔일 모델 ${m.small}`);
|
|
496
|
+
out("");
|
|
497
|
+
out(dim(' 바꾸려면 ethos use <코드> (코드는 ethos models 로 봅니다)'));
|
|
498
|
+
out(dim(' 잔일까지 ethos use <코드> --small <코드>'));
|
|
499
|
+
out("");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const { json: d } = await call("/v1/prices");
|
|
504
|
+
const ids = new Set(d.models.map((m) => m.id));
|
|
505
|
+
for (const c of [code, args.small].filter(Boolean)) {
|
|
506
|
+
if (!ids.has(c)) {
|
|
507
|
+
// 비슷한 것을 같이 보여준다. "없습니다" 만 남기면 다음 행동이 없다.
|
|
508
|
+
const near = [...ids].filter((x) => x.includes(c.split("/").pop().slice(0, 6))).slice(0, 5);
|
|
509
|
+
die(`카탈로그에 없는 모델입니다: ${c}`,
|
|
510
|
+
near.length ? `비슷한 코드: ${near.join(", ")}` : "ethos models 로 코드를 확인하세요.");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
cfg.model = code;
|
|
514
|
+
if (args.small) cfg.smallModel = args.small;
|
|
515
|
+
saveConfig(cfg);
|
|
516
|
+
out(green("✓ ") + `${code} 로 붙습니다.` + (args.small ? ` (잔일 ${args.small})` : ""));
|
|
517
|
+
}
|
|
518
|
+
|
|
434
519
|
// ── whoami ────────────────────────────────────────────────
|
|
435
520
|
function cmdWhoami() {
|
|
436
521
|
const cfg = loadConfig();
|
|
@@ -438,6 +523,7 @@ function cmdWhoami() {
|
|
|
438
523
|
out("");
|
|
439
524
|
out(` 워크스페이스 ${bold(cfg.workspace?.name ?? dim("알 수 없음"))}`);
|
|
440
525
|
out(` 창구 주소 ${baseUrl(cfg)}`);
|
|
526
|
+
out(` 모델 ${modelFor(cfg).main}`);
|
|
441
527
|
// 키 전체를 찍지 않는다. 스크린샷과 화면 공유로 새어 나가는 자리다.
|
|
442
528
|
out(` 키 ${apiKey(cfg).slice(0, 12)}…`);
|
|
443
529
|
out("");
|
|
@@ -451,6 +537,7 @@ function help() {
|
|
|
451
537
|
${bold("ethos login")} 브라우저에서 승인하고 키를 받습니다
|
|
452
538
|
${bold("ethos logout")} 키를 정지하고 설정을 지웁니다
|
|
453
539
|
${bold("ethos whoami")} 어느 워크스페이스로 붙어 있는지 봅니다
|
|
540
|
+
${bold("ethos use")} ${dim("[코드]")} 어느 모델로 붙을지 정합니다 ${dim("(빈 값이면 지금 값)")}
|
|
454
541
|
|
|
455
542
|
${bold("ethos claude")} ${dim("[인자…]")} Claude Code 를 에토스 라우터로 실행합니다
|
|
456
543
|
${bold("ethos codex")} ${dim("[인자…]")} Codex 를 에토스 라우터로 실행합니다
|
|
@@ -501,6 +588,7 @@ async function main() {
|
|
|
501
588
|
case "login": return await cmdLogin(args);
|
|
502
589
|
case "logout": return await cmdLogout();
|
|
503
590
|
case "whoami": return cmdWhoami();
|
|
591
|
+
case "use": return await cmdUse(args);
|
|
504
592
|
case "env": return cmdEnv(args);
|
|
505
593
|
case "models": return await cmdModels(args);
|
|
506
594
|
case "chat": return await cmdChat(args);
|