@companion-ai/feynman 0.5.19 → 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 +8 -0
- package/dist/config/json-file.js +9 -0
- package/dist/model/catalog.js +2 -2
- package/dist/model/commands.js +5 -5
- package/dist/model/models-json.js +31 -1
- package/dist/model/service-tier.js +3 -2
- package/dist/pi/settings.js +9 -4
- package/dist/pi/web-access.js +3 -2
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,14 @@ 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
|
+
|
|
13
|
+
## v0.5.20 - 2026-10-01
|
|
14
|
+
|
|
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.
|
|
16
|
+
|
|
9
17
|
## v0.5.19 - 2026-10-01
|
|
10
18
|
|
|
11
19
|
- **Word drafts open without LibreOffice.** `document_parse` needs LibreOffice for `.docx` files, which most machines lack, so reviewing a thesis or draft in Word format failed. When LibreOffice is missing, Feynman now reads the document's paragraph text directly from the `.docx` and says so; install LibreOffice for page layout, tables, footnotes, and images.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// Windows Notepad and PowerShell 5.1 save UTF-8 with a byte-order mark, which
|
|
3
|
+
// JSON.parse rejects. Pi strips it when it reads the same files.
|
|
4
|
+
export function parseJsonText(source) {
|
|
5
|
+
return JSON.parse(source.replace(/^/, ""));
|
|
6
|
+
}
|
|
7
|
+
export function readJsonFile(path) {
|
|
8
|
+
return parseJsonText(readFileSync(path, "utf8"));
|
|
9
|
+
}
|
package/dist/model/catalog.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { getEnvApiKey } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { readJsonFile } from "../config/json-file.js";
|
|
3
3
|
import { createModelRuntime } from "./registry.js";
|
|
4
4
|
const PRO_CLASS_MODEL_PATTERN = /(?:^|[-_.:/])pro(?:$|[-_.:/])/i;
|
|
5
5
|
// Pi catalogs these exact DeepSeek V4 Pro model IDs as ordinary open-weight
|
|
@@ -334,7 +334,7 @@ export async function getSupportedModelRecords(authPath) {
|
|
|
334
334
|
function readExpiredOAuthProviders(authPath) {
|
|
335
335
|
const expired = new Set();
|
|
336
336
|
try {
|
|
337
|
-
const parsed =
|
|
337
|
+
const parsed = readJsonFile(authPath);
|
|
338
338
|
for (const [provider, credential] of Object.entries(parsed)) {
|
|
339
339
|
if (!credential || typeof credential !== "object")
|
|
340
340
|
continue;
|
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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdirSync,
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
+
import { readJsonFile } from "../config/json-file.js";
|
|
3
4
|
export const FEYNMAN_SERVICE_TIERS = [
|
|
4
5
|
"auto",
|
|
5
6
|
"default",
|
|
@@ -12,7 +13,7 @@ const OPENAI_SERVICE_TIERS = new Set(["auto", "default", "flex", "priority"]);
|
|
|
12
13
|
const ANTHROPIC_SERVICE_TIERS = new Set(["auto", "standard_only"]);
|
|
13
14
|
function readSettings(settingsPath) {
|
|
14
15
|
try {
|
|
15
|
-
return
|
|
16
|
+
return readJsonFile(settingsPath);
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
19
|
return {};
|
package/dist/pi/settings.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { parseJsonText, readJsonFile } from "../config/json-file.js";
|
|
3
4
|
import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
|
|
4
5
|
import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
|
|
6
|
+
import { migrateBareEnvApiKeys } from "../model/models-json.js";
|
|
7
|
+
import { getModelsJsonPath } from "../model/registry.js";
|
|
5
8
|
// Written by Feynman <= 0.4.0 next to a subagent extension path it managed.
|
|
6
9
|
const LEGACY_RESEARCHER_EXTENSION_MARKER = "_feynmanResearchToolsExtension";
|
|
7
10
|
function findModel(modelLookup, provider, id) {
|
|
@@ -50,7 +53,7 @@ export function readJson(path) {
|
|
|
50
53
|
return {};
|
|
51
54
|
}
|
|
52
55
|
try {
|
|
53
|
-
return
|
|
56
|
+
return readJsonFile(path);
|
|
54
57
|
}
|
|
55
58
|
catch (error) {
|
|
56
59
|
if (process.env.FEYNMAN_DEBUG === "1") {
|
|
@@ -66,10 +69,11 @@ function readConfigObject(path, label) {
|
|
|
66
69
|
const source = readFileSync(path, "utf8");
|
|
67
70
|
let value;
|
|
68
71
|
try {
|
|
69
|
-
value =
|
|
72
|
+
value = parseJsonText(source);
|
|
70
73
|
}
|
|
71
|
-
catch {
|
|
72
|
-
|
|
74
|
+
catch (error) {
|
|
75
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
76
|
+
throw new Error(`Invalid ${label} at ${path}: ${reason}. Fix the JSON or delete the file. The file was not changed.`);
|
|
73
77
|
}
|
|
74
78
|
if (!isRecord(value)) {
|
|
75
79
|
throw new Error(`Invalid ${label} at ${path}: expected a JSON object. The file was not changed.`);
|
|
@@ -178,6 +182,7 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
|
|
|
178
182
|
const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings") : undefined;
|
|
179
183
|
const settings = existing ? { ...existing.value } : {};
|
|
180
184
|
const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
|
|
185
|
+
migrateBareEnvApiKeys(getModelsJsonPath(authPath));
|
|
181
186
|
for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
|
|
182
187
|
if (settings[key] === undefined)
|
|
183
188
|
settings[key] = value;
|
package/dist/pi/web-access.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync,
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { readJsonFile } from "../config/json-file.js";
|
|
3
4
|
import { getFeynmanAgentDir, getFeynmanHome } from "../config/paths.js";
|
|
4
5
|
// pi-web-access reads web-search.json from PI_CODING_AGENT_DIR.
|
|
5
6
|
export function getPiWebSearchConfigPath(home) {
|
|
@@ -20,7 +21,7 @@ export function loadPiWebAccessConfig(configPath = getPiWebSearchConfigPath()) {
|
|
|
20
21
|
return {};
|
|
21
22
|
}
|
|
22
23
|
try {
|
|
23
|
-
const parsed =
|
|
24
|
+
const parsed = readJsonFile(configPath);
|
|
24
25
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
25
26
|
}
|
|
26
27
|
catch {
|