pleumrouter 0.5.30 → 0.5.31
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/ansi.mjs +4 -0
- package/src/interactive.mjs +35 -9
- package/src/locales/es.mjs +10 -2
- package/src/locales/ja.mjs +10 -2
- package/src/locales/ko.mjs +10 -2
- package/src/locales/zh.mjs +10 -2
- package/src/menu.mjs +125 -25
- package/src/models.mjs +191 -5
- package/src/recipes.mjs +54 -52
package/package.json
CHANGED
package/src/ansi.mjs
CHANGED
|
@@ -102,6 +102,10 @@ export function terminalWidth(fallback = 80) {
|
|
|
102
102
|
return process.stdout.columns || fallback;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export function terminalHeight(fallback = 24) {
|
|
106
|
+
return process.stdout.rows || fallback;
|
|
107
|
+
}
|
|
108
|
+
|
|
105
109
|
export function divider(width = Math.min(terminalWidth(), 60), styler = style) {
|
|
106
110
|
return styler.gray("─".repeat(Math.max(1, width)));
|
|
107
111
|
}
|
package/src/interactive.mjs
CHANGED
|
@@ -109,6 +109,7 @@ export async function select(items, {
|
|
|
109
109
|
return new Promise((resolve) => {
|
|
110
110
|
let query = "";
|
|
111
111
|
let cursor = Math.max(0, Math.min(initialCursor, Math.max(0, (items?.length ?? 1) - 1)));
|
|
112
|
+
let windowStart = 0;
|
|
112
113
|
let lastRows = 0;
|
|
113
114
|
|
|
114
115
|
const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
|
|
@@ -121,15 +122,33 @@ export async function select(items, {
|
|
|
121
122
|
const render = () => {
|
|
122
123
|
const pool = matches(items, query);
|
|
123
124
|
if (cursor >= pool.length) cursor = Math.max(0, pool.length - 1);
|
|
124
|
-
|
|
125
|
+
if (cursor < windowStart) {
|
|
126
|
+
windowStart = cursor;
|
|
127
|
+
} else if (cursor >= windowStart + limit) {
|
|
128
|
+
windowStart = cursor - limit + 1;
|
|
129
|
+
}
|
|
130
|
+
windowStart = Math.max(0, Math.min(windowStart, Math.max(0, pool.length - limit)));
|
|
131
|
+
|
|
132
|
+
const shown = pool.slice(windowStart, windowStart + limit);
|
|
125
133
|
const lines = [
|
|
126
134
|
`${style.plum("?")} ${style.bold(message)} ${style.dim(t("type to filter · ↑↓ move · Enter select · Esc cancel"))}`,
|
|
127
135
|
`${style.gray("›")} ${query}`,
|
|
128
|
-
...shown.map((it, i) =>
|
|
129
|
-
i === cursor ? `${style.plum("❯")} ${style.bold(formatItem(it))}` : ` ${style.dim(formatItem(it))}`,
|
|
130
|
-
),
|
|
131
136
|
];
|
|
132
|
-
if (
|
|
137
|
+
if (windowStart > 0) {
|
|
138
|
+
lines.push(style.dim(` ${t("↑ {n} more above", { n: windowStart })}`));
|
|
139
|
+
}
|
|
140
|
+
lines.push(
|
|
141
|
+
...shown.map((it, idx) => {
|
|
142
|
+
const itemIndex = windowStart + idx;
|
|
143
|
+
return itemIndex === cursor
|
|
144
|
+
? `${style.plum("❯")} ${style.bold(formatItem(it))}`
|
|
145
|
+
: ` ${style.dim(formatItem(it))}`;
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
const remainingBelow = pool.length - (windowStart + shown.length);
|
|
149
|
+
if (remainingBelow > 0) {
|
|
150
|
+
lines.push(style.dim(` ${t("… {n} more — type to narrow down", { n: remainingBelow })}`));
|
|
151
|
+
}
|
|
133
152
|
if (!pool.length) lines.push(style.dim(` ${t("No matches")}`));
|
|
134
153
|
erase();
|
|
135
154
|
const frame = lines.join("\n");
|
|
@@ -158,12 +177,19 @@ export async function select(items, {
|
|
|
158
177
|
const safeCursor = Math.max(0, Math.min(cursor, pool.length - 1));
|
|
159
178
|
return finish(pool[safeCursor] ?? null);
|
|
160
179
|
}
|
|
161
|
-
if (key.type === "up")
|
|
162
|
-
|
|
163
|
-
else if (key.type === "
|
|
164
|
-
|
|
180
|
+
if (key.type === "up") {
|
|
181
|
+
cursor = Math.max(0, cursor - 1);
|
|
182
|
+
} else if (key.type === "down") {
|
|
183
|
+
const pool = matches(items, query);
|
|
184
|
+
cursor = Math.min(Math.max(0, pool.length - 1), cursor + 1);
|
|
185
|
+
} else if (key.type === "backspace") {
|
|
186
|
+
query = query.slice(0, -1);
|
|
187
|
+
cursor = 0;
|
|
188
|
+
windowStart = 0;
|
|
189
|
+
} else if (key.type === "char") {
|
|
165
190
|
query += key.value;
|
|
166
191
|
cursor = 0;
|
|
192
|
+
windowStart = 0;
|
|
167
193
|
} else continue;
|
|
168
194
|
dirty = true;
|
|
169
195
|
}
|
package/src/locales/es.mjs
CHANGED
|
@@ -71,8 +71,16 @@ export const ES = {
|
|
|
71
71
|
"Necesita GitHub Copilot CLI 2026-04 o posterior para el enrutamiento de proveedores BYOK.",
|
|
72
72
|
"Gemini CLI speaks Google's native generateContent format — set the model after launch with `/model google/<id>`.":
|
|
73
73
|
"Gemini CLI usa el formato nativo generateContent de Google — establece el modelo tras el inicio con `/model google/<id>`.",
|
|
74
|
-
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json.
|
|
75
|
-
"Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json.
|
|
74
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.":
|
|
75
|
+
"Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json. Enruta cualquier modelo de codificación a través de PleumRouter.",
|
|
76
|
+
"↑ {n} more above": "↑ {n} más arriba",
|
|
77
|
+
"↓ {n} more below": "↓ {n} más abajo",
|
|
78
|
+
"Auto (best route)": "Auto (mejor ruta)",
|
|
79
|
+
"PleumRouter auto-routing — lowest price, lowest latency, automatic failover":
|
|
80
|
+
"Enrutamiento automático de PleumRouter: mejor precio, menor latencia y conmutación por error",
|
|
81
|
+
"Pin to {provider} (@{provider})": "Fijar al proveedor {provider} (@{provider})",
|
|
82
|
+
"Select routing provider": "Seleccionar proveedor de enrutamiento",
|
|
83
|
+
"... view all 200+ models": "... ver todos los 200+ modelos",
|
|
76
84
|
"Uses an isolated ~/.pleum/pi-home (PI_CODING_AGENT_DIR). Your ~/.pi is untouched.":
|
|
77
85
|
"Usa un ~/.pleum/pi-home aislado (PI_CODING_AGENT_DIR). No toca ~/.pi.",
|
|
78
86
|
"Uses an isolated ~/.pleum/crush-home (CRUSH_GLOBAL_CONFIG). Your crush.json is untouched.":
|
package/src/locales/ja.mjs
CHANGED
|
@@ -72,8 +72,16 @@ export const JA = {
|
|
|
72
72
|
"BYOK プロバイダールーティングに対応した 2026-04 以降の GitHub Copilot CLI が必要です。",
|
|
73
73
|
"Gemini CLI speaks Google's native generateContent format — set the model after launch with `/model google/<id>`.":
|
|
74
74
|
"Gemini CLI は Google のネイティブ generateContent 形式を使用します — 起動後に `/model google/<id>` でモデルを設定してください。",
|
|
75
|
-
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json.
|
|
76
|
-
"~/.gemini/antigravity-cli/settings.json に modelProvider=gemini
|
|
75
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.":
|
|
76
|
+
"~/.gemini/antigravity-cli/settings.json に modelProvider=gemini を書き込みます。あらゆるコーディングモデルを PleumRouter でルーティングします。",
|
|
77
|
+
"↑ {n} more above": "↑ 上にあと {n} 件",
|
|
78
|
+
"↓ {n} more below": "↓ 下にあと {n} 件",
|
|
79
|
+
"Auto (best route)": "Auto (最適な自動ルーティング)",
|
|
80
|
+
"PleumRouter auto-routing — lowest price, lowest latency, automatic failover":
|
|
81
|
+
"PleumRouter 自動ルーティング — 最安値・低遅延・自動フェイルオーバー",
|
|
82
|
+
"Pin to {provider} (@{provider})": "{provider} プロバイダーに固定 (@{provider})",
|
|
83
|
+
"Select routing provider": "ルーティングプロバイダーの選択",
|
|
84
|
+
"... view all 200+ models": "... 全 200+ モデルを表示",
|
|
77
85
|
"Uses an isolated ~/.pleum/pi-home (PI_CODING_AGENT_DIR). Your ~/.pi is untouched.":
|
|
78
86
|
"隔離した ~/.pleum/pi-home (PI_CODING_AGENT_DIR) だけを使います。~/.pi は触れません。",
|
|
79
87
|
"Uses an isolated ~/.pleum/crush-home (CRUSH_GLOBAL_CONFIG). Your crush.json is untouched.":
|
package/src/locales/ko.mjs
CHANGED
|
@@ -52,8 +52,16 @@ export const KO = {
|
|
|
52
52
|
"BYOK provider 라우팅을 지원하는 2026-04 이후 버전의 GitHub Copilot CLI가 필요합니다.",
|
|
53
53
|
"Gemini CLI speaks Google's native generateContent format — set the model after launch with `/model google/<id>`.":
|
|
54
54
|
"Gemini CLI는 Google의 네이티브 generateContent 포맷을 사용합니다 — 실행 후 `/model google/<id>`로 모델을 설정하세요.",
|
|
55
|
-
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json.
|
|
56
|
-
"~/.gemini/antigravity-cli/settings.json 에 modelProvider=gemini 를 씁니다.
|
|
55
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.":
|
|
56
|
+
"~/.gemini/antigravity-cli/settings.json 에 modelProvider=gemini 를 씁니다. 모든 코딩 모델을 PleumRouter로 라우팅합니다.",
|
|
57
|
+
"↑ {n} more above": "↑ 위에 {n}개 더",
|
|
58
|
+
"↓ {n} more below": "↓ 아래에 {n}개 더",
|
|
59
|
+
"Auto (best route)": "Auto (최적 자동 라우팅)",
|
|
60
|
+
"PleumRouter auto-routing — lowest price, lowest latency, automatic failover":
|
|
61
|
+
"PleumRouter 최적 자동 라우팅 — 최저가·최저지연·자동 페일오버",
|
|
62
|
+
"Pin to {provider} (@{provider})": "{provider} 제공자로 고정 (@{provider})",
|
|
63
|
+
"Select routing provider": "라우팅 제공자 선택",
|
|
64
|
+
"... view all 200+ models": "... 전체 200+ 모델 보기",
|
|
57
65
|
"Uses an isolated ~/.pleum/pi-home (PI_CODING_AGENT_DIR). Your ~/.pi is untouched.":
|
|
58
66
|
"격리된 ~/.pleum/pi-home (PI_CODING_AGENT_DIR)만 씁니다. ~/.pi는 건드리지 않습니다.",
|
|
59
67
|
"Uses an isolated ~/.pleum/crush-home (CRUSH_GLOBAL_CONFIG). Your crush.json is untouched.":
|
package/src/locales/zh.mjs
CHANGED
|
@@ -71,8 +71,16 @@ export const ZH = {
|
|
|
71
71
|
"需要 2026-04 或更新版本的 GitHub Copilot CLI 才支持 BYOK provider 路由。",
|
|
72
72
|
"Gemini CLI speaks Google's native generateContent format — set the model after launch with `/model google/<id>`.":
|
|
73
73
|
"Gemini CLI 使用 Google 原生 generateContent 格式 — 启动后用 `/model google/<id>` 设置模型。",
|
|
74
|
-
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json.
|
|
75
|
-
"
|
|
74
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.":
|
|
75
|
+
"将 modelProvider=gemini 写入 ~/.gemini/antigravity-cli/settings.json。通过 PleumRouter 路由所有编码模型。",
|
|
76
|
+
"↑ {n} more above": "↑ 上方还有 {n} 项",
|
|
77
|
+
"↓ {n} more below": "↓ 下方还有 {n} 项",
|
|
78
|
+
"Auto (best route)": "Auto (最佳自动路由)",
|
|
79
|
+
"PleumRouter auto-routing — lowest price, lowest latency, automatic failover":
|
|
80
|
+
"PleumRouter 自动路由 — 最优价格·超低延迟·自动故障转移",
|
|
81
|
+
"Pin to {provider} (@{provider})": "固定到 {provider} 提供商 (@{provider})",
|
|
82
|
+
"Select routing provider": "选择路由提供商",
|
|
83
|
+
"... view all 200+ models": "... 查看全部 200+ 个模型",
|
|
76
84
|
"Uses an isolated ~/.pleum/pi-home (PI_CODING_AGENT_DIR). Your ~/.pi is untouched.":
|
|
77
85
|
"只用隔离的 ~/.pleum/pi-home(PI_CODING_AGENT_DIR),不改 ~/.pi。",
|
|
78
86
|
"Uses an isolated ~/.pleum/crush-home (CRUSH_GLOBAL_CONFIG). Your crush.json is untouched.":
|
package/src/menu.mjs
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
|
-
import { style, displayWidth, terminalWidth, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
14
|
+
import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
15
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
16
|
import { readConfig, writeConfig, isLoggedIn, touchSession, getActiveContext } from "./config.mjs";
|
|
17
17
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
|
-
import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
|
|
19
|
+
import { fetchModels, matchModels, listModels, formatModelLabel, fetchCoreCodingModels, CORE_MODEL_PROVIDERS } from "./models.mjs";
|
|
20
20
|
import { run } from "./run.mjs";
|
|
21
21
|
import { login } from "./login.mjs";
|
|
22
22
|
import { top } from "./top.mjs";
|
|
@@ -123,24 +123,81 @@ function saveLastSelection(id) {
|
|
|
123
123
|
writeConfig(cfg);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export async function pickProviderForModel(modelId, { input = process.stdin, output = process.stdout } = {}) {
|
|
127
|
+
if (!modelId || modelId === "__view_all__") return modelId;
|
|
128
|
+
const providers = CORE_MODEL_PROVIDERS[modelId] || [];
|
|
129
|
+
if (!providers.length) return modelId;
|
|
130
|
+
|
|
131
|
+
const options = [
|
|
132
|
+
{
|
|
133
|
+
id: null,
|
|
134
|
+
title: `⚡ ${t("Auto (best route)")}`,
|
|
135
|
+
desc: t("PleumRouter auto-routing — lowest price, lowest latency, automatic failover"),
|
|
136
|
+
},
|
|
137
|
+
...providers.map((p) => ({
|
|
138
|
+
id: p.id,
|
|
139
|
+
title: p.name,
|
|
140
|
+
desc: t("Pin to {provider} (@{provider})", { provider: p.id }),
|
|
141
|
+
})),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const chosen = await select(options, {
|
|
145
|
+
message: `${modelId} — ${t("Select routing provider")}`,
|
|
146
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc || "")}`,
|
|
147
|
+
limit: 8,
|
|
148
|
+
input,
|
|
149
|
+
output,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!chosen) return null;
|
|
153
|
+
if (!chosen.id) return modelId;
|
|
154
|
+
return `${modelId}@${chosen.id}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
export async function pickModelFor(item, defaultModel) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
for (;;) {
|
|
159
|
+
const core = fetchCoreCodingModels();
|
|
160
|
+
const items = [
|
|
161
|
+
...core,
|
|
162
|
+
{
|
|
163
|
+
id: "__view_all__",
|
|
164
|
+
display_name: t("... view all 200+ models"),
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
const targetId = defaultModel || DEFAULT_MODEL;
|
|
168
|
+
const initialIndex = items.findIndex((x) => x.id === targetId || targetId.startsWith(`${x.id}@`));
|
|
169
|
+
const m = await select(items, {
|
|
170
|
+
message: `${item.title} — ${t("select a model")}`,
|
|
171
|
+
formatItem: (x) => (x.id === "__view_all__" ? style.dim(x.display_name) : formatModelLabel(x)),
|
|
172
|
+
matches: matchModels,
|
|
173
|
+
limit: 11,
|
|
174
|
+
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
175
|
+
});
|
|
176
|
+
if (!m) return undefined;
|
|
177
|
+
|
|
178
|
+
let pickedModelId = m.id;
|
|
179
|
+
if (pickedModelId === "__view_all__") {
|
|
180
|
+
let allModels;
|
|
181
|
+
try {
|
|
182
|
+
allModels = await fetchModels();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const allPicked = await select(allModels, {
|
|
188
|
+
message: `${item.title} — ${t("select a model")}`,
|
|
189
|
+
formatItem: (x) => formatModelLabel(x),
|
|
190
|
+
matches: matchModels,
|
|
191
|
+
limit: 10,
|
|
192
|
+
});
|
|
193
|
+
if (!allPicked) continue;
|
|
194
|
+
pickedModelId = allPicked.id;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const withProvider = await pickProviderForModel(pickedModelId);
|
|
198
|
+
if (!withProvider) continue;
|
|
199
|
+
return withProvider;
|
|
133
200
|
}
|
|
134
|
-
const targetId = defaultModel || DEFAULT_MODEL;
|
|
135
|
-
const initialIndex = models.findIndex((x) => x.id === targetId);
|
|
136
|
-
const m = await select(models, {
|
|
137
|
-
message: `${item.title} — ${t("select a model")}`,
|
|
138
|
-
formatItem: (x) => formatModelLabel(x),
|
|
139
|
-
matches: matchModels,
|
|
140
|
-
limit: 10,
|
|
141
|
-
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
142
|
-
});
|
|
143
|
-
return m?.id;
|
|
144
201
|
}
|
|
145
202
|
|
|
146
203
|
// 미설치 도구: ollama처럼 확인 후 그 자리에서 설치(stdio 상속). 성공 시 true.
|
|
@@ -184,6 +241,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
184
241
|
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return undefined;
|
|
185
242
|
|
|
186
243
|
let cursor = Math.max(0, Math.min(initialCursor, items.length - 1));
|
|
244
|
+
let windowStart = 0;
|
|
187
245
|
let lastRows = 0;
|
|
188
246
|
|
|
189
247
|
const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
|
|
@@ -192,28 +250,68 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
192
250
|
output.write("\r" + (lastRows > 1 ? `\x1b[${lastRows - 1}A` : "") + "\x1b[0J");
|
|
193
251
|
lastRows = 0;
|
|
194
252
|
};
|
|
253
|
+
|
|
254
|
+
const getMaxVisible = () => {
|
|
255
|
+
const h = terminalHeight();
|
|
256
|
+
return Math.max(3, Math.min(9, Math.floor((h - 6) / 2)));
|
|
257
|
+
};
|
|
258
|
+
|
|
195
259
|
const render = () => {
|
|
260
|
+
const maxVisible = getMaxVisible();
|
|
261
|
+
if (cursor < windowStart) {
|
|
262
|
+
windowStart = cursor;
|
|
263
|
+
} else if (cursor >= windowStart + maxVisible) {
|
|
264
|
+
windowStart = cursor - maxVisible + 1;
|
|
265
|
+
}
|
|
266
|
+
windowStart = Math.max(0, Math.min(windowStart, Math.max(0, items.length - maxVisible)));
|
|
267
|
+
|
|
268
|
+
const visibleItems = items.slice(windowStart, windowStart + maxVisible);
|
|
196
269
|
const lines = [
|
|
197
270
|
header,
|
|
198
271
|
"",
|
|
199
|
-
...items.flatMap((it, i) =>
|
|
200
|
-
i === cursor
|
|
201
|
-
? [`${style.plum("▸")} ${style.bold(it.title)}`, ` ${style.dim(it.desc)}`]
|
|
202
|
-
: [` ${it.title}`, ` ${style.dim(it.desc)}`],
|
|
203
|
-
),
|
|
204
|
-
"",
|
|
205
|
-
style.dim(footer),
|
|
206
272
|
];
|
|
273
|
+
|
|
274
|
+
if (windowStart > 0) {
|
|
275
|
+
lines.push(style.dim(` ${t("↑ {n} more above", { n: windowStart })}`));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (let idx = 0; idx < visibleItems.length; idx++) {
|
|
279
|
+
const i = windowStart + idx;
|
|
280
|
+
const it = visibleItems[idx];
|
|
281
|
+
if (i === cursor) {
|
|
282
|
+
lines.push(`${style.plum("▸")} ${style.bold(it.title)}`);
|
|
283
|
+
lines.push(` ${style.dim(it.desc)}`);
|
|
284
|
+
} else {
|
|
285
|
+
lines.push(` ${it.title}`);
|
|
286
|
+
lines.push(` ${style.dim(it.desc)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const remainingBelow = items.length - (windowStart + visibleItems.length);
|
|
291
|
+
if (remainingBelow > 0) {
|
|
292
|
+
lines.push(style.dim(` ${t("↓ {n} more below", { n: remainingBelow })}`));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
lines.push("");
|
|
296
|
+
lines.push(style.dim(footer));
|
|
297
|
+
|
|
207
298
|
erase();
|
|
208
299
|
output.write(lines.join("\n"));
|
|
209
300
|
lastRows = lines.reduce((n, l) => n + rowsFor(l), 0);
|
|
210
301
|
};
|
|
302
|
+
|
|
211
303
|
const move = (dir) => {
|
|
212
304
|
cursor = Math.max(0, Math.min(items.length - 1, cursor + dir));
|
|
213
305
|
};
|
|
214
306
|
|
|
215
307
|
return new Promise((resolve) => {
|
|
308
|
+
const onResize = () => {
|
|
309
|
+
render();
|
|
310
|
+
};
|
|
311
|
+
output.on("resize", onResize);
|
|
312
|
+
|
|
216
313
|
const suspend = () => {
|
|
314
|
+
output.off("resize", onResize);
|
|
217
315
|
input.off("data", onData);
|
|
218
316
|
input.setRawMode(false);
|
|
219
317
|
output.write("\x1b[?25h");
|
|
@@ -223,10 +321,12 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
223
321
|
input.setRawMode(true);
|
|
224
322
|
input.resume();
|
|
225
323
|
output.write("\x1b[?25l");
|
|
324
|
+
output.on("resize", onResize);
|
|
226
325
|
input.on("data", onData);
|
|
227
326
|
render();
|
|
228
327
|
};
|
|
229
328
|
const finish = (value) => {
|
|
329
|
+
output.off("resize", onResize);
|
|
230
330
|
suspend();
|
|
231
331
|
input.pause();
|
|
232
332
|
resolve(value);
|
package/src/models.mjs
CHANGED
|
@@ -1,13 +1,199 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
1
4
|
import { V1 } from "./config.mjs";
|
|
2
5
|
import { style, padDisplay, divider, sanitizeTerminalText } from "./ansi.mjs";
|
|
3
6
|
import { t } from "./i18n.mjs";
|
|
4
7
|
|
|
8
|
+
export const CORE_MODEL_PROVIDERS = {
|
|
9
|
+
"deepseek-ai/DeepSeek-V4.1-Flash": [
|
|
10
|
+
{ id: "deepseek", name: "DeepSeek (Official)" },
|
|
11
|
+
{ id: "together", name: "Together AI" },
|
|
12
|
+
{ id: "fireworks", name: "Fireworks AI" },
|
|
13
|
+
{ id: "siliconflow", name: "SiliconFlow" },
|
|
14
|
+
],
|
|
15
|
+
"claude-sonnet-5": [
|
|
16
|
+
{ id: "anthropic", name: "Anthropic (Official)" },
|
|
17
|
+
{ id: "aws_bedrock", name: "AWS Bedrock" },
|
|
18
|
+
{ id: "gcp_vertex", name: "Google Cloud Vertex AI" },
|
|
19
|
+
],
|
|
20
|
+
"claude-opus-5-5": [
|
|
21
|
+
{ id: "anthropic", name: "Anthropic (Official)" },
|
|
22
|
+
{ id: "aws_bedrock", name: "AWS Bedrock" },
|
|
23
|
+
{ id: "gcp_vertex", name: "Google Cloud Vertex AI" },
|
|
24
|
+
],
|
|
25
|
+
"gpt-6-astra": [
|
|
26
|
+
{ id: "openai", name: "OpenAI (Official)" },
|
|
27
|
+
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
28
|
+
],
|
|
29
|
+
"gpt-6-sol": [
|
|
30
|
+
{ id: "openai", name: "OpenAI (Official)" },
|
|
31
|
+
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
32
|
+
],
|
|
33
|
+
"gpt-6-luna": [
|
|
34
|
+
{ id: "openai", name: "OpenAI (Official)" },
|
|
35
|
+
{ id: "azure", name: "Microsoft Azure OpenAI" },
|
|
36
|
+
],
|
|
37
|
+
"gemini-3.8-flash": [
|
|
38
|
+
{ id: "google", name: "Google AI Studio (Official)" },
|
|
39
|
+
{ id: "vertex", name: "Google Cloud Vertex AI" },
|
|
40
|
+
],
|
|
41
|
+
"qwen3.8-max": [
|
|
42
|
+
{ id: "alibaba", name: "Alibaba DashScope (Official)" },
|
|
43
|
+
{ id: "together", name: "Together AI" },
|
|
44
|
+
{ id: "siliconflow", name: "SiliconFlow" },
|
|
45
|
+
],
|
|
46
|
+
"glm-5.3-flash": [
|
|
47
|
+
{ id: "zhipu", name: "Zhipu AI BigModel (Official)" },
|
|
48
|
+
{ id: "modelark", name: "ByteDance ModelArk" },
|
|
49
|
+
],
|
|
50
|
+
"kimi-k3": [
|
|
51
|
+
{ id: "moonshot", name: "Moonshot AI (Official)" },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const CORE_MODEL_DEFAULTS = [
|
|
56
|
+
{
|
|
57
|
+
id: "deepseek-ai/DeepSeek-V4.1-Flash",
|
|
58
|
+
display_name: "DeepSeek V4.1 Flash (Ultra-fast coding)",
|
|
59
|
+
maker: "DeepSeek",
|
|
60
|
+
context_length: 131072,
|
|
61
|
+
pricing: { prompt: "0.00000014", completion: "0.00000028" },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "claude-sonnet-5",
|
|
65
|
+
display_name: "Claude Sonnet 5 (Anthropic flagship coding)",
|
|
66
|
+
maker: "Anthropic",
|
|
67
|
+
context_length: 200000,
|
|
68
|
+
pricing: { prompt: "0.000003", completion: "0.000015" },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "claude-opus-5-5",
|
|
72
|
+
display_name: "Claude Opus 5.5 (Deep reasoning & architecture)",
|
|
73
|
+
maker: "Anthropic",
|
|
74
|
+
context_length: 200000,
|
|
75
|
+
pricing: { prompt: "0.000005", completion: "0.000025" },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "gpt-6-astra",
|
|
79
|
+
display_name: "GPT-6 Astra (OpenAI flagship reasoning)",
|
|
80
|
+
maker: "OpenAI",
|
|
81
|
+
context_length: 128000,
|
|
82
|
+
pricing: { prompt: "0.0000025", completion: "0.00001" },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "gpt-6-sol",
|
|
86
|
+
display_name: "GPT-6 Sol (Fast coding & logic)",
|
|
87
|
+
maker: "OpenAI",
|
|
88
|
+
context_length: 128000,
|
|
89
|
+
pricing: { prompt: "0.000001", completion: "0.000004" },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "gpt-6-luna",
|
|
93
|
+
display_name: "GPT-6 Luna (Lightweight coding)",
|
|
94
|
+
maker: "OpenAI",
|
|
95
|
+
context_length: 128000,
|
|
96
|
+
pricing: { prompt: "0.0000005", completion: "0.000002" },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "gemini-3.8-flash",
|
|
100
|
+
display_name: "Gemini 3.8 Flash (1M context fast multimodal)",
|
|
101
|
+
maker: "Google",
|
|
102
|
+
context_length: 1048576,
|
|
103
|
+
pricing: { prompt: "0.00000035", completion: "0.0000014" },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "qwen3.8-max",
|
|
107
|
+
display_name: "Qwen 3.8 Max (Alibaba flagship coding)",
|
|
108
|
+
maker: "Alibaba",
|
|
109
|
+
context_length: 131072,
|
|
110
|
+
pricing: { prompt: "0.0000016", completion: "0.0000064" },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "glm-5.3-flash",
|
|
114
|
+
display_name: "GLM 5.3 Flash (Zhipu AI high-speed coding)",
|
|
115
|
+
maker: "Zhipu AI",
|
|
116
|
+
context_length: 128000,
|
|
117
|
+
pricing: { prompt: "0.0000001", completion: "0.0000002" },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "kimi-k3",
|
|
121
|
+
display_name: "Kimi K3 (Moonshot 256K long-context coding)",
|
|
122
|
+
maker: "Moonshot",
|
|
123
|
+
context_length: 262144,
|
|
124
|
+
pricing: { prompt: "0.0000006", completion: "0.0000024" },
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
let memoryModelsCache = null;
|
|
129
|
+
let memoryCacheExpiry = 0;
|
|
130
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
131
|
+
|
|
132
|
+
function getCacheFilePath() {
|
|
133
|
+
return join(homedir(), ".pleum", "models-cache.json");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function fetchCoreCodingModels() {
|
|
137
|
+
const cacheFile = getCacheFilePath();
|
|
138
|
+
let liveList = memoryModelsCache;
|
|
139
|
+
if (!liveList) {
|
|
140
|
+
try {
|
|
141
|
+
liveList = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
const byId = new Map((liveList || []).map((m) => [m.id, m]));
|
|
145
|
+
return CORE_MODEL_DEFAULTS.map((def) => {
|
|
146
|
+
const live = byId.get(def.id);
|
|
147
|
+
return live ? { ...def, ...live } : { ...def };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
5
151
|
// GET /v1/models 는 공개(인증 불필요). 슬러그는 provider/model 형식.
|
|
6
|
-
export async function fetchModels() {
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
9
|
-
|
|
10
|
-
|
|
152
|
+
export async function fetchModels({ forceRefresh = false } = {}) {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
if (!forceRefresh && memoryModelsCache && now < memoryCacheExpiry) {
|
|
155
|
+
return memoryModelsCache;
|
|
156
|
+
}
|
|
157
|
+
const cacheFile = getCacheFilePath();
|
|
158
|
+
if (!forceRefresh && !memoryModelsCache) {
|
|
159
|
+
try {
|
|
160
|
+
const stats = statSync(cacheFile);
|
|
161
|
+
if (now - stats.mtimeMs < CACHE_TTL_MS) {
|
|
162
|
+
const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
163
|
+
if (Array.isArray(cached) && cached.length > 0) {
|
|
164
|
+
memoryModelsCache = cached;
|
|
165
|
+
memoryCacheExpiry = stats.mtimeMs + CACHE_TTL_MS;
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`${V1}/models`);
|
|
174
|
+
if (!res.ok) throw new Error(t("Failed to fetch model list ({status})", { status: res.status }));
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
const list = data.data ?? [];
|
|
177
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
178
|
+
memoryModelsCache = list;
|
|
179
|
+
memoryCacheExpiry = now + CACHE_TTL_MS;
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(join(homedir(), ".pleum"), { recursive: true, mode: 0o700 });
|
|
182
|
+
writeFileSync(cacheFile, JSON.stringify(list), { mode: 0o600 });
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
return list;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
try {
|
|
188
|
+
const cached = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
189
|
+
if (Array.isArray(cached) && cached.length > 0) {
|
|
190
|
+
memoryModelsCache = cached;
|
|
191
|
+
memoryCacheExpiry = now + 60 * 1000;
|
|
192
|
+
return cached;
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
11
197
|
}
|
|
12
198
|
|
|
13
199
|
// id/표시명 부분일치 필터 — run 피커·REPL /models·목록이 공유하는 단일 구현.
|
package/src/recipes.mjs
CHANGED
|
@@ -7,12 +7,12 @@ export const CORE_MODELS = [
|
|
|
7
7
|
"claude-sonnet-5",
|
|
8
8
|
"claude-opus-5-5",
|
|
9
9
|
"gpt-6-astra",
|
|
10
|
-
"
|
|
10
|
+
"gpt-6-sol",
|
|
11
|
+
"gpt-6-luna",
|
|
11
12
|
"gemini-3.8-flash",
|
|
13
|
+
"qwen3.8-max",
|
|
12
14
|
"glm-5.3-flash",
|
|
13
15
|
"kimi-k3",
|
|
14
|
-
"gpt-6-sol",
|
|
15
|
-
"gpt-6-luna",
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
@@ -48,6 +48,8 @@ export const RECIPES = {
|
|
|
48
48
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
49
49
|
// /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
|
|
50
50
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
51
|
+
ANTHROPIC_SMALL_MODEL: "anthropic/deepseek-ai/DeepSeek-V4.1-Flash",
|
|
52
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/deepseek-ai/DeepSeek-V4.1-Flash",
|
|
51
53
|
}),
|
|
52
54
|
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
|
|
53
55
|
},
|
|
@@ -63,6 +65,55 @@ export const RECIPES = {
|
|
|
63
65
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
64
66
|
args: () => [],
|
|
65
67
|
},
|
|
68
|
+
agy: {
|
|
69
|
+
label: "Antigravity CLI",
|
|
70
|
+
bin: "agy",
|
|
71
|
+
type: "agy-settings",
|
|
72
|
+
install: "https://antigravity.google/cli/install.sh",
|
|
73
|
+
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.",
|
|
74
|
+
attribution: {
|
|
75
|
+
title: "Antigravity CLI",
|
|
76
|
+
referer: "https://www.antigravity.google/docs/cli/install",
|
|
77
|
+
},
|
|
78
|
+
env: (key) => ({
|
|
79
|
+
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
80
|
+
GEMINI_API_KEY: key,
|
|
81
|
+
GOOGLE_API_KEY: key,
|
|
82
|
+
}),
|
|
83
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
84
|
+
},
|
|
85
|
+
opencode: {
|
|
86
|
+
label: "opencode",
|
|
87
|
+
bin: "opencode",
|
|
88
|
+
type: "env",
|
|
89
|
+
install: "npm i -g opencode-ai@latest",
|
|
90
|
+
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다 — env var는 없다.
|
|
91
|
+
// 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
|
|
92
|
+
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
93
|
+
attribution: {
|
|
94
|
+
title: "opencode",
|
|
95
|
+
referer: "https://opencode.ai",
|
|
96
|
+
},
|
|
97
|
+
env: (key, model) => {
|
|
98
|
+
const active = model || DEFAULT_MODEL;
|
|
99
|
+
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
100
|
+
models[active] = {};
|
|
101
|
+
return {
|
|
102
|
+
PLEUM_API_KEY: key,
|
|
103
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
104
|
+
provider: {
|
|
105
|
+
pleum: {
|
|
106
|
+
npm: "@ai-sdk/openai-compatible",
|
|
107
|
+
name: "PleumRouter",
|
|
108
|
+
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
109
|
+
models,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
116
|
+
},
|
|
66
117
|
aider: {
|
|
67
118
|
label: "Aider",
|
|
68
119
|
bin: "aider",
|
|
@@ -101,38 +152,6 @@ export const RECIPES = {
|
|
|
101
152
|
env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
|
|
102
153
|
args: () => [],
|
|
103
154
|
},
|
|
104
|
-
opencode: {
|
|
105
|
-
label: "opencode",
|
|
106
|
-
bin: "opencode",
|
|
107
|
-
type: "env",
|
|
108
|
-
install: "npm i -g opencode-ai@latest",
|
|
109
|
-
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다 — env var는 없다.
|
|
110
|
-
// 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
|
|
111
|
-
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
112
|
-
attribution: {
|
|
113
|
-
title: "opencode",
|
|
114
|
-
referer: "https://opencode.ai",
|
|
115
|
-
},
|
|
116
|
-
env: (key, model) => {
|
|
117
|
-
const active = model || DEFAULT_MODEL;
|
|
118
|
-
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
119
|
-
models[active] = {};
|
|
120
|
-
return {
|
|
121
|
-
PLEUM_API_KEY: key,
|
|
122
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
123
|
-
provider: {
|
|
124
|
-
pleum: {
|
|
125
|
-
npm: "@ai-sdk/openai-compatible",
|
|
126
|
-
name: "PleumRouter",
|
|
127
|
-
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
128
|
-
models,
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
135
|
-
},
|
|
136
155
|
omp: {
|
|
137
156
|
label: "omp (Oh My Pi)",
|
|
138
157
|
bin: "omp",
|
|
@@ -211,23 +230,6 @@ export const RECIPES = {
|
|
|
211
230
|
}),
|
|
212
231
|
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
213
232
|
},
|
|
214
|
-
agy: {
|
|
215
|
-
label: "Antigravity CLI",
|
|
216
|
-
bin: "agy",
|
|
217
|
-
type: "agy-settings",
|
|
218
|
-
install: "https://antigravity.google/cli/install.sh",
|
|
219
|
-
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. GEMINI_API_KEY alone is ignored. Gemini-family models only.",
|
|
220
|
-
attribution: {
|
|
221
|
-
title: "Antigravity CLI",
|
|
222
|
-
referer: "https://www.antigravity.google/docs/cli/install",
|
|
223
|
-
},
|
|
224
|
-
env: (key) => ({
|
|
225
|
-
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
226
|
-
GEMINI_API_KEY: key,
|
|
227
|
-
GOOGLE_API_KEY: key,
|
|
228
|
-
}),
|
|
229
|
-
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
230
|
-
},
|
|
231
233
|
pi: {
|
|
232
234
|
label: "Pi",
|
|
233
235
|
bin: "pi",
|