pennyrouter 0.3.25 → 0.3.27
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 +35 -8
- package/src/harnesses/claude-code.js +32 -14
- package/src/harnesses/codex.js +23 -7
- package/src/harnesses/codex.test.js +1 -1
- package/src/launch.js +14 -13
- package/src/launch.test.js +28 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -19,7 +19,13 @@ import {
|
|
|
19
19
|
} from "./harnesses/claude-code.js";
|
|
20
20
|
import { clineHarness } from "./harnesses/cline.js";
|
|
21
21
|
import { codeGptHarness } from "./harnesses/codegpt.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
codexHarness,
|
|
24
|
+
detectCodexChatgptAuth,
|
|
25
|
+
isCodexSubscriptionAuthGateway,
|
|
26
|
+
isHostedPennyRouterGateway,
|
|
27
|
+
isLocalPennyRouterGateway,
|
|
28
|
+
} from "./harnesses/codex.js";
|
|
23
29
|
import { fetchOpenCodeModels, opencodeHarness } from "./harnesses/opencode.js";
|
|
24
30
|
import { windsurfHarness } from "./harnesses/windsurf.js";
|
|
25
31
|
import { zedHarness } from "./harnesses/zed.js";
|
|
@@ -38,8 +44,12 @@ import {
|
|
|
38
44
|
storeUpstreamCredential,
|
|
39
45
|
} from "./session.js";
|
|
40
46
|
|
|
41
|
-
async function codexNativeAuthAvailable({ gatewayBaseUrl, apiKey, localAuth }) {
|
|
42
|
-
if (
|
|
47
|
+
async function codexNativeAuthAvailable({ gatewayBaseUrl, apiKey, localAuth, privateGateway = false }) {
|
|
48
|
+
if (privateGateway) return false;
|
|
49
|
+
if (!isCodexSubscriptionAuthGateway(gatewayBaseUrl)) return false;
|
|
50
|
+
// A local gateway should use Codex's detected login as the opt-in. Do not infer
|
|
51
|
+
// subscription auth from some other upstream credential stored on the local gateway.
|
|
52
|
+
if (isLocalPennyRouterGateway(gatewayBaseUrl)) return Boolean(localAuth);
|
|
43
53
|
const capabilities = await fetchGatewayCapabilities({ gatewayBaseUrl, apiKey });
|
|
44
54
|
const hasOpenAiCredential = Array.isArray(capabilities?.upstream_credentials)
|
|
45
55
|
&& capabilities.upstream_credentials.includes("openai_api");
|
|
@@ -535,7 +545,7 @@ async function authProvider(flags) {
|
|
|
535
545
|
apiKey: pennyKey,
|
|
536
546
|
gatewayBaseUrl,
|
|
537
547
|
subscriptionAuth: true,
|
|
538
|
-
models: CLAUDE_CODE_MODELS,
|
|
548
|
+
models: flags.profile === "private" ? null : CLAUDE_CODE_MODELS,
|
|
539
549
|
profile: flags.profile,
|
|
540
550
|
});
|
|
541
551
|
let record = existingRecord || {
|
|
@@ -552,6 +562,7 @@ async function authProvider(flags) {
|
|
|
552
562
|
...record,
|
|
553
563
|
...(await claudeCodeHarness.installStoredToken({
|
|
554
564
|
apiKey: pennyKey, gatewayBaseUrl, plan, existingRecord,
|
|
565
|
+
privateGateway: flags.profile === "private",
|
|
555
566
|
})),
|
|
556
567
|
};
|
|
557
568
|
}
|
|
@@ -883,6 +894,7 @@ export async function install(flags = {}) {
|
|
|
883
894
|
? await codexNativeAuthAvailable({
|
|
884
895
|
gatewayBaseUrl: gatewayBaseUrl,
|
|
885
896
|
apiKey: claim.api_key,
|
|
897
|
+
privateGateway: flags.privateGatewayFlow,
|
|
886
898
|
localAuth: harnessNeededAuth
|
|
887
899
|
? funding.codexSubscriptionAuth
|
|
888
900
|
: Boolean(existingRecord?.openai_subscription_auth),
|
|
@@ -898,7 +910,11 @@ export async function install(flags = {}) {
|
|
|
898
910
|
subscriptionAuth: harness.id === "claude-code"
|
|
899
911
|
? claudeSubscriptionAuth
|
|
900
912
|
: codexSubscriptionAuth,
|
|
901
|
-
|
|
913
|
+
privateGateway: flags.privateGatewayFlow,
|
|
914
|
+
// Private gateways advertise their complete upstream catalog. Leave the
|
|
915
|
+
// Claude defaults unset there so the picker does not merge hosted-only ids.
|
|
916
|
+
models: harness.id === "claude-code" && !flags.privateGatewayFlow
|
|
917
|
+
? CLAUDE_CODE_MODELS : null,
|
|
902
918
|
profile: flags.profile,
|
|
903
919
|
});
|
|
904
920
|
}
|
|
@@ -911,9 +927,11 @@ export async function install(flags = {}) {
|
|
|
911
927
|
record = harness.id === "claude-code" && claudeSubscriptionAuth
|
|
912
928
|
? await harness.installStoredToken({
|
|
913
929
|
apiKey: claim.api_key, gatewayBaseUrl, plan, existingRecord,
|
|
930
|
+
privateGateway: flags.privateGatewayFlow,
|
|
914
931
|
})
|
|
915
932
|
: await harness.install({
|
|
916
933
|
apiKey: claim.api_key, gatewayBaseUrl, plan, existingRecord, models: opencodeModels,
|
|
934
|
+
privateGateway: flags.privateGatewayFlow,
|
|
917
935
|
});
|
|
918
936
|
} else {
|
|
919
937
|
if (hadGlobalRouting) {
|
|
@@ -1364,6 +1382,7 @@ async function update(flags) {
|
|
|
1364
1382
|
? await codexNativeAuthAvailable({
|
|
1365
1383
|
gatewayBaseUrl,
|
|
1366
1384
|
apiKey,
|
|
1385
|
+
privateGateway: profile === "private",
|
|
1367
1386
|
localAuth: await detectCodexChatgptAuth(),
|
|
1368
1387
|
})
|
|
1369
1388
|
: false;
|
|
@@ -1375,7 +1394,9 @@ async function update(flags) {
|
|
|
1375
1394
|
subscriptionAuth: record.harness === "claude-code"
|
|
1376
1395
|
? claudeSubscriptionAuth
|
|
1377
1396
|
: codexSubscriptionAuth,
|
|
1378
|
-
|
|
1397
|
+
privateGateway: profile === "private",
|
|
1398
|
+
models: record.harness === "claude-code" && profile !== "private"
|
|
1399
|
+
? CLAUDE_CODE_MODELS : null,
|
|
1379
1400
|
profile,
|
|
1380
1401
|
});
|
|
1381
1402
|
}
|
|
@@ -1387,8 +1408,14 @@ async function update(flags) {
|
|
|
1387
1408
|
// (previous_model_provider, claude_ai_connectors_before) survives the rewrite
|
|
1388
1409
|
// and a later uninstall still restores the user's original values.
|
|
1389
1410
|
const installed = record.harness === "claude-code" && claudeSubscriptionAuth
|
|
1390
|
-
? await harness.installStoredToken({
|
|
1391
|
-
|
|
1411
|
+
? await harness.installStoredToken({
|
|
1412
|
+
apiKey, gatewayBaseUrl, plan, existingRecord: record,
|
|
1413
|
+
privateGateway: profile === "private",
|
|
1414
|
+
})
|
|
1415
|
+
: await harness.install({
|
|
1416
|
+
apiKey, gatewayBaseUrl, plan, existingRecord: record,
|
|
1417
|
+
privateGateway: profile === "private",
|
|
1418
|
+
});
|
|
1392
1419
|
next = { ...record, ...installed, installed_at: record.installed_at };
|
|
1393
1420
|
}
|
|
1394
1421
|
|
|
@@ -79,7 +79,7 @@ export const claudeCodeHarness = {
|
|
|
79
79
|
};
|
|
80
80
|
},
|
|
81
81
|
|
|
82
|
-
async install({ apiKey, gatewayBaseUrl, plan, existingRecord }) {
|
|
82
|
+
async install({ apiKey, gatewayBaseUrl, plan, existingRecord, privateGateway = false }) {
|
|
83
83
|
const backupPath = await backupFile(CONFIG_PATH, "claude-code");
|
|
84
84
|
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
85
85
|
const claudeAiConnectorsBefore = disableClaudeAiConnectors(current, existingRecord);
|
|
@@ -90,13 +90,23 @@ export const claudeCodeHarness = {
|
|
|
90
90
|
current.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
91
91
|
delete current.env.ANTHROPIC_API_KEY;
|
|
92
92
|
current.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
if (privateGateway) {
|
|
94
|
+
// A private gateway's /v1/models response is the complete model catalog. Do not
|
|
95
|
+
// merge the hosted Claude defaults into that picker: they may not exist upstream.
|
|
96
|
+
for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
|
|
97
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`];
|
|
98
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`];
|
|
99
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// Claude Code displays these env values directly in its model picker; the
|
|
103
|
+
// gateway decodes the friendly labels into bundle/profile overrides.
|
|
104
|
+
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
105
|
+
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
106
|
+
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
107
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
108
|
+
writeClaudeCodeModelPresentation(current.env);
|
|
109
|
+
}
|
|
100
110
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
|
101
111
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
|
|
102
112
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
|
|
@@ -136,7 +146,7 @@ export const claudeCodeHarness = {
|
|
|
136
146
|
// config therefore carries only the pr- bearer + base URL, not the Anthropic token.
|
|
137
147
|
// The gateway pulls the stored token per request. This avoids putting the Anthropic
|
|
138
148
|
// credential on disk and avoids a dual-credential configuration.
|
|
139
|
-
async installStoredToken({ apiKey, gatewayBaseUrl, plan, existingRecord }) {
|
|
149
|
+
async installStoredToken({ apiKey, gatewayBaseUrl, plan, existingRecord, privateGateway = false }) {
|
|
140
150
|
const backupPath = await backupFile(CONFIG_PATH, "claude-code");
|
|
141
151
|
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
142
152
|
const claudeAiConnectorsBefore = disableClaudeAiConnectors(current, existingRecord);
|
|
@@ -146,11 +156,19 @@ export const claudeCodeHarness = {
|
|
|
146
156
|
// Single credential only: no stale api key, no separate Anthropic bearer.
|
|
147
157
|
delete current.env.ANTHROPIC_API_KEY;
|
|
148
158
|
current.env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
if (privateGateway) {
|
|
160
|
+
for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
|
|
161
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`];
|
|
162
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`];
|
|
163
|
+
delete current.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`];
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
current.env.ANTHROPIC_DEFAULT_OPUS_MODEL = CLAUDE_CODE_MODELS.opus;
|
|
167
|
+
current.env.ANTHROPIC_DEFAULT_SONNET_MODEL = CLAUDE_CODE_MODELS.sonnet;
|
|
168
|
+
current.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = CLAUDE_CODE_MODELS.haiku;
|
|
169
|
+
current.env.ANTHROPIC_DEFAULT_FABLE_MODEL = CLAUDE_CODE_MODELS.fable;
|
|
170
|
+
writeClaudeCodeModelPresentation(current.env);
|
|
171
|
+
}
|
|
154
172
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
|
155
173
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
|
|
156
174
|
delete current.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
|
package/src/harnesses/codex.js
CHANGED
|
@@ -27,9 +27,9 @@ export const codexHarness = {
|
|
|
27
27
|
};
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
-
async install({ apiKey, gatewayBaseUrl, plan, existingRecord }) {
|
|
30
|
+
async install({ apiKey, gatewayBaseUrl, plan, existingRecord, privateGateway = false }) {
|
|
31
31
|
const current = await readText(CONFIG_PATH, "");
|
|
32
|
-
const subscriptionAuth = await detectCodexChatgptAuth();
|
|
32
|
+
const subscriptionAuth = !privateGateway && await detectCodexChatgptAuth();
|
|
33
33
|
const previousModelProvider = existingRecord?.previous_model_provider
|
|
34
34
|
|| rootAssignment(removeManagedCodexConfig(current), "model_provider");
|
|
35
35
|
const backupPath = await backupFile(CONFIG_PATH, "codex");
|
|
@@ -37,6 +37,7 @@ export const codexHarness = {
|
|
|
37
37
|
apiKey,
|
|
38
38
|
gatewayBaseUrl,
|
|
39
39
|
subscriptionAuth,
|
|
40
|
+
privateGateway,
|
|
40
41
|
surface: "codex-terminal",
|
|
41
42
|
});
|
|
42
43
|
|
|
@@ -101,14 +102,15 @@ export async function detectCodexChatgptAuth() {
|
|
|
101
102
|
*/
|
|
102
103
|
export function renderCodexConfig(
|
|
103
104
|
text,
|
|
104
|
-
{ apiKey, gatewayBaseUrl, subscriptionAuth, surface = "codex-terminal" },
|
|
105
|
+
{ apiKey, gatewayBaseUrl, subscriptionAuth, privateGateway = false, surface = "codex-terminal" },
|
|
105
106
|
) {
|
|
106
|
-
// Native Codex subscription
|
|
107
|
-
//
|
|
108
|
-
// ordinary PennyRouter bearer provider, even when Codex itself is logged into ChatGPT.
|
|
107
|
+
// Native Codex ChatGPT subscription auth is allowed through the hosted gateway and the
|
|
108
|
+
// explicitly supported local development gateway. Other custom gateways use Penny auth.
|
|
109
109
|
// Keep this guard at the final renderer so every install/update/desktop path gets the
|
|
110
110
|
// same safety rule regardless of which profile selected it.
|
|
111
|
-
subscriptionAuth = Boolean(
|
|
111
|
+
subscriptionAuth = Boolean(
|
|
112
|
+
subscriptionAuth && !privateGateway && isCodexSubscriptionAuthGateway(gatewayBaseUrl),
|
|
113
|
+
);
|
|
112
114
|
const clean = removeManagedCodexConfig(text, { removeRootProvider: true }).trimEnd();
|
|
113
115
|
if (new RegExp(`^\\s*\\[model_providers\\.${escapeRegex(PROVIDER_ID)}\\]\\s*$`, "m").test(clean)) {
|
|
114
116
|
throw new Error(
|
|
@@ -156,6 +158,20 @@ export function isHostedPennyRouterGateway(gatewayBaseUrl) {
|
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
export function isLocalPennyRouterGateway(gatewayBaseUrl) {
|
|
162
|
+
try {
|
|
163
|
+
const url = new URL(String(gatewayBaseUrl));
|
|
164
|
+
return url.hostname.toLowerCase() === "localhost" && url.port === "8400";
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isCodexSubscriptionAuthGateway(gatewayBaseUrl) {
|
|
171
|
+
return isHostedPennyRouterGateway(gatewayBaseUrl)
|
|
172
|
+
|| isLocalPennyRouterGateway(gatewayBaseUrl);
|
|
173
|
+
}
|
|
174
|
+
|
|
159
175
|
export function removeManagedCodexConfig(text, { removeRootProvider = false } = {}) {
|
|
160
176
|
let out = String(text || "").replace(
|
|
161
177
|
new RegExp(`${escapeRegex(MANAGED_START)}[\\s\\S]*?${escapeRegex(MANAGED_END)}\\n?`, "g"),
|
|
@@ -40,7 +40,6 @@ function testSubscriptionProviderIsRootSafe() {
|
|
|
40
40
|
function testPrivateAndProxyGatewaysNeverEnableNativeSubscription() {
|
|
41
41
|
assert.equal(isHostedPennyRouterGateway("https://api.pennyrouter.com"), true);
|
|
42
42
|
for (const gateway of [
|
|
43
|
-
"http://localhost:8400",
|
|
44
43
|
"https://private.example.test",
|
|
45
44
|
"https://openrouter.example.test",
|
|
46
45
|
]) {
|
|
@@ -49,6 +48,7 @@ function testPrivateAndProxyGatewaysNeverEnableNativeSubscription() {
|
|
|
49
48
|
apiKey: "pr-private",
|
|
50
49
|
gatewayBaseUrl: gateway,
|
|
51
50
|
subscriptionAuth: true,
|
|
51
|
+
privateGateway: true,
|
|
52
52
|
});
|
|
53
53
|
assert.ok(next.includes('model_provider = "pennyrouter"'));
|
|
54
54
|
assert.ok(next.includes('experimental_bearer_token = "pr-private"'));
|
package/src/launch.js
CHANGED
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
upgradeClaudeCodeModelValue,
|
|
25
25
|
writeClaudeCodeModelPresentation,
|
|
26
26
|
} from "./harnesses/claude-code.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isCodexSubscriptionAuthGateway,
|
|
29
|
+
renderCodexConfig,
|
|
30
|
+
} from "./harnesses/codex.js";
|
|
28
31
|
import {
|
|
29
32
|
OPENCODE_MODEL_CHOICES,
|
|
30
33
|
fetchOpenCodeModels,
|
|
@@ -439,17 +442,13 @@ export async function installPennyRuntime({ updatePath = true } = {}) {
|
|
|
439
442
|
return { supported: true, ...path, shim_path: pennyShimPath(), compare };
|
|
440
443
|
}
|
|
441
444
|
|
|
442
|
-
export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
|
|
443
|
-
//
|
|
444
|
-
// Apply the same
|
|
445
|
+
export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, privateGateway = false }) {
|
|
446
|
+
// Native ChatGPT subscription auth is allowed for the hosted gateway and the explicitly
|
|
447
|
+
// supported local development gateway. Apply the same rule as the config renderer at this
|
|
445
448
|
// boundary, since this path is what bare `penny codex` selects at runtime.
|
|
446
|
-
|
|
447
|
-
subscriptionAuth
|
|
448
|
-
|
|
449
|
-
);
|
|
450
|
-
} catch {
|
|
451
|
-
subscriptionAuth = false;
|
|
452
|
-
}
|
|
449
|
+
subscriptionAuth = Boolean(
|
|
450
|
+
subscriptionAuth && !privateGateway && isCodexSubscriptionAuthGateway(gatewayBaseUrl),
|
|
451
|
+
);
|
|
453
452
|
if (subscriptionAuth) {
|
|
454
453
|
return [
|
|
455
454
|
CODEX_PROFILE_MARKER,
|
|
@@ -880,7 +879,7 @@ async function writeClaudeUserSkill() {
|
|
|
880
879
|
}
|
|
881
880
|
}
|
|
882
881
|
|
|
883
|
-
async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, profile }) {
|
|
882
|
+
async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, profile, privateGateway = false }) {
|
|
884
883
|
const path = codexLaunchProfilePath(profile);
|
|
885
884
|
const existing = await readText(path, "");
|
|
886
885
|
if (existing.trim() && !existing.startsWith(CODEX_PROFILE_MARKER)) {
|
|
@@ -888,13 +887,14 @@ async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, profi
|
|
|
888
887
|
`${path} already exists and is not managed by PennyRouter. Rename it before installing.`,
|
|
889
888
|
);
|
|
890
889
|
}
|
|
891
|
-
await privateWrite(path, renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }));
|
|
890
|
+
await privateWrite(path, renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, privateGateway }));
|
|
892
891
|
}
|
|
893
892
|
|
|
894
893
|
export async function configurePennyHarness(harness, {
|
|
895
894
|
apiKey,
|
|
896
895
|
gatewayBaseUrl,
|
|
897
896
|
subscriptionAuth = false,
|
|
897
|
+
privateGateway = false,
|
|
898
898
|
models = null,
|
|
899
899
|
profile = null,
|
|
900
900
|
}) {
|
|
@@ -920,6 +920,7 @@ export async function configurePennyHarness(harness, {
|
|
|
920
920
|
await writeCodexLaunchProfile({
|
|
921
921
|
gatewayBaseUrl,
|
|
922
922
|
subscriptionAuth,
|
|
923
|
+
privateGateway,
|
|
923
924
|
profile: config.active_profile,
|
|
924
925
|
});
|
|
925
926
|
config.harnesses.codex = {
|
package/src/launch.test.js
CHANGED
|
@@ -266,6 +266,25 @@ async function main() {
|
|
|
266
266
|
assert.equal(persistedClaude.env.ANTHROPIC_DEFAULT_SONNET_MODEL, "claude-sonnet-5[1m]");
|
|
267
267
|
assert.equal("CLAUDE_CODE_MAX_CONTEXT_TOKENS" in persistedClaude.env, false);
|
|
268
268
|
|
|
269
|
+
// Private gateways own the model catalog. Their picker must not merge the
|
|
270
|
+
// hosted Claude defaults, which may not exist in the configured proxy.
|
|
271
|
+
await claudeCodeHarness.install({
|
|
272
|
+
apiKey: "pr-super-secret",
|
|
273
|
+
gatewayBaseUrl: "http://localhost:8400",
|
|
274
|
+
plan: { configPath: path.join(home, ".claude", "settings.json") },
|
|
275
|
+
privateGateway: true,
|
|
276
|
+
});
|
|
277
|
+
const persistedPrivateClaude = JSON.parse(
|
|
278
|
+
fs.readFileSync(path.join(home, ".claude", "settings.json"), "utf8"),
|
|
279
|
+
);
|
|
280
|
+
for (const slot of ["OPUS", "SONNET", "HAIKU", "FABLE"]) {
|
|
281
|
+
assert.equal(persistedPrivateClaude.env[`ANTHROPIC_DEFAULT_${slot}_MODEL`], undefined);
|
|
282
|
+
assert.equal(persistedPrivateClaude.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_NAME`], undefined);
|
|
283
|
+
assert.equal(
|
|
284
|
+
persistedPrivateClaude.env[`ANTHROPIC_DEFAULT_${slot}_MODEL_DESCRIPTION`], undefined,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
269
288
|
await removePennyHarness("claude-code");
|
|
270
289
|
assert.ok(!fs.existsSync(claudeUserSkillPath()));
|
|
271
290
|
fs.mkdirSync(path.dirname(claudeUserSkillPath()), { recursive: true });
|
|
@@ -300,6 +319,15 @@ async function main() {
|
|
|
300
319
|
});
|
|
301
320
|
assert.ok(paid.includes('env_key = "PENNYROUTER_API_KEY"'));
|
|
302
321
|
assert.ok(!paid.includes("requires_openai_auth"));
|
|
322
|
+
const localSubscription = renderCodexLaunchProfile({
|
|
323
|
+
gatewayBaseUrl: "http://localhost:8400", subscriptionAuth: true,
|
|
324
|
+
});
|
|
325
|
+
assert.ok(localSubscription.includes('model_provider = "openai"'));
|
|
326
|
+
const localPrivate = renderCodexLaunchProfile({
|
|
327
|
+
gatewayBaseUrl: "http://localhost:8400", subscriptionAuth: true, privateGateway: true,
|
|
328
|
+
});
|
|
329
|
+
assert.ok(localPrivate.includes('model_provider = "pennyrouter"'));
|
|
330
|
+
assert.ok(localPrivate.includes('env_key = "PENNYROUTER_API_KEY"'));
|
|
303
331
|
|
|
304
332
|
// Inside the fixture: testProfiles persists a config, which must land in the
|
|
305
333
|
// temporary HOME and never in the developer's real ~/.config/pennyrouter.
|