pleumrouter 0.5.2 → 0.5.4

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
@@ -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.4",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/recipes.mjs CHANGED
@@ -16,6 +16,11 @@ export const RECIPES = {
16
16
  bin: "claude",
17
17
  type: "env",
18
18
  install: "npm i -g @anthropic-ai/claude-code",
19
+ // 앱 랭킹 귀속 메타 — 백엔드 featured_agents / UA fallback과 동기화.
20
+ attribution: {
21
+ title: "Claude Code",
22
+ referer: "https://docs.anthropic.com/en/docs/claude-code",
23
+ },
19
24
  // 함정: ANTHROPIC_BASE_URL은 반드시 루트(/v1 없이) — CLI가 /v1/messages를 스스로 덧붙인다.
20
25
  // /v1을 넣으면 /v1/v1/messages가 되어 실패.
21
26
  env: (key) => ({
@@ -30,6 +35,10 @@ export const RECIPES = {
30
35
  bin: "codex",
31
36
  type: "codex-home",
32
37
  install: "npm i -g @openai/codex",
38
+ attribution: {
39
+ title: "Codex",
40
+ referer: "https://github.com/openai/codex",
41
+ },
33
42
  env: (key) => ({ PLEUM_API_KEY: key }),
34
43
  args: () => [],
35
44
  },
@@ -38,6 +47,10 @@ export const RECIPES = {
38
47
  bin: "aider",
39
48
  type: "env",
40
49
  install: "python -m pip install aider-install && aider-install",
50
+ attribution: {
51
+ title: "Aider",
52
+ referer: "https://aider.chat",
53
+ },
41
54
  env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
42
55
  args: (m) => (m ? ["--model", `openai/${m}`] : []),
43
56
  },
@@ -47,6 +60,10 @@ export const RECIPES = {
47
60
  type: "env",
48
61
  install: "https://block.github.io/goose/docs/getting-started/installation",
49
62
  note: "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.",
63
+ attribution: {
64
+ title: "Goose",
65
+ referer: "https://block.github.io/goose",
66
+ },
50
67
  env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
51
68
  args: () => [],
52
69
  },
@@ -56,6 +73,10 @@ export const RECIPES = {
56
73
  type: "env",
57
74
  install: "python -m pip install openhands-ai",
58
75
  note: "For OpenHands, prefix the model name with openai/ in its config.",
76
+ attribution: {
77
+ title: "OpenHands",
78
+ referer: "https://www.all-hands.dev",
79
+ },
59
80
  env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
60
81
  args: () => [],
61
82
  },
@@ -67,6 +88,10 @@ export const RECIPES = {
67
88
  // opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다 — env var는 없다.
68
89
  // 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
69
90
  note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
91
+ attribution: {
92
+ title: "opencode",
93
+ referer: "https://opencode.ai",
94
+ },
70
95
  env: (key, model) => ({
71
96
  PLEUM_API_KEY: key,
72
97
  OPENCODE_CONFIG_CONTENT: JSON.stringify({
@@ -90,6 +115,10 @@ export const RECIPES = {
90
115
  // omp는 커스텀 OpenAI 호환 엔드포인트는 YAML 설정 파일로만 읽지만, 내장 anthropic
91
116
  // provider만은 claude와 동일한 ANTHROPIC_BASE_URL env var로 리디렉션된다.
92
117
  note: "Needs the Bun runtime (bun >= 1.3.14) on PATH. Only Claude-family models are routed via env vars.",
118
+ attribution: {
119
+ title: "omp (Oh My Pi)",
120
+ referer: "https://omp.sh",
121
+ },
93
122
  env: (key) => ({ ANTHROPIC_BASE_URL: ROOT, ANTHROPIC_API_KEY: key }),
94
123
  args: (m) => (m ? ["--model", `anthropic/${m}`] : []),
95
124
  },
@@ -99,6 +128,10 @@ export const RECIPES = {
99
128
  type: "env",
100
129
  install: "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash",
101
130
  note: "Pick the model after it starts with the `hermes model` command.",
131
+ attribution: {
132
+ title: "Hermes Agent",
133
+ referer: "https://hermes-agent.nousresearch.com",
134
+ },
102
135
  env: (key) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key }),
103
136
  args: () => [],
104
137
  },
@@ -107,6 +140,10 @@ export const RECIPES = {
107
140
  bin: "qwen",
108
141
  type: "env",
109
142
  install: "npm install -g @qwen-code/qwen-code@latest",
143
+ attribution: {
144
+ title: "Qwen Code",
145
+ referer: "https://github.com/QwenLM/qwen-code",
146
+ },
110
147
  env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-5.4-mini" }),
111
148
  args: () => [],
112
149
  },
@@ -116,6 +153,10 @@ export const RECIPES = {
116
153
  type: "env",
117
154
  install: "npm install -g @github/copilot",
118
155
  note: "Needs GitHub Copilot CLI 2026-04 or later for BYOK provider routing support.",
156
+ attribution: {
157
+ title: "GitHub Copilot CLI",
158
+ referer: "https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli",
159
+ },
119
160
  env: (key, model) => ({
120
161
  COPILOT_PROVIDER_BASE_URL: V1,
121
162
  COPILOT_PROVIDER_TYPE: "openai",
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) {