pleumrouter 0.5.35 → 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 +14 -9
- package/bin/pleum.mjs +17 -4
- package/package.json +1 -1
- package/src/completion.mjs +1 -1
- package/src/launch.mjs +159 -28
- 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 +70 -252
- package/src/models.mjs +9 -133
- package/src/recipes.mjs +73 -38
- package/src/settings.mjs +1 -13
- package/src/wire.mjs +37 -21
package/src/recipes.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const CORE_MODELS = [
|
|
|
17
17
|
|
|
18
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
19
19
|
// type "env" → 환경변수만 세팅하고 바로 실행.
|
|
20
|
-
//
|
|
20
|
+
// Codex는 -c config override로 사용자 CODEX_HOME을 유지하면서 provider만 이번 실행에 덮어쓴다.
|
|
21
21
|
// type "pi-home" → 격리 PI_CODING_AGENT_DIR에 models.json (사용자 ~/.pi 보존).
|
|
22
22
|
// type "crush-home" → 격리 CRUSH_GLOBAL_CONFIG 디렉터리에 crush.json (사용자 crush.json 보존).
|
|
23
23
|
// type "agy-settings" → ~/.gemini/antigravity-cli/settings.json 에 modelProvider=gemini 병합.
|
|
@@ -34,6 +34,10 @@ export const RECIPES = {
|
|
|
34
34
|
label: "Claude Code",
|
|
35
35
|
bin: "claude",
|
|
36
36
|
type: "env",
|
|
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" },
|
|
37
41
|
install: "npm i -g @anthropic-ai/claude-code",
|
|
38
42
|
// 앱 랭킹 귀속 메타 — 백엔드 featured_agents / UA fallback과 동기화.
|
|
39
43
|
attribution: {
|
|
@@ -42,56 +46,82 @@ export const RECIPES = {
|
|
|
42
46
|
},
|
|
43
47
|
// 함정: ANTHROPIC_BASE_URL은 반드시 루트(/v1 없이) — CLI가 /v1/messages를 스스로 덧붙인다.
|
|
44
48
|
// /v1을 넣으면 /v1/v1/messages가 되어 실패.
|
|
45
|
-
env: (key,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const haiku = (isObj ? m.haiku : null) || DEFAULT_MODEL;
|
|
50
|
-
const toAnthropic = (x) => (x.startsWith("anthropic/") ? x : `anthropic/${x}`);
|
|
49
|
+
env: (key, selection) => {
|
|
50
|
+
// Claude 모델의 provider-qualified ID만 보정한다. 다른 회사 모델에
|
|
51
|
+
// anthropic/을 붙이면 카탈로그에 없는 ID가 되어 요청이 실패한다.
|
|
52
|
+
const toAnthropic = (x) => /^claude-/.test(x) ? `anthropic/${x}` : x;
|
|
51
53
|
return {
|
|
52
54
|
ANTHROPIC_BASE_URL: ROOT,
|
|
53
55
|
ANTHROPIC_AUTH_TOKEN: key,
|
|
54
56
|
// CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY는 의도적으로 설정하지 않는다:
|
|
55
57
|
// 활성화 시 Claude Code가 GET /v1/models를 호출해 카탈로그 200+개 모델을
|
|
56
58
|
// /model 메뉴에 쏟아부어 화면이 난잡해지는 원인이 된다.
|
|
57
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic(sonnet),
|
|
59
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic("claude-sonnet-5"),
|
|
58
60
|
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: "Sonnet",
|
|
59
61
|
ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION: "Sonnet 5 · Efficient for routine tasks",
|
|
60
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic(opus),
|
|
62
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic("claude-opus-5-5"),
|
|
61
63
|
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: "Opus",
|
|
62
64
|
ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION: "Opus 5.5 · Best for everyday, complex tasks",
|
|
63
|
-
|
|
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"),
|
|
64
69
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: "Haiku",
|
|
65
70
|
ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION: "Haiku 4.5 · Fastest for quick answers",
|
|
66
|
-
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
|
+
} : {}),
|
|
67
79
|
};
|
|
68
80
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
args: (selection) => ["--model", selection?.mode === "custom"
|
|
82
|
+
? (/^claude-/.test(selection.model) ? `anthropic/${selection.model}` : selection.model)
|
|
83
|
+
: "sonnet"],
|
|
72
84
|
},
|
|
73
85
|
codex: {
|
|
74
86
|
label: "Codex CLI",
|
|
75
87
|
bin: "codex",
|
|
76
|
-
type: "
|
|
88
|
+
type: "env",
|
|
89
|
+
defaultModel: "gpt-6-sol",
|
|
90
|
+
resolveModel: (explicit) => explicit && typeof explicit === "object"
|
|
91
|
+
? explicit
|
|
92
|
+
: { mode: explicit ? "custom" : "native", model: explicit || "gpt-6-sol" },
|
|
77
93
|
install: "npm i -g @openai/codex",
|
|
78
94
|
attribution: {
|
|
79
95
|
title: "Codex",
|
|
80
96
|
referer: "https://github.com/openai/codex",
|
|
81
97
|
},
|
|
82
98
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
83
|
-
args: (
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
99
|
+
args: (model) => {
|
|
100
|
+
const m = model && typeof model === "object" ? model.model : model;
|
|
101
|
+
const provider = {
|
|
102
|
+
name: "PleumRouter",
|
|
103
|
+
base_url: V1,
|
|
104
|
+
env_key: "PLEUM_API_KEY",
|
|
105
|
+
wire_api: "responses",
|
|
106
|
+
supports_websockets: false,
|
|
107
|
+
};
|
|
108
|
+
const providerToml = `{ ${Object.entries(provider).map(([k, v]) => `${k} = ${typeof v === "boolean" ? v : tomlBasicString(v)}`).join(", ")} }`;
|
|
109
|
+
return [
|
|
110
|
+
"-c", 'model_provider="pleum"',
|
|
111
|
+
"-c", `model_providers.pleum=${providerToml}`,
|
|
112
|
+
"--model", m || "gpt-6-sol",
|
|
113
|
+
];
|
|
87
114
|
},
|
|
88
115
|
},
|
|
89
116
|
agy: {
|
|
90
117
|
label: "Antigravity CLI",
|
|
91
118
|
bin: "agy",
|
|
92
119
|
type: "agy-settings",
|
|
120
|
+
resolveModel: (explicit) => explicit && typeof explicit === "object"
|
|
121
|
+
? explicit
|
|
122
|
+
: explicit ? { mode: "custom", model: explicit } : { mode: "native" },
|
|
93
123
|
install: "https://antigravity.google/cli/install.sh",
|
|
94
|
-
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.",
|
|
95
125
|
attribution: {
|
|
96
126
|
title: "Antigravity CLI",
|
|
97
127
|
referer: "https://www.antigravity.google/docs/cli/install",
|
|
@@ -102,45 +132,48 @@ export const RECIPES = {
|
|
|
102
132
|
GOOGLE_API_KEY: key,
|
|
103
133
|
}),
|
|
104
134
|
args: (m) => {
|
|
105
|
-
|
|
106
|
-
const model =
|
|
107
|
-
|
|
108
|
-
const res = ["--model", model];
|
|
109
|
-
if (effort) res.push("--effort", effort);
|
|
110
|
-
return res;
|
|
135
|
+
if (!m || m.mode === "native") return [];
|
|
136
|
+
const model = typeof m === "object" ? (m.cliModel || m.model) : m;
|
|
137
|
+
return ["--model", model.replace(/^google\//, "")];
|
|
111
138
|
},
|
|
112
139
|
},
|
|
113
140
|
opencode: {
|
|
114
141
|
label: "opencode",
|
|
115
142
|
bin: "opencode",
|
|
116
143
|
type: "env",
|
|
144
|
+
defaultModel: "gpt-6-sol",
|
|
117
145
|
install: "npm i -g opencode-ai@latest",
|
|
118
|
-
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만
|
|
119
|
-
//
|
|
146
|
+
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다.
|
|
147
|
+
// OpenCode는 openai 공급자 ID에만 Responses용 getModel을 등록한다.
|
|
148
|
+
// 사용자 설정 파일을 수정하지 않고 이번 프로세스의 openai 공급자를 Pleum으로 덮는다.
|
|
149
|
+
// OPENCODE_CONFIG_CONTENT로 이번 실행에만 공급자를 주입한다.
|
|
120
150
|
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
121
151
|
attribution: {
|
|
122
152
|
title: "opencode",
|
|
123
153
|
referer: "https://opencode.ai",
|
|
124
154
|
},
|
|
125
|
-
env: (key, model) => {
|
|
126
|
-
const active = model ||
|
|
127
|
-
const models = Object.fromEntries(
|
|
155
|
+
env: (key, model, catalog = []) => {
|
|
156
|
+
const active = model || "gpt-6-sol";
|
|
157
|
+
const models = Object.fromEntries(catalog.map(({ id }) => [id, {}]));
|
|
128
158
|
models[active] = {};
|
|
129
159
|
return {
|
|
130
160
|
PLEUM_API_KEY: key,
|
|
131
161
|
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
162
|
+
enabled_providers: ["openai"],
|
|
163
|
+
disabled_providers: [],
|
|
132
164
|
provider: {
|
|
133
|
-
|
|
134
|
-
npm: "@ai-sdk/openai
|
|
165
|
+
openai: {
|
|
166
|
+
npm: "@ai-sdk/openai",
|
|
135
167
|
name: "PleumRouter",
|
|
136
168
|
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
137
169
|
models,
|
|
170
|
+
whitelist: Object.keys(models),
|
|
138
171
|
},
|
|
139
172
|
},
|
|
140
173
|
}),
|
|
141
174
|
};
|
|
142
175
|
},
|
|
143
|
-
args: (m) => ["--model", `
|
|
176
|
+
args: (m) => ["--model", `openai/${m || "gpt-6-sol"}`],
|
|
144
177
|
},
|
|
145
178
|
aider: {
|
|
146
179
|
label: "Aider",
|
|
@@ -408,7 +441,7 @@ export function tomlBasicString(value) {
|
|
|
408
441
|
|
|
409
442
|
export function codexToml(model, effort) {
|
|
410
443
|
const isObj = model && typeof model === "object";
|
|
411
|
-
const m = (isObj ? model.model : model) ||
|
|
444
|
+
const m = (isObj ? model.model : model) || "gpt-6-sol";
|
|
412
445
|
const eff = (isObj ? model.effort : effort) || null;
|
|
413
446
|
const effortLine = eff ? `model_reasoning_effort = ${tomlBasicString(eff)}\n` : "";
|
|
414
447
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
@@ -432,9 +465,11 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
432
465
|
throw new Error("agy settings.json must be a JSON object");
|
|
433
466
|
}
|
|
434
467
|
}
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
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;
|
|
438
473
|
}
|
|
439
474
|
|
|
440
475
|
export function piModelsJson(model) {
|
package/src/settings.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { style, sanitizeTerminalText } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
|
-
import { readConfig, writeConfig, isLoggedIn, getActiveContext
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
13
13
|
import { RECIPES } from "./recipes.mjs";
|
|
14
14
|
import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
|
|
@@ -27,7 +27,6 @@ export function buildSettingsItems({
|
|
|
27
27
|
keyPrefix,
|
|
28
28
|
version,
|
|
29
29
|
activeContext = { type: "personal" },
|
|
30
|
-
autoUpdate = true,
|
|
31
30
|
latestVersion = null,
|
|
32
31
|
}) {
|
|
33
32
|
const contextDesc = activeContext?.type === "team"
|
|
@@ -51,11 +50,6 @@ export function buildSettingsItems({
|
|
|
51
50
|
: `${t("Logged in")} (${t("key")} ${keyPrefix}…)`
|
|
52
51
|
: t("Not logged in"),
|
|
53
52
|
},
|
|
54
|
-
{
|
|
55
|
-
id: "settings:auto_update",
|
|
56
|
-
title: t("Auto update"),
|
|
57
|
-
desc: autoUpdate ? t("Enabled (updates automatically on start)") : t("Disabled"),
|
|
58
|
-
},
|
|
59
53
|
{ id: "settings:update", title: t("Update CLI"), desc: updateDesc },
|
|
60
54
|
{ id: "settings:completion", title: t("Shell completion"), desc: t("Print a zsh/bash completion script") },
|
|
61
55
|
];
|
|
@@ -101,7 +95,6 @@ async function settingsSelect() {
|
|
|
101
95
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
102
96
|
version: readVersion(),
|
|
103
97
|
activeContext: getActiveContext(cfg),
|
|
104
|
-
autoUpdate: isAutoUpdateEnabled(cfg),
|
|
105
98
|
latestVersion: updateInfo?.latestVersion,
|
|
106
99
|
});
|
|
107
100
|
return runList(items, {
|
|
@@ -188,11 +181,6 @@ export async function runSettings() {
|
|
|
188
181
|
case "settings:account":
|
|
189
182
|
await runAccount();
|
|
190
183
|
continue;
|
|
191
|
-
case "settings:auto_update": {
|
|
192
|
-
const next = !isAutoUpdateEnabled();
|
|
193
|
-
setAutoUpdateEnabled(next);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
184
|
case "settings:update":
|
|
197
185
|
console.log(t("Updating pleumrouter CLI..."));
|
|
198
186
|
await runUpdateCommand([], { exit: false });
|
package/src/wire.mjs
CHANGED
|
@@ -69,12 +69,22 @@ function removeTomlTable(toml, name) {
|
|
|
69
69
|
return out.join("\n");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
function rootValue(toml, name) {
|
|
73
|
+
const root = toml.split(/^\s*\[/m, 1)[0];
|
|
74
|
+
const found = root.match(new RegExp(`^${name}\\s*=\\s*(.+)$`, "m"));
|
|
75
|
+
return found?.[1]?.trim() ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function previousValue(toml, name) {
|
|
79
|
+
const marked = toml.match(new RegExp(`^# previous_${name} = (.*)$`, "m"));
|
|
80
|
+
return marked ? marked[1].trim() : rootValue(stripWireBlock(toml), name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeRootValue(toml, name) {
|
|
84
|
+
const firstTable = toml.search(/^\s*\[/m);
|
|
85
|
+
const root = firstTable < 0 ? toml : toml.slice(0, firstTable);
|
|
86
|
+
const rest = firstTable < 0 ? "" : toml.slice(firstTable);
|
|
87
|
+
return root.replace(new RegExp(`^${name}\\s*=\\s*.+\\n?`, "m"), "") + rest;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
export function wireClaudeSettings(existingJson, { root, key }) {
|
|
@@ -107,18 +117,22 @@ export function unwireClaudeSettings(existingJson) {
|
|
|
107
117
|
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
export function wireCodexToml(existingToml, { v1, key }) {
|
|
120
|
+
export function wireCodexToml(existingToml, { v1, key, model = "gpt-6-sol" }) {
|
|
111
121
|
const src = existingToml ?? "";
|
|
112
|
-
const
|
|
122
|
+
const previousProvider = previousValue(src, "model_provider");
|
|
123
|
+
const previousModel = previousValue(src, "model");
|
|
113
124
|
let body = stripWireBlock(src);
|
|
114
|
-
body = body.replace(/^model_provider\s*=\s*.+\n?/m, "");
|
|
115
125
|
body = removeTomlTable(body, "model_providers.pleum.http_headers");
|
|
116
126
|
body = removeTomlTable(body, "model_providers.pleum");
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
body = removeRootValue(removeRootValue(body, "model_provider"), "model");
|
|
128
|
+
const rootBlock = `${WIRE_MARKER_START}
|
|
129
|
+
# previous_model_provider = ${previousProvider}
|
|
130
|
+
# previous_model = ${previousModel}
|
|
131
|
+
model = ${tomlBasicString(model)}
|
|
119
132
|
model_provider = "pleum"
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
${WIRE_MARKER_END}
|
|
134
|
+
`;
|
|
135
|
+
const providerBlock = `[model_providers.pleum]
|
|
122
136
|
name = "PleumRouter"
|
|
123
137
|
base_url = ${tomlBasicString(v1)}
|
|
124
138
|
wire_api = "responses"
|
|
@@ -126,22 +140,24 @@ supports_websockets = false
|
|
|
126
140
|
|
|
127
141
|
[model_providers.pleum.http_headers]
|
|
128
142
|
Authorization = ${tomlBasicString(`Bearer ${key}`)}
|
|
129
|
-
${WIRE_MARKER_END}
|
|
130
143
|
`;
|
|
131
|
-
const trimmed = body.
|
|
132
|
-
return `${trimmed ?
|
|
144
|
+
const trimmed = body.trim();
|
|
145
|
+
return `${rootBlock}${trimmed ? `\n${trimmed}\n\n` : "\n"}${providerBlock}`;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
export function unwireCodexToml(existingToml) {
|
|
136
149
|
const src = existingToml ?? "";
|
|
137
|
-
const
|
|
150
|
+
const previousProvider = previousValue(src, "model_provider");
|
|
151
|
+
const previousModel = previousValue(src, "model");
|
|
138
152
|
let body = stripWireBlock(src);
|
|
139
153
|
body = removeTomlTable(body, "model_providers.pleum.http_headers");
|
|
140
154
|
body = removeTomlTable(body, "model_providers.pleum");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
body = removeRootValue(removeRootValue(body, "model_provider"), "model");
|
|
156
|
+
const restored = [
|
|
157
|
+
previousModel ? `model = ${previousModel}` : "",
|
|
158
|
+
previousProvider ? `model_provider = ${previousProvider}` : "",
|
|
159
|
+
].filter(Boolean).join("\n");
|
|
160
|
+
return `${restored ? `${restored}\n\n` : ""}${body.trim()}`.replace(/\n{3,}/g, "\n\n").trimStart() + "\n";
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
function parseJsonBody(res) {
|