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 +1 -1
- package/src/models.mjs +14 -1
- package/src/run.mjs +80 -8
package/package.json
CHANGED
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)
|
|
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 {
|
|
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 {
|
|
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,
|
|
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,
|
|
156
|
+
await repl(key, model, verbose, callOpts);
|
|
112
157
|
}
|
|
113
158
|
|
|
114
|
-
async function repl(key, initialModel, initialVerbose, {
|
|
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, {
|
|
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, {
|
|
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,
|