pleumrouter 0.5.3 → 0.5.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/models.mjs CHANGED
@@ -128,7 +128,20 @@ export function renderModelInfo(m, styler = style) {
128
128
  ["max output", fmtCtx(m.max_output_tokens || m.max_output)],
129
129
  [t("price/1M"), `${t("in")} ${pin} · ${t("out")} ${pout} ${styler.dim(t("(incl. {pct}% markup)", { pct: m.effective_markup_percent ?? 0 }))}`],
130
130
  ];
131
- if (m.supports_thinking) facts.push(["thinking", m.thinking_forced ? t("forced (can't be turned off)") : t("supported")]);
131
+ if (m.supports_thinking) {
132
+ facts.push([
133
+ "thinking",
134
+ m.thinking_forced
135
+ ? t("forced (can't be turned off)")
136
+ : t("supported (reasoning_effort: minimal|none|low|medium|high|xhigh|max)"),
137
+ ]);
138
+ }
139
+ if (Array.isArray(m.supported_parameters) && m.supported_parameters.includes("service_tier")) {
140
+ facts.push(["service_tier", "auto|default|flex|background|priority"]);
141
+ }
142
+ if (Array.isArray(m.supported_parameters) && m.supported_parameters.includes("reasoning_mode")) {
143
+ facts.push(["reasoning_mode", "standard|pro"]);
144
+ }
132
145
  for (const [k, v] of facts) lines.push(`${styler.dim(padDisplay(k, 10))} ${v}`);
133
146
 
134
147
  const u = m.usage;
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,12 +10,20 @@ 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] [--skip-cache] [--tag a,b]`
13
+ // `pleum run [model] ["프롬프트"...] [--verbose] [--skip-cache] [--tag a,b]
14
+ // [--reasoning-effort LEVEL] [--service-tier TIER] [--reasoning-mode MODE]`
14
15
  // 인자 파싱(순수 함수 — 테스트 대상).
16
+ const REASONING_EFFORTS = new Set(["minimal", "none", "low", "medium", "high", "xhigh", "max"]);
17
+ const SERVICE_TIERS = new Set(["auto", "default", "flex", "background", "priority"]);
18
+ const REASONING_MODES = new Set(["standard", "pro"]);
19
+
15
20
  export function parseRunArgs(rest = []) {
16
21
  let verbose = false;
17
22
  let skipCache = false;
18
23
  let tags = [];
24
+ let reasoningEffort = null;
25
+ let serviceTier = null;
26
+ let reasoningMode = null;
19
27
  const pos = [];
20
28
  for (let i = 0; i < rest.length; i++) {
21
29
  const a = rest[i];
@@ -27,9 +35,36 @@ export function parseRunArgs(rest = []) {
27
35
  tags = v.split(",").map((s) => s.trim()).filter(Boolean).slice(0, 10);
28
36
  i++;
29
37
  }
38
+ } else if (a === "--reasoning-effort") {
39
+ const v = rest[i + 1];
40
+ if (typeof v === "string" && REASONING_EFFORTS.has(v)) {
41
+ reasoningEffort = v;
42
+ i++;
43
+ }
44
+ } else if (a === "--service-tier") {
45
+ const v = rest[i + 1];
46
+ if (typeof v === "string" && SERVICE_TIERS.has(v)) {
47
+ serviceTier = v;
48
+ i++;
49
+ }
50
+ } else if (a === "--reasoning-mode") {
51
+ const v = rest[i + 1];
52
+ if (typeof v === "string" && REASONING_MODES.has(v)) {
53
+ reasoningMode = v;
54
+ i++;
55
+ }
30
56
  } else pos.push(a);
31
57
  }
32
- return { model: pos[0], prompt: pos.slice(1).join(" "), verbose, skipCache, tags };
58
+ return {
59
+ model: pos[0],
60
+ prompt: pos.slice(1).join(" "),
61
+ verbose,
62
+ skipCache,
63
+ tags,
64
+ reasoningEffort,
65
+ serviceTier,
66
+ reasoningMode,
67
+ };
33
68
  }
34
69
 
35
70
  // 입력에서 존재하는 이미지 파일 경로를 찾아 멀티모달 content로 만든다(없으면 문자열 그대로).
@@ -70,11 +105,21 @@ function isAbort(e) {
70
105
 
71
106
  export async function run(rest) {
72
107
  const args = Array.isArray(rest) ? rest : rest == null ? [] : [rest];
73
- const { verbose, prompt: inlinePrompt, model: argModel, skipCache, tags } = parseRunArgs(args);
108
+ const {
109
+ verbose,
110
+ prompt: inlinePrompt,
111
+ model: argModel,
112
+ skipCache,
113
+ tags,
114
+ reasoningEffort,
115
+ serviceTier,
116
+ reasoningMode,
117
+ } = parseRunArgs(args);
74
118
  let model = argModel;
75
119
  const piped = await readStdin();
76
120
  const oneShot = Boolean(inlinePrompt || piped);
77
121
  const key = requireKey();
122
+ const callOpts = { tags, skipCache, reasoningEffort, serviceTier, reasoningMode };
78
123
 
79
124
  if (!model && !oneShot) {
80
125
  if (!process.stdin.isTTY) {
@@ -100,7 +145,7 @@ export async function run(rest) {
100
145
  }
101
146
  const messages = [{ role: "user", content }];
102
147
  try {
103
- await streamChat(key, model, messages, { verbose, quiet: true, tags, skipCache });
148
+ await streamChat(key, model, messages, { verbose, quiet: true, ...callOpts });
104
149
  } catch (e) {
105
150
  console.error(`${style.red("✗")} ${e.message}`);
106
151
  process.exit(1);
@@ -108,10 +153,16 @@ export async function run(rest) {
108
153
  process.exit(0);
109
154
  }
110
155
 
111
- await repl(key, model, verbose, { tags, skipCache });
156
+ await repl(key, model, verbose, callOpts);
112
157
  }
113
158
 
114
- async function repl(key, initialModel, initialVerbose, { tags = [], skipCache = false } = {}) {
159
+ async function repl(key, initialModel, initialVerbose, {
160
+ tags = [],
161
+ skipCache = false,
162
+ reasoningEffort = null,
163
+ serviceTier = null,
164
+ reasoningMode = null,
165
+ } = {}) {
115
166
  let model = initialModel;
116
167
  let system = "";
117
168
  let verbose = initialVerbose;
@@ -128,7 +179,15 @@ async function repl(key, initialModel, initialVerbose, { tags = [], skipCache =
128
179
  const ac = new AbortController();
129
180
  currentAbort = ac;
130
181
  try {
131
- const reply = await streamChat(key, model, req, { verbose, signal: ac.signal, tags, skipCache });
182
+ const reply = await streamChat(key, model, req, {
183
+ verbose,
184
+ signal: ac.signal,
185
+ tags,
186
+ skipCache,
187
+ reasoningEffort,
188
+ serviceTier,
189
+ reasoningMode,
190
+ });
132
191
  if (reply) messages.push({ role: "assistant", content: reply });
133
192
  else messages.pop(); // 응답 없음(즉시 중단 등) — user 턴 되돌림
134
193
  } catch (e) {
@@ -344,7 +403,17 @@ async function handleSlash(text, ctx) {
344
403
 
345
404
  // SSE 스트리밍 채팅. fetchImpl 주입 가능(테스트용 — 네트워크 없이 SSE 파싱 검증).
346
405
  // 비용은 마지막 SSE 청크의 cost.krw — 스트리밍 응답엔 X-Cost-Krw 헤더가 없다(백엔드 계약).
347
- export async function streamChat(key, model, messages, { verbose = false, quiet = false, signal, tags = [], skipCache = false, fetchImpl = fetch } = {}) {
406
+ export async function streamChat(key, model, messages, {
407
+ verbose = false,
408
+ quiet = false,
409
+ signal,
410
+ tags = [],
411
+ skipCache = false,
412
+ reasoningEffort = null,
413
+ serviceTier = null,
414
+ reasoningMode = null,
415
+ fetchImpl = fetch,
416
+ } = {}) {
348
417
  const t0 = Date.now();
349
418
  const sp = spinner(t("{model} thinking…", { model }));
350
419
  let res;
@@ -354,6 +423,9 @@ export async function streamChat(key, model, messages, { verbose = false, quiet
354
423
  if (skipCache) headers["X-Pleum-Skip-Cache"] = "1";
355
424
  const bodyObj = { model, messages, stream: true, stream_options: { include_usage: true } };
356
425
  if (tags.length > 0) bodyObj.tags = tags.slice(0, 10);
426
+ if (reasoningEffort) bodyObj.reasoning_effort = reasoningEffort;
427
+ if (serviceTier) bodyObj.service_tier = serviceTier;
428
+ if (reasoningMode) bodyObj.reasoning_mode = reasoningMode;
357
429
  res = await fetchImpl(`${V1}/chat/completions`, {
358
430
  method: "POST",
359
431
  headers,