pleumrouter 0.5.4 → 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.4",
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/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,