diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb43d5..51dda67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,3 +167,22 @@ Use this file to track chronology, not release notes. Keep entries short, factua - Failed / learned: Website typecheck was previously a no-op prompt because `@astrojs/check` was missing; installing it exposed dev-audit findings that needed explicit overrides before the full website audit was clean. - Blockers: Docker Desktop remained unreliable after restart attempts, so this pass still does not include a second successful public-installer Linux Docker run. - Next: Push the RPC/website verification commit and keep future Docker/public-installer validation separate from repo correctness unless Docker is stable. + +### 2026-04-12 09:32 PDT — pi-0.66.1-upgrade-pass + +- Objective: Update Feynman from Pi `0.64.0` to the current `0.66.1` packages and absorb any downstream SDK/runtime compatibility changes instead of leaving the repo pinned behind upstream. +- Changed: Bumped `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` to `0.66.1` plus `@companion-ai/alpha-hub` to `0.1.3` in `package.json` and `package-lock.json`; updated `extensions/research-tools.ts` to stop listening for the removed `session_switch` extension event and rely on `session_start`, which now carries startup/reload/new/resume/fork reasons in Pi `0.66.x`. +- Verified: Ran `npm test`, `npm run typecheck`, and `npm run build` successfully after the upgrade; smoke-ran `node bin/feynman.js --version`, `node bin/feynman.js doctor`, and `node bin/feynman.js status` successfully; checked upstream package diffs and confirmed the breaking change that affected this repo was the typed extension lifecycle change in `pi-coding-agent`, while `pi-ai` mainly brought refreshed provider/model catalog code including Bedrock/OpenAI provider updates and new generated model entries. +- Failed / learned: `ctx7` resolved Pi correctly to `/badlogic/pi-mono`, but its docs snapshot was not release-note oriented; the concrete downstream-impact analysis came from the actual `0.64.0` → `0.66.1` package diffs and local validation, not from prose docs alone. +- Failed / learned: The first post-upgrade CLI smoke test failed before Feynman startup because `@companion-ai/alpha-hub@0.1.2` shipped a zero-byte `src/lib/auth.js`; bumping to `0.1.3` fixed that adjacent runtime blocker. +- Blockers: `npm install` reports two high-severity vulnerabilities remain in the dependency tree; this pass focused on the Pi upgrade and did not remediate unrelated audit findings. +- Next: Push the Pi upgrade, then decide whether to layer the pending model-command fixes on top of this branch or land them separately to keep the dependency bump easy to review. + +### 2026-04-12 13:00 PDT — model-command-and-bedrock-fix-pass + +- Objective: Finish the remaining user-facing model-management regressions instead of stopping at the Pi dependency bump. +- Changed: Updated `src/model/commands.ts` so `feynman model login ` resolves both OAuth and API-key providers; `feynman model logout ` clears either auth mode; `feynman model set` accepts both `provider/model` and `provider:model`; ambiguous bare model IDs now prefer explicitly configured providers from auth storage; added an `amazon-bedrock` setup path that validates the AWS credential chain with the AWS SDK and stores Pi's `` sentinel so Bedrock models appear in `model list`; synced `src/cli.ts`, `metadata/commands.mjs`, `README.md`, and the website docs to the new behavior. +- Verified: Added regression tests in `tests/model-harness.test.ts` for `provider:model`, API-key provider resolution, and ambiguous bare-ID handling; ran `npm test`, `npm run typecheck`, `npm run build`, and `cd website && npm run build`; exercised command-level flows against throwaway `FEYNMAN_HOME` directories: interactive `node bin/feynman.js model login google`, `node bin/feynman.js model set google:gemini-3-pro-preview`, `node bin/feynman.js model set gpt-5.4` with only OpenAI configured, and `node bin/feynman.js model login amazon-bedrock`; confirmed `model list` shows Bedrock models after the new setup path; ran a live one-shot prompt `node bin/feynman.js --prompt "Reply with exactly OK"` and got `OK`. +- Failed / learned: The website build still emits duplicate-id warnings for a handful of docs pages, but it completes successfully; those warnings predate this pass and were not introduced by the model-command edits. +- Blockers: The Bedrock path is verified with the current shell's AWS credential chain, not with a fresh machine lacking AWS config; broader upstream Pi behavior around IMDS/default-profile autodiscovery without the sentinel is still outside this repo. +- Next: Commit and push the combined Pi/model/docs maintenance branch, then decide whether to tackle the deeper search/deepresearch hang issues separately or leave them for focused repro work. diff --git a/README.md b/README.md index cb82803..2d5aaa8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ The one-line installer fetches the latest tagged release. To pin a version, pass The installer downloads a standalone native bundle with its own Node.js runtime. +To upgrade the standalone app later, rerun the installer. `feynman update` only refreshes installed Pi packages inside Feynman's environment; it does not replace the standalone runtime bundle itself. + Local models are supported through the custom-provider flow. For Ollama, run `feynman setup`, choose `Custom provider (baseUrl + API key)`, use `openai-completions`, and point it at `http://localhost:11434/v1`. ### Skills Only diff --git a/extensions/research-tools.ts b/extensions/research-tools.ts index 39e39d5..f32b705 100644 --- a/extensions/research-tools.ts +++ b/extensions/research-tools.ts @@ -11,14 +11,11 @@ import { registerServiceTierControls } from "./research-tools/service-tier.js"; export default function researchTools(pi: ExtensionAPI): void { const cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }> } = {}; + // Pi 0.66.x folds post-switch/resume lifecycle into session_start. pi.on("session_start", async (_event, ctx) => { await installFeynmanHeader(pi, ctx, cache); }); - pi.on("session_switch", async (_event, ctx) => { - await installFeynmanHeader(pi, ctx, cache); - }); - registerAlphaTools(pi); registerDiscoveryCommands(pi); registerFeynmanModelCommand(pi); diff --git a/metadata/commands.mjs b/metadata/commands.mjs index 9ec7e14..4fe8e47 100644 --- a/metadata/commands.mjs +++ b/metadata/commands.mjs @@ -86,9 +86,9 @@ export const cliCommandSections = [ title: "Model Management", commands: [ { usage: "feynman model list", description: "List available models in Pi auth storage." }, - { usage: "feynman model login [id]", description: "Login to a Pi OAuth model provider." }, - { usage: "feynman model logout [id]", description: "Logout from a Pi OAuth model provider." }, - { usage: "feynman model set ", description: "Set the default model." }, + { usage: "feynman model login [id]", description: "Authenticate a model provider with OAuth or API-key setup." }, + { usage: "feynman model logout [id]", description: "Clear stored auth for a model provider." }, + { usage: "feynman model set ", description: "Set the default model (also accepts provider:model)." }, { usage: "feynman model tier [value]", description: "View or set the request service tier override." }, ], }, @@ -118,7 +118,7 @@ export const legacyFlags = [ { usage: "--alpha-login", description: "Sign in to alphaXiv and exit." }, { usage: "--alpha-logout", description: "Clear alphaXiv auth and exit." }, { usage: "--alpha-status", description: "Show alphaXiv auth status and exit." }, - { usage: "--model ", description: "Force a specific model." }, + { usage: "--model ", description: "Force a specific model." }, { usage: "--service-tier ", description: "Override request service tier for this run." }, { usage: "--thinking ", description: "Set thinking level: off | minimal | low | medium | high | xhigh." }, { usage: "--cwd ", description: "Set the working directory for tools." }, diff --git a/package-lock.json b/package-lock.json index 069ae7d..857616e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@companion-ai/alpha-hub": "^0.1.2", - "@mariozechner/pi-ai": "^0.64.0", - "@mariozechner/pi-coding-agent": "^0.64.0", + "@companion-ai/alpha-hub": "^0.1.3", + "@mariozechner/pi-ai": "^0.66.1", + "@mariozechner/pi-coding-agent": "^0.66.1", "@sinclair/typebox": "^0.34.48", "dotenv": "^17.3.1" }, @@ -781,9 +781,9 @@ } }, "node_modules/@companion-ai/alpha-hub": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@companion-ai/alpha-hub/-/alpha-hub-0.1.2.tgz", - "integrity": "sha512-YAFh4B6loo7lKRjW3UFsdoiW3ZRvLdSdP7liDsHhCxY1dzfbxNU8vDAloodiK4ieDVRqMBTmG9NYbnsb4NZUGw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@companion-ai/alpha-hub/-/alpha-hub-0.1.3.tgz", + "integrity": "sha512-g/JoqeGDCoSvkgs1ZSTYJhbTak0zVanQyoYOvf2tDgfqJ09gfkqmSGFDmiP4PkTn1bocPqywZIABgmv25x1uYA==", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", @@ -1469,21 +1469,21 @@ } }, "node_modules/@mariozechner/pi-agent-core": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.64.0.tgz", - "integrity": "sha512-IN/sIxWOD0v1OFVXHB605SGiZhO5XdEWG5dO8EAV08n3jz/p12o4OuYGvhGXmHhU28WXa/FGWC+FO5xiIih8Uw==", + "version": "0.66.1", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.66.1.tgz", + "integrity": "sha512-Nj54A7SuB/EQi8r3Gs+glFOr9wz/a9uxYFf0pCLf2DE7VmzA9O7WSejrvArna17K6auftLSdNyRRe2bIO0qezg==", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.64.0" + "@mariozechner/pi-ai": "^0.66.1" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@mariozechner/pi-ai": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.64.0.tgz", - "integrity": "sha512-Z/Jnf+JSVDPLRcxJsa8XhYTJKIqKekNueaCpBLGQHgizL1F9RQ1Rur3rIfZpfXkt2cLu/AIPtOs223ueuoWaWg==", + "version": "0.66.1", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.66.1.tgz", + "integrity": "sha512-7IZHvpsFdKEBkTmjNrdVL7JLUJVIpha6bwTr12cZ5XyDrxij06wP6Ncpnf4HT5BXAzD5w2JnoqTOSbMEIZj3dg==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -1508,15 +1508,15 @@ } }, "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.64.0.tgz", - "integrity": "sha512-Q4tcqSqFGQtOgCtRyIp1D80Nv2if13Q2pfbnrOlaT/mix90mLcZGML9jKVnT1jGSy5GMYudU1HsS7cx53kxb0g==", + "version": "0.66.1", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.66.1.tgz", + "integrity": "sha512-cNmatT+5HvYzQ78cRhRih00wCeUTH/fFx9ecJh5AbN7axgWU+bwiZYy0cjrTsGVgMGF4xMYlPRn/Nze9JEB+/w==", "license": "MIT", "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.64.0", - "@mariozechner/pi-ai": "^0.64.0", - "@mariozechner/pi-tui": "^0.64.0", + "@mariozechner/pi-agent-core": "^0.66.1", + "@mariozechner/pi-ai": "^0.66.1", + "@mariozechner/pi-tui": "^0.66.1", "@silvia-odwyer/photon-node": "^0.3.4", "ajv": "^8.17.1", "chalk": "^5.5.0", @@ -1545,9 +1545,9 @@ } }, "node_modules/@mariozechner/pi-tui": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.64.0.tgz", - "integrity": "sha512-W1qLry9MAuN/V3YJmMv/BJa0VaYv721NkXPg/DGItdqWxuDc+1VdNbyAnRwxblNkIpXVUWL26x64BlyFXpxmkg==", + "version": "0.66.1", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.66.1.tgz", + "integrity": "sha512-hNFN42ebjwtfGooqoUwM+QaPR1XCyqPuueuP3aLOWS1bZ2nZP/jq8MBuGNrmMw1cgiDcotvOlSNj3BatzEOGsw==", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -3844,9 +3844,9 @@ } }, "node_modules/koffi": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz", - "integrity": "sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==", + "version": "2.15.6", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.6.tgz", + "integrity": "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw==", "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index 5550852..e497b3d 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,9 @@ ] }, "dependencies": { - "@companion-ai/alpha-hub": "^0.1.2", - "@mariozechner/pi-ai": "^0.64.0", - "@mariozechner/pi-coding-agent": "^0.64.0", + "@companion-ai/alpha-hub": "^0.1.3", + "@mariozechner/pi-ai": "^0.66.1", + "@mariozechner/pi-coding-agent": "^0.66.1", "@sinclair/typebox": "^0.34.48", "dotenv": "^17.3.1" }, diff --git a/src/cli.ts b/src/cli.ts index 2990885..edbfc74 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -130,7 +130,7 @@ async function handleModelCommand(subcommand: string | undefined, args: string[] if (subcommand === "login") { if (args[0]) { - // Specific provider given - use OAuth login directly + // Specific provider given - resolve OAuth vs API-key setup automatically await loginModelProvider(feynmanAuthPath, args[0], feynmanSettingsPath); } else { // No provider specified - show auth method choice @@ -147,7 +147,7 @@ async function handleModelCommand(subcommand: string | undefined, args: string[] if (subcommand === "set") { const spec = args[0]; if (!spec) { - throw new Error("Usage: feynman model set "); + throw new Error("Usage: feynman model set "); } setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec); return; diff --git a/src/model/commands.ts b/src/model/commands.ts index 004178f..42b6057 100644 --- a/src/model/commands.ts +++ b/src/model/commands.ts @@ -75,6 +75,7 @@ const API_KEY_PROVIDERS: ApiKeyProviderInfo[] = [ { id: "openai", label: "OpenAI Platform API", envVar: "OPENAI_API_KEY" }, { id: "anthropic", label: "Anthropic API", envVar: "ANTHROPIC_API_KEY" }, { id: "google", label: "Google Gemini API", envVar: "GEMINI_API_KEY" }, + { id: "amazon-bedrock", label: "Amazon Bedrock (AWS credential chain)" }, { id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" }, { id: "zai", label: "Z.AI / GLM", envVar: "ZAI_API_KEY" }, { id: "kimi-coding", label: "Kimi / Moonshot", envVar: "KIMI_API_KEY" }, @@ -91,6 +92,31 @@ const API_KEY_PROVIDERS: ApiKeyProviderInfo[] = [ { id: "azure-openai-responses", label: "Azure OpenAI (Responses)", envVar: "AZURE_OPENAI_API_KEY" }, ]; +function resolveApiKeyProvider(input: string): ApiKeyProviderInfo | undefined { + const normalizedInput = normalizeProviderId(input); + if (!normalizedInput) { + return undefined; + } + return API_KEY_PROVIDERS.find((provider) => provider.id === normalizedInput); +} + +export function resolveModelProviderForCommand( + authPath: string, + input: string, +): { kind: "oauth" | "api-key"; id: string } | undefined { + const oauthProvider = resolveOAuthProvider(authPath, input); + if (oauthProvider) { + return { kind: "oauth", id: oauthProvider.id }; + } + + const apiKeyProvider = resolveApiKeyProvider(input); + if (apiKeyProvider) { + return { kind: "api-key", id: apiKeyProvider.id }; + } + + return undefined; +} + async function selectApiKeyProvider(): Promise { const choices = API_KEY_PROVIDERS.map( (provider) => `${provider.id} — ${provider.label}${provider.envVar ? ` (${provider.envVar})` : ""}`, @@ -447,13 +473,66 @@ async function verifyCustomProvider(setup: CustomProviderSetup, authPath: string printInfo("Verification: skipped network probe for this API mode."); } -async function configureApiKeyProvider(authPath: string): Promise { - const provider = await selectApiKeyProvider(); +async function verifyBedrockCredentialChain(): Promise { + const { defaultProvider } = await import("@aws-sdk/credential-provider-node"); + const credentials = await defaultProvider({})(); + if (!credentials?.accessKeyId || !credentials?.secretAccessKey) { + throw new Error("AWS credential chain resolved without usable Bedrock credentials."); + } +} + +async function configureBedrockProvider(authPath: string): Promise { + printSection("AWS Credentials: Amazon Bedrock"); + printInfo("Feynman will verify the AWS SDK credential chain used by Pi's Bedrock provider."); + printInfo("Supported sources include AWS_PROFILE, ~/.aws credentials/config, SSO, ECS/IRSA, and EC2 instance roles."); + + try { + await verifyBedrockCredentialChain(); + AuthStorage.create(authPath).set("amazon-bedrock", { type: "api_key", key: "" }); + printSuccess("Verified AWS credential chain and marked Amazon Bedrock as configured."); + printInfo("Use `feynman model list` to see available Bedrock models."); + return true; + } catch (error) { + printWarning(`AWS credential verification failed: ${error instanceof Error ? error.message : String(error)}`); + printInfo("Configure AWS credentials first, for example:"); + printInfo(" export AWS_PROFILE=default"); + printInfo(" # or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY"); + printInfo(" # or use an EC2/ECS/IRSA role with valid Bedrock access"); + return false; + } +} + +function maybeSetRecommendedDefaultModel(settingsPath: string | undefined, authPath: string): void { + if (!settingsPath) { + return; + } + + const currentSpec = getCurrentModelSpec(settingsPath); + const available = getAvailableModelRecords(authPath); + const currentValid = currentSpec ? available.some((m) => `${m.provider}/${m.id}` === currentSpec) : false; + + if ((!currentSpec || !currentValid) && available.length > 0) { + const recommended = chooseRecommendedModel(authPath); + if (recommended) { + setDefaultModelSpec(settingsPath, authPath, recommended.spec); + } + } +} + +async function configureApiKeyProvider(authPath: string, providerId?: string): Promise { + const provider = providerId ? resolveApiKeyProvider(providerId) : await selectApiKeyProvider(); if (!provider) { + if (providerId) { + throw new Error(`Unknown API-key model provider: ${providerId}`); + } printInfo("API key setup cancelled."); return false; } + if (provider.id === "amazon-bedrock") { + return configureBedrockProvider(authPath); + } + if (provider.id === "__custom__") { const setup = await promptCustomProviderSetup(); if (!setup) { @@ -512,7 +591,7 @@ async function configureApiKeyProvider(authPath: string): Promise { } function resolveAvailableModelSpec(authPath: string, input: string): string | undefined { - const normalizedInput = input.trim().toLowerCase(); + const normalizedInput = input.trim().replace(/^([^/:]+):(.+)$/, "$1/$2").toLowerCase(); if (!normalizedInput) { return undefined; } @@ -528,6 +607,17 @@ function resolveAvailableModelSpec(authPath: string, input: string): string | un return `${exactIdMatches[0]!.provider}/${exactIdMatches[0]!.id}`; } + // When multiple providers expose the same bare model ID, prefer providers the + // user explicitly configured in auth storage. + if (exactIdMatches.length > 1) { + const authData = readJson(authPath) as Record; + const configuredProviders = new Set(Object.keys(authData)); + const configuredMatches = exactIdMatches.filter((model) => configuredProviders.has(model.provider)); + if (configuredMatches.length === 1) { + return `${configuredMatches[0]!.provider}/${configuredMatches[0]!.id}`; + } + } + return undefined; } @@ -574,16 +664,8 @@ export async function authenticateModelProvider(authPath: string, settingsPath?: if (selection === 0) { const configured = await configureApiKeyProvider(authPath); - if (configured && settingsPath) { - const currentSpec = getCurrentModelSpec(settingsPath); - const available = getAvailableModelRecords(authPath); - const currentValid = currentSpec ? available.some((m) => `${m.provider}/${m.id}` === currentSpec) : false; - if ((!currentSpec || !currentValid) && available.length > 0) { - const recommended = chooseRecommendedModel(authPath); - if (recommended) { - setDefaultModelSpec(settingsPath, authPath, recommended.spec); - } - } + if (configured) { + maybeSetRecommendedDefaultModel(settingsPath, authPath); } return configured; } @@ -597,10 +679,24 @@ export async function authenticateModelProvider(authPath: string, settingsPath?: } export async function loginModelProvider(authPath: string, providerId?: string, settingsPath?: string): Promise { + if (providerId) { + const resolvedProvider = resolveModelProviderForCommand(authPath, providerId); + if (!resolvedProvider) { + throw new Error(`Unknown model provider: ${providerId}`); + } + if (resolvedProvider.kind === "api-key") { + const configured = await configureApiKeyProvider(authPath, resolvedProvider.id); + if (configured) { + maybeSetRecommendedDefaultModel(settingsPath, authPath); + } + return configured; + } + } + const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "login"); if (!provider) { if (providerId) { - throw new Error(`Unknown OAuth model provider: ${providerId}`); + throw new Error(`Unknown model provider: ${providerId}`); } printInfo("Login cancelled."); return false; @@ -637,35 +733,38 @@ export async function loginModelProvider(authPath: string, providerId?: string, printSuccess(`Model provider login complete: ${provider.id}`); - if (settingsPath) { - const currentSpec = getCurrentModelSpec(settingsPath); - const available = getAvailableModelRecords(authPath); - const currentValid = currentSpec - ? available.some((m) => `${m.provider}/${m.id}` === currentSpec) - : false; - - if ((!currentSpec || !currentValid) && available.length > 0) { - const recommended = chooseRecommendedModel(authPath); - if (recommended) { - setDefaultModelSpec(settingsPath, authPath, recommended.spec); - } - } - } + maybeSetRecommendedDefaultModel(settingsPath, authPath); return true; } export async function logoutModelProvider(authPath: string, providerId?: string): Promise { - const provider = providerId ? resolveOAuthProvider(authPath, providerId) : await selectOAuthProvider(authPath, "logout"); - if (!provider) { - if (providerId) { - throw new Error(`Unknown OAuth model provider: ${providerId}`); + const authStorage = AuthStorage.create(authPath); + if (providerId) { + const resolvedProvider = resolveModelProviderForCommand(authPath, providerId); + if (resolvedProvider) { + authStorage.logout(resolvedProvider.id); + printSuccess(`Model provider logout complete: ${resolvedProvider.id}`); + return; } + + const normalizedProviderId = normalizeProviderId(providerId); + if (authStorage.has(normalizedProviderId)) { + authStorage.logout(normalizedProviderId); + printSuccess(`Model provider logout complete: ${normalizedProviderId}`); + return; + } + + throw new Error(`Unknown model provider: ${providerId}`); + } + + const provider = await selectOAuthProvider(authPath, "logout"); + if (!provider) { printInfo("Logout cancelled."); return; } - AuthStorage.create(authPath).logout(provider.id); + authStorage.logout(provider.id); printSuccess(`Model provider logout complete: ${provider.id}`); } diff --git a/tests/model-harness.test.ts b/tests/model-harness.test.ts index ee9a989..87050e3 100644 --- a/tests/model-harness.test.ts +++ b/tests/model-harness.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { resolveInitialPrompt } from "../src/cli.js"; import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel } from "../src/model/catalog.js"; -import { setDefaultModelSpec } from "../src/model/commands.js"; +import { resolveModelProviderForCommand, setDefaultModelSpec } from "../src/model/commands.js"; function createAuthPath(contents: Record): string { const root = mkdtempSync(join(tmpdir(), "feynman-auth-")); @@ -42,6 +42,56 @@ test("setDefaultModelSpec accepts a unique bare model id from authenticated mode assert.equal(settings.defaultModel, "gpt-5.4"); }); +test("setDefaultModelSpec accepts provider:model syntax for authenticated models", () => { + const authPath = createAuthPath({ + google: { type: "api_key", key: "google-test-key" }, + }); + const settingsPath = join(mkdtempSync(join(tmpdir(), "feynman-settings-")), "settings.json"); + + setDefaultModelSpec(settingsPath, authPath, "google:gemini-3-pro-preview"); + + const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as { + defaultProvider?: string; + defaultModel?: string; + }; + assert.equal(settings.defaultProvider, "google"); + assert.equal(settings.defaultModel, "gemini-3-pro-preview"); +}); + +test("resolveModelProviderForCommand falls back to API-key providers when OAuth is unavailable", () => { + const authPath = createAuthPath({}); + + const resolved = resolveModelProviderForCommand(authPath, "google"); + + assert.equal(resolved?.kind, "api-key"); + assert.equal(resolved?.id, "google"); +}); + +test("resolveModelProviderForCommand prefers OAuth when a provider supports both auth modes", () => { + const authPath = createAuthPath({}); + + const resolved = resolveModelProviderForCommand(authPath, "anthropic"); + + assert.equal(resolved?.kind, "oauth"); + assert.equal(resolved?.id, "anthropic"); +}); + +test("setDefaultModelSpec prefers the explicitly configured provider when a bare model id is ambiguous", () => { + const authPath = createAuthPath({ + openai: { type: "api_key", key: "openai-test-key" }, + }); + const settingsPath = join(mkdtempSync(join(tmpdir(), "feynman-settings-")), "settings.json"); + + setDefaultModelSpec(settingsPath, authPath, "gpt-5.4"); + + const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as { + defaultProvider?: string; + defaultModel?: string; + }; + assert.equal(settings.defaultProvider, "openai"); + assert.equal(settings.defaultModel, "gpt-5.4"); +}); + test("buildModelStatusSnapshotFromRecords flags an invalid current model and suggests a replacement", () => { const snapshot = buildModelStatusSnapshotFromRecords( [ diff --git a/website/src/content/docs/getting-started/configuration.md b/website/src/content/docs/getting-started/configuration.md index 5e653a1..1920640 100644 --- a/website/src/content/docs/getting-started/configuration.md +++ b/website/src/content/docs/getting-started/configuration.md @@ -22,17 +22,18 @@ The `settings.json` file is the primary configuration file. It is created by `fe ```json { - "defaultModel": "anthropic:claude-sonnet-4-20250514", - "thinkingLevel": "medium" + "defaultProvider": "anthropic", + "defaultModel": "claude-sonnet-4-20250514", + "defaultThinkingLevel": "medium" } ``` ## Model configuration -The `defaultModel` field sets which model is used when you launch Feynman without the `--model` flag. The format is `provider:model-name`. You can change it via the CLI: +The `defaultProvider` and `defaultModel` fields set which model is used when you launch Feynman without the `--model` flag. You can change them via the CLI: ```bash -feynman model set anthropic:claude-opus-4-20250514 +feynman model set anthropic/claude-opus-4-20250514 ``` To see all models you have configured: @@ -48,6 +49,7 @@ To add another provider, authenticate it first: ```bash feynman model login anthropic feynman model login google +feynman model login amazon-bedrock ``` Then switch the default model: @@ -56,6 +58,8 @@ Then switch the default model: feynman model set anthropic/claude-opus-4-6 ``` +The `model set` command accepts both `provider/model` and `provider:model` formats. `feynman model login google` opens the API-key flow directly, while `feynman model login amazon-bedrock` verifies the AWS credential chain that Pi uses for Bedrock access. + ## Subagent model overrides Feynman's bundled subagents inherit the main default model unless you override them explicitly. Inside the REPL, run: @@ -90,7 +94,8 @@ Feynman respects the following environment variables, which take precedence over | `FEYNMAN_THINKING` | Override the thinking level | | `ANTHROPIC_API_KEY` | Anthropic API key | | `OPENAI_API_KEY` | OpenAI API key | -| `GOOGLE_API_KEY` | Google AI API key | +| `GEMINI_API_KEY` | Google Gemini API key | +| `AWS_PROFILE` | Preferred AWS profile for Amazon Bedrock | | `TAVILY_API_KEY` | Tavily web search API key | | `SERPER_API_KEY` | Serper web search API key | diff --git a/website/src/content/docs/getting-started/installation.md b/website/src/content/docs/getting-started/installation.md index 224df73..1912560 100644 --- a/website/src/content/docs/getting-started/installation.md +++ b/website/src/content/docs/getting-started/installation.md @@ -27,6 +27,12 @@ irm https://feynman.is/install.ps1 | iex This installs the Windows runtime bundle under `%LOCALAPPDATA%\Programs\feynman`, adds its launcher to your user `PATH`, and lets you re-run the installer at any time to update. +## Updating the standalone app + +To update the standalone Feynman app on macOS, Linux, or Windows, rerun the installer you originally used. That replaces the downloaded runtime bundle with the latest tagged release. + +`feynman update` is different: it updates installed Pi packages inside Feynman's environment, not the standalone app bundle itself. + ## Skills only If you only want Feynman's research skills and not the full terminal runtime, install the skill library separately. diff --git a/website/src/content/docs/getting-started/setup.md b/website/src/content/docs/getting-started/setup.md index 608449b..619387c 100644 --- a/website/src/content/docs/getting-started/setup.md +++ b/website/src/content/docs/getting-started/setup.md @@ -28,7 +28,7 @@ Feynman supports multiple model providers. The setup wizard presents a list of a google:gemini-2.5-pro ``` -The model you choose here becomes the default for all sessions. You can override it per-session with the `--model` flag or change it later via `feynman model set `. +The model you choose here becomes the default for all sessions. You can override it per-session with the `--model` flag or change it later via `feynman model set ` or `feynman model set `. ## Stage 2: Authentication @@ -42,6 +42,16 @@ For API key providers, you are prompted to paste your key directly: Keys are encrypted at rest and never sent anywhere except the provider's API endpoint. +### Amazon Bedrock + +For Amazon Bedrock, choose: + +```text +Amazon Bedrock (AWS credential chain) +``` + +Feynman verifies the same AWS credential chain Pi uses at runtime, including `AWS_PROFILE`, `~/.aws` credentials/config, SSO, ECS/IRSA, and EC2 instance roles. Once that check passes, Bedrock models become available in `feynman model list` without needing a traditional API key. + ### Local models: Ollama, LM Studio, vLLM If you want to use a model running locally, choose the API-key flow and then select: diff --git a/website/src/content/docs/reference/cli-commands.md b/website/src/content/docs/reference/cli-commands.md index 8505a0d..788227a 100644 --- a/website/src/content/docs/reference/cli-commands.md +++ b/website/src/content/docs/reference/cli-commands.md @@ -23,11 +23,11 @@ This page covers the dedicated Feynman CLI commands and flags. Workflow commands | Command | Description | | --- | --- | | `feynman model list` | List available models in Pi auth storage | -| `feynman model login [id]` | Login to a Pi OAuth model provider | -| `feynman model logout [id]` | Logout from a Pi OAuth model provider | -| `feynman model set ` | Set the default model for all sessions | +| `feynman model login [id]` | Authenticate a model provider with OAuth or API-key setup | +| `feynman model logout [id]` | Clear stored auth for a model provider | +| `feynman model set ` | Set the default model for all sessions | -These commands manage your model provider configuration. The `model set` command updates `~/.feynman/settings.json` with the new default. The format is `provider:model-name`, for example `anthropic:claude-sonnet-4-20250514`. +These commands manage your model provider configuration. The `model set` command updates `~/.feynman/settings.json` with the new default. It accepts either `provider/model-name` or `provider:model-name`, for example `anthropic/claude-sonnet-4-20250514` or `anthropic:claude-sonnet-4-20250514`. Running `feynman model login google` or `feynman model login amazon-bedrock` routes directly into the relevant API-key setup flow instead of requiring the interactive picker. ## AlphaXiv commands @@ -76,7 +76,7 @@ These are equivalent to launching the REPL and typing the corresponding slash co | Flag | Description | | --- | --- | | `--prompt ""` | Run one prompt and exit (one-shot mode) | -| `--model ` | Force a specific model for this session | +| `--model ` | Force a specific model for this session | | `--thinking ` | Set thinking level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | | `--cwd ` | Set the working directory for all file operations | | `--session-dir ` | Set the session storage directory | diff --git a/website/src/content/docs/reference/package-stack.md b/website/src/content/docs/reference/package-stack.md index a6d6960..52a84ce 100644 --- a/website/src/content/docs/reference/package-stack.md +++ b/website/src/content/docs/reference/package-stack.md @@ -74,3 +74,5 @@ feynman update pi-subagents ``` Running `feynman update` without arguments updates everything. Pass a specific package name to update just that one. Updates are safe and preserve your configuration. + +This command updates Pi packages inside Feynman's environment. To upgrade the standalone Feynman app itself, rerun the installer from the [Installation guide](/docs/getting-started/installation).