pleumrouter 0.5.36 → 0.5.39

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/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, cfg) => {
39
- if (explicit && typeof explicit === "object") return explicit;
40
- return {
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, m) => {
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
- ANTHROPIC_DEFAULT_HAIKU_MODEL: toAnthropic(haiku),
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
- // CLI 인자에 임의의 --model을 넘기지 않고 Claude Code의 내장 티어(Default/Opus)로 기동하게 하여
81
- // /model 화면에 깨끗한 5개 표준 항목(Default, Opus, Fable, Sonnet, Haiku)만 표시되도록 한다.
82
- args: () => [],
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, cfg) => explicit && typeof explicit === "object"
90
+ resolveModel: (explicit) => explicit && typeof explicit === "object"
90
91
  ? explicit
91
- : { model: explicit || cfg?.launch_models?.codex || "gpt-6-sol", effort: cfg?.codex_effort || "medium" },
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
- defaultModel: "gemini-3.8-flash",
122
- resolveModel: (explicit, cfg) => explicit && typeof explicit === "object"
120
+ resolveModel: (explicit) => explicit && typeof explicit === "object"
123
121
  ? explicit
124
- : { model: explicit || cfg?.launch_models?.agy || "gemini-3.8-flash", effort: cfg?.agy_effort || "high" },
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. Routes any coding model via PleumRouter.",
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
- const isObj = m && typeof m === "object";
138
- const model = (isObj ? m.model : m) || "gemini-3.8-flash";
139
- const effort = isObj ? m.effort : null;
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(CORE_MODELS.map((id) => [id, {}]));
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 isObj = model && typeof model === "object";
473
- const m = (isObj ? model.model : model) || "gemini-3.8-flash";
474
- return { ...obj, modelProvider: "gemini", model: m };
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) {
package/src/settings.mjs CHANGED
@@ -13,7 +13,6 @@ import { getLocale, LANGS, t } from "./i18n.mjs";
13
13
  import { RECIPES } from "./recipes.mjs";
14
14
  import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
15
15
  import { login, logout } from "./login.mjs";
16
- import { upgrade } from "./upgrade.mjs";
17
16
  import { completion } from "./completion.mjs";
18
17
  import { lang } from "./lang.mjs";
19
18
  import { switchCreditWorkspace } from "./balance.mjs";
package/src/update.mjs CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { t } from "./i18n.mjs";
6
+ import { resolveLaunchCommand } from "./launch.mjs";
6
7
 
7
8
  const REGISTRY_URL = "https://registry.npmjs.org/pleumrouter/latest";
8
9
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1시간 캐시
@@ -112,10 +113,14 @@ export async function checkLatestVersion({
112
113
  };
113
114
  }
114
115
 
115
- export function performUpdate({ bin = null } = {}) {
116
- const npmCmd = bin || (process.platform === "win32" ? "npm.cmd" : "npm");
116
+ export function performUpdate({ bin = null, resolve = resolveLaunchCommand, spawn: spawnImpl = spawn } = {}) {
117
+ // Windows에서 npm.cmd 를 shell 없이 spawn 하면 EINVAL(Node 18.20+) — 공용
118
+ // 경로로 node 진입점을 찾아 우회한다(#10). 비윈도는 명령을 그대로 둔다.
119
+ const target = bin
120
+ ? { command: bin, args: ["install", "-g", "pleumrouter@latest"] }
121
+ : resolve("npm", ["install", "-g", "pleumrouter@latest"]);
117
122
  return new Promise((resolve) => {
118
- const child = spawn(npmCmd, ["install", "-g", "pleumrouter@latest"], {
123
+ const child = spawnImpl(target.command, target.args, {
119
124
  stdio: "inherit",
120
125
  shell: false,
121
126
  });
package/src/wire.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { ROOT, V1, authFetch } from "./config.mjs";
5
5
  import { tomlBasicString } from "./recipes.mjs";
6
6
  import { t } from "./i18n.mjs";
@@ -40,9 +40,19 @@ function readText(path) {
40
40
  }
41
41
  }
42
42
 
43
- function writeSecretFile(path, contents, mode = 0o600) {
43
+ export function writeSecretFile(path, contents, mode = 0o600) {
44
44
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
45
- writeFileSync(path, contents, { mode });
45
+ // 기존 파일이 0644 등으로 있으면 writeFileSync의 mode는 무시된다(#41) —
46
+ // tmp(강제 chmod) + rename 원자적 교체로 결과물이 항상 mode가 되게 한다.
47
+ const tmp = `${path}.pleum-tmp-${process.pid}`;
48
+ writeFileSync(tmp, contents, { mode });
49
+ try {
50
+ chmodSync(tmp, mode);
51
+ renameSync(tmp, path);
52
+ } catch (e) {
53
+ try { unlinkSync(tmp); } catch {} // 키 평문 잔여를 남기지 않는다
54
+ throw e;
55
+ }
46
56
  }
47
57
 
48
58
  function backupOnce(src, bak) {
package/src/upgrade.mjs DELETED
@@ -1,10 +0,0 @@
1
- import { performUpdate, runUpdateCommand } from "./update.mjs";
2
-
3
- export { performUpdate, runUpdateCommand };
4
-
5
- // 최신 버전으로 전역 재설치. 하위 호환을 위해 유지하며 process.exit() 호출 규약을 보존한다.
6
- export async function upgrade() {
7
- const code = await runUpdateCommand(["--force"], { exit: false });
8
- process.exit(code ?? 0);
9
- }
10
-