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:
committed by
GitHub
parent
dbd89d8e3d
commit
30d07246d1
@@ -1,6 +1,7 @@
|
||||
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import { getUserName as getAlphaUserName, isLoggedIn as isAlphaLoggedIn } from "@companion-ai/alpha-hub/lib";
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
import { formatPiWebAccessDoctorLines, getPiWebAccessStatus } from "../pi/web-access.js";
|
||||
import { BROWSER_FALLBACK_PATHS, PANDOC_FALLBACK_PATHS, resolveExecutable } from "../system/executables.js";
|
||||
import { readJson } from "../pi/settings.js";
|
||||
@@ -8,6 +9,30 @@ import { validatePiInstallation } from "../pi/runtime.js";
|
||||
import { printInfo, printPanel, printSection } from "../ui/terminal.js";
|
||||
import { getCurrentModelSpec } from "../model/commands.js";
|
||||
import { buildModelStatusSnapshotFromRecords, getAvailableModelRecords, getSupportedModelRecords } from "../model/catalog.js";
|
||||
import { createModelRegistry, getModelsJsonPath } from "../model/registry.js";
|
||||
|
||||
function findProvidersMissingApiKey(modelsJsonPath: string): string[] {
|
||||
try {
|
||||
const raw = readFileSync(modelsJsonPath, "utf8").trim();
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw) as any;
|
||||
const providers = parsed?.providers;
|
||||
if (!providers || typeof providers !== "object") return [];
|
||||
const missing: string[] = [];
|
||||
for (const [providerId, config] of Object.entries(providers as Record<string, unknown>)) {
|
||||
if (!config || typeof config !== "object") continue;
|
||||
const models = (config as any).models;
|
||||
if (!Array.isArray(models) || models.length === 0) continue;
|
||||
const apiKey = (config as any).apiKey;
|
||||
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
||||
missing.push(providerId);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export type DoctorOptions = {
|
||||
settingsPath: string;
|
||||
@@ -104,7 +129,7 @@ export function runStatus(options: DoctorOptions): void {
|
||||
|
||||
export function runDoctor(options: DoctorOptions): void {
|
||||
const settings = readJson(options.settingsPath);
|
||||
const modelRegistry = new ModelRegistry(AuthStorage.create(options.authPath));
|
||||
const modelRegistry = createModelRegistry(options.authPath);
|
||||
const availableModels = modelRegistry.getAvailable();
|
||||
const pandocPath = resolveExecutable("pandoc", PANDOC_FALLBACK_PATHS);
|
||||
const browserPath = process.env.PUPPETEER_EXECUTABLE_PATH ?? resolveExecutable("google-chrome", BROWSER_FALLBACK_PATHS);
|
||||
@@ -144,6 +169,21 @@ export function runDoctor(options: DoctorOptions): void {
|
||||
if (modelStatus.recommendedModelReason) {
|
||||
console.log(` why: ${modelStatus.recommendedModelReason}`);
|
||||
}
|
||||
const modelsError = modelRegistry.getError();
|
||||
if (modelsError) {
|
||||
console.log("models.json: error");
|
||||
for (const line of modelsError.split("\n")) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
} else {
|
||||
const modelsJsonPath = getModelsJsonPath(options.authPath);
|
||||
console.log(`models.json: ${modelsJsonPath}`);
|
||||
const missingApiKeyProviders = findProvidersMissingApiKey(modelsJsonPath);
|
||||
if (missingApiKeyProviders.length > 0) {
|
||||
console.log(` warning: provider(s) missing apiKey: ${missingApiKeyProviders.join(", ")}`);
|
||||
console.log(" note: custom providers with a models[] list need apiKey in models.json to be available.");
|
||||
}
|
||||
}
|
||||
console.log(`pandoc: ${pandocPath ?? "missing"}`);
|
||||
console.log(`browser preview runtime: ${browserPath ?? "missing"}`);
|
||||
for (const line of formatPiWebAccessDoctorLines()) {
|
||||
|
||||
Reference in New Issue
Block a user