pleumrouter 0.5.36 → 0.5.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/launch.mjs +110 -6
- package/src/locales/es.mjs +15 -3
- package/src/locales/ja.mjs +15 -3
- package/src/locales/ko.mjs +15 -3
- package/src/locales/zh.mjs +15 -3
- package/src/menu.mjs +66 -156
- package/src/models.mjs +9 -133
- package/src/recipes.mjs +40 -42
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ bun install -g pleumrouter # bun
|
|
|
17
17
|
## 빠른 시작
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
pleum # Claude Code · Codex · Antigravity ·
|
|
20
|
+
pleum # 에이전트 실행 → Claude Code · Codex · Antigravity · OpenCode
|
|
21
21
|
pleum claude # Claude Code 바로 실행
|
|
22
22
|
pleum codex # Codex 바로 실행
|
|
23
23
|
pleum agy # Antigravity CLI 바로 실행
|
|
@@ -33,8 +33,8 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
|
33
33
|
|
|
34
34
|
| 명령 | 설명 |
|
|
35
35
|
|------|------|
|
|
36
|
-
| `pleum` | (인자 없음, TTY)
|
|
37
|
-
| `pleum claude\|codex\|agy\|opencode [--model id]` | 네 우선 지원 도구를 한 명령으로 실행. 최초 실행은 브라우저 로그인 후 에이전트 키 발급 |
|
|
36
|
+
| `pleum` | (인자 없음, TTY) 에이전트 실행 → Claude Code·Codex·Antigravity·OpenCode. 앞의 세 도구는 공식/기본 모델 또는 custom(beta)을 선택. 파이프에선 도움말 출력 |
|
|
37
|
+
| `pleum claude\|codex\|agy\|opencode [--model id]` | 네 우선 지원 도구를 한 명령으로 실행. Claude·Codex·agy는 모델 미지정 시 공식/기본 모드, `--model` 지정 시 custom 모드. 최초 실행은 브라우저 로그인 후 에이전트 키 발급 |
|
|
38
38
|
| `pleum login [--device]` | 브라우저 OAuth 로그인 → access/refresh 토큰을 `~/.pleum/config.json`에 저장(API 키 미발급). `--device`는 SSH·CI |
|
|
39
39
|
| `pleum logout` | 서버 세션 revoke + 로컬 토큰 삭제(언어·기본 모델 등 나머지 설정은 유지) |
|
|
40
40
|
| `pleum launch [tool] [--model id] [-- args]` | 에이전트 실행. `--` 뒤 인자는 도구로 그대로 전달. 도구 생략 시 지원 목록·설치 여부 표시 |
|
|
@@ -81,7 +81,9 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
|
|
|
81
81
|
env·실행 인자 또는 일부 도구의 격리 홈(`PI_CODING_AGENT_DIR` / `CRUSH_GLOBAL_CONFIG`)으로 연결한다. Codex는 `-c` 임시 설정을 써서 기존 `~/.codex`의 세션과 설정을 유지한다.
|
|
82
82
|
별칭: `antigravity`→`agy`, `picode`→`pi`, `ohmypi`/`oh-my-pi`→`omp`, `qwencode`/`qwen-code`→`qwen`, `minimax-code`/`mcode`→`minimax`, `mimocode`/`mimo-code`→`mimo`, `open-interpreter`→`interpreter`.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
공식/기본 모드에서도 요청은 PleumRouter를 거쳐 Pleum API 크레딧으로 정산된다. Claude custom은 주 대화 모델만 바꾸며 일반 서브에이전트는 Pleum의 Sonnet으로 실행한다(Claude의 fork·`model: inherit` skill은 주 대화 모델을 상속할 수 있다). OpenCode 실행 세션의 `/models`에는 활성 Pleum 텍스트 대화 모델만 표시하며 추천 10개를 즐겨찾기 상단에 둔다. 기존 즐겨찾기와 최근 기록은 보존한다.
|
|
85
|
+
|
|
86
|
+
`agy`만 사용자 `~/.gemini/antigravity-cli/settings.json`에 `modelProvider=gemini`를 임시 병합하고 종료 시 복구한다(제품이 이 파일 없이 키를 무시함). agy custom은 설치 버전의 모델 목록과 실제 요청 ID가 확인된 Gemini 모델만 실행한다. Kiro는 공식 BYOK가 없어 지원하지 않는다.
|
|
85
87
|
|
|
86
88
|
## 환경변수
|
|
87
89
|
|
package/package.json
CHANGED
package/src/launch.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, mkdtempSync, renameSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, delimiter, dirname, resolve } from "node:path";
|
|
5
|
-
import { readConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
5
|
+
import { readConfig, isLoggedIn, getActiveContext, ROOT } from "./config.mjs";
|
|
6
6
|
import { login } from "./login.mjs";
|
|
7
7
|
import { ensureWireKey } from "./wire.mjs";
|
|
8
8
|
import {
|
|
9
9
|
RECIPES,
|
|
10
|
+
CORE_MODELS,
|
|
10
11
|
MANUAL,
|
|
11
12
|
LAUNCH_ALIASES,
|
|
12
13
|
resolveLaunchTool,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
} from "./recipes.mjs";
|
|
18
19
|
import { t } from "./i18n.mjs";
|
|
19
20
|
import { sanitizeTerminalText } from "./ansi.mjs";
|
|
21
|
+
import { fetchModels, codingModels } from "./models.mjs";
|
|
20
22
|
|
|
21
23
|
// PATH에서 실행 파일을 찾는다(설치 여부 표시용). menu.mjs도 사용.
|
|
22
24
|
export function onPath(bin) {
|
|
@@ -102,6 +104,79 @@ export function resolveLaunchModel(tool, explicit, cfg = readConfig()) {
|
|
|
102
104
|
return explicit || cfg?.launch_models?.[tool] || recipe?.defaultModel;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
// agy의 Google API-key 모드는 계정 모드보다 모델이 적다. 임시 HOME으로 실제
|
|
108
|
+
// 설치 버전의 목록을 읽어, 선택 모델이 다른 Gemini 모델로 조용히 바뀌는 일을 막는다.
|
|
109
|
+
export function listAgyModelIds({ run = execFileSync } = {}) {
|
|
110
|
+
const home = mkdtempSync(join(tmpdir(), "pleum-agy-models-"));
|
|
111
|
+
try {
|
|
112
|
+
const settingsDir = join(home, ".gemini", "antigravity-cli");
|
|
113
|
+
mkdirSync(settingsDir, { recursive: true, mode: 0o700 });
|
|
114
|
+
writeFileSync(join(settingsDir, "settings.json"), '{"modelProvider":"gemini"}', { mode: 0o600 });
|
|
115
|
+
const output = run("agy", ["models"], {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
timeout: 15000,
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
HOME: home,
|
|
121
|
+
USERPROFILE: home,
|
|
122
|
+
GEMINI_API_KEY: "pleum-model-list-probe",
|
|
123
|
+
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return new Set(String(output).split(/\r?\n/).map((line) => line.split(/\s/)[0]).filter((id) => /^gemini-/.test(id)));
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(home, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 설치된 agy 1.2.12를 모의 Gemini 엔드포인트에 연결해 실제 요청 ID가
|
|
133
|
+
// 선택 ID와 일치함을 확인한 모델만 열어 둔다. 새 모델은 같은 검증 후 추가한다.
|
|
134
|
+
const AGY_VERIFIED_MODELS = new Map([
|
|
135
|
+
["gemini-3.8-flash", "gemini-3.8-flash"],
|
|
136
|
+
["gemini-3.7-flash", "gemini-3.7-flash"],
|
|
137
|
+
["gemini-3.6-flash", "gemini-3.6-flash"],
|
|
138
|
+
["gemini-3.1-pro-preview", "gemini-3.1-pro"],
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
export function agyCliModel(model, ids) {
|
|
142
|
+
const cliBase = AGY_VERIFIED_MODELS.get(model);
|
|
143
|
+
if (!cliBase) return null;
|
|
144
|
+
return [cliBase, `${cliBase}-medium`, `${cliBase}-high`, `${cliBase}-low`, `${cliBase}-max`].find((id) => ids.has(id)) || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function supportsAgyModel(model, ids) {
|
|
148
|
+
return Boolean(agyCliModel(model, ids));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function opencodeStateDir({ run = execFileSync } = {}) {
|
|
152
|
+
try {
|
|
153
|
+
const paths = run("opencode", ["debug", "paths"], { encoding: "utf8", timeout: 5000 });
|
|
154
|
+
const state = /^state\s+(.+)$/m.exec(String(paths))?.[1]?.trim();
|
|
155
|
+
if (state) return state;
|
|
156
|
+
} catch {}
|
|
157
|
+
return join(process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "opencode");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function seedOpenCodeFavorites(models, { stateDir = opencodeStateDir() } = {}) {
|
|
161
|
+
const path = join(stateDir, "model.json");
|
|
162
|
+
const state = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
163
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) throw new Error("Invalid OpenCode model state");
|
|
164
|
+
const current = Array.isArray(state.favorite) ? state.favorite : [];
|
|
165
|
+
const active = new Set(codingModels(models).map((m) => m.id));
|
|
166
|
+
const pins = CORE_MODELS.filter((id) => active.has(id)).map((modelID) => ({ providerID: "openai", modelID }));
|
|
167
|
+
const pinKeys = new Set(pins.map((m) => `${m.providerID}/${m.modelID}`));
|
|
168
|
+
const favorite = [...pins, ...current.filter((m) => !pinKeys.has(`${m.providerID}/${m.modelID}`))];
|
|
169
|
+
if (JSON.stringify(favorite) === JSON.stringify(current)) return;
|
|
170
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
171
|
+
const temp = join(stateDir, `model.json.pleum-${process.pid}`);
|
|
172
|
+
try {
|
|
173
|
+
writeFileSync(temp, JSON.stringify({ ...state, favorite }), { mode: 0o600 });
|
|
174
|
+
renameSync(temp, path);
|
|
175
|
+
} finally {
|
|
176
|
+
rmSync(temp, { force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
105
180
|
// 공통 실행 파이프라인의 설정 어댑터. 신규 에이전트는 env/args만 쓰거나
|
|
106
181
|
// 별도 홈이 필요할 때 타입 핸들러 하나를 등록하면 된다.
|
|
107
182
|
const SETUP = {
|
|
@@ -130,6 +205,9 @@ export async function launch(tool, model, passthrough, {
|
|
|
130
205
|
getKey = agentKey,
|
|
131
206
|
pathCheck = onPath,
|
|
132
207
|
spawnChild = spawn,
|
|
208
|
+
getModels = fetchModels,
|
|
209
|
+
getAgyModels = listAgyModelIds,
|
|
210
|
+
seedFavorites = seedOpenCodeFavorites,
|
|
133
211
|
} = {}) {
|
|
134
212
|
recoverAgySettings();
|
|
135
213
|
const resolved = resolveLaunchTool(tool);
|
|
@@ -156,15 +234,41 @@ export async function launch(tool, model, passthrough, {
|
|
|
156
234
|
return 127;
|
|
157
235
|
}
|
|
158
236
|
|
|
159
|
-
const key = await getKey();
|
|
160
237
|
model = resolveLaunchModel(resolved, model);
|
|
238
|
+
let catalog;
|
|
239
|
+
if (model?.mode === "custom" || resolved === "opencode") {
|
|
240
|
+
catalog = codingModels(await getModels());
|
|
241
|
+
const chosen = model?.mode === "custom" ? model.model : model;
|
|
242
|
+
const [id, provider] = String(chosen || "").split("@");
|
|
243
|
+
const entry = catalog.find((m) => m.id === id);
|
|
244
|
+
if (!entry || (provider && !entry.providers?.some((p) => p.provider === provider))) {
|
|
245
|
+
throw new Error(t("Model is not an active coding model: {model}", { model: sanitizeTerminalText(chosen) }));
|
|
246
|
+
}
|
|
247
|
+
if (resolved === "agy") {
|
|
248
|
+
const cliModel = agyCliModel(chosen, getAgyModels());
|
|
249
|
+
if (!cliModel) {
|
|
250
|
+
throw new Error(t("Antigravity does not support this model through its Gemini API mode: {model}", { model: sanitizeTerminalText(chosen) }));
|
|
251
|
+
}
|
|
252
|
+
model = { ...model, cliModel };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const key = await getKey();
|
|
161
256
|
|
|
162
|
-
const env = { ...process.env
|
|
257
|
+
const env = { ...process.env };
|
|
163
258
|
for (const name of r.unsetEnv || []) delete env[name];
|
|
259
|
+
Object.assign(env, r.env(key, model, catalog));
|
|
164
260
|
const setup = SETUP[r.type || "env"];
|
|
165
261
|
if (!setup) throw new Error(`Unknown agent setup type: ${r.type}`);
|
|
166
262
|
let restore = setup(env, model);
|
|
167
263
|
|
|
264
|
+
if (resolved === "opencode") {
|
|
265
|
+
try {
|
|
266
|
+
seedFavorites(catalog);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.log(`ℹ ${t("Could not add Pleum models to OpenCode favorites: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
168
272
|
const cleanup = () => {
|
|
169
273
|
if (restore) {
|
|
170
274
|
restore();
|
package/src/locales/es.mjs
CHANGED
|
@@ -71,8 +71,6 @@ 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. 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
74
|
"↑ {n} more above": "↑ {n} más arriba",
|
|
77
75
|
"↓ {n} more below": "↓ {n} más abajo",
|
|
78
76
|
"Auto (best route)": "Auto (mejor ruta)",
|
|
@@ -331,5 +329,19 @@ export const ES = {
|
|
|
331
329
|
"Medium (Standard)": "Medio (Estándar)",
|
|
332
330
|
"Balanced reasoning effort": "Esfuerzo de razonamiento equilibrado",
|
|
333
331
|
"Low reasoning effort for rapid iteration": "Bajo esfuerzo de razonamiento para iteraciones rápidas",
|
|
332
|
+
// Agent launch modes
|
|
333
|
+
"Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom solo admite modelos Gemini verificados en modo de clave API.",
|
|
334
|
+
"Anthropic official models": "Continuar con modelos oficiales de Anthropic",
|
|
335
|
+
"OpenAI official models": "Continuar con modelos oficiales de OpenAI",
|
|
336
|
+
"Antigravity default models": "Continuar con modelos predeterminados de Antigravity",
|
|
337
|
+
"Choose models inside the agent": "Elegir modelo dentro del agente",
|
|
338
|
+
"Choose my model (beta)": "Elegir mi modelo (beta)",
|
|
339
|
+
"Select a PleumRouter coding model": "Elegir un modelo de código de PleumRouter",
|
|
340
|
+
"No compatible coding models available": "No hay modelos de código compatibles",
|
|
341
|
+
"Antigravity model discovery failed: {error}": "No se pudo consultar los modelos de Antigravity: {error}",
|
|
342
|
+
"Model is not an active coding model: {model}": "No es un modelo de código activo: {model}",
|
|
343
|
+
"Antigravity does not support this model through its Gemini API mode: {model}": "Antigravity no admite este modelo en modo Gemini API: {model}",
|
|
344
|
+
"Could not add Pleum models to OpenCode favorites: {error}": "No se pudieron añadir modelos Pleum a favoritos de OpenCode: {error}",
|
|
345
|
+
"↑/↓ move • enter select • esc quit": "↑/↓ mover • enter seleccionar • esc salir",
|
|
346
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "Escribe modelProvider=gemini en ~/.gemini/antigravity-cli/settings.json. Los modelos Gemini compatibles usan PleumRouter.",
|
|
334
347
|
};
|
|
335
|
-
|
package/src/locales/ja.mjs
CHANGED
|
@@ -72,8 +72,6 @@ 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. Routes any coding model via PleumRouter.":
|
|
76
|
-
"~/.gemini/antigravity-cli/settings.json に modelProvider=gemini を書き込みます。あらゆるコーディングモデルを PleumRouter でルーティングします。",
|
|
77
75
|
"↑ {n} more above": "↑ 上にあと {n} 件",
|
|
78
76
|
"↓ {n} more below": "↓ 下にあと {n} 件",
|
|
79
77
|
"Auto (best route)": "Auto (最適な自動ルーティング)",
|
|
@@ -331,5 +329,19 @@ export const JA = {
|
|
|
331
329
|
"Medium (Standard)": "中 (標準)",
|
|
332
330
|
"Balanced reasoning effort": "バランスの取れた標準的な推論強度",
|
|
333
331
|
"Low reasoning effort for rapid iteration": "迅速なイテレーション向けの低い推論強度",
|
|
332
|
+
// Agent launch modes
|
|
333
|
+
"Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom は API キーモードで検証済みの Gemini モデルのみ対応します。",
|
|
334
|
+
"Anthropic official models": "Anthropic 公式モデルで続行",
|
|
335
|
+
"OpenAI official models": "OpenAI 公式モデルで続行",
|
|
336
|
+
"Antigravity default models": "Antigravity 標準モデルで続行",
|
|
337
|
+
"Choose models inside the agent": "エージェント内でモデルを選択",
|
|
338
|
+
"Choose my model (beta)": "モデルを自分で選ぶ (beta)",
|
|
339
|
+
"Select a PleumRouter coding model": "PleumRouter のコーディングモデルを選択",
|
|
340
|
+
"No compatible coding models available": "対応するコーディングモデルがありません",
|
|
341
|
+
"Antigravity model discovery failed: {error}": "Antigravity のモデル一覧を取得できません: {error}",
|
|
342
|
+
"Model is not an active coding model: {model}": "有効なコーディングモデルではありません: {model}",
|
|
343
|
+
"Antigravity does not support this model through its Gemini API mode: {model}": "Antigravity の Gemini API モードでは使用できません: {model}",
|
|
344
|
+
"Could not add Pleum models to OpenCode favorites: {error}": "OpenCode のお気に入りに Pleum モデルを追加できません: {error}",
|
|
345
|
+
"↑/↓ move • enter select • esc quit": "↑/↓ 移動 • enter 選択 • esc 終了",
|
|
346
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "~/.gemini/antigravity-cli/settings.json に modelProvider=gemini を書き込みます。対応する Gemini モデルを PleumRouter 経由で使用します。",
|
|
334
347
|
};
|
|
335
|
-
|
package/src/locales/ko.mjs
CHANGED
|
@@ -52,8 +52,6 @@ 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. Routes any coding model via PleumRouter.":
|
|
56
|
-
"~/.gemini/antigravity-cli/settings.json 에 modelProvider=gemini 를 씁니다. 모든 코딩 모델을 PleumRouter로 라우팅합니다.",
|
|
57
55
|
"↑ {n} more above": "↑ 위에 {n}개 더",
|
|
58
56
|
"↓ {n} more below": "↓ 아래에 {n}개 더",
|
|
59
57
|
"Auto (best route)": "Auto (최적 자동 라우팅)",
|
|
@@ -394,5 +392,19 @@ export const KO = {
|
|
|
394
392
|
"Medium (Standard)": "보통 (표준)",
|
|
395
393
|
"Balanced reasoning effort": "균형 잡힌 표준 추론 강도",
|
|
396
394
|
"Low reasoning effort for rapid iteration": "빠른 반복 작업을 위한 낮은 추론 강도",
|
|
395
|
+
// Agent launch modes
|
|
396
|
+
"Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom은 API 키 모드에서 검증된 Gemini 모델만 지원합니다.",
|
|
397
|
+
"Anthropic official models": "Anthropic 공식 모델로 진행",
|
|
398
|
+
"OpenAI official models": "OpenAI 공식 모델로 진행",
|
|
399
|
+
"Antigravity default models": "Antigravity 기본 모델로 진행",
|
|
400
|
+
"Choose models inside the agent": "에이전트 안에서 모델 선택",
|
|
401
|
+
"Choose my model (beta)": "모델 내가 custom하기(beta)",
|
|
402
|
+
"Select a PleumRouter coding model": "PleumRouter 코딩 모델 선택",
|
|
403
|
+
"No compatible coding models available": "호환되는 코딩 모델이 없습니다",
|
|
404
|
+
"Antigravity model discovery failed: {error}": "Antigravity 모델 목록 조회 실패: {error}",
|
|
405
|
+
"Model is not an active coding model: {model}": "활성 코딩 모델이 아닙니다: {model}",
|
|
406
|
+
"Antigravity does not support this model through its Gemini API mode: {model}": "Antigravity Gemini API 모드에서 지원하지 않는 모델입니다: {model}",
|
|
407
|
+
"Could not add Pleum models to OpenCode favorites: {error}": "OpenCode 즐겨찾기에 Pleum 모델을 추가하지 못했습니다: {error}",
|
|
408
|
+
"↑/↓ move • enter select • esc quit": "↑/↓ 이동 • enter 선택 • esc 종료",
|
|
409
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "~/.gemini/antigravity-cli/settings.json에 modelProvider=gemini를 씁니다. 지원되는 Gemini 모델을 PleumRouter로 라우팅합니다.",
|
|
397
410
|
};
|
|
398
|
-
|
package/src/locales/zh.mjs
CHANGED
|
@@ -71,8 +71,6 @@ 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. Routes any coding model via PleumRouter.":
|
|
75
|
-
"将 modelProvider=gemini 写入 ~/.gemini/antigravity-cli/settings.json。通过 PleumRouter 路由所有编码模型。",
|
|
76
74
|
"↑ {n} more above": "↑ 上方还有 {n} 项",
|
|
77
75
|
"↓ {n} more below": "↓ 下方还有 {n} 项",
|
|
78
76
|
"Auto (best route)": "Auto (最佳自动路由)",
|
|
@@ -331,5 +329,19 @@ export const ZH = {
|
|
|
331
329
|
"Medium (Standard)": "中 (标准)",
|
|
332
330
|
"Balanced reasoning effort": "平衡的标准推理强度",
|
|
333
331
|
"Low reasoning effort for rapid iteration": "快速迭代的低推理强度",
|
|
332
|
+
// Agent launch modes
|
|
333
|
+
"Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity 自选模型在 API 密钥模式下仅支持已验证的 Gemini 模型。",
|
|
334
|
+
"Anthropic official models": "使用 Anthropic 官方模型",
|
|
335
|
+
"OpenAI official models": "使用 OpenAI 官方模型",
|
|
336
|
+
"Antigravity default models": "使用 Antigravity 默认模型",
|
|
337
|
+
"Choose models inside the agent": "在代理中选择模型",
|
|
338
|
+
"Choose my model (beta)": "自选模型 (beta)",
|
|
339
|
+
"Select a PleumRouter coding model": "选择 PleumRouter 编程模型",
|
|
340
|
+
"No compatible coding models available": "没有兼容的编程模型",
|
|
341
|
+
"Antigravity model discovery failed: {error}": "获取 Antigravity 模型列表失败:{error}",
|
|
342
|
+
"Model is not an active coding model: {model}": "不是可用的编程模型:{model}",
|
|
343
|
+
"Antigravity does not support this model through its Gemini API mode: {model}": "Antigravity 的 Gemini API 模式不支持此模型:{model}",
|
|
344
|
+
"Could not add Pleum models to OpenCode favorites: {error}": "无法将 Pleum 模型加入 OpenCode 收藏:{error}",
|
|
345
|
+
"↑/↓ move • enter select • esc quit": "↑/↓ 移动 • enter 选择 • esc 退出",
|
|
346
|
+
"Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.": "将 modelProvider=gemini 写入 ~/.gemini/antigravity-cli/settings.json。支持的 Gemini 模型经由 PleumRouter 路由。",
|
|
334
347
|
};
|
|
335
|
-
|
package/src/menu.mjs
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// 동작 패리티(ollama cmd/tui/tui.go·cmd/launch 소스 분석 기반):
|
|
3
|
-
// · 인라인 렌더(대체 화면 아님), ▸ 커서 + dim 설명 2줄 항목, 하단 키 안내
|
|
4
|
-
// · →(configure) = 모델 피커를 강제로 띄우고 선택 즉시 실행(+ launch_models에 저장)
|
|
5
|
-
// · Enter = 저장된 모델(launch_models)로 바로 실행, 미설치면 Y/n 확인 후 자동 설치
|
|
6
|
-
// · 실행 종료 후 메뉴로 복귀(ollama runInteractiveTUI 루프), last_selection 커서 복원
|
|
7
|
-
// · 키보드 전용: ↑/k ↓/j 이동 · enter/space 실행 · →/l 모델 설정 · esc/q 뒤로(최상위에선 종료) · ctrl+c 즉시 종료
|
|
8
|
-
// 4대 에이전트는 최상위에서 바로 실행하고, 나머지는 "에이전트 실행" 서브메뉴에서 고른다.
|
|
1
|
+
// bare `pleum` 인라인 런처. 첫 화면의 에이전트 실행 → 도구 → 실행 모드를 공유한다.
|
|
9
2
|
|
|
10
3
|
import { readFileSync } from "node:fs";
|
|
11
4
|
import { spawn } from "node:child_process";
|
|
@@ -14,8 +7,8 @@ import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitize
|
|
|
14
7
|
import { keyFeeder, select } from "./interactive.mjs";
|
|
15
8
|
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
16
9
|
import { RECIPES, MANUAL, DEFAULT_MODEL } from "./recipes.mjs";
|
|
17
|
-
import { onPath, launch } from "./launch.mjs";
|
|
18
|
-
import { fetchModels, matchModels, listModels, formatModelLabel,
|
|
10
|
+
import { onPath, launch, listAgyModelIds, supportsAgyModel } from "./launch.mjs";
|
|
11
|
+
import { fetchModels, matchModels, listModels, formatModelLabel, codingModels, coreCodingModels } from "./models.mjs";
|
|
19
12
|
import { run } from "./run.mjs";
|
|
20
13
|
import { login } from "./login.mjs";
|
|
21
14
|
import { balance } from "./balance.mjs";
|
|
@@ -50,24 +43,14 @@ const AGENT_DESC = {
|
|
|
50
43
|
const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
|
|
51
44
|
|
|
52
45
|
// 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
|
|
53
|
-
export function buildItems({ savedModels = {}, activeContext = { type: "personal" }
|
|
54
|
-
const items = [];
|
|
55
|
-
for (const tool of ["claude", "codex", "agy", "opencode"]) {
|
|
56
|
-
const r = RECIPES[tool];
|
|
57
|
-
items.push({
|
|
58
|
-
id: `launch:${tool}`,
|
|
59
|
-
tool,
|
|
60
|
-
title: `${r.label}${installed[tool] === false ? ` (${t("install")})` : ""}`,
|
|
61
|
-
desc: `${t(AGENT_DESC[tool])}${savedModels[tool] ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
46
|
+
export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
|
|
47
|
+
const items = [{ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") }];
|
|
64
48
|
items.push({
|
|
65
49
|
id: "chat",
|
|
66
50
|
tool: "chat",
|
|
67
51
|
title: t("Chat with a model"),
|
|
68
52
|
desc: `${t("Start an interactive chat")}${savedModels.chat ? ` · ${t("model")}: ${savedModels.chat}` : ""}`,
|
|
69
53
|
});
|
|
70
|
-
items.push({ id: "agents", title: t("Run an agent"), desc: t("Pick a coding agent to run through PleumRouter") });
|
|
71
54
|
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
72
55
|
? `${t("Team")}: ${activeContext.org_name}`
|
|
73
56
|
: t("Personal");
|
|
@@ -88,7 +71,7 @@ export function buildAgentItems({ installed, savedModels = {} }) {
|
|
|
88
71
|
tool,
|
|
89
72
|
title: `${r.label}${installed[tool] ? "" : autoInstallable(r) ? ` (${t("install")})` : ` (${t("not installed")})`}`,
|
|
90
73
|
desc: installed[tool]
|
|
91
|
-
? `${t(AGENT_DESC[tool] ?? "")}${savedModels[tool] ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`
|
|
74
|
+
? `${t(AGENT_DESC[tool] ?? "")}${savedModels[tool] && !["claude", "codex", "agy"].includes(tool) ? ` · ${t("model")}: ${savedModels[tool]}` : ""}`
|
|
92
75
|
: autoInstallable(r)
|
|
93
76
|
? `${t("Press Enter to install now")} — ${r.install}`
|
|
94
77
|
: `${t("Manual install:")} ${hyperlink(r.install)}`,
|
|
@@ -123,9 +106,10 @@ function saveLastSelection(id) {
|
|
|
123
106
|
writeConfig(cfg);
|
|
124
107
|
}
|
|
125
108
|
|
|
126
|
-
export async function pickProviderForModel(
|
|
109
|
+
export async function pickProviderForModel(model, { input = process.stdin, output = process.stdout } = {}) {
|
|
110
|
+
const modelId = typeof model === "string" ? model : model?.id;
|
|
127
111
|
if (!modelId || modelId === "__view_all__") return modelId;
|
|
128
|
-
const providers =
|
|
112
|
+
const providers = [...new Set((typeof model === "string" ? [] : model.providers || []).map((p) => p.provider).filter(Boolean))];
|
|
129
113
|
if (!providers.length) return modelId;
|
|
130
114
|
|
|
131
115
|
const options = [
|
|
@@ -134,10 +118,10 @@ export async function pickProviderForModel(modelId, { input = process.stdin, out
|
|
|
134
118
|
title: `⚡ ${t("Auto (best route)")}`,
|
|
135
119
|
desc: t("PleumRouter auto-routing — lowest price, lowest latency, automatic failover"),
|
|
136
120
|
},
|
|
137
|
-
...providers.map((
|
|
138
|
-
id:
|
|
139
|
-
title:
|
|
140
|
-
desc: t("Pin to {provider} (@{provider})", { provider
|
|
121
|
+
...providers.map((provider) => ({
|
|
122
|
+
id: provider,
|
|
123
|
+
title: provider,
|
|
124
|
+
desc: t("Pin to {provider} (@{provider})", { provider }),
|
|
141
125
|
})),
|
|
142
126
|
];
|
|
143
127
|
|
|
@@ -154,9 +138,20 @@ export async function pickProviderForModel(modelId, { input = process.stdin, out
|
|
|
154
138
|
return `${modelId}@${chosen.id}`;
|
|
155
139
|
}
|
|
156
140
|
|
|
157
|
-
export async function pickModelFor(item, defaultModel) {
|
|
141
|
+
export async function pickModelFor(item, defaultModel, { supported = () => true, allowProvider = true } = {}) {
|
|
142
|
+
let allModels;
|
|
143
|
+
try {
|
|
144
|
+
allModels = codingModels(await fetchModels()).filter((m) => supported(m.id));
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.log(`${style.red("✗")} ${t("Failed to fetch model list: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (!allModels.length) {
|
|
150
|
+
console.log(`${style.red("✗")} ${t("No compatible coding models available")}`);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
158
153
|
for (;;) {
|
|
159
|
-
const core =
|
|
154
|
+
const core = coreCodingModels(allModels);
|
|
160
155
|
const items = [
|
|
161
156
|
...core,
|
|
162
157
|
{
|
|
@@ -177,13 +172,6 @@ export async function pickModelFor(item, defaultModel) {
|
|
|
177
172
|
|
|
178
173
|
let pickedModelId = m.id;
|
|
179
174
|
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
175
|
const allPicked = await select(allModels, {
|
|
188
176
|
message: `${item.title} — ${t("select a model")}`,
|
|
189
177
|
formatItem: (x) => formatModelLabel(x),
|
|
@@ -194,113 +182,20 @@ export async function pickModelFor(item, defaultModel) {
|
|
|
194
182
|
pickedModelId = allPicked.id;
|
|
195
183
|
}
|
|
196
184
|
|
|
197
|
-
const
|
|
185
|
+
const selected = allModels.find((x) => x.id === pickedModelId);
|
|
186
|
+
const withProvider = allowProvider ? await pickProviderForModel(selected) : pickedModelId;
|
|
198
187
|
if (!withProvider) continue;
|
|
199
188
|
return withProvider;
|
|
200
189
|
}
|
|
201
190
|
}
|
|
202
191
|
|
|
203
|
-
export
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
haiku: "claude-haiku-4-5",
|
|
210
|
-
};
|
|
211
|
-
const baseTiers = {
|
|
212
|
-
sonnet: savedTiers?.sonnet || defaultTiers.sonnet,
|
|
213
|
-
opus: savedTiers?.opus || defaultTiers.opus,
|
|
214
|
-
haiku: savedTiers?.haiku || defaultTiers.haiku,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// 기존 설정이 존재하면 1-클릭 확인 메뉴: Enter 1회로 기존 설정대로 즉시 실행
|
|
218
|
-
if (savedTiers && savedTiers.sonnet && savedTiers.opus && savedTiers.haiku) {
|
|
219
|
-
const keepChoice = await select(
|
|
220
|
-
[
|
|
221
|
-
{
|
|
222
|
-
id: "keep",
|
|
223
|
-
title: t("Keep existing configuration"),
|
|
224
|
-
desc: `Sonnet: ${savedTiers.sonnet} · Opus: ${savedTiers.opus} · Haiku: ${savedTiers.haiku}`,
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
id: "change",
|
|
228
|
-
title: t("Reconfigure tiers"),
|
|
229
|
-
desc: t("Choose Sonnet, Opus, and Haiku models again"),
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
{
|
|
233
|
-
message: `${agentItem.title} — ${t("Use existing model configuration?")}`,
|
|
234
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
235
|
-
limit: 5,
|
|
236
|
-
input,
|
|
237
|
-
output,
|
|
238
|
-
},
|
|
239
|
-
);
|
|
240
|
-
if (!keepChoice) return null;
|
|
241
|
-
if (keepChoice.id === "keep") {
|
|
242
|
-
return savedTiers;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// 1/3단계: Sonnet 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
247
|
-
const sonnet = await pickModelFor(
|
|
248
|
-
{ title: `${agentItem.title} (1/3) — ${t("Sonnet model")}` },
|
|
249
|
-
baseTiers.sonnet,
|
|
250
|
-
);
|
|
251
|
-
if (!sonnet) return null;
|
|
252
|
-
|
|
253
|
-
// 2/3단계: Opus 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
254
|
-
const opus = await pickModelFor(
|
|
255
|
-
{ title: `${agentItem.title} (2/3) — ${t("Opus model")}` },
|
|
256
|
-
baseTiers.opus,
|
|
257
|
-
);
|
|
258
|
-
if (!opus) return null;
|
|
259
|
-
|
|
260
|
-
// 3/3단계: Haiku 모델 지정 (10개 핵심 추천 + 200+ 전체 검색 + 프로바이더 라우팅)
|
|
261
|
-
const haiku = await pickModelFor(
|
|
262
|
-
{ title: `${agentItem.title} (3/3) — ${t("Haiku model")}` },
|
|
263
|
-
baseTiers.haiku,
|
|
264
|
-
);
|
|
265
|
-
if (!haiku) return null;
|
|
266
|
-
|
|
267
|
-
const resultTiers = { sonnet, opus, haiku };
|
|
268
|
-
cfg.claude_tiers = resultTiers;
|
|
269
|
-
writeConfig(cfg);
|
|
270
|
-
return resultTiers;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export async function pickCodexEffort(defaultEffort = "medium") {
|
|
274
|
-
const options = [
|
|
275
|
-
{ id: "medium", title: `⚡ ${t("Medium (Recommended)")}`, desc: t("Balanced reasoning speed and depth") },
|
|
276
|
-
{ id: "high", title: `🔥 ${t("High (Deep reasoning)")}`, desc: t("Maximum reasoning depth for complex tasks") },
|
|
277
|
-
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Lightweight reasoning for quick answers") },
|
|
278
|
-
];
|
|
279
|
-
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
280
|
-
const picked = await select(options, {
|
|
281
|
-
message: `Codex — ${t("Select reasoning effort")}`,
|
|
282
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
283
|
-
limit: 5,
|
|
284
|
-
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
285
|
-
});
|
|
286
|
-
return picked ? picked.id : null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export async function pickAgyEffort(defaultEffort = "high") {
|
|
290
|
-
const options = [
|
|
291
|
-
{ id: "high", title: `⚡ ${t("High (Recommended)")}`, desc: t("High reasoning effort for standard tasks") },
|
|
292
|
-
{ id: "max", title: `🚀 ${t("Max (Maximum reasoning)")}`, desc: t("Full reasoning effort for difficult problems") },
|
|
293
|
-
{ id: "medium", title: `⚖️ ${t("Medium (Standard)")}`, desc: t("Balanced reasoning effort") },
|
|
294
|
-
{ id: "low", title: `⚡ ${t("Low (Fast response)")}`, desc: t("Low reasoning effort for rapid iteration") },
|
|
192
|
+
export function agentModeItems(tool) {
|
|
193
|
+
const native = tool === "claude" ? "Anthropic official models"
|
|
194
|
+
: tool === "codex" ? "OpenAI official models" : "Antigravity default models";
|
|
195
|
+
return [
|
|
196
|
+
{ id: "native", title: t(native), desc: t("Choose models inside the agent") },
|
|
197
|
+
{ id: "custom", title: t("Choose my model (beta)"), desc: t("Select a PleumRouter coding model") },
|
|
295
198
|
];
|
|
296
|
-
const initialIndex = options.findIndex((x) => x.id === defaultEffort);
|
|
297
|
-
const picked = await select(options, {
|
|
298
|
-
message: `Antigravity CLI (agy) — ${t("Select reasoning effort")}`,
|
|
299
|
-
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
300
|
-
limit: 6,
|
|
301
|
-
initialCursor: initialIndex >= 0 ? initialIndex : 0,
|
|
302
|
-
});
|
|
303
|
-
return picked ? picked.id : null;
|
|
304
199
|
}
|
|
305
200
|
|
|
306
201
|
// 미설치 도구: ollama처럼 확인 후 그 자리에서 설치(stdio 상속). 성공 시 true.
|
|
@@ -465,7 +360,7 @@ export async function runList(items, { header, footer, initialCursor = 0 } = {})
|
|
|
465
360
|
else if (key.type === "down" || (key.type === "char" && key.value === "j")) move(1);
|
|
466
361
|
else if (key.type === "right" || (key.type === "char" && key.value === "l")) {
|
|
467
362
|
const it = items[cursor];
|
|
468
|
-
if (!it.tool || it.manual
|
|
363
|
+
if (!it.tool || it.manual || ["claude", "codex", "agy", "opencode"].includes(it.tool)) continue;
|
|
469
364
|
return void (await configure(it));
|
|
470
365
|
} else continue;
|
|
471
366
|
dirty = true;
|
|
@@ -488,14 +383,13 @@ async function menuSelect() {
|
|
|
488
383
|
const items = buildItems({
|
|
489
384
|
savedModels: cfg?.launch_models ?? {},
|
|
490
385
|
activeContext: getActiveContext(cfg),
|
|
491
|
-
installed: Object.fromEntries(["claude", "codex", "agy", "opencode"].map((tool) => [tool, onPath(RECIPES[tool].bin)])),
|
|
492
386
|
});
|
|
493
387
|
const last = cfg?.last_selection;
|
|
494
388
|
const initialCursor = last ? Math.max(0, items.findIndex((it) => it.id === last)) : 0;
|
|
495
389
|
const headerLines = [`${style.bold(style.plum("PleumRouter"))} ${style.dim(`pleum ${version}`)}`];
|
|
496
390
|
return runList(items, {
|
|
497
391
|
header: headerLines.join("\n"),
|
|
498
|
-
footer: t("↑/↓ move • enter
|
|
392
|
+
footer: t("↑/↓ move • enter select • esc quit"),
|
|
499
393
|
initialCursor,
|
|
500
394
|
});
|
|
501
395
|
}
|
|
@@ -507,12 +401,11 @@ async function agentSelect() {
|
|
|
507
401
|
const items = buildAgentItems({ installed, savedModels: readConfig()?.launch_models ?? {} });
|
|
508
402
|
return runList(items, {
|
|
509
403
|
header: style.bold(style.plum(t("Run an agent via PleumRouter"))),
|
|
510
|
-
footer: t("↑/↓ move • enter
|
|
404
|
+
footer: t("↑/↓ move • enter select • esc back"),
|
|
511
405
|
});
|
|
512
406
|
}
|
|
513
407
|
|
|
514
|
-
//
|
|
515
|
-
// Enter는 저장 모델로 즉시 실행하고, →는 모델 선택 후 실행한다.
|
|
408
|
+
// bare pleum의 "에이전트 실행"과 pleum launch (인자 없음)가 공유한다.
|
|
516
409
|
export async function runAgentMenu({ loop = false } = {}) {
|
|
517
410
|
const io = process.stdin;
|
|
518
411
|
if (!io.isTTY || !process.stdout.isTTY || typeof io.setRawMode !== "function") return false;
|
|
@@ -535,8 +428,34 @@ export async function runAgentMenu({ loop = false } = {}) {
|
|
|
535
428
|
continue;
|
|
536
429
|
}
|
|
537
430
|
|
|
538
|
-
|
|
539
|
-
|
|
431
|
+
let model = agentPicked.model;
|
|
432
|
+
if (["claude", "codex", "agy"].includes(agentItem.tool)) {
|
|
433
|
+
const mode = await select(agentModeItems(agentItem.tool), {
|
|
434
|
+
message: agentItem.title,
|
|
435
|
+
formatItem: (x) => `${x.title} ${style.dim(x.desc)}`,
|
|
436
|
+
limit: 4,
|
|
437
|
+
});
|
|
438
|
+
if (!mode) continue;
|
|
439
|
+
if (mode.id === "custom") {
|
|
440
|
+
let supported = () => true;
|
|
441
|
+
if (agentItem.tool === "agy") {
|
|
442
|
+
try {
|
|
443
|
+
const ids = listAgyModelIds();
|
|
444
|
+
supported = (id) => supportsAgyModel(id, ids);
|
|
445
|
+
console.log(style.dim(t("Antigravity custom supports only verified Gemini models in API-key mode.")));
|
|
446
|
+
} catch (e) {
|
|
447
|
+
console.log(`${style.red("✗")} ${t("Antigravity model discovery failed: {error}", { error: sanitizeTerminalText(e.message) })}`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const saved = readConfig()?.launch_models?.[agentItem.tool];
|
|
452
|
+
const selected = await pickModelFor(agentItem, saved, { supported, allowProvider: agentItem.tool !== "agy" });
|
|
453
|
+
if (!selected) continue;
|
|
454
|
+
saveModel(agentItem.tool, selected);
|
|
455
|
+
model = { mode: "custom", model: selected };
|
|
456
|
+
} else model = { mode: "native" };
|
|
457
|
+
} else if (agentPicked.model) saveModel(agentItem.tool, agentPicked.model);
|
|
458
|
+
await launch(agentItem.tool, model, [], { exit: false });
|
|
540
459
|
console.log();
|
|
541
460
|
if (!loop) return true;
|
|
542
461
|
}
|
|
@@ -590,15 +509,6 @@ export async function mainMenu() {
|
|
|
590
509
|
if (r === "quit") return true;
|
|
591
510
|
continue;
|
|
592
511
|
}
|
|
593
|
-
case "launch:claude":
|
|
594
|
-
case "launch:codex":
|
|
595
|
-
case "launch:agy":
|
|
596
|
-
case "launch:opencode": {
|
|
597
|
-
const tool = item.tool;
|
|
598
|
-
if (!onPath(RECIPES[tool].bin) && !(await ensureInstalled(RECIPES[tool]))) continue;
|
|
599
|
-
await launch(tool, model, [], { exit: false });
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
512
|
}
|
|
603
513
|
}
|
|
604
514
|
}
|
package/src/models.mjs
CHANGED
|
@@ -2,129 +2,10 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
4
4
|
import { V1 } from "./config.mjs";
|
|
5
|
+
import { CORE_MODELS } from "./recipes.mjs";
|
|
5
6
|
import { style, padDisplay, divider, sanitizeTerminalText } from "./ansi.mjs";
|
|
6
7
|
import { t } from "./i18n.mjs";
|
|
7
8
|
|
|
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
9
|
let memoryModelsCache = null;
|
|
129
10
|
let memoryCacheExpiry = 0;
|
|
130
11
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -133,19 +14,14 @@ function getCacheFilePath() {
|
|
|
133
14
|
return join(homedir(), ".pleum", "models-cache.json");
|
|
134
15
|
}
|
|
135
16
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
});
|
|
17
|
+
// 코딩 에이전트는 텍스트 대화 API를 호출한다. 공개 카탈로그의 다른 생성 방식은 피커에서 제외한다.
|
|
18
|
+
export function codingModels(models) {
|
|
19
|
+
return models.filter((m) => m.is_active !== false && m.modality === "text" && m.operations?.includes("chat.generate"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function coreCodingModels(models) {
|
|
23
|
+
const byId = new Map(codingModels(models).map((m) => [m.id, m]));
|
|
24
|
+
return CORE_MODELS.map((id) => byId.get(id)).filter(Boolean);
|
|
149
25
|
}
|
|
150
26
|
|
|
151
27
|
// GET /v1/models 는 공개(인증 불필요). 슬러그는 provider/model 형식.
|
package/src/recipes.mjs
CHANGED
|
@@ -34,15 +34,10 @@ export const RECIPES = {
|
|
|
34
34
|
label: "Claude Code",
|
|
35
35
|
bin: "claude",
|
|
36
36
|
type: "env",
|
|
37
|
-
unsetEnv: ["ANTHROPIC_API_KEY"],
|
|
38
|
-
resolveModel: (explicit
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sonnet: explicit || cfg?.launch_models?.claude || cfg?.claude_tiers?.sonnet || "claude-sonnet-5",
|
|
42
|
-
opus: cfg?.claude_tiers?.opus || "claude-opus-5-5",
|
|
43
|
-
haiku: cfg?.claude_tiers?.haiku || "claude-haiku-4-5",
|
|
44
|
-
};
|
|
45
|
-
},
|
|
37
|
+
unsetEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_CUSTOM_MODEL_OPTION", "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION"],
|
|
38
|
+
resolveModel: (explicit) => explicit && typeof explicit === "object"
|
|
39
|
+
? explicit
|
|
40
|
+
: explicit ? { mode: "custom", model: explicit } : { mode: "native" },
|
|
46
41
|
install: "npm i -g @anthropic-ai/claude-code",
|
|
47
42
|
// 앱 랭킹 귀속 메타 — 백엔드 featured_agents / UA fallback과 동기화.
|
|
48
43
|
attribution: {
|
|
@@ -51,11 +46,7 @@ export const RECIPES = {
|
|
|
51
46
|
},
|
|
52
47
|
// 함정: ANTHROPIC_BASE_URL은 반드시 루트(/v1 없이) — CLI가 /v1/messages를 스스로 덧붙인다.
|
|
53
48
|
// /v1을 넣으면 /v1/v1/messages가 되어 실패.
|
|
54
|
-
env: (key,
|
|
55
|
-
const isObj = m && typeof m === "object";
|
|
56
|
-
const sonnet = (isObj ? m.sonnet : m) || "claude-sonnet-5";
|
|
57
|
-
const opus = (isObj ? m.opus : null) || "claude-opus-5-5";
|
|
58
|
-
const haiku = (isObj ? m.haiku : null) || "claude-haiku-4-5";
|
|
49
|
+
env: (key, selection) => {
|
|
59
50
|
// Claude 모델의 provider-qualified ID만 보정한다. 다른 회사 모델에
|
|
60
51
|
// anthropic/을 붙이면 카탈로그에 없는 ID가 되어 요청이 실패한다.
|
|
61
52
|
const toAnthropic = (x) => /^claude-/.test(x) ? `anthropic/${x}` : x;
|
|
@@ -65,30 +56,40 @@ export const RECIPES = {
|
|
|
65
56
|
// CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY는 의도적으로 설정하지 않는다:
|
|
66
57
|
// 활성화 시 Claude Code가 GET /v1/models를 호출해 카탈로그 200+개 모델을
|
|
67
58
|
// /model 메뉴에 쏟아부어 화면이 난잡해지는 원인이 된다.
|
|
68
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic(sonnet),
|
|
59
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic("claude-sonnet-5"),
|
|
69
60
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: "Sonnet",
|
|
70
61
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION: "Sonnet 5 · Efficient for routine tasks",
|
|
71
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic(opus),
|
|
62
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic("claude-opus-5-5"),
|
|
72
63
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: "Opus",
|
|
73
64
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION: "Opus 5.5 · Best for everyday, complex tasks",
|
|
74
|
-
|
|
65
|
+
ANTHROPIC_DEFAULT_FABLE_MODEL: toAnthropic("claude-fable-5-1"),
|
|
66
|
+
ANTHROPIC_DEFAULT_FABLE_MODEL_NAME: "Fable",
|
|
67
|
+
ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION: "Fable 5.1 · Long-running tasks",
|
|
68
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: toAnthropic("claude-haiku-4-5"),
|
|
75
69
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: "Haiku",
|
|
76
70
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION: "Haiku 4.5 · Fastest for quick answers",
|
|
77
|
-
ANTHROPIC_SMALL_MODEL: toAnthropic(haiku),
|
|
71
|
+
ANTHROPIC_SMALL_MODEL: toAnthropic("claude-haiku-4-5"),
|
|
72
|
+
...(selection?.mode === "custom" ? {
|
|
73
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION: toAnthropic(selection.model),
|
|
74
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: `Pleum · ${selection.model}`,
|
|
75
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: "Selected in Pleum for this session",
|
|
76
|
+
CLAUDE_CODE_SUBAGENT_MODEL: "sonnet",
|
|
77
|
+
CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "1",
|
|
78
|
+
} : {}),
|
|
78
79
|
};
|
|
79
80
|
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
args: (selection) => ["--model", selection?.mode === "custom"
|
|
82
|
+
? (/^claude-/.test(selection.model) ? `anthropic/${selection.model}` : selection.model)
|
|
83
|
+
: "sonnet"],
|
|
83
84
|
},
|
|
84
85
|
codex: {
|
|
85
86
|
label: "Codex CLI",
|
|
86
87
|
bin: "codex",
|
|
87
88
|
type: "env",
|
|
88
89
|
defaultModel: "gpt-6-sol",
|
|
89
|
-
resolveModel: (explicit
|
|
90
|
+
resolveModel: (explicit) => explicit && typeof explicit === "object"
|
|
90
91
|
? explicit
|
|
91
|
-
: {
|
|
92
|
+
: { mode: explicit ? "custom" : "native", model: explicit || "gpt-6-sol" },
|
|
92
93
|
install: "npm i -g @openai/codex",
|
|
93
94
|
attribution: {
|
|
94
95
|
title: "Codex",
|
|
@@ -97,7 +98,6 @@ export const RECIPES = {
|
|
|
97
98
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
98
99
|
args: (model) => {
|
|
99
100
|
const m = model && typeof model === "object" ? model.model : model;
|
|
100
|
-
const effort = model && typeof model === "object" ? model.effort : null;
|
|
101
101
|
const provider = {
|
|
102
102
|
name: "PleumRouter",
|
|
103
103
|
base_url: V1,
|
|
@@ -110,7 +110,6 @@ export const RECIPES = {
|
|
|
110
110
|
"-c", 'model_provider="pleum"',
|
|
111
111
|
"-c", `model_providers.pleum=${providerToml}`,
|
|
112
112
|
"--model", m || "gpt-6-sol",
|
|
113
|
-
...(effort ? ["-c", `model_reasoning_effort=${tomlBasicString(effort)}`] : []),
|
|
114
113
|
];
|
|
115
114
|
},
|
|
116
115
|
},
|
|
@@ -118,12 +117,11 @@ export const RECIPES = {
|
|
|
118
117
|
label: "Antigravity CLI",
|
|
119
118
|
bin: "agy",
|
|
120
119
|
type: "agy-settings",
|
|
121
|
-
|
|
122
|
-
resolveModel: (explicit, cfg) => explicit && typeof explicit === "object"
|
|
120
|
+
resolveModel: (explicit) => explicit && typeof explicit === "object"
|
|
123
121
|
? explicit
|
|
124
|
-
:
|
|
122
|
+
: explicit ? { mode: "custom", model: explicit } : { mode: "native" },
|
|
125
123
|
install: "https://antigravity.google/cli/install.sh",
|
|
126
|
-
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json.
|
|
124
|
+
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Supported Gemini models route via PleumRouter.",
|
|
127
125
|
attribution: {
|
|
128
126
|
title: "Antigravity CLI",
|
|
129
127
|
referer: "https://www.antigravity.google/docs/cli/install",
|
|
@@ -134,14 +132,9 @@ export const RECIPES = {
|
|
|
134
132
|
GOOGLE_API_KEY: key,
|
|
135
133
|
}),
|
|
136
134
|
args: (m) => {
|
|
137
|
-
|
|
138
|
-
const model =
|
|
139
|
-
|
|
140
|
-
// agy의 --model은 내장 Gemini 모델만 받는다. 다른 카탈로그 ID는
|
|
141
|
-
// settings.json model로 전달하고 CLI 인자에는 싣지 않는다.
|
|
142
|
-
return /^(?:google\/)?gemini-/.test(model)
|
|
143
|
-
? ["--model", model.replace(/^google\//, ""), "--effort", effort || "high"]
|
|
144
|
-
: [];
|
|
135
|
+
if (!m || m.mode === "native") return [];
|
|
136
|
+
const model = typeof m === "object" ? (m.cliModel || m.model) : m;
|
|
137
|
+
return ["--model", model.replace(/^google\//, "")];
|
|
145
138
|
},
|
|
146
139
|
},
|
|
147
140
|
opencode: {
|
|
@@ -159,19 +152,22 @@ export const RECIPES = {
|
|
|
159
152
|
title: "opencode",
|
|
160
153
|
referer: "https://opencode.ai",
|
|
161
154
|
},
|
|
162
|
-
env: (key, model) => {
|
|
155
|
+
env: (key, model, catalog = []) => {
|
|
163
156
|
const active = model || "gpt-6-sol";
|
|
164
|
-
const models = Object.fromEntries(
|
|
157
|
+
const models = Object.fromEntries(catalog.map(({ id }) => [id, {}]));
|
|
165
158
|
models[active] = {};
|
|
166
159
|
return {
|
|
167
160
|
PLEUM_API_KEY: key,
|
|
168
161
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
162
|
+
enabled_providers: ["openai"],
|
|
163
|
+
disabled_providers: [],
|
|
169
164
|
provider: {
|
|
170
165
|
openai: {
|
|
171
166
|
npm: "@ai-sdk/openai",
|
|
172
167
|
name: "PleumRouter",
|
|
173
168
|
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
174
169
|
models,
|
|
170
|
+
whitelist: Object.keys(models),
|
|
175
171
|
},
|
|
176
172
|
},
|
|
177
173
|
}),
|
|
@@ -469,9 +465,11 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
469
465
|
throw new Error("agy settings.json must be a JSON object");
|
|
470
466
|
}
|
|
471
467
|
}
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
|
|
468
|
+
const m = typeof model === "object" ? model?.cliModel || model?.model : model;
|
|
469
|
+
const settings = { ...obj, modelProvider: "gemini" };
|
|
470
|
+
if (m) settings.model = m;
|
|
471
|
+
else delete settings.model;
|
|
472
|
+
return settings;
|
|
475
473
|
}
|
|
476
474
|
|
|
477
475
|
export function piModelsJson(model) {
|