fix: update Pi and model provider flows
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user