pleumrouter 0.5.29 → 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/health.mjs +26 -13
- package/src/interactive.mjs +35 -9
- package/src/locales/es.mjs +12 -3
- package/src/locales/ja.mjs +12 -3
- package/src/locales/ko.mjs +12 -3
- package/src/locales/zh.mjs +12 -3
- 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/health.mjs
CHANGED
|
@@ -9,6 +9,10 @@ const ok = (s) => `${style.green("●")} ${s}`;
|
|
|
9
9
|
const bad = (s) => `${style.red("●")} ${s}`;
|
|
10
10
|
const warn = (s) => `${style.yellow("●")} ${s}`;
|
|
11
11
|
|
|
12
|
+
// 백엔드는 "ok" 문자열. db는 "ok:supabase"처럼 백엔드 힌트가 붙을 수 있다 — 정상으로 본다
|
|
13
|
+
// (과거엔 "ok"만 인정해 정상 DB가 빨간 점으로 떴다).
|
|
14
|
+
export const isHealthy = (v) => v === true || v === "ok" || (typeof v === "string" && v.startsWith("ok:"));
|
|
15
|
+
|
|
12
16
|
// health 응답 + 로컬 상태 → 표시 문자열(순수 함수 — fixture 테스트 대상).
|
|
13
17
|
export function renderHealth({ root, health, error, ms, keyPrefix }, styler = style) {
|
|
14
18
|
const lines = [];
|
|
@@ -17,16 +21,16 @@ export function renderHealth({ root, health, error, ms, keyPrefix }, styler = st
|
|
|
17
21
|
if (error) {
|
|
18
22
|
lines.push(bad(t("Gateway connection failed: {error}", { error })));
|
|
19
23
|
} else {
|
|
20
|
-
const isOk =
|
|
24
|
+
const isOk = isHealthy;
|
|
21
25
|
lines.push((isOk(health.status) ? ok : warn)(`${t("Gateway")} ${health.status}${ms != null ? styler.dim(` (${ms}ms)`) : ""}`));
|
|
22
26
|
if (health.db != null) lines.push((isOk(health.db) ? ok : bad)(`DB ${isOk(health.db) ? t("healthy") : health.db}`));
|
|
23
27
|
if (health.redis != null) lines.push((isOk(health.redis) ? ok : bad)(`Redis ${isOk(health.redis) ? t("healthy") : health.redis}`));
|
|
24
28
|
if (health.providers_configured != null) lines.push(ok(t("{n} providers configured", { n: health.providers_configured })));
|
|
25
29
|
const fx = health.fx_rate;
|
|
26
30
|
if (fx != null) {
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
if (rate != null) lines.push(ok(t("FX rate USD/KRW {rate}
|
|
31
|
+
// {rate, applied_rate, source} — 출처(source)는 운영 관측용이라 화면엔 표시하지 않는다.
|
|
32
|
+
const rate = typeof fx === "object" ? fx.rate : fx;
|
|
33
|
+
if (rate != null) lines.push(ok(t("FX rate USD/KRW {rate}", { rate })));
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
lines.push(
|
|
@@ -37,6 +41,23 @@ export function renderHealth({ root, health, error, ms, keyPrefix }, styler = st
|
|
|
37
41
|
return lines.join("\n");
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
// 게이트웨이 응답 대기 상한. 없으면 서버·네트워크가 멈췄을 때 끝없이(79초 실측) 기다린다.
|
|
45
|
+
export const HEALTH_TIMEOUT_MS = 15_000;
|
|
46
|
+
|
|
47
|
+
// /v1/health 1회 조회 → {data, error}. 예외를 던지지 않는다(테스트 대상).
|
|
48
|
+
export async function fetchHealth(url, timeoutMs = HEALTH_TIMEOUT_MS) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
51
|
+
if (!res.ok) return { data: null, error: `HTTP ${res.status}` };
|
|
52
|
+
return { data: await res.json(), error: null };
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e?.name === "TimeoutError" || e?.name === "AbortError") {
|
|
55
|
+
return { data: null, error: t("No response within {s}s", { s: Math.ceil(timeoutMs / 1000) }) };
|
|
56
|
+
}
|
|
57
|
+
return { data: null, error: e.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
export async function health({ exitOnError = true } = {}) {
|
|
41
62
|
const cfg = readConfig();
|
|
42
63
|
const keyPrefix = isLoggedIn(cfg)
|
|
@@ -45,15 +66,7 @@ export async function health({ exitOnError = true } = {}) {
|
|
|
45
66
|
? cfg.key.slice(0, 8)
|
|
46
67
|
: null;
|
|
47
68
|
const t0 = Date.now();
|
|
48
|
-
|
|
49
|
-
let error = null;
|
|
50
|
-
try {
|
|
51
|
-
const res = await fetch(`${V1}/health`);
|
|
52
|
-
if (!res.ok) error = `HTTP ${res.status}`;
|
|
53
|
-
else data = await res.json();
|
|
54
|
-
} catch (e) {
|
|
55
|
-
error = e.message;
|
|
56
|
-
}
|
|
69
|
+
const { data, error } = await fetchHealth(`${V1}/health`);
|
|
57
70
|
const ms = Date.now() - t0;
|
|
58
71
|
console.log(renderHealth({ root: ROOT, health: data, error, ms, keyPrefix }));
|
|
59
72
|
if (error && exitOnError) process.exit(1);
|
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
|
@@ -53,7 +53,7 @@ export const ES = {
|
|
|
53
53
|
"Error:": "Error:",
|
|
54
54
|
"Example: pleum launch claude --model claude-sonnet-4": "Ejemplo: pleum launch claude --model claude-sonnet-4",
|
|
55
55
|
"Examples:": "Ejemplos:",
|
|
56
|
-
"FX rate USD/KRW {rate}
|
|
56
|
+
"FX rate USD/KRW {rate}": "Tipo de cambio USD/KRW {rate}",
|
|
57
57
|
"Failed to check balance (HTTP {status})": "No se pudo verificar el saldo (HTTP {status})",
|
|
58
58
|
"Failed to fetch leaderboard ({status})": "No se pudo obtener el ranking ({status})",
|
|
59
59
|
"Failed to fetch model detail ({status})": "No se pudo obtener el detalle del modelo ({status})",
|
|
@@ -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.":
|
|
@@ -80,6 +88,7 @@ export const ES = {
|
|
|
80
88
|
"Aliases: {list}": "Alias: {list}",
|
|
81
89
|
"Gateway": "Gateway",
|
|
82
90
|
"Gateway connection failed: {error}": "Error de conexión al gateway: {error}",
|
|
91
|
+
"No response within {s}s": "Sin respuesta en {s} s",
|
|
83
92
|
"Gateway connectivity · login status": "Conectividad del gateway · estado de la sesión",
|
|
84
93
|
"Goodbye.": "Adiós.",
|
|
85
94
|
"Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose define su modelo en su propia configuración — usa el formato openai/<id> en `goose configure`.",
|
package/src/locales/ja.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export const JA = {
|
|
|
53
53
|
"Error:": "エラー:",
|
|
54
54
|
"Example: pleum launch claude --model claude-sonnet-4": "例: pleum launch claude --model claude-sonnet-4",
|
|
55
55
|
"Examples:": "例:",
|
|
56
|
-
"FX rate USD/KRW {rate}
|
|
56
|
+
"FX rate USD/KRW {rate}": "為替レート USD/KRW {rate}",
|
|
57
57
|
"Failed to check balance (HTTP {status})": "残高確認に失敗しました (HTTP {status})",
|
|
58
58
|
"Failed to fetch leaderboard ({status})": "リーダーボードの取得に失敗しました ({status})",
|
|
59
59
|
"Failed to fetch model detail ({status})": "モデル詳細の取得に失敗しました ({status})",
|
|
@@ -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.":
|
|
@@ -81,6 +89,7 @@ export const JA = {
|
|
|
81
89
|
"Aliases: {list}": "エイリアス: {list}",
|
|
82
90
|
"Gateway": "ゲートウェイ",
|
|
83
91
|
"Gateway connection failed: {error}": "ゲートウェイ接続に失敗しました: {error}",
|
|
92
|
+
"No response within {s}s": "{s}秒以内に応答がありません",
|
|
84
93
|
"Gateway connectivity · login status": "ゲートウェイ接続 · ログイン状態",
|
|
85
94
|
"Goodbye.": "さようなら。",
|
|
86
95
|
"Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose は自身の設定でモデルを指定します — `goose configure` で openai/<id> 形式を使用してください。",
|
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.":
|
|
@@ -120,10 +128,11 @@ export const KO = {
|
|
|
120
128
|
|
|
121
129
|
// health.mjs
|
|
122
130
|
"Gateway connection failed: {error}": "게이트웨이 연결 실패: {error}",
|
|
131
|
+
"No response within {s}s": "{s}초 안에 응답 없음",
|
|
123
132
|
"Gateway": "게이트웨이",
|
|
124
133
|
"healthy": "정상",
|
|
125
134
|
"{n} providers configured": "providers {n}개 구성됨",
|
|
126
|
-
"FX rate USD/KRW {rate}
|
|
135
|
+
"FX rate USD/KRW {rate}": "환율 USD/KRW {rate}",
|
|
127
136
|
"Logged in": "로그인됨",
|
|
128
137
|
"key": "키",
|
|
129
138
|
"Not logged in": "로그인 안 됨",
|
package/src/locales/zh.mjs
CHANGED
|
@@ -53,7 +53,7 @@ export const ZH = {
|
|
|
53
53
|
"Error:": "错误:",
|
|
54
54
|
"Example: pleum launch claude --model claude-sonnet-4": "示例:pleum launch claude --model claude-sonnet-4",
|
|
55
55
|
"Examples:": "示例:",
|
|
56
|
-
"FX rate USD/KRW {rate}
|
|
56
|
+
"FX rate USD/KRW {rate}": "汇率 USD/KRW {rate}",
|
|
57
57
|
"Failed to check balance (HTTP {status})": "查询余额失败(HTTP {status})",
|
|
58
58
|
"Failed to fetch leaderboard ({status})": "获取排行榜失败({status})",
|
|
59
59
|
"Failed to fetch model detail ({status})": "获取模型详情失败({status})",
|
|
@@ -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.":
|
|
@@ -80,6 +88,7 @@ export const ZH = {
|
|
|
80
88
|
"Aliases: {list}": "别名:{list}",
|
|
81
89
|
"Gateway": "网关",
|
|
82
90
|
"Gateway connection failed: {error}": "网关连接失败:{error}",
|
|
91
|
+
"No response within {s}s": "{s} 秒内无响应",
|
|
83
92
|
"Gateway connectivity · login status": "网关连通性 · 登录状态",
|
|
84
93
|
"Goodbye.": "再见。",
|
|
85
94
|
"Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose 在自己的配置中设置模型 — 在 `goose configure` 中使用 openai/<id> 格式。",
|
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",
|