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.
- package/README.md +1 -1
- package/package.json +1 -1
- 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
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 (
|
|
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
|
|
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
|
-
// 프롬프트 내 로컬 이미지 경로를 자동
|
|
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
|
|
344
|
-
body: JSON.stringify(
|
|
359
|
+
headers,
|
|
360
|
+
body: JSON.stringify(bodyObj),
|
|
345
361
|
signal,
|
|
346
362
|
});
|
|
347
363
|
} catch (e) {
|