pleumrouter 0.5.0 → 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 +7 -7
- package/bin/pleum.mjs +6 -6
- package/package.json +1 -1
- package/src/balance.mjs +5 -13
- package/src/launch.mjs +1 -1
- package/src/locales/es.mjs +1 -1
- package/src/locales/ja.mjs +1 -1
- package/src/locales/ko.mjs +1 -1
- package/src/locales/zh.mjs +1 -1
- package/src/recipes.mjs +6 -6
- package/src/run.mjs +29 -13
package/README.md
CHANGED
|
@@ -18,11 +18,11 @@ bun install -g pleumrouter # bun
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npx pleumrouter login # 브라우저 로그인 + 키 자동 저장
|
|
21
|
-
npx pleumrouter launch claude --model claude-sonnet-
|
|
22
|
-
npx pleumrouter launch codex --model gpt-
|
|
23
|
-
npx pleumrouter run gpt-4
|
|
24
|
-
npx pleumrouter run gpt-4
|
|
25
|
-
cat error.log | npx pleumrouter run gpt-4
|
|
21
|
+
npx pleumrouter launch claude --model claude-sonnet-5
|
|
22
|
+
npx pleumrouter launch codex --model gpt-5.3-codex
|
|
23
|
+
npx pleumrouter run gpt-5.4-mini # 터미널 채팅
|
|
24
|
+
npx pleumrouter run gpt-5.4-mini "한 줄 요약: ..." # 1회 실행(스크립트용)
|
|
25
|
+
cat error.log | npx pleumrouter run gpt-5.4-mini "원인은?" # 파이프
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## 명령
|
|
@@ -33,7 +33,7 @@ cat error.log | npx pleumrouter run gpt-4.1 "원인은?" # 파이프
|
|
|
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]` | 라우터 전체 모델 리더보드 + 제공자 점유율(공개 통계) |
|
|
@@ -61,7 +61,7 @@ cat error.log | npx pleumrouter run gpt-4.1 "원인은?" # 파이프
|
|
|
61
61
|
| `/exit`, `/quit` | 종료 |
|
|
62
62
|
|
|
63
63
|
- 응답은 **마크다운 스트리밍 렌더**(굵게·코드블록 하이라이트·리스트) — 파이프로 빼면 자동으로 원문 그대로
|
|
64
|
-
- 응답 후 비용이
|
|
64
|
+
- 응답 후 비용이 현지 통화(한국 ₩)로 표시(스트리밍 마지막 청크 기준)
|
|
65
65
|
- **Ctrl+C**: 생성 중엔 1회로 중단(대화 유지), 대기 중엔 2초 내 2회로 종료
|
|
66
66
|
- 여러 줄 입력: `"""` 한 줄 → 본문 → `"""` 한 줄
|
|
67
67
|
- 이미지: 메시지에 이미지 파일 경로를 그대로 포함 (예: `이 그림 설명해줘 ./shot.png`)
|
package/bin/pleum.mjs
CHANGED
|
@@ -43,12 +43,12 @@ function buildHelp() {
|
|
|
43
43
|
const body = rows.map(([cmd, desc]) => ` ${cmd.padEnd(width)}${desc}`).join("\n");
|
|
44
44
|
|
|
45
45
|
const examples = [
|
|
46
|
-
["pleum launch claude --model claude-sonnet-
|
|
47
|
-
["pleum launch codex --model gpt-
|
|
48
|
-
["pleum run gpt-4
|
|
49
|
-
[`pleum run gpt-4
|
|
50
|
-
[`cat error.log | pleum run gpt-4
|
|
51
|
-
["pleum models deepseek/deepseek-
|
|
46
|
+
["pleum launch claude --model claude-sonnet-5", null],
|
|
47
|
+
["pleum launch codex --model gpt-5.3-codex -- --full-auto", null],
|
|
48
|
+
["pleum run gpt-5.4-mini", t("interactive")],
|
|
49
|
+
[`pleum run gpt-5.4-mini "${t("Summarize in one line: ...")}"`, t("one-shot")],
|
|
50
|
+
[`cat error.log | pleum run gpt-5.4-mini "${t("what caused this?")}"`, t("pipe")],
|
|
51
|
+
["pleum models deepseek/deepseek-v4-flash --info", t("model detail")],
|
|
52
52
|
["pleum usage --by day --days 7", t("last 7 days, by day")],
|
|
53
53
|
["pleum login --device", t("SSH/CI environment")],
|
|
54
54
|
];
|
package/package.json
CHANGED
package/src/balance.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { V1
|
|
2
|
-
import {
|
|
3
|
-
import { t } from "./i18n.mjs";
|
|
1
|
+
import { V1 } from "./config.mjs";
|
|
2
|
+
import { requireKey } from "./config.mjs";
|
|
4
3
|
|
|
5
4
|
export async function balance() {
|
|
6
5
|
const key = requireKey();
|
|
@@ -9,18 +8,11 @@ export async function balance() {
|
|
|
9
8
|
});
|
|
10
9
|
if (!res.ok) {
|
|
11
10
|
const body = await res.text().catch(() => "");
|
|
12
|
-
console.error(
|
|
13
|
-
if (res.status === 401) {
|
|
14
|
-
console.error(style.dim(t("The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.")));
|
|
15
|
-
}
|
|
11
|
+
console.error(`잔액 조회 실패 (HTTP ${res.status})${body ? `: ${body}` : ""}`);
|
|
16
12
|
process.exit(1);
|
|
17
13
|
}
|
|
18
14
|
const data = await res.json();
|
|
19
15
|
const plm = data.balance_krw;
|
|
20
|
-
const usd = data.balance_usd != null ?
|
|
21
|
-
console.log(
|
|
22
|
-
const threshold = data.auto_charge_threshold_krw;
|
|
23
|
-
if (threshold != null && plm < threshold) {
|
|
24
|
-
console.log(style.yellow(`⚠ ${t("Balance is below the auto-charge threshold ({threshold} PLM).", { threshold: threshold.toLocaleString("ko-KR") })}`));
|
|
25
|
-
}
|
|
16
|
+
const usd = data.balance_usd != null ? ` / $${data.balance_usd.toFixed(4)}` : "";
|
|
17
|
+
console.log(`잔액: ₩${plm.toLocaleString("ko-KR")}${usd}`);
|
|
26
18
|
}
|
package/src/launch.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export function launchHelp() {
|
|
|
27
27
|
const ok = onPath(m.bin) ? t("✓ installed") : `${t("✗ not installed —")} ${m.install}`;
|
|
28
28
|
console.log(` ${name.padEnd(11)} ${m.label.padEnd(13)} ${ok}`);
|
|
29
29
|
}
|
|
30
|
-
console.log(`\n${t("Example: pleum launch claude --model claude-sonnet-
|
|
30
|
+
console.log(`\n${t("Example: pleum launch claude --model claude-sonnet-5")}`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// exit:false면 process.exit 대신 자식 종료 코드를 반환 — 런처 메뉴의 실행 후 복귀용.
|
package/src/locales/es.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export const ES = {
|
|
|
22
22
|
"GitHub's terminal coding agent": "Agente de codificación de terminal de GitHub",
|
|
23
23
|
"Available: {list}": "Disponibles: {list}",
|
|
24
24
|
"Balance": "Saldo",
|
|
25
|
-
"Balance is below the auto-charge threshold ({threshold}
|
|
25
|
+
"Balance is below the auto-charge threshold ({threshold} ₩).": "El saldo está por debajo del umbral de recarga automática ({threshold} ₩).",
|
|
26
26
|
"Balance:": "Saldo:",
|
|
27
27
|
"Block's local-first agent": "Agente local-first de Block",
|
|
28
28
|
"Chat with a model": "Chatear con un modelo",
|
package/src/locales/ja.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export const JA = {
|
|
|
22
22
|
"GitHub's terminal coding agent": "GitHub のターミナルコーディングエージェント",
|
|
23
23
|
"Available: {list}": "利用可能: {list}",
|
|
24
24
|
"Balance": "残高",
|
|
25
|
-
"Balance is below the auto-charge threshold ({threshold}
|
|
25
|
+
"Balance is below the auto-charge threshold ({threshold} ₩).": "残高が自動チャージのしきい値 ({threshold} ₩) を下回っています。",
|
|
26
26
|
"Balance:": "残高:",
|
|
27
27
|
"Block's local-first agent": "Block のローカルファーストエージェント",
|
|
28
28
|
"Chat with a model": "モデルとチャット",
|
package/src/locales/ko.mjs
CHANGED
|
@@ -101,7 +101,7 @@ export const KO = {
|
|
|
101
101
|
"The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.":
|
|
102
102
|
"서버가 아직 API 키 잔액 조회를 지원하지 않거나 키가 만료됐을 수 있습니다 — pleum login 재시도.",
|
|
103
103
|
"Balance:": "잔액:",
|
|
104
|
-
"Balance is below the auto-charge threshold ({threshold}
|
|
104
|
+
"Balance is below the auto-charge threshold ({threshold} ₩).": "잔액이 자동충전 임계값({threshold} ₩) 아래입니다.",
|
|
105
105
|
|
|
106
106
|
// health.mjs
|
|
107
107
|
"Gateway connection failed: {error}": "게이트웨이 연결 실패: {error}",
|
package/src/locales/zh.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export const ZH = {
|
|
|
22
22
|
"GitHub's terminal coding agent": "GitHub 的终端编程代理",
|
|
23
23
|
"Available: {list}": "可用:{list}",
|
|
24
24
|
"Balance": "余额",
|
|
25
|
-
"Balance is below the auto-charge threshold ({threshold}
|
|
25
|
+
"Balance is below the auto-charge threshold ({threshold} ₩).": "余额低于自动充值阈值({threshold} ₩)。",
|
|
26
26
|
"Balance:": "余额:",
|
|
27
27
|
"Block's local-first agent": "Block 的本地优先代理",
|
|
28
28
|
"Chat with a model": "与模型对话",
|
package/src/recipes.mjs
CHANGED
|
@@ -75,12 +75,12 @@ export const RECIPES = {
|
|
|
75
75
|
npm: "@ai-sdk/openai-compatible",
|
|
76
76
|
name: "PleumRouter",
|
|
77
77
|
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
78
|
-
models: { [model || "gpt-4
|
|
78
|
+
models: { [model || "gpt-5.4-mini"]: {} },
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
}),
|
|
82
82
|
}),
|
|
83
|
-
args: (m) => ["--model", `pleum/${m || "gpt-4
|
|
83
|
+
args: (m) => ["--model", `pleum/${m || "gpt-5.4-mini"}`],
|
|
84
84
|
},
|
|
85
85
|
omp: {
|
|
86
86
|
label: "omp (Oh My Pi)",
|
|
@@ -107,7 +107,7 @@ export const RECIPES = {
|
|
|
107
107
|
bin: "qwen",
|
|
108
108
|
type: "env",
|
|
109
109
|
install: "npm install -g @qwen-code/qwen-code@latest",
|
|
110
|
-
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-4
|
|
110
|
+
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || "gpt-5.4-mini" }),
|
|
111
111
|
args: () => [],
|
|
112
112
|
},
|
|
113
113
|
copilot: {
|
|
@@ -120,7 +120,7 @@ export const RECIPES = {
|
|
|
120
120
|
COPILOT_PROVIDER_BASE_URL: V1,
|
|
121
121
|
COPILOT_PROVIDER_TYPE: "openai",
|
|
122
122
|
COPILOT_PROVIDER_API_KEY: key,
|
|
123
|
-
COPILOT_MODEL: model || "gpt-4
|
|
123
|
+
COPILOT_MODEL: model || "gpt-5.4-mini",
|
|
124
124
|
}),
|
|
125
125
|
args: () => [],
|
|
126
126
|
},
|
|
@@ -129,7 +129,7 @@ export const RECIPES = {
|
|
|
129
129
|
// Codex는 Chat Completions가 제거돼 Responses API(wire_api="responses")만 지원.
|
|
130
130
|
// 사용자의 ~/.codex/config.toml을 건드리지 않도록 격리 CODEX_HOME에 이 파일을 쓴다.
|
|
131
131
|
export function codexToml(model) {
|
|
132
|
-
const m = model || "gpt-4
|
|
132
|
+
const m = model || "gpt-5.4-mini";
|
|
133
133
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
134
134
|
model = "${m}"
|
|
135
135
|
model_provider = "pleum"
|
|
@@ -155,7 +155,7 @@ export const MANUAL = {
|
|
|
155
155
|
"type": "openai-compat",
|
|
156
156
|
"base_url": "${V1}",
|
|
157
157
|
"api_key": "$PLEUM_API_KEY",
|
|
158
|
-
"models": [{ "id": "gpt-4
|
|
158
|
+
"models": [{ "id": "gpt-5.4-mini", "name": "gpt-5.4-mini" }]
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
}`,
|
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) {
|