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/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
- // type "codex-home" → 격리 CODEX_HOME에 config.toml (사용자 ~/.codex 보존).
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, m) => {
46
- const isObj = m && typeof m === "object";
47
- const sonnet = (isObj ? m.sonnet : m) || "claude-sonnet-5";
48
- const opus = (isObj ? m.opus : null) || "claude-opus-5-5";
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
- 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"),
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
- // CLI 인자에 임의의 --model을 넘기지 않고 Claude Code의 내장 티어(Default/Opus)로 기동하게 하여
70
- // /model 화면에 깨끗한 5개 표준 항목(Default, Opus, Fable, Sonnet, Haiku)만 표시되도록 한다.
71
- args: () => [],
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: "codex-home",
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: (m) => {
84
- const isObj = m && typeof m === "object";
85
- const effort = isObj ? m.effort : null;
86
- return effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
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. Routes any coding model via PleumRouter.",
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
- const isObj = m && typeof m === "object";
106
- const model = (isObj ? m.model : m) || DEFAULT_MODEL;
107
- const effort = isObj ? m.effort : null;
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 블록으로만 읽는다 — env var는 없다.
119
- // 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
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 || DEFAULT_MODEL;
127
- const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
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
- pleum: {
134
- npm: "@ai-sdk/openai-compatible",
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", `pleum/${m || DEFAULT_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) || DEFAULT_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 isObj = model && typeof model === "object";
436
- const m = (isObj ? model.model : model) || DEFAULT_MODEL;
437
- 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;
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, isAutoUpdateEnabled, setAutoUpdateEnabled } from "./config.mjs";
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 readPreviousProvider(toml) {
73
- const marked = toml.match(/# previous_model_provider = (.*)/);
74
- if (marked) return marked[1].trim();
75
- const stripped = stripWireBlock(toml);
76
- const top = stripped.match(/^model_provider\s*=\s*(.+)$/m);
77
- return top ? top[1].trim() : "";
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 previous = readPreviousProvider(src);
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
- const block = `${WIRE_MARKER_START}
118
- # previous_model_provider = ${previous}
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
- [model_providers.pleum]
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.replace(/\s+$/, "");
132
- return `${trimmed ? `${trimmed}\n\n` : ""}${block}`;
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 previous = readPreviousProvider(src);
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
- if (previous && previous !== '""' && !/^model_provider\s*=/m.test(body)) {
142
- body = `model_provider = ${previous}\n${body.replace(/^\n+/, "")}`;
143
- }
144
- return body.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "");
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) {