feat: add API key and custom provider configuration (#4)

* feat: add API key and custom provider configuration

Previously, model setup only offered OAuth login. This adds:

- API key configuration for 17 built-in providers (OpenAI, Anthropic,
  Google, Mistral, Groq, xAI, OpenRouter, etc.)
- Custom provider setup via models.json (for Ollama, vLLM, LM Studio,
  proxies, or any OpenAI/Anthropic/Google-compatible endpoint)
- Interactive prompts with smart defaults and auto-detection of models
- Verification flow that probes endpoints and provides actionable tips
- Doctor diagnostics for models.json path and missing apiKey warnings
- Dev environment fallback for running without dist/ build artifacts
- Unified auth flow: `feynman model login` now offers both API key
  and OAuth options (OAuth-only when a specific provider is given)

New files:
- src/model/models-json.ts: Read/write models.json with proper merging
- src/model/registry.ts: Centralized ModelRegistry creation with modelsJsonPath
- tests/models-json.test.ts: Unit tests for provider config upsert

* fix: harden runtime env and custom provider auth

---------

Co-authored-by: Advait Paliwal <advaitspaliwal@gmail.com>
This commit is contained in:
Mochamad Chairulridjal
2026-03-27 07:09:38 +07:00
committed by GitHub
parent dbd89d8e3d
commit 30d07246d1
13 changed files with 745 additions and 23 deletions

View File

@@ -7,11 +7,14 @@ import { ensureSupportedNodeVersion } from "../system/node-version.js";
export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
ensureSupportedNodeVersion();
const { piCliPath, promisePolyfillPath } = resolvePiPaths(options.appRoot);
const { piCliPath, promisePolyfillPath, promisePolyfillSourcePath, tsxLoaderPath } = resolvePiPaths(options.appRoot);
if (!existsSync(piCliPath)) {
throw new Error(`Pi CLI not found: ${piCliPath}`);
}
if (!existsSync(promisePolyfillPath)) {
const useBuiltPolyfill = existsSync(promisePolyfillPath);
const useDevPolyfill = !useBuiltPolyfill && existsSync(promisePolyfillSourcePath) && existsSync(tsxLoaderPath);
if (!useBuiltPolyfill && !useDevPolyfill) {
throw new Error(`Promise polyfill not found: ${promisePolyfillPath}`);
}
@@ -19,7 +22,11 @@ export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
}
const child = spawn(process.execPath, ["--import", promisePolyfillPath, piCliPath, ...buildPiArgs(options)], {
const importArgs = useDevPolyfill
? ["--import", tsxLoaderPath, "--import", promisePolyfillSourcePath]
: ["--import", promisePolyfillPath];
const child = spawn(process.execPath, [...importArgs, piCliPath, ...buildPiArgs(options)], {
cwd: options.workingDir,
stdio: "inherit",
env: buildPiEnv(options),