fix: update Pi and model provider flows

This commit is contained in:
Advait Paliwal
2026-04-12 13:02:16 -07:00
parent b3a82d4a92
commit aa96b5ee14
14 changed files with 273 additions and 83 deletions

View File

@@ -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 <provider/model>");
throw new Error("Usage: feynman model set <provider/model|provider:model>");
}
setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
return;

View File

@@ -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<ApiKeyProviderInfo | undefined> {
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<boolean> {
const provider = await selectApiKeyProvider();
async function verifyBedrockCredentialChain(): Promise<void> {
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<boolean> {
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: "<authenticated>" });
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<boolean> {
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<boolean> {
}
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<string, unknown>;
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<boolean> {
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<void> {
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}`);
}