@companion-ai/feynman 0.5.42 → 0.5.43
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 +5 -0
- package/dist/cli.js +5 -5
- package/dist/model/commands.js +15 -20
- package/dist/model/registry.js +1 -4
- package/dist/model/service-tier.js +1 -0
- package/dist/pi/settings.js +54 -41
- package/dist/setup/doctor.js +25 -41
- package/extensions/research-tools/service-tier.ts +1 -0
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,11 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.43 - 2026-10-13
|
|
10
|
+
|
|
11
|
+
- **Feynman reads and writes Pi's configuration only through Pi.** Settings now go through Pi's settings manager, which locks the file and changes only the keys Feynman sets, so a change Pi or another Feynman window makes at the same moment is no longer overwritten. Model checks, local-model detection and `feynman doctor` read your models through Pi, so comments in `models.json`, a `baseUrl` set on one model, and a key saved with `feynman model login` count the same way they do when Feynman runs.
|
|
12
|
+
- `feynman doctor` no longer warns about a custom provider whose key you saved with `feynman model login` rather than writing it into `models.json`.
|
|
13
|
+
|
|
9
14
|
## v0.5.42 - 2026-10-13
|
|
10
15
|
|
|
11
16
|
- **Pi 1.2.0 and pi-subagents 0.78.0.**
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ensureFeynmanAgentDir } from "./bootstrap/home.js";
|
|
|
19
19
|
import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
|
|
20
20
|
import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
|
|
21
21
|
import { installPiPackage, removePiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
|
|
22
|
-
import { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel,
|
|
22
|
+
import { resolveExplicitModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, } from "./pi/settings.js";
|
|
23
23
|
import { BUNDLED_PI_PACKAGES, buildPiEnv, findWindowsBash, WINDOWS_BASH_MISSING_NOTICE } from "./pi/runtime.js";
|
|
24
24
|
import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
|
|
25
25
|
import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, selectDefaultModel, setDefaultModelSpec, } from "./model/commands.js";
|
|
@@ -255,7 +255,8 @@ async function handleUpdateCommand(piOptions, feynmanVersion, source) {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
async function handlePackagesCommand(subcommand, args, piOptions) {
|
|
258
|
-
const configuredSources = new Set((
|
|
258
|
+
const configuredSources = new Set(SettingsManager.create(process.cwd(), piOptions.feynmanAgentDir, { projectTrusted: false })
|
|
259
|
+
.getPackages()
|
|
259
260
|
.map((entry) => (typeof entry === "string" ? entry : entry.source)));
|
|
260
261
|
if (!subcommand || subcommand === "list") {
|
|
261
262
|
printPanel("Feynman Packages", [
|
|
@@ -711,13 +712,12 @@ async function runMain(input) {
|
|
|
711
712
|
if (command && workflowCommandNames.has(command) && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
|
|
712
713
|
const effectiveSpec = explicitModelSpec ?? getCurrentModelSpec(feynmanSettingsPath);
|
|
713
714
|
const providerId = effectiveSpec?.split("/")[0] ?? "";
|
|
714
|
-
if (effectiveSpec && isLocalModelProvider(feynmanAuthPath, providerId)) {
|
|
715
|
+
if (effectiveSpec && await isLocalModelProvider(feynmanAuthPath, providerId)) {
|
|
715
716
|
preLaunchNotice = buildLocalModelWorkflowNotice(effectiveSpec, command);
|
|
716
717
|
}
|
|
717
718
|
}
|
|
718
719
|
if (process.platform === "win32" && mode !== "rpc" && mode !== "json" && process.stdout.isTTY) {
|
|
719
|
-
|
|
720
|
-
if (typeof shellPath !== "string" && !findWindowsBash()) {
|
|
720
|
+
if (!SettingsManager.create(workingDir, feynmanAgentDir, { projectTrusted: false }).getShellPath() && !findWindowsBash()) {
|
|
721
721
|
preLaunchNotice = [preLaunchNotice, WINDOWS_BASH_MISSING_NOTICE].filter(Boolean).join("\n");
|
|
722
722
|
}
|
|
723
723
|
}
|
package/dist/model/commands.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { exec as execCallback } from "node:child_process";
|
|
4
3
|
import { dirname } from "node:path";
|
|
5
4
|
import { promisify } from "node:util";
|
|
@@ -8,7 +7,7 @@ import { openUrl } from "../system/open-url.js";
|
|
|
8
7
|
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
|
9
8
|
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
|
|
10
9
|
import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
|
|
11
|
-
import {
|
|
10
|
+
import { createModelRuntime, getModelsJsonPath } from "./registry.js";
|
|
12
11
|
import { apiKeyReference, upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
|
|
13
12
|
const exec = promisify(execCallback);
|
|
14
13
|
async function collectModelStatus(settingsPath, authPath) {
|
|
@@ -140,22 +139,18 @@ function isLocalBaseUrl(baseUrl) {
|
|
|
140
139
|
}
|
|
141
140
|
// "llama.cpp" is the id of Pi's bundled llama.cpp provider.
|
|
142
141
|
const LOCAL_PROVIDER_IDS = new Set(["ollama", "lm-studio", "vllm", "llama.cpp", "llama-cpp", "llamacpp"]);
|
|
143
|
-
|
|
142
|
+
// Reads models.json through Pi, so its comments, BOM handling, and per-model
|
|
143
|
+
// baseUrl overrides count the same way they do at launch.
|
|
144
|
+
export async function isLocalModelProvider(authPath, providerId) {
|
|
144
145
|
if (!providerId) {
|
|
145
146
|
return false;
|
|
146
147
|
}
|
|
147
148
|
if (LOCAL_PROVIDER_IDS.has(providerId)) {
|
|
148
149
|
return true;
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const provider = parsed.providers?.[providerId];
|
|
154
|
-
return typeof provider?.baseUrl === "string" && isLocalBaseUrl(provider.baseUrl);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
151
|
+
const modelRuntime = await createModelRuntime(authPath);
|
|
152
|
+
return [modelRuntime.getProvider(providerId)?.baseUrl, ...modelRuntime.getModels(providerId).map((model) => model.baseUrl)]
|
|
153
|
+
.some((baseUrl) => typeof baseUrl === "string" && isLocalBaseUrl(baseUrl));
|
|
159
154
|
}
|
|
160
155
|
async function resolveApiKeyConfig(apiKeyConfig) {
|
|
161
156
|
const trimmed = apiKeyConfig.trim();
|
|
@@ -394,8 +389,8 @@ async function promptLiteLlmProviderSetup() {
|
|
|
394
389
|
};
|
|
395
390
|
}
|
|
396
391
|
async function verifyCustomProvider(setup, authPath) {
|
|
397
|
-
const
|
|
398
|
-
const modelsError =
|
|
392
|
+
const modelRuntime = await createModelRuntime(authPath);
|
|
393
|
+
const modelsError = modelRuntime.getError();
|
|
399
394
|
if (modelsError) {
|
|
400
395
|
printWarning("Verification: models.json failed to load.");
|
|
401
396
|
for (const line of modelsError.split("\n")) {
|
|
@@ -403,19 +398,19 @@ async function verifyCustomProvider(setup, authPath) {
|
|
|
403
398
|
}
|
|
404
399
|
return;
|
|
405
400
|
}
|
|
406
|
-
const all =
|
|
407
|
-
const hasModel = setup.modelIds.some((id) => all.some((model) => model.
|
|
401
|
+
const all = modelRuntime.getModels(setup.providerId);
|
|
402
|
+
const hasModel = setup.modelIds.some((id) => all.some((model) => model.id === id));
|
|
408
403
|
if (!hasModel) {
|
|
409
404
|
printWarning("Verification: model registry does not contain the configured provider/model ids.");
|
|
410
405
|
return;
|
|
411
406
|
}
|
|
412
|
-
const available =
|
|
413
|
-
const hasAvailable = setup.modelIds.some((id) => available.some((model) => model.
|
|
407
|
+
const available = await modelRuntime.getAvailable(setup.providerId);
|
|
408
|
+
const hasAvailable = setup.modelIds.some((id) => available.some((model) => model.id === id));
|
|
414
409
|
if (!hasAvailable) {
|
|
415
410
|
printWarning("Verification: provider is not considered authenticated/available.");
|
|
416
411
|
return;
|
|
417
412
|
}
|
|
418
|
-
const apiKey = await
|
|
413
|
+
const apiKey = (await modelRuntime.getAuth(setup.providerId).catch(() => undefined))?.auth.apiKey;
|
|
419
414
|
if (!apiKey) {
|
|
420
415
|
printWarning("Verification: API key could not be resolved (check env var name / !command).");
|
|
421
416
|
return;
|
|
@@ -865,7 +860,7 @@ async function describeUnavailableModelSpec(authPath, spec) {
|
|
|
865
860
|
if (isProClassModelSpec(knownSpec)) {
|
|
866
861
|
return `Pro-class model disabled: ${knownSpec}. Choose an approved research model.`;
|
|
867
862
|
}
|
|
868
|
-
if (isLocalModelProvider(authPath, known.provider)) {
|
|
863
|
+
if (await isLocalModelProvider(authPath, known.provider)) {
|
|
869
864
|
return `${knownSpec} has no API key. Pi only lists a local provider when it has one: add "apiKey": "local" to the ${known.provider} provider in ${getModelsJsonPath(authPath)}.`;
|
|
870
865
|
}
|
|
871
866
|
return `${knownSpec} has no credentials. Run \`feynman model login ${known.provider}\` first.`;
|
package/dist/model/registry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
export function getModelsJsonPath(authPath) {
|
|
4
4
|
return resolve(dirname(authPath), "models.json");
|
|
5
5
|
}
|
|
@@ -10,6 +10,3 @@ export async function createModelRuntime(authPath) {
|
|
|
10
10
|
allowModelNetwork: false,
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
-
export async function createModelRegistry(authPath) {
|
|
14
|
-
return new ModelRegistry(await createModelRuntime(authPath));
|
|
15
|
-
}
|
|
@@ -11,6 +11,7 @@ export const FEYNMAN_SERVICE_TIERS = [
|
|
|
11
11
|
const SERVICE_TIER_SET = new Set(FEYNMAN_SERVICE_TIERS);
|
|
12
12
|
const OPENAI_SERVICE_TIERS = new Set(["auto", "default", "flex", "priority"]);
|
|
13
13
|
const ANTHROPIC_SERVICE_TIERS = new Set(["auto", "standard_only"]);
|
|
14
|
+
// serviceTier is Feynman's key; Pi's SettingsManager has no getter or setter for it, so it is read and written directly.
|
|
14
15
|
function readSettings(settingsPath) {
|
|
15
16
|
try {
|
|
16
17
|
return readJsonFile(settingsPath);
|
package/dist/pi/settings.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { resolveCliModel } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { parseJsonText
|
|
3
|
+
import { resolveCliModel, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { parseJsonText } from "../config/json-file.js";
|
|
5
5
|
import { BUNDLED_PI_PACKAGES, getFeynmanPackageSources, resolvePackageRoot } from "./runtime.js";
|
|
6
6
|
import { choosePreferredModelRecord, getAvailableModelRecords } from "../model/catalog.js";
|
|
7
7
|
import { migrateBareEnvApiKeys } from "../model/models-json.js";
|
|
8
8
|
import { getModelsJsonPath } from "../model/registry.js";
|
|
9
9
|
// Written by Feynman <= 0.4.0 next to a subagent extension path it managed.
|
|
10
10
|
const LEGACY_RESEARCHER_EXTENSION_MARKER = "_feynmanResearchToolsExtension";
|
|
11
|
-
function findModel(modelLookup, provider, id) {
|
|
12
|
-
return "find" in modelLookup
|
|
13
|
-
? modelLookup.find(provider, id)
|
|
14
|
-
: modelLookup.getModel(provider, id);
|
|
15
|
-
}
|
|
16
11
|
export function parseModelSpec(spec, modelLookup) {
|
|
17
12
|
const trimmed = spec.trim();
|
|
18
13
|
for (const separator of ["/", ":"]) {
|
|
@@ -22,7 +17,7 @@ export function parseModelSpec(spec, modelLookup) {
|
|
|
22
17
|
}
|
|
23
18
|
const provider = trimmed.slice(0, separatorIndex);
|
|
24
19
|
const id = trimmed.slice(separatorIndex + 1);
|
|
25
|
-
const model =
|
|
20
|
+
const model = modelLookup.getModel(provider, id);
|
|
26
21
|
if (model) {
|
|
27
22
|
return model;
|
|
28
23
|
}
|
|
@@ -72,20 +67,6 @@ export function normalizeThinkingLevel(value) {
|
|
|
72
67
|
}
|
|
73
68
|
return undefined;
|
|
74
69
|
}
|
|
75
|
-
export function readJson(path) {
|
|
76
|
-
if (!existsSync(path)) {
|
|
77
|
-
return {};
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
return readJsonFile(path);
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
if (process.env.FEYNMAN_DEBUG === "1") {
|
|
84
|
-
process.stderr.write(`[feynman] warning: failed to parse ${path}, treating as empty (${error instanceof Error ? error.message : "unknown error"})\n`);
|
|
85
|
-
}
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
70
|
function isRecord(value) {
|
|
90
71
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
91
72
|
}
|
|
@@ -208,19 +189,25 @@ export function feynmanSubagentExtensions(appRoot) {
|
|
|
208
189
|
function isFeynmanSubagentExtension(path) {
|
|
209
190
|
return typeof path === "string" && (/[\\/]extensions[\\/]research-tools\.ts$/.test(path) || /[\\/]pi-web-access$/.test(path));
|
|
210
191
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
192
|
+
// Bundled defaults (.feynman/settings.json) and the thinking level, seeded
|
|
193
|
+
// through Pi's SettingsManager setters.
|
|
194
|
+
const PI_SETTING_SETTERS = {
|
|
195
|
+
theme: (settings, value) => settings.setTheme(value),
|
|
196
|
+
quietStartup: (settings, value) => settings.setQuietStartup(value),
|
|
197
|
+
collapseChangelog: (settings, value) => settings.setCollapseChangelog(value),
|
|
198
|
+
editorPaddingX: (settings, value) => settings.setEditorPaddingX(value),
|
|
199
|
+
showHardwareCursor: (settings, value) => settings.setShowHardwareCursor(value),
|
|
200
|
+
defaultThinkingLevel: (settings, value) => settings.setDefaultThinkingLevel(value),
|
|
201
|
+
};
|
|
202
|
+
// SettingsManager has no setter for `subagents` (pi-subagents' key), for deleting `retry`, or for a
|
|
203
|
+
// bundled default outside PI_SETTING_SETTERS, so those are edited in the file directly.
|
|
204
|
+
function updateSettingsWithoutPiSetter(settingsPath, appRoot, defaults) {
|
|
205
|
+
const existing = existsSync(settingsPath) ? readConfigObject(settingsPath, "Feynman settings").value : {};
|
|
206
|
+
const settings = structuredClone(existing);
|
|
207
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
208
|
+
if (!(key in PI_SETTING_SETTERS) && settings[key] === undefined)
|
|
221
209
|
settings[key] = value;
|
|
222
210
|
}
|
|
223
|
-
settings.packages = reconcileFeynmanPackages(settings.packages, appRoot);
|
|
224
211
|
removeLegacyResearcherExtension(settings);
|
|
225
212
|
// 0.5.0-0.5.5 seeded a 6-retry, 5s backoff that left a dead connection
|
|
226
213
|
// retrying silently for over five minutes; drop that exact value for Pi's default.
|
|
@@ -238,11 +225,35 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
|
|
|
238
225
|
settings.subagents.defaultSubagentOnlyExtensions = feynmanSubagentExtensions(appRoot);
|
|
239
226
|
}
|
|
240
227
|
}
|
|
241
|
-
if (
|
|
228
|
+
if (JSON.stringify(settings) === JSON.stringify(existing))
|
|
229
|
+
return;
|
|
230
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
231
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
232
|
+
}
|
|
233
|
+
export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, appRoot, defaultThinkingLevel, authPath) {
|
|
234
|
+
// Validate before model discovery or either settings write. Invalid custom
|
|
235
|
+
// configuration must not silently turn into an empty/default configuration.
|
|
236
|
+
const subagentDefaults = prepareSubagentDefaults(settingsPath);
|
|
237
|
+
// Feynman's agent settings are global; an untrusted project's .pi/settings.json stays out.
|
|
238
|
+
const settings = SettingsManager.create(process.cwd(), dirname(settingsPath), { projectTrusted: false });
|
|
239
|
+
const loadError = settings.drainErrors().find((error) => error.scope === "global")?.error;
|
|
240
|
+
const current = settings.getGlobalSettings();
|
|
241
|
+
if (loadError || !isRecord(current)) {
|
|
242
|
+
throw new Error(`Invalid Feynman settings at ${settingsPath}: ${loadError?.message ?? "expected a JSON object"}. Fix the JSON or delete the file. The file was not changed.`);
|
|
243
|
+
}
|
|
244
|
+
const defaults = readConfigObject(bundledSettingsPath, "bundled Feynman settings").value;
|
|
245
|
+
migrateBareEnvApiKeys(getModelsJsonPath(authPath));
|
|
246
|
+
for (const [key, value] of Object.entries({ ...defaults, defaultThinkingLevel })) {
|
|
247
|
+
if (current[key] === undefined)
|
|
248
|
+
PI_SETTING_SETTERS[key]?.(settings, value);
|
|
249
|
+
}
|
|
250
|
+
const packages = reconcileFeynmanPackages(current.packages, appRoot);
|
|
251
|
+
if (JSON.stringify(packages) !== JSON.stringify(current.packages))
|
|
252
|
+
settings.setPackages(packages);
|
|
253
|
+
if (!settings.getDefaultProvider() || !settings.getDefaultModel()) {
|
|
242
254
|
const preferredModel = choosePreferredModelRecord(await getAvailableModelRecords(authPath));
|
|
243
255
|
if (preferredModel) {
|
|
244
|
-
settings.
|
|
245
|
-
settings.defaultModel = preferredModel.id;
|
|
256
|
+
settings.setDefaultModelAndProvider(preferredModel.provider, preferredModel.id);
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
259
|
// If another process wrote the subagent config while models were being
|
|
@@ -260,9 +271,11 @@ export async function ensureFeynmanSettings(settingsPath, bundledSettingsPath, a
|
|
|
260
271
|
throw error;
|
|
261
272
|
}
|
|
262
273
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
274
|
+
// Read the file again after model discovery so edits made meanwhile survive;
|
|
275
|
+
// SettingsManager likewise merges only the keys it changed, under Pi's lock.
|
|
276
|
+
updateSettingsWithoutPiSetter(settingsPath, appRoot, defaults);
|
|
277
|
+
await settings.flush();
|
|
278
|
+
const [writeError] = settings.drainErrors();
|
|
279
|
+
if (writeError)
|
|
280
|
+
throw writeError.error;
|
|
268
281
|
}
|
package/dist/setup/doctor.js
CHANGED
|
@@ -1,43 +1,26 @@
|
|
|
1
1
|
import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn } from "@companion-ai/alpha-hub/lib";
|
|
2
2
|
import { getValidToken as getValidAlphaToken } from "@companion-ai/alpha-hub/lib/auth";
|
|
3
|
-
import {
|
|
3
|
+
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { dirname } from "node:path";
|
|
4
5
|
import { verifyAlphaAuthStatus } from "../alpha-auth-status.js";
|
|
5
6
|
import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
|
|
6
7
|
import { PANDOC_FALLBACK_PATHS, resolveBrowser, resolveExecutable } from "../system/executables.js";
|
|
7
|
-
import { readJson } from "../pi/settings.js";
|
|
8
8
|
import { findWindowsBash, validatePiInstallation, WINDOWS_BASH_MISSING_NOTICE } from "../pi/runtime.js";
|
|
9
9
|
import { isTelemetryDisabled } from "../telemetry/posthog.js";
|
|
10
10
|
import { printInfo, printPanel, printSection } from "../ui/terminal.js";
|
|
11
11
|
import { getCurrentModelSpec } from "../model/commands.js";
|
|
12
12
|
import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js";
|
|
13
|
-
import {
|
|
13
|
+
import { createModelRuntime, getModelsJsonPath } from "../model/registry.js";
|
|
14
14
|
import { getConfiguredServiceTier } from "../model/service-tier.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const missing = [];
|
|
25
|
-
for (const [providerId, config] of Object.entries(providers)) {
|
|
26
|
-
if (!config || typeof config !== "object")
|
|
27
|
-
continue;
|
|
28
|
-
const models = config.models;
|
|
29
|
-
if (!Array.isArray(models) || models.length === 0)
|
|
30
|
-
continue;
|
|
31
|
-
const apiKey = config.apiKey;
|
|
32
|
-
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
33
|
-
missing.push(providerId);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return missing;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
15
|
+
// Custom providers (in models.json, not Pi's built-in catalog) that list models
|
|
16
|
+
// but have no credential Pi can resolve, so none of their models are available.
|
|
17
|
+
async function findProvidersMissingApiKey(modelRuntime, authPath) {
|
|
18
|
+
const builtIn = await ModelRuntime.create({ authPath, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
19
|
+
return modelRuntime
|
|
20
|
+
.getProviders()
|
|
21
|
+
.filter((provider) => !builtIn.getProvider(provider.id) && modelRuntime.getModels(provider.id).length > 0)
|
|
22
|
+
.filter((provider) => !modelRuntime.getProviderAuthStatus(provider.id).configured)
|
|
23
|
+
.map((provider) => provider.id);
|
|
41
24
|
}
|
|
42
25
|
export async function collectStatusSnapshot(options) {
|
|
43
26
|
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
@@ -100,8 +83,8 @@ export async function runStatus(options) {
|
|
|
100
83
|
}
|
|
101
84
|
}
|
|
102
85
|
export async function runDoctor(options) {
|
|
103
|
-
const settings =
|
|
104
|
-
const
|
|
86
|
+
const settings = SettingsManager.create(options.workingDir, dirname(options.settingsPath), { projectTrusted: false });
|
|
87
|
+
const modelRuntime = await createModelRuntime(options.authPath);
|
|
105
88
|
const supportedModels = await getSupportedModelRecords(options.authPath);
|
|
106
89
|
const modelStatus = await collectStatusSnapshot(options);
|
|
107
90
|
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
|
@@ -135,9 +118,9 @@ export async function runDoctor(options) {
|
|
|
135
118
|
.join(", ");
|
|
136
119
|
console.log(` authenticated sample: ${sample}`);
|
|
137
120
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
121
|
+
const defaultProvider = settings.getDefaultProvider();
|
|
122
|
+
const defaultModel = settings.getDefaultModel();
|
|
123
|
+
console.log(`default model: ${defaultProvider && defaultModel ? `${defaultProvider}/${defaultModel}` : "not set"}`);
|
|
141
124
|
console.log(`default model valid: ${modelStatus.modelValid ? "yes" : "no"}`);
|
|
142
125
|
console.log(`authenticated providers: ${modelStatus.authenticatedProviderCount}`);
|
|
143
126
|
console.log(`authenticated models: ${modelStatus.authenticatedModelCount}`);
|
|
@@ -146,7 +129,7 @@ export async function runDoctor(options) {
|
|
|
146
129
|
if (modelStatus.recommendedModelReason) {
|
|
147
130
|
console.log(` why: ${modelStatus.recommendedModelReason}`);
|
|
148
131
|
}
|
|
149
|
-
const modelsError =
|
|
132
|
+
const modelsError = modelRuntime.getError();
|
|
150
133
|
if (modelsError) {
|
|
151
134
|
console.log("models.json: error");
|
|
152
135
|
for (const line of modelsError.split("\n")) {
|
|
@@ -156,23 +139,24 @@ export async function runDoctor(options) {
|
|
|
156
139
|
else {
|
|
157
140
|
const modelsJsonPath = getModelsJsonPath(options.authPath);
|
|
158
141
|
console.log(`models.json: ${modelsJsonPath}`);
|
|
159
|
-
const missingApiKeyProviders = findProvidersMissingApiKey(
|
|
142
|
+
const missingApiKeyProviders = await findProvidersMissingApiKey(modelRuntime, options.authPath);
|
|
160
143
|
if (missingApiKeyProviders.length > 0) {
|
|
161
144
|
console.log(` warning: provider(s) missing apiKey: ${missingApiKeyProviders.join(", ")}`);
|
|
162
|
-
console.log(" note: custom providers with a models[] list need apiKey in models.json to be available.");
|
|
145
|
+
console.log(" note: custom providers with a models[] list need an apiKey in models.json or a stored key to be available.");
|
|
163
146
|
}
|
|
164
147
|
}
|
|
165
148
|
console.log(`pandoc: ${pandocPath ?? "missing"}`);
|
|
166
149
|
if (process.platform === "win32") {
|
|
167
|
-
const shellPath = settings.
|
|
168
|
-
console.log(`bash: ${
|
|
150
|
+
const shellPath = settings.getShellPath();
|
|
151
|
+
console.log(`bash: ${shellPath ? `${shellPath} (shellPath)` : findWindowsBash()?.path ?? `missing. ${WINDOWS_BASH_MISSING_NOTICE}`}`);
|
|
169
152
|
}
|
|
170
153
|
console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
|
|
171
154
|
for (const line of formatPiWebAccessDoctorLines()) {
|
|
172
155
|
console.log(line);
|
|
173
156
|
}
|
|
174
|
-
|
|
175
|
-
console.log(`
|
|
157
|
+
const quietStartup = settings.getQuietStartup();
|
|
158
|
+
console.log(`quiet startup: ${quietStartup === "header" ? "header only" : quietStartup === true ? "enabled" : "disabled"}`);
|
|
159
|
+
console.log(`theme: ${settings.getThemeSetting() ?? "not set"}`);
|
|
176
160
|
if (missingPiBits.length > 0) {
|
|
177
161
|
console.log("pi runtime: missing files");
|
|
178
162
|
for (const entry of missingPiBits) {
|
|
@@ -34,6 +34,7 @@ function normalizeServiceTier(value: string | undefined): FeynmanServiceTier | u
|
|
|
34
34
|
return SERVICE_TIER_SET.has(normalized) ? (normalized as FeynmanServiceTier) : undefined;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// serviceTier is Feynman's key; Pi's SettingsManager has no getter or setter for it, so it is read and written directly.
|
|
37
38
|
function getConfiguredServiceTier(settingsPath: string): FeynmanServiceTier | undefined {
|
|
38
39
|
try {
|
|
39
40
|
const parsed = JSON.parse(readFileSync(settingsPath, "utf8")) as { serviceTier?: string };
|