pleumrouter 0.5.0 → 0.5.3

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 CHANGED
@@ -18,11 +18,11 @@ bun install -g pleumrouter # bun
18
18
 
19
19
  ```bash
20
20
  npx pleumrouter login # 브라우저 로그인 + 키 자동 저장
21
- npx pleumrouter launch claude --model claude-sonnet-4
22
- npx pleumrouter launch codex --model gpt-4.1
23
- npx pleumrouter run gpt-4.1 # 터미널 채팅
24
- npx pleumrouter run gpt-4.1 "한 줄 요약: ..." # 1회 실행(스크립트용)
25
- cat error.log | npx pleumrouter run gpt-4.1 "원인은?" # 파이프
21
+ npx pleumrouter launch claude --model claude-sonnet-5
22
+ npx pleumrouter launch codex --model gpt-5.3-codex
23
+ npx pleumrouter run gpt-5.4-mini # 터미널 채팅
24
+ npx pleumrouter run gpt-5.4-mini "한 줄 요약: ..." # 1회 실행(스크립트용)
25
+ cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
26
26
  ```
27
27
 
28
28
  ## 명령
@@ -33,7 +33,7 @@ cat error.log | npx pleumrouter run gpt-4.1 "원인은?" # 파이프
33
33
  | `pleum login [--device]` | 브라우저로 로그인 → CLI가 API 키를 자동 발급·저장(`~/.pleum/config.json`). `--device`는 SSH·CI |
34
34
  | `pleum logout` | 저장된 API 키만 삭제(언어·기본 모델 등 나머지 설정은 유지) |
35
35
  | `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
36
- | `pleum run [model] ["프롬프트"]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택 |
36
+ | `pleum run [model] ["프롬프트"]` | 채팅 — 대화형 REPL, 1회 실행, stdin 파이프. 모델 생략 시 화살표로 선택. `--skip-cache`(이번 요청 응답 캐시 스킵), `--tag a,b`(리포팅 태그, ≤10) |
37
37
  | `pleum models [query] [--json]` | 모델 목록(공개) — provider·컨텍스트·가격(₩/1M) 컬럼, `--json`은 스크립트용 |
38
38
  | `pleum models <id> --info` | 모델 상세 — 가격·컨텍스트·provider별 지연/처리량/업타임·주간 사용량·형제 모델 |
39
39
  | `pleum top [--period daily\|weekly\|monthly]` | 라우터 전체 모델 리더보드 + 제공자 점유율(공개 통계) |
@@ -61,7 +61,7 @@ cat error.log | npx pleumrouter run gpt-4.1 "원인은?" # 파이프
61
61
  | `/exit`, `/quit` | 종료 |
62
62
 
63
63
  - 응답은 **마크다운 스트리밍 렌더**(굵게·코드블록 하이라이트·리스트) — 파이프로 빼면 자동으로 원문 그대로
64
- - 응답 후 비용이 PLM으로 표시(스트리밍 마지막 청크 기준)
64
+ - 응답 후 비용이 현지 통화(한국 ₩)로 표시(스트리밍 마지막 청크 기준)
65
65
  - **Ctrl+C**: 생성 중엔 1회로 중단(대화 유지), 대기 중엔 2초 내 2회로 종료
66
66
  - 여러 줄 입력: `"""` 한 줄 → 본문 → `"""` 한 줄
67
67
  - 이미지: 메시지에 이미지 파일 경로를 그대로 포함 (예: `이 그림 설명해줘 ./shot.png`)
package/bin/pleum.mjs CHANGED
@@ -43,12 +43,12 @@ function buildHelp() {
43
43
  const body = rows.map(([cmd, desc]) => ` ${cmd.padEnd(width)}${desc}`).join("\n");
44
44
 
45
45
  const examples = [
46
- ["pleum launch claude --model claude-sonnet-4", null],
47
- ["pleum launch codex --model gpt-4.1 -- --full-auto", null],
48
- ["pleum run gpt-4.1", t("interactive")],
49
- [`pleum run gpt-4.1 "${t("Summarize in one line: ...")}"`, t("one-shot")],
50
- [`cat error.log | pleum run gpt-4.1 "${t("what caused this?")}"`, t("pipe")],
51
- ["pleum models deepseek/deepseek-chat --info", t("model detail")],
46
+ ["pleum launch claude --model claude-sonnet-5", null],
47
+ ["pleum launch codex --model gpt-5.3-codex -- --full-auto", null],
48
+ ["pleum run gpt-5.4-mini", t("interactive")],
49
+ [`pleum run gpt-5.4-mini "${t("Summarize in one line: ...")}"`, t("one-shot")],
50
+ [`cat error.log | pleum run gpt-5.4-mini "${t("what caused this?")}"`, t("pipe")],
51
+ ["pleum models deepseek/deepseek-v4-flash --info", t("model detail")],
52
52
  ["pleum usage --by day --days 7", t("last 7 days, by day")],
53
53
  ["pleum login --device", t("SSH/CI environment")],
54
54
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/balance.mjs CHANGED
@@ -1,6 +1,5 @@
1
- import { V1, requireKey } from "./config.mjs";
2
- import { style } from "./ansi.mjs";
3
- import { t } from "./i18n.mjs";
1
+ import { V1 } from "./config.mjs";
2
+ import { requireKey } from "./config.mjs";
4
3
 
5
4
  export async function balance() {
6
5
  const key = requireKey();
@@ -9,18 +8,11 @@ export async function balance() {
9
8
  });
10
9
  if (!res.ok) {
11
10
  const body = await res.text().catch(() => "");
12
- console.error(`${t("Failed to check balance (HTTP {status})", { status: res.status })}${body ? `: ${body.slice(0, 200)}` : ""}`);
13
- if (res.status === 401) {
14
- console.error(style.dim(t("The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.")));
15
- }
11
+ console.error(`잔액 조회 실패 (HTTP ${res.status})${body ? `: ${body}` : ""}`);
16
12
  process.exit(1);
17
13
  }
18
14
  const data = await res.json();
19
15
  const plm = data.balance_krw;
20
- const usd = data.balance_usd != null ? style.dim(` / $${data.balance_usd.toFixed(4)}`) : "";
21
- console.log(`${t("Balance:")} ${style.bold(style.plum(`${plm.toLocaleString("ko-KR")} PLM`))}${usd}`);
22
- const threshold = data.auto_charge_threshold_krw;
23
- if (threshold != null && plm < threshold) {
24
- console.log(style.yellow(`⚠ ${t("Balance is below the auto-charge threshold ({threshold} PLM).", { threshold: threshold.toLocaleString("ko-KR") })}`));
25
- }
16
+ const usd = data.balance_usd != null ? ` / $${data.balance_usd.toFixed(4)}` : "";
17
+ console.log(`잔액: ₩${plm.toLocaleString("ko-KR")}${usd}`);
26
18
  }
package/src/launch.mjs CHANGED
@@ -27,7 +27,7 @@ export function launchHelp() {
27
27
  const ok = onPath(m.bin) ? t("✓ installed") : `${t("✗ not installed —")} ${m.install}`;
28
28
  console.log(` ${name.padEnd(11)} ${m.label.padEnd(13)} ${ok}`);
29
29
  }
30
- console.log(`\n${t("Example: pleum launch claude --model claude-sonnet-4")}`);
30
+ console.log(`\n${t("Example: pleum launch claude --model claude-sonnet-5")}`);
31
31
  }
32
32
 
33
33
  // exit:false면 process.exit 대신 자식 종료 코드를 반환 — 런처 메뉴의 실행 후 복귀용.
@@ -22,7 +22,7 @@ export const ES = {
22
22
  "GitHub's terminal coding agent": "Agente de codificación de terminal de GitHub",
23
23
  "Available: {list}": "Disponibles: {list}",
24
24
  "Balance": "Saldo",
25
- "Balance is below the auto-charge threshold ({threshold} PLM).": "El saldo está por debajo del umbral de recarga automática ({threshold} PLM).",
25
+ "Balance is below the auto-charge threshold ({threshold} ₩).": "El saldo está por debajo del umbral de recarga automática ({threshold} ₩).",
26
26
  "Balance:": "Saldo:",
27
27
  "Block's local-first agent": "Agente local-first de Block",
28
28
  "Chat with a model": "Chatear con un modelo",
@@ -22,7 +22,7 @@ export const JA = {
22
22
  "GitHub's terminal coding agent": "GitHub のターミナルコーディングエージェント",
23
23
  "Available: {list}": "利用可能: {list}",
24
24
  "Balance": "残高",
25
- "Balance is below the auto-charge threshold ({threshold} PLM).": "残高が自動チャージのしきい値 ({threshold} PLM) を下回っています。",
25
+ "Balance is below the auto-charge threshold ({threshold} ₩).": "残高が自動チャージのしきい値 ({threshold} ₩) を下回っています。",
26
26
  "Balance:": "残高:",
27
27
  "Block's local-first agent": "Block のローカルファーストエージェント",
28
28
  "Chat with a model": "モデルとチャット",
@@ -101,7 +101,7 @@ export const KO = {
101
101
  "The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.":
102
102
  "서버가 아직 API 키 잔액 조회를 지원하지 않거나 키가 만료됐을 수 있습니다 — pleum login 재시도.",
103
103
  "Balance:": "잔액:",
104
- "Balance is below the auto-charge threshold ({threshold} PLM).": "잔액이 자동충전 임계값({threshold} PLM) 아래입니다.",
104
+ "Balance is below the auto-charge threshold ({threshold} ₩).": "잔액이 자동충전 임계값({threshold} ₩) 아래입니다.",
105
105
 
106
106
  // health.mjs
107
107
  "Gateway connection failed: {error}": "게이트웨이 연결 실패: {error}",
@@ -22,7 +22,7 @@ export const ZH = {
22
22
  "GitHub's terminal coding agent": "GitHub 的终端编程代理",
23
23
  "Available: {list}": "可用:{list}",
24
24
  "Balance": "余额",
25
- "Balance is below the auto-charge threshold ({threshold} PLM).": "余额低于自动充值阈值({threshold} PLM)。",
25
+ "Balance is below the auto-charge threshold ({threshold} ₩).": "余额低于自动充值阈值({threshold} ₩)。",
26
26
  "Balance:": "余额:",
27
27
  "Block's local-first agent": "Block 的本地优先代理",
28
28
  "Chat with a model": "与模型对话",
package/src/recipes.mjs CHANGED
@@ -75,12 +75,12 @@ export const RECIPES = {
75
75
  npm: "@ai-sdk/openai-compatible",
76
76
  name: "PleumRouter",
77
77
  options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
78
- models: { [model || "gpt-4.1"]: {} },
78
+ models: { [model || "gpt-5.4-mini"]: {} },
79
79
  },
80
80
  },
81
81
  }),
82
82
  }),
83
- args: (m) => ["--model", `pleum/${m || "gpt-4.1"}`],
83
+ args: (m) => ["--model", `pleum/${m || "gpt-5.4-mini"}`],
84
84
  },
85
85
  omp: {
86
86
  label: "omp (Oh My Pi)",
@@ -107,7 +107,7 @@ export const RECIPES = {
107
107
  bin: "qwen",
108
108
  type: "env",
109
109
  install: "npm install -g @qwen-code/qwen-code@latest",
110
- env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-4.1" }),
110
+ env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-5.4-mini" }),
111
111
  args: () => [],
112
112
  },
113
113
  copilot: {
@@ -120,7 +120,7 @@ export const RECIPES = {
120
120
  COPILOT_PROVIDER_BASE_URL: V1,
121
121
  COPILOT_PROVIDER_TYPE: "openai",
122
122
  COPILOT_PROVIDER_API_KEY: key,
123
- COPILOT_MODEL: model || "gpt-4.1",
123
+ COPILOT_MODEL: model || "gpt-5.4-mini",
124
124
  }),
125
125
  args: () => [],
126
126
  },
@@ -129,7 +129,7 @@ export const RECIPES = {
129
129
  // Codex는 Chat Completions가 제거돼 Responses API(wire_api="responses")만 지원.
130
130
  // 사용자의 ~/.codex/config.toml을 건드리지 않도록 격리 CODEX_HOME에 이 파일을 쓴다.
131
131
  export function codexToml(model) {
132
- const m = model || "gpt-4.1";
132
+ const m = model || "gpt-5.4-mini";
133
133
  return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
134
134
  model = "${m}"
135
135
  model_provider = "pleum"
@@ -155,7 +155,7 @@ export const MANUAL = {
155
155
  "type": "openai-compat",
156
156
  "base_url": "${V1}",
157
157
  "api_key": "$PLEUM_API_KEY",
158
- "models": [{ "id": "gpt-4.1", "name": "gpt-4.1" }]
158
+ "models": [{ "id": "gpt-5.4-mini", "name": "gpt-5.4-mini" }]
159
159
  }
160
160
  }
161
161
  }`,
package/src/run.mjs CHANGED
@@ -10,15 +10,26 @@ import { t } from "./i18n.mjs";
10
10
 
11
11
  const IMG_EXT = /\.(png|jpe?g|gif|webp|bmp)$/i;
12
12
 
13
- // `pleum run [model] ["프롬프트"...] [--verbose]` 인자 파싱(순수 함수 — 테스트 대상).
13
+ // `pleum run [model] ["프롬프트"...] [--verbose] [--skip-cache] [--tag a,b]`
14
+ // 인자 파싱(순수 함수 — 테스트 대상).
14
15
  export function parseRunArgs(rest = []) {
15
16
  let verbose = false;
17
+ let skipCache = false;
18
+ let tags = [];
16
19
  const pos = [];
17
- for (const a of rest) {
20
+ for (let i = 0; i < rest.length; i++) {
21
+ const a = rest[i];
18
22
  if (a === "--verbose" || a === "-v") verbose = true;
19
- else pos.push(a);
23
+ else if (a === "--skip-cache") skipCache = true;
24
+ else if (a === "--tag" || a === "--tags") {
25
+ const v = rest[i + 1];
26
+ if (typeof v === "string") {
27
+ tags = v.split(",").map((s) => s.trim()).filter(Boolean).slice(0, 10);
28
+ i++;
29
+ }
30
+ } else pos.push(a);
20
31
  }
21
- return { model: pos[0], prompt: pos.slice(1).join(" "), verbose };
32
+ return { model: pos[0], prompt: pos.slice(1).join(" "), verbose, skipCache, tags };
22
33
  }
23
34
 
24
35
  // 입력에서 존재하는 이미지 파일 경로를 찾아 멀티모달 content로 만든다(없으면 문자열 그대로).
@@ -59,7 +70,7 @@ function isAbort(e) {
59
70
 
60
71
  export async function run(rest) {
61
72
  const args = Array.isArray(rest) ? rest : rest == null ? [] : [rest];
62
- const { verbose, prompt: inlinePrompt, model: argModel } = parseRunArgs(args);
73
+ const { verbose, prompt: inlinePrompt, model: argModel, skipCache, tags } = parseRunArgs(args);
63
74
  let model = argModel;
64
75
  const piped = await readStdin();
65
76
  const oneShot = Boolean(inlinePrompt || piped);
@@ -84,12 +95,12 @@ export async function run(rest) {
84
95
  const content = buildContent(prompt);
85
96
  if (Array.isArray(content)) {
86
97
  const n = content.filter((p) => p.type === "image_url").length;
87
- // 프롬프트 내 로컬 이미지 경로를 자동 첨부·업로드하므로 조용히 올리지 않고 명시 고지한다(M8).
98
+ // 프롬프트 내 로컬 이미지 경로를 자동 첨부·업송하므로 조용히 올리지 않고 명시 고지한다(M8).
88
99
  console.error(`ℹ ${t("Attaching and uploading {n} local image(s) from the prompt.", { n })}`);
89
100
  }
90
101
  const messages = [{ role: "user", content }];
91
102
  try {
92
- await streamChat(key, model, messages, { verbose, quiet: true });
103
+ await streamChat(key, model, messages, { verbose, quiet: true, tags, skipCache });
93
104
  } catch (e) {
94
105
  console.error(`${style.red("✗")} ${e.message}`);
95
106
  process.exit(1);
@@ -97,10 +108,10 @@ export async function run(rest) {
97
108
  process.exit(0);
98
109
  }
99
110
 
100
- await repl(key, model, verbose);
111
+ await repl(key, model, verbose, { tags, skipCache });
101
112
  }
102
113
 
103
- async function repl(key, initialModel, initialVerbose) {
114
+ async function repl(key, initialModel, initialVerbose, { tags = [], skipCache = false } = {}) {
104
115
  let model = initialModel;
105
116
  let system = "";
106
117
  let verbose = initialVerbose;
@@ -117,7 +128,7 @@ async function repl(key, initialModel, initialVerbose) {
117
128
  const ac = new AbortController();
118
129
  currentAbort = ac;
119
130
  try {
120
- const reply = await streamChat(key, model, req, { verbose, signal: ac.signal });
131
+ const reply = await streamChat(key, model, req, { verbose, signal: ac.signal, tags, skipCache });
121
132
  if (reply) messages.push({ role: "assistant", content: reply });
122
133
  else messages.pop(); // 응답 없음(즉시 중단 등) — user 턴 되돌림
123
134
  } catch (e) {
@@ -333,15 +344,20 @@ async function handleSlash(text, ctx) {
333
344
 
334
345
  // SSE 스트리밍 채팅. fetchImpl 주입 가능(테스트용 — 네트워크 없이 SSE 파싱 검증).
335
346
  // 비용은 마지막 SSE 청크의 cost.krw — 스트리밍 응답엔 X-Cost-Krw 헤더가 없다(백엔드 계약).
336
- export async function streamChat(key, model, messages, { verbose = false, quiet = false, signal, fetchImpl = fetch } = {}) {
347
+ export async function streamChat(key, model, messages, { verbose = false, quiet = false, signal, tags = [], skipCache = false, fetchImpl = fetch } = {}) {
337
348
  const t0 = Date.now();
338
349
  const sp = spinner(t("{model} thinking…", { model }));
339
350
  let res;
340
351
  try {
352
+ // 게이트웨이 패리티(0095): X-Pleum-Skip-Cache 헤더 + bounded tags 본문.
353
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${key}` };
354
+ if (skipCache) headers["X-Pleum-Skip-Cache"] = "1";
355
+ const bodyObj = { model, messages, stream: true, stream_options: { include_usage: true } };
356
+ if (tags.length > 0) bodyObj.tags = tags.slice(0, 10);
341
357
  res = await fetchImpl(`${V1}/chat/completions`, {
342
358
  method: "POST",
343
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
344
- body: JSON.stringify({ model, messages, stream: true, stream_options: { include_usage: true } }),
359
+ headers,
360
+ body: JSON.stringify(bodyObj),
345
361
  signal,
346
362
  });
347
363
  } catch (e) {