pleumrouter 0.5.2 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/run.mjs +29 -13
package/README.md CHANGED
@@ -33,7 +33,7 @@ cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
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]` | 라우터 전체 모델 리더보드 + 제공자 점유율(공개 통계) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.2",
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/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) {