pennyrouter 0.2.28 → 0.2.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
package/src/cli.js CHANGED
@@ -677,11 +677,10 @@ export async function install(flags = {}) {
677
677
  }
678
678
  if (supportsPennyCommand) {
679
679
  record.launch_available = true;
680
- record.launch_config_path = compactHome(
681
- harness.id === "claude-code"
682
- ? claudeLaunchSettingsPath()
683
- : harness.id === "codex" ? codexLaunchProfilePath() : null,
684
- );
680
+ const launchConfigPath = harness.id === "claude-code"
681
+ ? claudeLaunchSettingsPath()
682
+ : harness.id === "codex" ? codexLaunchProfilePath() : null;
683
+ record.launch_config_path = launchConfigPath ? compactHome(launchConfigPath) : null;
685
684
  record.default_routing = Boolean(makeDefault);
686
685
  }
687
686
  if (harness.id === "claude-code" && claudeSubscriptionAuth) {
@@ -731,7 +730,7 @@ export async function install(flags = {}) {
731
730
  console.log(color(`How to Run ${harnessName}`, "bold"));
732
731
  console.log("");
733
732
  console.log(` ${color(command.padEnd(commandWidth), "bold")}→ ${harnessName} with PennyRouter`);
734
- console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${record.default_routing ? `${harnessName} with PennyRouter (default routing enabled)` : `${harnessName} native`}`);
733
+ console.log(` ${color(native.padEnd(commandWidth), "bold")}→ ${record.default_routing ? `${harnessName}` : `${harnessName} native`}`);
735
734
  } else {
736
735
  console.log(`Installed on ${harness?.name || record.harness}${installNoteForRecord(record)}.`);
737
736
  }
@@ -11,8 +11,20 @@ import {
11
11
  const JSONC_PATH = "~/.config/opencode/opencode.jsonc";
12
12
  const JSON_PATH = "~/.config/opencode/opencode.json";
13
13
  const MODEL_STATE_PATH = "~/.local/state/opencode/model.json";
14
- const FALLBACK_MODEL_CHOICES = [
14
+ export const OPENCODE_MODEL_CHOICES = [
15
+ { id: "pennyrouter/penny-core:premium", name: "Penny Core" },
16
+ { id: "pennyrouter/penny-speed:standard", name: "Penny Speed" },
17
+ { id: "pennyrouter/penny-power:premium", name: "Penny Power" },
15
18
  { id: "pennyrouter/auto", name: "PennyRouter Auto" },
19
+ { id: "claude-penny-deepseek-v4-flash", name: "DeepSeek V4 Flash" },
20
+ { id: "claude-penny-glm-5-3", name: "GLM 5.3" },
21
+ { id: "claude-penny-gpt-5-6-luna", name: "GPT-5.6 Luna" },
22
+ { id: "claude-penny-grok-4-6", name: "Grok 4.6" },
23
+ { id: "claude-penny-kimi-k3", name: "Kimi K3" },
24
+ { id: "claude-penny-ling-3-flash", name: "Ling-3.0 Flash" },
25
+ { id: "claude-penny-minimax-m3", name: "MiniMax M3" },
26
+ { id: "claude-penny-nemotron-ultra-free", name: "Nemotron 3 Ultra (free)" },
27
+ { id: "claude-penny-solar-pro4", name: "Solar Pro 4" },
16
28
  ];
17
29
 
18
30
  export const opencodeHarness = {
@@ -30,7 +42,7 @@ export const opencodeHarness = {
30
42
  summary: [
31
43
  `write ${configPath}`,
32
44
  "merge PennyRouter provider without replacing existing providers",
33
- "publish the four Penny model choices",
45
+ "publish the curated Penny bundles and individual model pins",
34
46
  `reset ${MODEL_STATE_PATH} so opencode rediscovers configured models`,
35
47
  ],
36
48
  };
@@ -49,7 +61,7 @@ export const opencodeHarness = {
49
61
  apiKey,
50
62
  },
51
63
  models: Object.fromEntries(
52
- (models || await fetchOpenCodeModels(gatewayBaseUrl)).map((model) => [
64
+ (models || OPENCODE_MODEL_CHOICES).map((model) => [
53
65
  opencodeModelId(model.id),
54
66
  { name: model.name, ...(model.description ? { description: model.description } : {}) },
55
67
  ]),
@@ -160,18 +172,23 @@ export async function fetchOpenCodeModels(gatewayBaseUrl) {
160
172
  const response = await fetch(`${String(gatewayBaseUrl).replace(/\/+$/, "")}/v1/models`);
161
173
  if (!response.ok) throw new Error(`model catalog returned ${response.status}`);
162
174
  const payload = await response.json();
163
- const models = Array.isArray(payload?.data)
164
- ? payload.data
165
- .filter((model) => typeof model?.id === "string" && model.id.startsWith("pennyrouter/"))
166
- .map((model) => ({
167
- id: model.id,
168
- name: model.display_name || openCodeModelName(model.id),
169
- description: model.description,
170
- }))
171
- : [];
175
+ const catalog = new Map(
176
+ Array.isArray(payload?.data)
177
+ ? payload.data
178
+ .filter((model) => typeof model?.id === "string")
179
+ .map((model) => [model.id, model])
180
+ : [],
181
+ );
182
+ const models = OPENCODE_MODEL_CHOICES.map((choice) => {
183
+ const model = catalog.get(choice.id);
184
+ return {
185
+ ...choice,
186
+ ...(model?.description ? { description: model.description } : {}),
187
+ };
188
+ });
172
189
  if (models.length > 0) return models;
173
190
  } catch {}
174
- return FALLBACK_MODEL_CHOICES;
191
+ return OPENCODE_MODEL_CHOICES;
175
192
  }
176
193
 
177
194
  function openCodeModelName(modelId) {
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
- import { opencodeHarness, opencodeModelId } from "./opencode.js";
6
+ import { opencodeHarness, opencodeModelId, OPENCODE_MODEL_CHOICES } from "./opencode.js";
7
7
 
8
8
  const ENV_KEYS = ["HOME", "XDG_STATE_HOME"];
9
9
 
@@ -39,21 +39,11 @@ async function main() {
39
39
  apiKey: "pr-test",
40
40
  gatewayBaseUrl: "https://api.pennyrouter.com/",
41
41
  plan,
42
- models: [
43
- { id: "pennyrouter/penny-core:premium", name: "Penny Core" },
44
- { id: "pennyrouter/penny-speed:standard", name: "Penny Speed" },
45
- { id: "pennyrouter/penny-power:premium", name: "Penny Power" },
46
- { id: "pennyrouter/auto", name: "Penny Custom" },
47
- ],
42
+ models: OPENCODE_MODEL_CHOICES,
48
43
  });
49
44
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
50
45
  assert.equal(config.provider.custom.models["model-a"] !== undefined, true);
51
- assert.deepEqual(Object.keys(config.provider.pennyrouter.models), [
52
- "pennyrouter/penny-core:premium",
53
- "pennyrouter/penny-speed:standard",
54
- "pennyrouter/penny-power:premium",
55
- "pennyrouter/auto",
56
- ]);
46
+ assert.deepEqual(Object.keys(config.provider.pennyrouter.models), OPENCODE_MODEL_CHOICES.map((model) => model.id));
57
47
  assert.equal(config.provider.pennyrouter.options.baseURL, "https://api.pennyrouter.com/v1");
58
48
  assert.deepEqual(JSON.parse(fs.readFileSync(modelStatePath, "utf8")).recent, ["custom/model-a"]);
59
49
 
@@ -96,6 +96,7 @@ async function main() {
96
96
 
97
97
  const config = await loadPennyConfig();
98
98
  assert.equal(config.api_key, "pr-super-secret");
99
+ assert.equal(config.harnesses.opencode.enabled, true);
99
100
  const codexProfile = fs.readFileSync(codexLaunchProfilePath(), "utf8");
100
101
  assert.ok(codexProfile.includes('env_http_headers = { "x-pennyrouter-key" = "PENNYROUTER_API_KEY" }'));
101
102
  assert.ok(codexProfile.includes("[tui]\nterminal_title = []"));
@@ -122,6 +123,7 @@ async function main() {
122
123
  assert.deepEqual(codex.args, ["--profile", "pennyrouter", "resume", "--last"]);
123
124
  assert.equal(codex.env.PENNYROUTER_API_KEY, "pr-super-secret");
124
125
  const opencode = buildPennyInvocation("opencode", ["--continue"], config);
126
+ assert.equal(opencode.env.PENNYROUTER_API_KEY, "pr-super-secret");
125
127
  assert.deepEqual(opencode, {
126
128
  command: "opencode",
127
129
  args: ["--continue"],