Polish Feynman harness and stabilize Pi web runtime

This commit is contained in:
Advait Paliwal
2026-03-22 20:20:26 -07:00
parent 7f0def3a4c
commit 46810f97b7
47 changed files with 3178 additions and 869 deletions

282
src/model/catalog.ts Normal file
View File

@@ -0,0 +1,282 @@
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
type ModelRecord = {
provider: string;
id: string;
name?: string;
};
export type ProviderStatus = {
id: string;
label: string;
supportedModels: number;
availableModels: number;
configured: boolean;
current: boolean;
recommended: boolean;
};
export type ModelStatusSnapshot = {
current?: string;
currentValid: boolean;
recommended?: string;
recommendationReason?: string;
availableModels: string[];
providers: ProviderStatus[];
guidance: string[];
};
const PROVIDER_LABELS: Record<string, string> = {
anthropic: "Anthropic",
openai: "OpenAI",
"openai-codex": "OpenAI Codex",
openrouter: "OpenRouter",
google: "Google",
"google-gemini-cli": "Google Gemini CLI",
zai: "Z.AI / GLM",
minimax: "MiniMax",
"minimax-cn": "MiniMax (China)",
"github-copilot": "GitHub Copilot",
"vercel-ai-gateway": "Vercel AI Gateway",
opencode: "OpenCode",
"opencode-go": "OpenCode Go",
"kimi-coding": "Kimi / Moonshot",
xai: "xAI",
groq: "Groq",
mistral: "Mistral",
cerebras: "Cerebras",
huggingface: "Hugging Face",
"amazon-bedrock": "Amazon Bedrock",
"azure-openai-responses": "Azure OpenAI Responses",
};
const RESEARCH_MODEL_PREFERENCES = [
{
spec: "anthropic/claude-opus-4-6",
reason: "strong long-context reasoning for source-heavy research work",
},
{
spec: "anthropic/claude-opus-4-5",
reason: "strong long-context reasoning for source-heavy research work",
},
{
spec: "anthropic/claude-sonnet-4-6",
reason: "balanced reasoning and speed for iterative research sessions",
},
{
spec: "anthropic/claude-sonnet-4-5",
reason: "balanced reasoning and speed for iterative research sessions",
},
{
spec: "openai/gpt-5.4",
reason: "strong general reasoning and drafting quality for research tasks",
},
{
spec: "openai/gpt-5",
reason: "strong general reasoning and drafting quality for research tasks",
},
{
spec: "openai-codex/gpt-5.4",
reason: "strong research + coding balance when Pi exposes Codex directly",
},
{
spec: "google/gemini-3-pro-preview",
reason: "good fallback for broad web-and-doc research work",
},
{
spec: "google/gemini-2.5-pro",
reason: "good fallback for broad web-and-doc research work",
},
{
spec: "openrouter/openai/gpt-5.1-codex",
reason: "good routed fallback when only OpenRouter is configured",
},
{
spec: "zai/glm-5",
reason: "good fallback when GLM is the available research model",
},
{
spec: "kimi-coding/kimi-k2-thinking",
reason: "good fallback when Kimi is the available research model",
},
];
const PROVIDER_SORT_ORDER = [
"anthropic",
"openai",
"openai-codex",
"google",
"openrouter",
"zai",
"kimi-coding",
"minimax",
"minimax-cn",
"github-copilot",
"vercel-ai-gateway",
];
function formatProviderLabel(provider: string): string {
return PROVIDER_LABELS[provider] ?? provider;
}
function modelSpec(model: ModelRecord): string {
return `${model.provider}/${model.id}`;
}
function compareByResearchPreference(left: ModelRecord, right: ModelRecord): number {
const leftSpec = modelSpec(left);
const rightSpec = modelSpec(right);
const leftIndex = RESEARCH_MODEL_PREFERENCES.findIndex((entry) => entry.spec === leftSpec);
const rightIndex = RESEARCH_MODEL_PREFERENCES.findIndex((entry) => entry.spec === rightSpec);
if (leftIndex !== -1 || rightIndex !== -1) {
if (leftIndex === -1) return 1;
if (rightIndex === -1) return -1;
return leftIndex - rightIndex;
}
const leftProviderIndex = PROVIDER_SORT_ORDER.indexOf(left.provider);
const rightProviderIndex = PROVIDER_SORT_ORDER.indexOf(right.provider);
if (leftProviderIndex !== -1 || rightProviderIndex !== -1) {
if (leftProviderIndex === -1) return 1;
if (rightProviderIndex === -1) return -1;
return leftProviderIndex - rightProviderIndex;
}
return modelSpec(left).localeCompare(modelSpec(right));
}
function sortProviders(left: ProviderStatus, right: ProviderStatus): number {
if (left.configured !== right.configured) {
return left.configured ? -1 : 1;
}
if (left.current !== right.current) {
return left.current ? -1 : 1;
}
if (left.recommended !== right.recommended) {
return left.recommended ? -1 : 1;
}
const leftIndex = PROVIDER_SORT_ORDER.indexOf(left.id);
const rightIndex = PROVIDER_SORT_ORDER.indexOf(right.id);
if (leftIndex !== -1 || rightIndex !== -1) {
if (leftIndex === -1) return 1;
if (rightIndex === -1) return -1;
return leftIndex - rightIndex;
}
return left.label.localeCompare(right.label);
}
function createModelRegistry(authPath: string): ModelRegistry {
return new ModelRegistry(AuthStorage.create(authPath));
}
export function getAvailableModelRecords(authPath: string): ModelRecord[] {
return createModelRegistry(authPath)
.getAvailable()
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
}
export function getSupportedModelRecords(authPath: string): ModelRecord[] {
return createModelRegistry(authPath)
.getAll()
.map((model) => ({ provider: model.provider, id: model.id, name: model.name }));
}
export function chooseRecommendedModel(authPath: string): { spec: string; reason: string } | undefined {
const available = getAvailableModelRecords(authPath).sort(compareByResearchPreference);
if (available.length === 0) {
return undefined;
}
const matchedPreference = RESEARCH_MODEL_PREFERENCES.find((entry) => entry.spec === modelSpec(available[0]!));
if (matchedPreference) {
return matchedPreference;
}
return {
spec: modelSpec(available[0]!),
reason: "best currently authenticated fallback for research work",
};
}
export function buildModelStatusSnapshotFromRecords(
supported: ModelRecord[],
available: ModelRecord[],
current: string | undefined,
): ModelStatusSnapshot {
const availableSpecs = available
.slice()
.sort(compareByResearchPreference)
.map((model) => modelSpec(model));
const recommended = available.length > 0
? (() => {
const preferred = available.slice().sort(compareByResearchPreference)[0]!;
const matched = RESEARCH_MODEL_PREFERENCES.find((entry) => entry.spec === modelSpec(preferred));
return {
spec: modelSpec(preferred),
reason: matched?.reason ?? "best currently authenticated fallback for research work",
};
})()
: undefined;
const currentValid = current ? availableSpecs.includes(current) : false;
const providerMap = new Map<string, ProviderStatus>();
for (const model of supported) {
const provider = providerMap.get(model.provider) ?? {
id: model.provider,
label: formatProviderLabel(model.provider),
supportedModels: 0,
availableModels: 0,
configured: false,
current: false,
recommended: false,
};
provider.supportedModels += 1;
provider.current ||= current?.startsWith(`${model.provider}/`) ?? false;
provider.recommended ||= recommended?.spec.startsWith(`${model.provider}/`) ?? false;
providerMap.set(model.provider, provider);
}
for (const model of available) {
const provider = providerMap.get(model.provider) ?? {
id: model.provider,
label: formatProviderLabel(model.provider),
supportedModels: 0,
availableModels: 0,
configured: false,
current: false,
recommended: false,
};
provider.availableModels += 1;
provider.configured = true;
provider.current ||= current?.startsWith(`${model.provider}/`) ?? false;
provider.recommended ||= recommended?.spec.startsWith(`${model.provider}/`) ?? false;
providerMap.set(model.provider, provider);
}
const guidance: string[] = [];
if (available.length === 0) {
guidance.push("No authenticated Pi models are available yet.");
guidance.push("Run `feynman model login <provider>` or add provider credentials that Pi can see.");
guidance.push("After auth is in place, rerun `feynman model list` or `feynman setup model`.");
} else if (!current) {
guidance.push(`No default research model is set. Recommended: ${recommended?.spec}.`);
guidance.push("Run `feynman model set <provider/model>` or `feynman setup model`.");
} else if (!currentValid) {
guidance.push(`Configured default model is unavailable: ${current}.`);
if (recommended) {
guidance.push(`Switch to the current research recommendation: ${recommended.spec}.`);
}
}
return {
current,
currentValid,
recommended: recommended?.spec,
recommendationReason: recommended?.reason,
availableModels: availableSpecs,
providers: Array.from(providerMap.values()).sort(sortProviders),
guidance,
};
}

273
src/model/commands.ts Normal file
View File

@@ -0,0 +1,273 @@
import { AuthStorage } from "@mariozechner/pi-coding-agent";
import { writeFileSync } from "node:fs";
import { readJson } from "../pi/settings.js";
import { promptChoice, promptText } from "../setup/prompts.js";
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
import {
buildModelStatusSnapshotFromRecords,
chooseRecommendedModel,
getAvailableModelRecords,
getSupportedModelRecords,
type ModelStatusSnapshot,
} from "./catalog.js";
function formatProviderSummaryLine(status: ModelStatusSnapshot["providers"][number]): string {
const state = status.configured ? `${status.availableModels} authenticated` : "not authenticated";
const flags = [
status.current ? "current" : undefined,
status.recommended ? "recommended" : undefined,
].filter(Boolean);
return `${status.label}: ${state}, ${status.supportedModels} supported${flags.length > 0 ? ` (${flags.join(", ")})` : ""}`;
}
function collectModelStatus(settingsPath: string, authPath: string): ModelStatusSnapshot {
return buildModelStatusSnapshotFromRecords(
getSupportedModelRecords(authPath),
getAvailableModelRecords(authPath),
getCurrentModelSpec(settingsPath),
);
}
type OAuthProviderInfo = {
id: string;
name?: string;
usesCallbackServer?: boolean;
};
function getOAuthProviders(authPath: string): OAuthProviderInfo[] {
return AuthStorage.create(authPath).getOAuthProviders() as OAuthProviderInfo[];
}
function resolveOAuthProvider(authPath: string, input: string): OAuthProviderInfo | undefined {
const normalizedInput = input.trim().toLowerCase();
if (!normalizedInput) {
return undefined;
}
return getOAuthProviders(authPath).find((provider) => provider.id.toLowerCase() === normalizedInput);
}
async function selectOAuthProvider(authPath: string, action: "login" | "logout"): Promise<OAuthProviderInfo | undefined> {
const providers = getOAuthProviders(authPath);
if (providers.length === 0) {
printWarning("No Pi OAuth model providers are available.");
return undefined;
}
if (providers.length === 1) {
return providers[0];
}
const choices = providers.map((provider) => `${provider.id}${provider.name ?? provider.id}`);
choices.push("Cancel");
const selection = await promptChoice(`Choose an OAuth provider to ${action}:`, choices, 0);
if (selection >= providers.length) {
return undefined;
}
return providers[selection];
}
function resolveAvailableModelSpec(authPath: string, input: string): string | undefined {
const normalizedInput = input.trim().toLowerCase();
if (!normalizedInput) {
return undefined;
}
const available = getAvailableModelRecords(authPath);
const fullSpecMatch = available.find((model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedInput);
if (fullSpecMatch) {
return `${fullSpecMatch.provider}/${fullSpecMatch.id}`;
}
const exactIdMatches = available.filter((model) => model.id.toLowerCase() === normalizedInput);
if (exactIdMatches.length === 1) {
return `${exactIdMatches[0]!.provider}/${exactIdMatches[0]!.id}`;
}
return undefined;
}
export function getCurrentModelSpec(settingsPath: string): string | undefined {
const settings = readJson(settingsPath);
if (typeof settings.defaultProvider === "string" && typeof settings.defaultModel === "string") {
return `${settings.defaultProvider}/${settings.defaultModel}`;
}
return undefined;
}
export function printModelStatus(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath);
printInfo(`Current default model: ${status.current ?? "not set"}`);
printInfo(`Current default valid: ${status.currentValid ? "yes" : "no"}`);
printInfo(`Authenticated models: ${status.availableModels.length}`);
printInfo(`Providers with auth: ${status.providers.filter((provider) => provider.configured).length}`);
printInfo(`Research recommendation: ${status.recommended ?? "none available"}`);
if (status.recommendationReason) {
printInfo(`Recommendation reason: ${status.recommendationReason}`);
}
if (status.providers.length > 0) {
printSection("Providers");
for (const provider of status.providers) {
printInfo(formatProviderSummaryLine(provider));
}
}
if (status.guidance.length > 0) {
printSection("Next Steps");
for (const line of status.guidance) {
printWarning(line);
}
}
}
export function printModelProviders(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath);
for (const provider of status.providers) {
printInfo(formatProviderSummaryLine(provider));
}
const oauthProviders = getOAuthProviders(authPath);
if (oauthProviders.length > 0) {
printSection("OAuth Login");
for (const provider of oauthProviders) {
printInfo(`${provider.id}${provider.name ?? provider.id}`);
}
}
if (status.providers.length === 0) {
printWarning("No Pi model providers are visible in the current runtime.");
}
}
export function printModelList(settingsPath: string, authPath: string): void {
const status = collectModelStatus(settingsPath, authPath);
if (status.availableModels.length === 0) {
printWarning("No authenticated Pi models are currently available.");
for (const line of status.guidance) {
printInfo(line);
}
return;
}
let lastProvider: string | undefined;
for (const spec of status.availableModels) {
const [provider] = spec.split("/", 1);
if (provider !== lastProvider) {
lastProvider = provider;
printSection(provider);
}
const markers = [
spec === status.current ? "current" : undefined,
spec === status.recommended ? "recommended" : undefined,
].filter(Boolean);
printInfo(`${spec}${markers.length > 0 ? ` (${markers.join(", ")})` : ""}`);
}
}
export function printModelRecommendation(authPath: string): void {
const recommendation = chooseRecommendedModel(authPath);
if (!recommendation) {
printWarning("No authenticated Pi models are available to recommend.");
printInfo("Run `feynman model login <provider>` or add provider credentials that Pi can see, then rerun this command.");
return;
}
printSuccess(`Recommended model: ${recommendation.spec}`);
printInfo(recommendation.reason);
}
export async function loginModelProvider(authPath: string, providerId?: string): Promise<void> {
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login");
if (!provider) {
if (providerId) {
throw new Error(`Unknown OAuth model provider: ${providerId}`);
}
printInfo("Login cancelled.");
return;
}
const authStorage = AuthStorage.create(authPath);
const abortController = new AbortController();
await authStorage.login(provider.id, {
onAuth: (info: { url: string; instructions?: string }) => {
printSection(`Login: ${provider.name ?? provider.id}`);
printInfo(`Open this URL: ${info.url}`);
if (info.instructions) {
printInfo(info.instructions);
}
},
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
return promptText(prompt.message, prompt.placeholder ?? "");
},
onProgress: (message: string) => {
printInfo(message);
},
onManualCodeInput: async () => {
return promptText("Paste redirect URL or auth code");
},
signal: abortController.signal,
});
printSuccess(`Model provider login complete: ${provider.id}`);
}
export async function logoutModelProvider(authPath: string, providerId?: string): Promise<void> {
const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "logout");
if (!provider) {
if (providerId) {
throw new Error(`Unknown OAuth model provider: ${providerId}`);
}
printInfo("Logout cancelled.");
return;
}
AuthStorage.create(authPath).logout(provider.id);
printSuccess(`Model provider logout complete: ${provider.id}`);
}
export function setDefaultModelSpec(settingsPath: string, authPath: string, spec: string): void {
const resolvedSpec = resolveAvailableModelSpec(authPath, spec);
if (!resolvedSpec) {
throw new Error(`Model not available in Pi auth storage: ${spec}. Run \`feynman model list\` first.`);
}
const [provider, ...rest] = resolvedSpec.split("/");
const modelId = rest.join("/");
const settings = readJson(settingsPath);
settings.defaultProvider = provider;
settings.defaultModel = modelId;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
printSuccess(`Default model set to ${resolvedSpec}`);
}
export async function runModelSetup(settingsPath: string, authPath: string): Promise<void> {
const status = collectModelStatus(settingsPath, authPath);
if (status.availableModels.length === 0) {
printWarning("No Pi models are currently authenticated for Feynman.");
for (const line of status.guidance) {
printInfo(line);
}
printInfo("Tip: run `feynman model login <provider>` if your provider supports Pi OAuth login.");
return;
}
const choices = status.availableModels.map((spec) => {
const markers = [
spec === status.recommended ? "recommended" : undefined,
spec === status.current ? "current" : undefined,
].filter(Boolean);
return `${spec}${markers.length > 0 ? ` (${markers.join(", ")})` : ""}`;
});
choices.push(`Keep current (${status.current ?? "unset"})`);
const defaultIndex = status.current ? Math.max(0, status.availableModels.indexOf(status.current)) : 0;
const selection = await promptChoice("Select your default research model:", choices, defaultIndex >= 0 ? defaultIndex : 0);
if (selection >= status.availableModels.length) {
printInfo("Skipped (keeping current model)");
return;
}
setDefaultModelSpec(settingsPath, authPath, status.availableModels[selection]!);
}