pleumrouter 0.5.24 → 0.5.26
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/bin/pleum.mjs +6 -1
- package/package.json +1 -1
- package/src/config.mjs +69 -22
- package/src/health.mjs +2 -2
- package/src/interactive.mjs +2 -1
- package/src/launch.mjs +3 -3
- package/src/menu.mjs +55 -18
- package/src/recipes.mjs +53 -33
package/bin/pleum.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { top } from "../src/top.mjs";
|
|
|
11
11
|
import { health } from "../src/health.mjs";
|
|
12
12
|
import { completion } from "../src/completion.mjs";
|
|
13
13
|
import { upgrade } from "../src/upgrade.mjs";
|
|
14
|
-
import { mainMenu } from "../src/menu.mjs";
|
|
14
|
+
import { mainMenu, runAgentMenu } from "../src/menu.mjs";
|
|
15
15
|
import { lang } from "../src/lang.mjs";
|
|
16
16
|
import { RECIPES } from "../src/recipes.mjs";
|
|
17
17
|
import { t } from "../src/i18n.mjs";
|
|
@@ -98,6 +98,11 @@ try {
|
|
|
98
98
|
case "launch": {
|
|
99
99
|
const tool = rest[0];
|
|
100
100
|
if (!tool) {
|
|
101
|
+
const io = process.stdin;
|
|
102
|
+
if (io.isTTY && process.stdout.isTTY && typeof io.setRawMode === "function") {
|
|
103
|
+
await runAgentMenu({ loop: false });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
101
106
|
launchHelp();
|
|
102
107
|
break;
|
|
103
108
|
}
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -62,15 +62,9 @@ export function clearCredentials(existing) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
* (아래 requireAuth 참조) 미로그인으로 취급한다 — 그래야 런처가 자동으로 재로그인을 태운다.
|
|
65
|
+
* JWT payload 파싱(서명 검증 없는 디코드) — iat, exp 등의 클레임 추출용.
|
|
67
66
|
*/
|
|
68
|
-
export function
|
|
69
|
-
return Boolean(cfg?.access_token);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** JWT payload `exp`(epoch sec) — 서명 검증 없이 refresh 트리거 힌트. */
|
|
73
|
-
export function readJwtExp(token) {
|
|
67
|
+
export function readJwtPayload(token) {
|
|
74
68
|
if (typeof token !== "string") return null;
|
|
75
69
|
const parts = token.split(".");
|
|
76
70
|
const segment = parts[1];
|
|
@@ -80,14 +74,32 @@ export function readJwtExp(token) {
|
|
|
80
74
|
const pad = b64.length % 4;
|
|
81
75
|
if (pad) b64 += "=".repeat(4 - pad);
|
|
82
76
|
const json = Buffer.from(b64, "base64").toString("utf8");
|
|
83
|
-
|
|
84
|
-
return typeof payload.exp === "number" ? payload.exp : null;
|
|
77
|
+
return JSON.parse(json);
|
|
85
78
|
} catch {
|
|
86
79
|
return null;
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
82
|
|
|
90
|
-
/**
|
|
83
|
+
/** JWT payload `exp`(epoch sec) */
|
|
84
|
+
export function readJwtExp(token) {
|
|
85
|
+
const payload = readJwtPayload(token);
|
|
86
|
+
return typeof payload?.exp === "number" ? payload.exp : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** JWT payload `iat`(epoch sec) */
|
|
90
|
+
export function readJwtIat(token) {
|
|
91
|
+
const payload = readJwtPayload(token);
|
|
92
|
+
return typeof payload?.iat === "number" ? payload.iat : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 토큰 만료 여부 판정 (skewSeconds 여유 포함, unparseable은 false 반환) */
|
|
96
|
+
export function isTokenExpired(token, skewSeconds = 0) {
|
|
97
|
+
const exp = readJwtExp(token);
|
|
98
|
+
if (exp == null) return false;
|
|
99
|
+
return exp * 1000 <= Date.now() + skewSeconds * 1000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** access가 곧 만료(기본 60s 여유)면 true. unparseable은 만료로 간주. */
|
|
91
103
|
export function accessTokenIsStale(accessToken, skewSeconds = 60) {
|
|
92
104
|
const exp = readJwtExp(accessToken);
|
|
93
105
|
if (exp == null) return true;
|
|
@@ -95,29 +107,59 @@ export function accessTokenIsStale(accessToken, skewSeconds = 60) {
|
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
/**
|
|
98
|
-
*
|
|
110
|
+
* 사용 가능한 세션 보유 여부.
|
|
111
|
+
* 1) access_token 또는 refresh_token이 없으면 미로그인.
|
|
112
|
+
* 2) refresh_token이 이미 7일 만료되었으면 미로그인.
|
|
113
|
+
* 3) 레거시 `key`(plm_)만 있는 상태는 requireAuth()가 거부하므로 미로그인.
|
|
114
|
+
*/
|
|
115
|
+
export function isLoggedIn(cfg = readConfig()) {
|
|
116
|
+
if (!cfg?.access_token || !cfg?.refresh_token) return false;
|
|
117
|
+
if (isTokenExpired(cfg.refresh_token)) return false;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 7일 슬라이딩 세션 갱신 (1시간 스로틀링).
|
|
123
|
+
* CLI 접속 시 마지막 갱신(iat)으로부터 minIntervalSeconds(기본 3600초=1시간) 이상 지났거나
|
|
124
|
+
* access_token이 곧 만료(stale)되면 /v1/auth/refresh를 호출해 만료일을 현재+7일로 연장한다.
|
|
125
|
+
*/
|
|
126
|
+
export async function touchSession(cfg = readConfig(), { minIntervalSeconds = 3600 } = {}) {
|
|
127
|
+
if (!isLoggedIn(cfg)) return null;
|
|
128
|
+
const iat = readJwtIat(cfg.refresh_token) ?? readJwtIat(cfg.access_token);
|
|
129
|
+
const ageSeconds = iat ? Math.max(0, Math.floor(Date.now() / 1000 - iat)) : Infinity;
|
|
130
|
+
const isStale = accessTokenIsStale(cfg.access_token, 300);
|
|
131
|
+
if (!isStale && ageSeconds < minIntervalSeconds) {
|
|
132
|
+
return cfg.access_token;
|
|
133
|
+
}
|
|
134
|
+
return await refreshAccessToken(cfg);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 유효한 access_token을 반환. stale이거나 1시간 경과 시 refresh하여 슬라이딩 윈도우 유지.
|
|
139
|
+
* 레거시 `key`(plm_)만 있으면 재로그인 안내.
|
|
99
140
|
*/
|
|
100
141
|
export async function requireAuth() {
|
|
101
|
-
|
|
142
|
+
let cfg = readConfig();
|
|
102
143
|
if (cfg?.key && !cfg?.access_token) {
|
|
103
144
|
console.error(
|
|
104
145
|
t("Legacy API key login is no longer supported. Run:\n pleum login"),
|
|
105
146
|
);
|
|
106
147
|
process.exit(1);
|
|
107
148
|
}
|
|
108
|
-
if (!cfg
|
|
149
|
+
if (!isLoggedIn(cfg)) {
|
|
109
150
|
console.error(t("Login required. Run this first:\n pleum login"));
|
|
110
151
|
process.exit(1);
|
|
111
152
|
}
|
|
112
|
-
|
|
153
|
+
const token = await touchSession(cfg);
|
|
154
|
+
if (token) return token;
|
|
155
|
+
|
|
156
|
+
cfg = readConfig();
|
|
157
|
+
if (cfg?.access_token && !accessTokenIsStale(cfg.access_token, 0)) {
|
|
113
158
|
return cfg.access_token;
|
|
114
159
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
return refreshed;
|
|
160
|
+
|
|
161
|
+
console.error(t("Session expired. Run:\n pleum login"));
|
|
162
|
+
process.exit(1);
|
|
121
163
|
}
|
|
122
164
|
|
|
123
165
|
export async function refreshAccessToken(cfg = readConfig()) {
|
|
@@ -129,7 +171,12 @@ export async function refreshAccessToken(cfg = readConfig()) {
|
|
|
129
171
|
headers: { "Content-Type": "application/json" },
|
|
130
172
|
body: JSON.stringify({ refresh_token: cfg.refresh_token }),
|
|
131
173
|
});
|
|
132
|
-
if (!res.ok)
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
if (res.status === 401 || res.status === 403) {
|
|
176
|
+
writeConfig(clearCredentials(cfg));
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
133
180
|
const data = await res.json();
|
|
134
181
|
if (!data.access_token || !data.refresh_token) return null;
|
|
135
182
|
const exp = readJwtExp(data.access_token);
|
package/src/health.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `pleum health` — 게이트웨이 연결성 + 로그인 상태 한눈에.
|
|
2
2
|
// 셀프호스트/스테이징(PLEUM_BASE_URL) 점검·고객지원 트리아지용. 공개 /v1/health 재사용.
|
|
3
3
|
|
|
4
|
-
import { ROOT, V1, readConfig } from "./config.mjs";
|
|
4
|
+
import { ROOT, V1, readConfig, isLoggedIn } from "./config.mjs";
|
|
5
5
|
import { style, divider } from "./ansi.mjs";
|
|
6
6
|
import { t } from "./i18n.mjs";
|
|
7
7
|
|
|
@@ -39,7 +39,7 @@ export function renderHealth({ root, health, error, ms, keyPrefix }, styler = st
|
|
|
39
39
|
|
|
40
40
|
export async function health() {
|
|
41
41
|
const cfg = readConfig();
|
|
42
|
-
const keyPrefix = cfg
|
|
42
|
+
const keyPrefix = isLoggedIn(cfg)
|
|
43
43
|
? "oauth"
|
|
44
44
|
: cfg?.key
|
|
45
45
|
? cfg.key.slice(0, 8)
|
package/src/interactive.mjs
CHANGED
|
@@ -100,6 +100,7 @@ export async function select(items, {
|
|
|
100
100
|
formatItem = String,
|
|
101
101
|
matches = (all, q) => (q ? all.filter((it) => String(formatItem(it)).toLowerCase().includes(q.toLowerCase())) : all),
|
|
102
102
|
limit = 10,
|
|
103
|
+
initialCursor = 0,
|
|
103
104
|
input = process.stdin,
|
|
104
105
|
output = process.stdout,
|
|
105
106
|
} = {}) {
|
|
@@ -107,7 +108,7 @@ export async function select(items, {
|
|
|
107
108
|
|
|
108
109
|
return new Promise((resolve) => {
|
|
109
110
|
let query = "";
|
|
110
|
-
let cursor = 0;
|
|
111
|
+
let cursor = Math.max(0, Math.min(initialCursor, Math.max(0, (items?.length ?? 1) - 1)));
|
|
111
112
|
let lastRows = 0;
|
|
112
113
|
|
|
113
114
|
const rowsFor = (s) => Math.max(1, Math.ceil(displayWidth(s) / Math.max(terminalWidth(), 1)));
|
package/src/launch.mjs
CHANGED
|
@@ -126,7 +126,7 @@ export async function launch(tool, model, passthrough, { exit = true } = {}) {
|
|
|
126
126
|
if (r.type === "crush-home") {
|
|
127
127
|
env.CRUSH_GLOBAL_CONFIG = writeIsolatedHome("crush-home", "crush.json", crushJson(model));
|
|
128
128
|
}
|
|
129
|
-
if (r.type === "agy-settings") ensureAgySettings();
|
|
129
|
+
if (r.type === "agy-settings") ensureAgySettings(model);
|
|
130
130
|
|
|
131
131
|
const args = [...r.args(model), ...passthrough];
|
|
132
132
|
if (r.note) console.log(`ℹ ${t(r.note)}`);
|
|
@@ -171,12 +171,12 @@ function writeIsolatedHome(name, filename, contents) {
|
|
|
171
171
|
return home;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
function ensureAgySettings() {
|
|
174
|
+
function ensureAgySettings(model) {
|
|
175
175
|
const dir = join(homedir(), ".gemini", "antigravity-cli");
|
|
176
176
|
const path = join(dir, "settings.json");
|
|
177
177
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
178
178
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
179
|
-
writeFileSync(path, `${JSON.stringify(mergeAgySettings(existing), null, 2)}\n`, { mode: 0o600 });
|
|
179
|
+
writeFileSync(path, `${JSON.stringify(mergeAgySettings(existing, model), null, 2)}\n`, { mode: 0o600 });
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function printManual(tool, key) {
|
package/src/menu.mjs
CHANGED
|
@@ -13,8 +13,8 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import { createInterface } from "node:readline";
|
|
14
14
|
import { style, displayWidth, terminalWidth, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
|
|
15
15
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
16
|
-
import { readConfig, writeConfig, isLoggedIn } from "./config.mjs";
|
|
17
|
-
import { RECIPES, MANUAL } from "./recipes.mjs";
|
|
16
|
+
import { readConfig, writeConfig, isLoggedIn, touchSession } from "./config.mjs";
|
|
17
|
+
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
18
18
|
import { onPath, launch } from "./launch.mjs";
|
|
19
19
|
import { fetchModels, matchModels, listModels, formatModelLabel } from "./models.mjs";
|
|
20
20
|
import { run } from "./run.mjs";
|
|
@@ -120,7 +120,7 @@ function saveLastSelection(id) {
|
|
|
120
120
|
writeConfig(cfg);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
async function pickModelFor(item) {
|
|
123
|
+
export async function pickModelFor(item, defaultModel) {
|
|
124
124
|
let models;
|
|
125
125
|
try {
|
|
126
126
|
models = await fetchModels();
|
|
@@ -128,11 +128,14 @@ async function pickModelFor(item) {
|
|
|
128
128
|
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
129
129
|
return undefined;
|
|
130
130
|
}
|
|
131
|
+
const targetId = defaultModel || DEFAULT_MODEL;
|
|
132
|
+
const initialIndex = models.findIndex((x) => x.id === targetId);
|
|
131
133
|
const m = await select(models, {
|
|
132
134
|
message: `${item.title} — ${t("select a model")}`,
|
|
133
135
|
formatItem: (x) => formatModelLabel(x),
|
|
134
136
|
matches: matchModels,
|
|
135
|
-
limit:
|
|
137
|
+
limit: 10,
|
|
138
|
+
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
136
139
|
});
|
|
137
140
|
return m?.id;
|
|
138
141
|
}
|
|
@@ -229,7 +232,8 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
229
232
|
// 선택되면 ollama처럼 즉시 실행으로 이어가고, 취소면 메뉴 복귀.
|
|
230
233
|
const configure = async (it) => {
|
|
231
234
|
suspend();
|
|
232
|
-
const
|
|
235
|
+
const currentModel = it.tool ? (readConfig()?.launch_models?.[it.tool] || DEFAULT_MODEL) : undefined;
|
|
236
|
+
const picked = await pickModelFor(it, currentModel);
|
|
233
237
|
if (picked) {
|
|
234
238
|
input.pause();
|
|
235
239
|
resolve({ item: it, model: picked });
|
|
@@ -292,6 +296,48 @@ async function agentSelect() {
|
|
|
292
296
|
});
|
|
293
297
|
}
|
|
294
298
|
|
|
299
|
+
// 에이전트 실행 메뉴 — bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유.
|
|
300
|
+
// 에이전트를 고르면 곧바로 핵심 10개 모델 피커를 띄워 기본 모델(deepseek-v4.1-flash) 또는
|
|
301
|
+
// 선택한 우리 모델로 코딩 에이전트를 실행한다.
|
|
302
|
+
export async function runAgentMenu({ loop = false } = {}) {
|
|
303
|
+
const io = process.stdin;
|
|
304
|
+
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
305
|
+
if (!isLoggedIn()) await login();
|
|
306
|
+
else await touchSession().catch(() => {});
|
|
307
|
+
|
|
308
|
+
for (;;) {
|
|
309
|
+
const agentPicked = await agentSelect();
|
|
310
|
+
if (agentPicked === "quit") return "quit";
|
|
311
|
+
if (!agentPicked) return null;
|
|
312
|
+
|
|
313
|
+
const { item: agentItem } = agentPicked;
|
|
314
|
+
if (agentItem.manual) {
|
|
315
|
+
await launch(agentItem.tool, undefined, [], { exit: false });
|
|
316
|
+
if (!loop) return true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const r = RECIPES[agentItem.tool];
|
|
321
|
+
if (!onPath(r.bin) && !(await ensureInstalled(r))) {
|
|
322
|
+
if (!loop) return false;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const agentSaved = readConfig()?.launch_models ?? {};
|
|
327
|
+
let chosenModel = agentPicked.model;
|
|
328
|
+
if (!chosenModel) {
|
|
329
|
+
const defaultForTool = agentSaved[agentItem.tool] || DEFAULT_MODEL;
|
|
330
|
+
chosenModel = await pickModelFor(agentItem, defaultForTool);
|
|
331
|
+
if (!chosenModel) continue; // Esc 취소 시 에이전트 목록으로 복귀
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
saveModel(agentItem.tool, chosenModel);
|
|
335
|
+
await launch(agentItem.tool, chosenModel, [], { exit: false });
|
|
336
|
+
console.log();
|
|
337
|
+
if (!loop) return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
295
341
|
// bare `pleum` 진입점. 비TTY면 false(호출자가 HELP 출력), 그 외 true.
|
|
296
342
|
// ollama처럼 실행이 끝나면 메뉴로 복귀하는 루프(REPL 채팅·ctrl+c 종료 제외).
|
|
297
343
|
export async function mainMenu() {
|
|
@@ -303,6 +349,7 @@ export async function mainMenu() {
|
|
|
303
349
|
// login()의 실패(취소·2분 타임아웃·state 불일치)는 그대로 던져 bin/pleum.mjs의
|
|
304
350
|
// 최상위 catch가 `✗ <메시지>` + exit 1로 처리하게 둔다(메뉴를 띄우지 않는다).
|
|
305
351
|
if (!isLoggedIn()) await login();
|
|
352
|
+
else await touchSession().catch(() => {});
|
|
306
353
|
|
|
307
354
|
for (;;) {
|
|
308
355
|
const picked = await menuSelect();
|
|
@@ -341,19 +388,9 @@ export async function mainMenu() {
|
|
|
341
388
|
continue; // esc/q — 메인 메뉴로 복귀
|
|
342
389
|
}
|
|
343
390
|
case "agents": {
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
const { item: agentItem, model: agentModel } = agentPicked;
|
|
348
|
-
if (agentModel && !agentItem.manual) saveModel(agentItem.tool, agentModel);
|
|
349
|
-
if (!agentItem.manual) {
|
|
350
|
-
const r = RECIPES[agentItem.tool];
|
|
351
|
-
if (!onPath(r.bin) && !(await ensureInstalled(r))) continue;
|
|
352
|
-
}
|
|
353
|
-
const agentSaved = readConfig()?.launch_models ?? {};
|
|
354
|
-
await launch(agentItem.tool, agentModel ?? agentSaved[agentItem.tool], [], { exit: false });
|
|
355
|
-
console.log(); // 에이전트 출력과 다음 메뉴 사이 여백
|
|
356
|
-
continue; // ollama처럼 에이전트 종료 후 메뉴 복귀
|
|
391
|
+
const r = await runAgentMenu({ loop: true });
|
|
392
|
+
if (r === "quit") return true;
|
|
393
|
+
continue;
|
|
357
394
|
}
|
|
358
395
|
}
|
|
359
396
|
}
|
package/src/recipes.mjs
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { ROOT, V1 } from "./config.mjs";
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash";
|
|
4
|
+
|
|
5
|
+
export const CORE_MODELS = [
|
|
6
|
+
"deepseek-ai/DeepSeek-V4.1-Flash",
|
|
7
|
+
"claude-sonnet-5",
|
|
8
|
+
"claude-opus-5-5",
|
|
9
|
+
"gpt-6-astra",
|
|
10
|
+
"claude-fable-5-1",
|
|
11
|
+
"gemini-3.8-flash",
|
|
12
|
+
"glm-5.3-flash",
|
|
13
|
+
"kimi-k3",
|
|
14
|
+
"gpt-6-sol",
|
|
15
|
+
"gpt-6-luna",
|
|
16
|
+
];
|
|
17
|
+
|
|
3
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
4
19
|
// type "env" → 환경변수만 세팅하고 바로 실행.
|
|
5
20
|
// type "codex-home" → 격리 CODEX_HOME에 config.toml (사용자 ~/.codex 보존).
|
|
@@ -34,7 +49,7 @@ export const RECIPES = {
|
|
|
34
49
|
// /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
|
|
35
50
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
36
51
|
}),
|
|
37
|
-
args: (m) =>
|
|
52
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
|
|
38
53
|
},
|
|
39
54
|
codex: {
|
|
40
55
|
label: "Codex CLI",
|
|
@@ -58,7 +73,7 @@ export const RECIPES = {
|
|
|
58
73
|
referer: "https://aider.chat",
|
|
59
74
|
},
|
|
60
75
|
env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
|
|
61
|
-
args: (m) =>
|
|
76
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
|
|
62
77
|
},
|
|
63
78
|
goose: {
|
|
64
79
|
label: "Goose",
|
|
@@ -98,20 +113,25 @@ export const RECIPES = {
|
|
|
98
113
|
title: "opencode",
|
|
99
114
|
referer: "https://opencode.ai",
|
|
100
115
|
},
|
|
101
|
-
env: (key, model) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
},
|
|
110
130
|
},
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
args: (m) => ["--model", `pleum/${m ||
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
115
135
|
},
|
|
116
136
|
omp: {
|
|
117
137
|
label: "omp (Oh My Pi)",
|
|
@@ -126,7 +146,7 @@ export const RECIPES = {
|
|
|
126
146
|
referer: "https://omp.sh",
|
|
127
147
|
},
|
|
128
148
|
env: (key) => ({ ANTHROPIC_BASE_URL: ROOT, ANTHROPIC_API_KEY: key }),
|
|
129
|
-
args: (m) =>
|
|
149
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("anthropic/") ? (m || DEFAULT_MODEL) : `anthropic/${m || DEFAULT_MODEL}`],
|
|
130
150
|
},
|
|
131
151
|
hermes: {
|
|
132
152
|
label: "Hermes Agent",
|
|
@@ -150,7 +170,7 @@ export const RECIPES = {
|
|
|
150
170
|
title: "Qwen Code",
|
|
151
171
|
referer: "https://github.com/QwenLM/qwen-code",
|
|
152
172
|
},
|
|
153
|
-
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model ||
|
|
173
|
+
env: (key, model) => ({ OPENAI_BASE_URL: V1, OPENAI_API_KEY: key, OPENAI_MODEL: model || DEFAULT_MODEL }),
|
|
154
174
|
args: () => [],
|
|
155
175
|
},
|
|
156
176
|
copilot: {
|
|
@@ -167,7 +187,7 @@ export const RECIPES = {
|
|
|
167
187
|
COPILOT_PROVIDER_BASE_URL: V1,
|
|
168
188
|
COPILOT_PROVIDER_TYPE: "openai",
|
|
169
189
|
COPILOT_PROVIDER_API_KEY: key,
|
|
170
|
-
COPILOT_MODEL: model ||
|
|
190
|
+
COPILOT_MODEL: model || DEFAULT_MODEL,
|
|
171
191
|
}),
|
|
172
192
|
args: () => [],
|
|
173
193
|
},
|
|
@@ -189,7 +209,7 @@ export const RECIPES = {
|
|
|
189
209
|
GEMINI_API_KEY: key,
|
|
190
210
|
GOOGLE_API_KEY: key,
|
|
191
211
|
}),
|
|
192
|
-
args: (m) =>
|
|
212
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
193
213
|
},
|
|
194
214
|
agy: {
|
|
195
215
|
label: "Antigravity CLI",
|
|
@@ -206,7 +226,7 @@ export const RECIPES = {
|
|
|
206
226
|
GEMINI_API_KEY: key,
|
|
207
227
|
GOOGLE_API_KEY: key,
|
|
208
228
|
}),
|
|
209
|
-
args: (m) =>
|
|
229
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
210
230
|
},
|
|
211
231
|
pi: {
|
|
212
232
|
label: "Pi",
|
|
@@ -219,7 +239,7 @@ export const RECIPES = {
|
|
|
219
239
|
referer: "https://pi.dev",
|
|
220
240
|
},
|
|
221
241
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
222
|
-
args: (m) => ["--model", m ||
|
|
242
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
223
243
|
},
|
|
224
244
|
crush: {
|
|
225
245
|
label: "Crush",
|
|
@@ -246,9 +266,9 @@ export const RECIPES = {
|
|
|
246
266
|
env: (key, model) => ({
|
|
247
267
|
OPENAI_BASE_URL: V1,
|
|
248
268
|
OPENAI_API_KEY: key,
|
|
249
|
-
|
|
269
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
250
270
|
}),
|
|
251
|
-
args: (m) =>
|
|
271
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
252
272
|
},
|
|
253
273
|
mimo: {
|
|
254
274
|
label: "MiMo Code",
|
|
@@ -262,9 +282,9 @@ export const RECIPES = {
|
|
|
262
282
|
env: (key, model) => ({
|
|
263
283
|
OPENAI_BASE_URL: V1,
|
|
264
284
|
OPENAI_API_KEY: key,
|
|
265
|
-
|
|
285
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
266
286
|
}),
|
|
267
|
-
args: (m) =>
|
|
287
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
268
288
|
},
|
|
269
289
|
openclaw: {
|
|
270
290
|
label: "OpenClaw",
|
|
@@ -278,9 +298,9 @@ export const RECIPES = {
|
|
|
278
298
|
env: (key, model) => ({
|
|
279
299
|
OPENAI_BASE_URL: V1,
|
|
280
300
|
OPENAI_API_KEY: key,
|
|
281
|
-
|
|
301
|
+
OPENAI_MODEL: model || DEFAULT_MODEL,
|
|
282
302
|
}),
|
|
283
|
-
args: (m) =>
|
|
303
|
+
args: (m) => ["--model", m || DEFAULT_MODEL],
|
|
284
304
|
},
|
|
285
305
|
plandex: {
|
|
286
306
|
label: "Plandex",
|
|
@@ -310,7 +330,7 @@ export const RECIPES = {
|
|
|
310
330
|
OPENAI_API_BASE: V1,
|
|
311
331
|
OPENAI_API_KEY: key,
|
|
312
332
|
}),
|
|
313
|
-
args: (m) =>
|
|
333
|
+
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("openai/") ? (m || DEFAULT_MODEL) : `openai/${m || DEFAULT_MODEL}`],
|
|
314
334
|
},
|
|
315
335
|
};
|
|
316
336
|
|
|
@@ -357,7 +377,7 @@ export function tomlBasicString(value) {
|
|
|
357
377
|
}
|
|
358
378
|
|
|
359
379
|
export function codexToml(model) {
|
|
360
|
-
const m = model ||
|
|
380
|
+
const m = model || DEFAULT_MODEL;
|
|
361
381
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
362
382
|
model = ${tomlBasicString(m)}
|
|
363
383
|
model_provider = "pleum"
|
|
@@ -371,7 +391,7 @@ supports_websockets = false
|
|
|
371
391
|
`;
|
|
372
392
|
}
|
|
373
393
|
|
|
374
|
-
export function mergeAgySettings(existingJson) {
|
|
394
|
+
export function mergeAgySettings(existingJson, model) {
|
|
375
395
|
let obj = {};
|
|
376
396
|
if (existingJson && String(existingJson).trim()) {
|
|
377
397
|
obj = JSON.parse(existingJson);
|
|
@@ -379,11 +399,11 @@ export function mergeAgySettings(existingJson) {
|
|
|
379
399
|
throw new Error("agy settings.json must be a JSON object");
|
|
380
400
|
}
|
|
381
401
|
}
|
|
382
|
-
return { ...obj, modelProvider: "gemini" };
|
|
402
|
+
return { ...obj, modelProvider: "gemini", model: model || DEFAULT_MODEL };
|
|
383
403
|
}
|
|
384
404
|
|
|
385
405
|
export function piModelsJson(model) {
|
|
386
|
-
const m = model ||
|
|
406
|
+
const m = model || DEFAULT_MODEL;
|
|
387
407
|
return `${JSON.stringify({
|
|
388
408
|
providers: {
|
|
389
409
|
pleum: {
|
|
@@ -397,7 +417,7 @@ export function piModelsJson(model) {
|
|
|
397
417
|
}
|
|
398
418
|
|
|
399
419
|
export function crushJson(model) {
|
|
400
|
-
const m = model ||
|
|
420
|
+
const m = model || DEFAULT_MODEL;
|
|
401
421
|
return `${JSON.stringify({
|
|
402
422
|
providers: {
|
|
403
423
|
pleum: {
|