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 +1 -1
- package/src/cli.js +5 -6
- package/src/harnesses/opencode.js +30 -13
- package/src/harnesses/opencode.test.js +3 -13
- package/src/launch.test.js +2 -0
package/package.json
CHANGED
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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}
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
|
package/src/launch.test.js
CHANGED
|
@@ -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"],
|