refactor(auth-choice): unify api-key resolution flows

This commit is contained in:
Peter Steinberger
2026-02-22 11:16:17 +00:00
parent 8e00965618
commit fc60f4923a
4 changed files with 622 additions and 816 deletions

View File

@@ -1,4 +1,8 @@
import { resolveEnvApiKey } from "../agents/model-auth.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { formatApiKeyPreview } from "./auth-choice.api-key.js";
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
export function createAuthChoiceAgentModelNoter(
params: ApplyAuthChoiceParams,
@@ -13,3 +17,152 @@ export function createAuthChoiceAgentModelNoter(
);
};
}
export interface ApplyAuthChoiceModelState {
config: ApplyAuthChoiceParams["config"];
agentModelOverride: string | undefined;
}
export function createAuthChoiceModelStateBridge(bindings: {
getConfig: () => ApplyAuthChoiceParams["config"];
setConfig: (config: ApplyAuthChoiceParams["config"]) => void;
getAgentModelOverride: () => string | undefined;
setAgentModelOverride: (model: string | undefined) => void;
}): ApplyAuthChoiceModelState {
return {
get config() {
return bindings.getConfig();
},
set config(config) {
bindings.setConfig(config);
},
get agentModelOverride() {
return bindings.getAgentModelOverride();
},
set agentModelOverride(model) {
bindings.setAgentModelOverride(model);
},
};
}
export function createAuthChoiceDefaultModelApplier(
params: ApplyAuthChoiceParams,
state: ApplyAuthChoiceModelState,
): (
options: Omit<
Parameters<typeof applyDefaultModelChoice>[0],
"config" | "setDefaultModel" | "noteAgentModel" | "prompter"
>,
) => Promise<void> {
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
return async (options) => {
const applied = await applyDefaultModelChoice({
config: state.config,
setDefaultModel: params.setDefaultModel,
noteAgentModel,
prompter: params.prompter,
...options,
});
state.config = applied.config;
state.agentModelOverride = applied.agentModelOverride ?? state.agentModelOverride;
};
}
export function normalizeTokenProviderInput(
tokenProvider: string | null | undefined,
): string | undefined {
const normalized = String(tokenProvider ?? "")
.trim()
.toLowerCase();
return normalized || undefined;
}
export async function maybeApplyApiKeyFromOption(params: {
token: string | undefined;
tokenProvider: string | undefined;
expectedProviders: string[];
normalize: (value: string) => string;
setCredential: (apiKey: string) => Promise<void>;
}): Promise<string | undefined> {
const tokenProvider = normalizeTokenProviderInput(params.tokenProvider);
const expectedProviders = params.expectedProviders
.map((provider) => normalizeTokenProviderInput(provider))
.filter((provider): provider is string => Boolean(provider));
if (!params.token || !tokenProvider || !expectedProviders.includes(tokenProvider)) {
return undefined;
}
const apiKey = params.normalize(params.token);
await params.setCredential(apiKey);
return apiKey;
}
export async function ensureApiKeyFromOptionEnvOrPrompt(params: {
token: string | undefined;
tokenProvider: string | undefined;
expectedProviders: string[];
provider: string;
envLabel: string;
promptMessage: string;
normalize: (value: string) => string;
validate: (value: string) => string | undefined;
prompter: WizardPrompter;
setCredential: (apiKey: string) => Promise<void>;
noteMessage?: string;
noteTitle?: string;
}): Promise<string> {
const optionApiKey = await maybeApplyApiKeyFromOption({
token: params.token,
tokenProvider: params.tokenProvider,
expectedProviders: params.expectedProviders,
normalize: params.normalize,
setCredential: params.setCredential,
});
if (optionApiKey) {
return optionApiKey;
}
if (params.noteMessage) {
await params.prompter.note(params.noteMessage, params.noteTitle);
}
return await ensureApiKeyFromEnvOrPrompt({
provider: params.provider,
envLabel: params.envLabel,
promptMessage: params.promptMessage,
normalize: params.normalize,
validate: params.validate,
prompter: params.prompter,
setCredential: params.setCredential,
});
}
export async function ensureApiKeyFromEnvOrPrompt(params: {
provider: string;
envLabel: string;
promptMessage: string;
normalize: (value: string) => string;
validate: (value: string) => string | undefined;
prompter: WizardPrompter;
setCredential: (apiKey: string) => Promise<void>;
}): Promise<string> {
const envKey = resolveEnvApiKey(params.provider);
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing ${params.envLabel} (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await params.setCredential(envKey.apiKey);
return envKey.apiKey;
}
}
const key = await params.prompter.text({
message: params.promptMessage,
validate: params.validate,
});
const apiKey = params.normalize(String(key ?? ""));
await params.setCredential(apiKey);
return apiKey;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,11 @@ import {
discoverHuggingfaceModels,
isHuggingfacePolicyLocked,
} from "../agents/huggingface-models.js";
import { resolveEnvApiKey } from "../agents/model-auth.js";
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import {
formatApiKeyPreview,
normalizeApiKeyInput,
validateApiKeyInput,
} from "./auth-choice.api-key.js";
import { createAuthChoiceAgentModelNoter } from "./auth-choice.apply-helpers.js";
createAuthChoiceAgentModelNoter,
ensureApiKeyFromOptionEnvOrPrompt,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
@@ -30,47 +28,23 @@ export async function applyAuthChoiceHuggingface(
let agentModelOverride: string | undefined;
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
let hasCredential = false;
let hfKey = "";
if (!hasCredential && params.opts?.token && params.opts.tokenProvider === "huggingface") {
hfKey = normalizeApiKeyInput(params.opts.token);
await setHuggingfaceApiKey(hfKey, params.agentDir);
hasCredential = true;
}
if (!hasCredential) {
await params.prompter.note(
[
"Hugging Face Inference Providers offer OpenAI-compatible chat completions.",
"Create a token at: https://huggingface.co/settings/tokens (fine-grained, 'Make calls to Inference Providers').",
].join("\n"),
"Hugging Face",
);
}
if (!hasCredential) {
const envKey = resolveEnvApiKey("huggingface");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing Hugging Face token (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
hfKey = envKey.apiKey;
await setHuggingfaceApiKey(hfKey, params.agentDir);
hasCredential = true;
}
}
}
if (!hasCredential) {
const key = await params.prompter.text({
message: "Enter Hugging Face API key (HF token)",
validate: validateApiKeyInput,
});
hfKey = normalizeApiKeyInput(String(key ?? ""));
await setHuggingfaceApiKey(hfKey, params.agentDir);
}
const hfKey = await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
tokenProvider: params.opts?.tokenProvider,
expectedProviders: ["huggingface"],
provider: "huggingface",
envLabel: "Hugging Face token",
promptMessage: "Enter Hugging Face API key (HF token)",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
setCredential: async (apiKey) => setHuggingfaceApiKey(apiKey, params.agentDir),
noteMessage: [
"Hugging Face Inference Providers offer OpenAI-compatible chat completions.",
"Create a token at: https://huggingface.co/settings/tokens (fine-grained, 'Make calls to Inference Providers').",
].join("\n"),
noteTitle: "Hugging Face",
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "huggingface:default",
provider: "huggingface",

View File

@@ -1,13 +1,11 @@
import { resolveEnvApiKey } from "../agents/model-auth.js";
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import {
formatApiKeyPreview,
normalizeApiKeyInput,
validateApiKeyInput,
} from "./auth-choice.api-key.js";
import { createAuthChoiceAgentModelNoter } from "./auth-choice.apply-helpers.js";
createAuthChoiceDefaultModelApplier,
createAuthChoiceModelStateBridge,
ensureApiKeyFromOptionEnvOrPrompt,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import {
applyAuthProfileConfig,
applyMinimaxApiConfig,
@@ -24,31 +22,64 @@ export async function applyAuthChoiceMiniMax(
): Promise<ApplyAuthChoiceResult | null> {
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplier(
params,
createAuthChoiceModelStateBridge({
getConfig: () => nextConfig,
setConfig: (config) => (nextConfig = config),
getAgentModelOverride: () => agentModelOverride,
setAgentModelOverride: (model) => (agentModelOverride = model),
}),
);
const ensureMinimaxApiKey = async (opts: {
profileId: string;
promptMessage: string;
}): Promise<void> => {
let hasCredential = false;
const envKey = resolveEnvApiKey("minimax");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing MINIMAX_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await setMinimaxApiKey(envKey.apiKey, params.agentDir, opts.profileId);
hasCredential = true;
}
}
if (!hasCredential) {
const key = await params.prompter.text({
message: opts.promptMessage,
validate: validateApiKeyInput,
});
await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir, opts.profileId);
}
await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
tokenProvider: params.opts?.tokenProvider,
expectedProviders: ["minimax", "minimax-cn"],
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: opts.promptMessage,
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
setCredential: async (apiKey) => setMinimaxApiKey(apiKey, params.agentDir, opts.profileId),
});
};
const applyMinimaxApiVariant = async (opts: {
profileId: string;
provider: "minimax" | "minimax-cn";
promptMessage: string;
modelRefPrefix: "minimax" | "minimax-cn";
modelId: string;
applyDefaultConfig: (
config: ApplyAuthChoiceParams["config"],
modelId: string,
) => ApplyAuthChoiceParams["config"];
applyProviderConfig: (
config: ApplyAuthChoiceParams["config"],
modelId: string,
) => ApplyAuthChoiceParams["config"];
}): Promise<ApplyAuthChoiceResult> => {
await ensureMinimaxApiKey({
profileId: opts.profileId,
promptMessage: opts.promptMessage,
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: opts.profileId,
provider: opts.provider,
mode: "api_key",
});
const modelRef = `${opts.modelRefPrefix}/${opts.modelId}`;
await applyProviderDefaultModel({
defaultModel: modelRef,
applyDefaultConfig: (config) => opts.applyDefaultConfig(config, opts.modelId),
applyProviderConfig: (config) => opts.applyProviderConfig(config, opts.modelId),
});
return { config: nextConfig, agentModelOverride };
};
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
if (params.authChoice === "minimax-portal") {
// Let user choose between Global/CN endpoints
const endpoint = await params.prompter.select({
@@ -73,74 +104,36 @@ export async function applyAuthChoiceMiniMax(
params.authChoice === "minimax-api" ||
params.authChoice === "minimax-api-lightning"
) {
const modelId =
params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-Lightning" : "MiniMax-M2.5";
await ensureMinimaxApiKey({
profileId: "minimax:default",
promptMessage: "Enter MiniMax API key",
});
nextConfig = applyAuthProfileConfig(nextConfig, {
return await applyMinimaxApiVariant({
profileId: "minimax:default",
provider: "minimax",
mode: "api_key",
promptMessage: "Enter MiniMax API key",
modelRefPrefix: "minimax",
modelId:
params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-Lightning" : "MiniMax-M2.5",
applyDefaultConfig: applyMinimaxApiConfig,
applyProviderConfig: applyMinimaxApiProviderConfig,
});
{
const modelRef = `minimax/${modelId}`;
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
defaultModel: modelRef,
applyDefaultConfig: (config) => applyMinimaxApiConfig(config, modelId),
applyProviderConfig: (config) => applyMinimaxApiProviderConfig(config, modelId),
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
}
return { config: nextConfig, agentModelOverride };
}
if (params.authChoice === "minimax-api-key-cn") {
const modelId = "MiniMax-M2.5";
await ensureMinimaxApiKey({
profileId: "minimax-cn:default",
promptMessage: "Enter MiniMax China API key",
});
nextConfig = applyAuthProfileConfig(nextConfig, {
return await applyMinimaxApiVariant({
profileId: "minimax-cn:default",
provider: "minimax-cn",
mode: "api_key",
promptMessage: "Enter MiniMax China API key",
modelRefPrefix: "minimax-cn",
modelId: "MiniMax-M2.5",
applyDefaultConfig: applyMinimaxApiConfigCn,
applyProviderConfig: applyMinimaxApiProviderConfigCn,
});
{
const modelRef = `minimax-cn/${modelId}`;
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
defaultModel: modelRef,
applyDefaultConfig: (config) => applyMinimaxApiConfigCn(config, modelId),
applyProviderConfig: (config) => applyMinimaxApiProviderConfigCn(config, modelId),
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
}
return { config: nextConfig, agentModelOverride };
}
if (params.authChoice === "minimax") {
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
await applyProviderDefaultModel({
defaultModel: "lmstudio/minimax-m2.1-gs32",
applyDefaultConfig: applyMinimaxConfig,
applyProviderConfig: applyMinimaxProviderConfig,
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
return { config: nextConfig, agentModelOverride };
}