@companion-ai/feynman 0.5.20 → 0.5.21
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/RELEASES.md +4 -0
- package/dist/model/commands.js +5 -5
- package/dist/model/models-json.js +31 -1
- package/dist/pi/settings.js +3 -0
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,10 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.21 - 2026-10-01
|
|
10
|
+
|
|
11
|
+
- **LiteLLM and custom providers that read their key from an environment variable work again.** Since Pi 0.99, `models.json` resolves an environment variable only when written as `$NAME`; a bare name is sent as the key itself. Feynman's LiteLLM setup wrote `LITELLM_MASTER_KEY`, so requests went out as `Authorization: Bearer LITELLM_MASTER_KEY` and the proxy rejected them, and custom providers set up with a variable name broke the same way. Feynman now writes `$NAME`, and on start rewrites any `apiKey` in `models.json` that is a bare all-caps variable name, such as `LITELLM_MASTER_KEY`, to `$LITELLM_MASTER_KEY`. Literal keys, placeholders such as `local`, and `!command` values are left alone.
|
|
12
|
+
|
|
9
13
|
## v0.5.20 - 2026-10-01
|
|
10
14
|
|
|
11
15
|
- **Settings saved by Windows Notepad or PowerShell no longer stop Feynman.** Both can save UTF-8 with a byte-order mark, which JSON parsing rejects, so Feynman refused to start with "Invalid Feynman settings ... expected a JSON object", and other commands read the file as empty. Feynman now ignores the mark in `settings.json`, `auth.json`, and `web-search.json`, as Pi does. A settings file with a real JSON error now names the error, for example a trailing comma, instead of the generic message.
|
package/dist/model/commands.js
CHANGED
|
@@ -10,7 +10,7 @@ import { printInfo, printSection, printSuccess, printWarning } from "../ui/termi
|
|
|
10
10
|
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
|
|
11
11
|
import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
|
|
12
12
|
import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
|
|
13
|
-
import { upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
|
|
13
|
+
import { apiKeyReference, upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
|
|
14
14
|
const exec = promisify(execCallback);
|
|
15
15
|
async function collectModelStatus(settingsPath, authPath) {
|
|
16
16
|
return buildModelStatusSnapshotFromRecords(await getSupportedModelRecords(authPath), await getAuthenticatedModelRecords(authPath), getCurrentModelSpec(settingsPath));
|
|
@@ -161,7 +161,7 @@ async function resolveApiKeyConfig(apiKeyConfig) {
|
|
|
161
161
|
return undefined;
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
const envValue = process.env[trimmed];
|
|
164
|
+
const envValue = process.env[trimmed.replace(/^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/, "$1")];
|
|
165
165
|
if (typeof envValue === "string" && envValue.trim()) {
|
|
166
166
|
return envValue.trim();
|
|
167
167
|
}
|
|
@@ -267,10 +267,10 @@ async function promptCustomProviderSetup() {
|
|
|
267
267
|
}
|
|
268
268
|
printInfo("API key value supports:");
|
|
269
269
|
printInfo(" - literal secret (stored in models.json)");
|
|
270
|
-
printInfo(" - env var name (resolved at runtime)");
|
|
270
|
+
printInfo(" - env var name such as MY_API_KEY or $MY_API_KEY (resolved at runtime)");
|
|
271
271
|
printInfo(" - !command (executes and uses stdout)");
|
|
272
272
|
const apiKeyConfigRaw = (await promptText("API key / resolver", "")).trim();
|
|
273
|
-
const apiKeyConfig = apiKeyConfigRaw || "local";
|
|
273
|
+
const apiKeyConfig = apiKeyReference(apiKeyConfigRaw) || "local";
|
|
274
274
|
if (!apiKeyConfigRaw) {
|
|
275
275
|
printInfo("Using placeholder apiKey value (required by Pi for custom providers).");
|
|
276
276
|
}
|
|
@@ -346,7 +346,7 @@ async function promptLiteLlmProviderSetup() {
|
|
|
346
346
|
return undefined;
|
|
347
347
|
}
|
|
348
348
|
const hasKey = keySelection === 0;
|
|
349
|
-
const apiKeyConfig = hasKey ? "LITELLM_MASTER_KEY" : "local";
|
|
349
|
+
const apiKeyConfig = hasKey ? "$LITELLM_MASTER_KEY" : "local";
|
|
350
350
|
const authHeader = hasKey;
|
|
351
351
|
if (hasKey) {
|
|
352
352
|
printInfo("Set LITELLM_MASTER_KEY in your shell or .env before using Feynman.");
|
|
@@ -63,7 +63,9 @@ export function upsertProviderConfig(modelsJsonPath, providerId, patch) {
|
|
|
63
63
|
if (patch.models !== undefined)
|
|
64
64
|
nextProvider.models = patch.models;
|
|
65
65
|
providers[providerId] = nextProvider;
|
|
66
|
-
|
|
66
|
+
return writeModelsJson(modelsJsonPath, { ...value, providers });
|
|
67
|
+
}
|
|
68
|
+
function writeModelsJson(modelsJsonPath, next) {
|
|
67
69
|
try {
|
|
68
70
|
mkdirSync(dirname(modelsJsonPath), { recursive: true });
|
|
69
71
|
writeFileSync(modelsJsonPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
@@ -80,3 +82,31 @@ export function upsertProviderConfig(modelsJsonPath, providerId, patch) {
|
|
|
80
82
|
return { ok: false, error: `Failed to write models.json: ${error instanceof Error ? error.message : String(error)}` };
|
|
81
83
|
}
|
|
82
84
|
}
|
|
85
|
+
// Pi resolves an environment variable in `apiKey` only as $NAME or ${NAME}; a
|
|
86
|
+
// bare name such as LITELLM_MASTER_KEY is sent as the key itself.
|
|
87
|
+
const BARE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
88
|
+
export function apiKeyReference(value) {
|
|
89
|
+
return BARE_ENV_NAME.test(value) ? `$${value}` : value;
|
|
90
|
+
}
|
|
91
|
+
// Feynman's setup wrote bare names before Pi 0.99 stopped resolving them, so a
|
|
92
|
+
// LiteLLM or custom provider sent "LITELLM_MASTER_KEY" as its key. Returns the
|
|
93
|
+
// providers it rewrote to $NAME.
|
|
94
|
+
export function migrateBareEnvApiKeys(modelsJsonPath) {
|
|
95
|
+
if (!existsSync(modelsJsonPath))
|
|
96
|
+
return [];
|
|
97
|
+
const read = readModelsJson(modelsJsonPath);
|
|
98
|
+
if (!read.ok)
|
|
99
|
+
return [];
|
|
100
|
+
const providers = { ...(read.value.providers ?? {}) };
|
|
101
|
+
const migrated = Object.keys(providers).filter((id) => {
|
|
102
|
+
const apiKey = providers[id]?.apiKey;
|
|
103
|
+
return typeof apiKey === "string" && BARE_ENV_NAME.test(apiKey);
|
|
104
|
+
});
|
|
105
|
+
if (migrated.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
for (const id of migrated) {
|
|
108
|
+
const provider = providers[id];
|
|
109
|
+
providers[id] = { ...provider, apiKey: apiKeyReference(provider.apiKey) };
|
|
110
|
+
}
|
|
111
|
+
return writeModelsJson(modelsJsonPath, { ...read.value, providers }).ok ? migrated : [];
|
|
112
|
+
}
|
package/dist/pi/settings.js
CHANGED
|
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { parseJsonText, readJsonFile } from "../config/json-file.js";
|
|
4
4
|
import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
|
|
5
5
|
import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
|
|
6
|
+
import { migrateBareEnvApiKeys } from "../model/models-json.js";
|
|
7
|
+
import { getModelsJsonPath } from "../model/registry.js";
|
|
6
8
|
// Written by Feynman <= 0.4.0 next to a subagent extension path it managed.
|
|
7
9
|
const LEGACY_RESEARCHER_EXTENSION_MARKER = "_feynmanResearchToolsExtension";
|
|
8
10
|
function findModel(modelLookup, provider, id) {
|
|
@@ -180,6 +182,7 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
|
|
|
180
182
|
const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings") : undefined;
|
|
181
183
|
const settings = existing ? { ...existing.value } : {};
|
|
182
184
|
const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
|
|
185
|
+
migrateBareEnvApiKeys(getModelsJsonPath(authPath));
|
|
183
186
|
for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
|
|
184
187
|
if (settings[key] === undefined)
|
|
185
188
|
settings[key] = value;
|