diff --git a/packages/ai/src/registry/api-key-login.ts b/packages/ai/src/registry/api-key-login.ts index f202eab63..38b078010 100644 --- a/packages/ai/src/registry/api-key-login.ts +++ b/packages/ai/src/registry/api-key-login.ts @@ -38,16 +38,18 @@ type ModelsEndpointValidation = { export type ApiKeyLoginConfig = { /** Display name used in error messages, e.g. "Cerebras", "NanoGPT". */ providerLabel: string; - /** URL opened in browser for the user to grab their key. */ - authUrl: string; - /** Instructions shown with the onAuth callback. */ - instructions: string; + /** URL opened in browser for the user to grab their key, or omitted to skip onAuth. */ + authUrl?: string; + /** Instructions shown with the onAuth callback, or omitted to skip onAuth. */ + instructions?: string; /** Prompt message shown when asking for the key paste. */ promptMessage: string; /** Placeholder string for the prompt (e.g. "sk-...", "csk-..."). */ placeholder: string; /** Validation strategy, or `null` to skip validation. */ validation: ChatCompletionsValidation | AnthropicMessagesValidation | ModelsEndpointValidation | null; + /** Value returned for an empty key; also allows an empty prompt response. */ + emptyKeyFallback?: string; }; export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthController) => Promise { @@ -56,15 +58,24 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon throw new AIError.OnPromptRequiredError(config.providerLabel); } - options.onAuth?.({ - url: config.authUrl, - instructions: config.instructions, - }); + if (config.authUrl && config.instructions) { + options.onAuth?.({ + url: config.authUrl, + instructions: config.instructions, + }); + } - const apiKey = await options.onPrompt({ - message: config.promptMessage, - placeholder: config.placeholder, - }); + const apiKey = + config.emptyKeyFallback === undefined + ? await options.onPrompt({ + message: config.promptMessage, + placeholder: config.placeholder, + }) + : await options.onPrompt({ + message: config.promptMessage, + placeholder: config.placeholder, + allowEmpty: true, + }); if (options.signal?.aborted) { throw new AIError.LoginCancelledError(); @@ -72,6 +83,9 @@ export function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthCon const trimmed = apiKey.trim(); if (!trimmed) { + if (config.emptyKeyFallback !== undefined) { + return config.emptyKeyFallback; + } throw new AIError.ApiKeyRequiredError(); } diff --git a/packages/ai/src/registry/cloudflare-ai-gateway.ts b/packages/ai/src/registry/cloudflare-ai-gateway.ts index 517592224..9bedf98ef 100644 --- a/packages/ai/src/registry/cloudflare-ai-gateway.ts +++ b/packages/ai/src/registry/cloudflare-ai-gateway.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const AUTH_URL = "https://developers.cloudflare.com/ai-gateway/configuration/authentication/"; @@ -10,33 +10,14 @@ const AUTH_URL = "https://developers.cloudflare.com/ai-gateway/configuration/aut * Opens browser to Cloudflare AI Gateway authentication docs and prompts for a gateway token/API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginCloudflareAiGateway(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("Cloudflare AI Gateway"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: - "Copy your Cloudflare AI Gateway token/API key. Configure account/gateway base URL in models config.", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your Cloudflare AI Gateway token/API key", - placeholder: "cf-aig-...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginCloudflareAiGateway = createApiKeyLogin({ + providerLabel: "Cloudflare AI Gateway", + authUrl: AUTH_URL, + instructions: "Copy your Cloudflare AI Gateway token/API key. Configure account/gateway base URL in models config.", + promptMessage: "Paste your Cloudflare AI Gateway token/API key", + placeholder: "cf-aig-...", + validation: null, +}); export const cloudflareAiGatewayProvider = { id: "cloudflare-ai-gateway", diff --git a/packages/ai/src/registry/kagi.ts b/packages/ai/src/registry/kagi.ts index d94a4684c..7b14eee19 100644 --- a/packages/ai/src/registry/kagi.ts +++ b/packages/ai/src/registry/kagi.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const AUTH_URL = "https://kagi.com/settings/api"; @@ -10,33 +10,15 @@ const AUTH_URL = "https://kagi.com/settings/api"; * Opens browser to API settings and prompts user to paste their API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginKagi(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("Kagi"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: - "Copy your Kagi Search API key from Kagi API settings. Search API access is beta-only; if unavailable, email support@kagi.com.", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your Kagi API key", - placeholder: "KG_...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginKagi = createApiKeyLogin({ + providerLabel: "Kagi", + authUrl: AUTH_URL, + instructions: + "Copy your Kagi Search API key from Kagi API settings. Search API access is beta-only; if unavailable, email support@kagi.com.", + promptMessage: "Paste your Kagi API key", + placeholder: "KG_...", + validation: null, +}); export const kagiProvider = { id: "kagi", diff --git a/packages/ai/src/registry/litellm.ts b/packages/ai/src/registry/litellm.ts index 0f9084f45..4b120b329 100644 --- a/packages/ai/src/registry/litellm.ts +++ b/packages/ai/src/registry/litellm.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const AUTH_URL = "https://docs.litellm.ai/docs/proxy/deploy"; @@ -10,33 +10,15 @@ const AUTH_URL = "https://docs.litellm.ai/docs/proxy/deploy"; * Opens browser to LiteLLM setup docs, prompts user to paste their API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginLiteLLM(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("LiteLLM"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: - "Run LiteLLM proxy (default http://localhost:4000/v1; set LITELLM_BASE_URL to customize it), then copy your master key or virtual key", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your LiteLLM API key (master key or virtual key)", - placeholder: "sk-...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginLiteLLM = createApiKeyLogin({ + providerLabel: "LiteLLM", + authUrl: AUTH_URL, + instructions: + "Run LiteLLM proxy (default http://localhost:4000/v1; set LITELLM_BASE_URL to customize it), then copy your master key or virtual key", + promptMessage: "Paste your LiteLLM API key (master key or virtual key)", + placeholder: "sk-...", + validation: null, +}); export const litellmProvider = { id: "litellm", diff --git a/packages/ai/src/registry/llama-cpp.ts b/packages/ai/src/registry/llama-cpp.ts index e21cbe9b6..87315af92 100644 --- a/packages/ai/src/registry/llama-cpp.ts +++ b/packages/ai/src/registry/llama-cpp.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const PROVIDER_ID = "llama.cpp"; @@ -7,25 +7,15 @@ const AUTH_URL = "https://github.com/ggml-org/llama.cpp#quick-start"; const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8080"; const DEFAULT_LOCAL_TOKEN = "llama-cpp-local"; -export async function loginLlamaCpp(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError(PROVIDER_ID); - } - options.onAuth?.({ - url: AUTH_URL, - instructions: `Paste your llama.cpp API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}; set LLAMA_CPP_BASE_URL to customize).`, - }); - const apiKey = await options.onPrompt({ - message: "Paste your llama.cpp API key (optional for local no-auth)", - placeholder: DEFAULT_LOCAL_TOKEN, - allowEmpty: true, - }); - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - const trimmed = apiKey.trim(); - return trimmed || DEFAULT_LOCAL_TOKEN; -} +export const loginLlamaCpp = createApiKeyLogin({ + providerLabel: PROVIDER_ID, + authUrl: AUTH_URL, + instructions: `Paste your llama.cpp API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}; set LLAMA_CPP_BASE_URL to customize).`, + promptMessage: "Paste your llama.cpp API key (optional for local no-auth)", + placeholder: DEFAULT_LOCAL_TOKEN, + validation: null, + emptyKeyFallback: DEFAULT_LOCAL_TOKEN, +}); export const llamaCppProvider = { id: PROVIDER_ID, diff --git a/packages/ai/src/registry/lm-studio.ts b/packages/ai/src/registry/lm-studio.ts index 30887c5d2..1de5c8ed6 100644 --- a/packages/ai/src/registry/lm-studio.ts +++ b/packages/ai/src/registry/lm-studio.ts @@ -1,28 +1,17 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const PROVIDER_ID = "lm-studio"; export const DEFAULT_LOCAL_TOKEN = "lm-studio-local"; -export async function loginLmStudio(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError(PROVIDER_ID); - } - - const apiKey = await options.onPrompt({ - message: "Optional: Paste LM Studio API key (to customize endpoint URL, set LM_STUDIO_BASE_URL env var)", - placeholder: DEFAULT_LOCAL_TOKEN, - allowEmpty: true, - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - return trimmed || DEFAULT_LOCAL_TOKEN; -} +export const loginLmStudio = createApiKeyLogin({ + providerLabel: PROVIDER_ID, + promptMessage: "Optional: Paste LM Studio API key (to customize endpoint URL, set LM_STUDIO_BASE_URL env var)", + placeholder: DEFAULT_LOCAL_TOKEN, + validation: null, + emptyKeyFallback: DEFAULT_LOCAL_TOKEN, +}); export const lmStudioProvider = { id: "lm-studio", diff --git a/packages/ai/src/registry/parallel.ts b/packages/ai/src/registry/parallel.ts index c69e12e35..d31ec14bf 100644 --- a/packages/ai/src/registry/parallel.ts +++ b/packages/ai/src/registry/parallel.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const AUTH_URL = "https://platform.parallel.ai/settings?tab=api-keys"; @@ -10,32 +10,14 @@ const AUTH_URL = "https://platform.parallel.ai/settings?tab=api-keys"; * Opens browser to the API keys page, prompts the user to paste their API key, * and returns the API key directly. */ -export async function loginParallel(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("Parallel"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: "Copy your Parallel API key from the Parallel settings page.", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your Parallel API key", - placeholder: "sk_...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginParallel = createApiKeyLogin({ + providerLabel: "Parallel", + authUrl: AUTH_URL, + instructions: "Copy your Parallel API key from the Parallel settings page.", + promptMessage: "Paste your Parallel API key", + placeholder: "sk_...", + validation: null, +}); export const parallelProvider = { id: "parallel", diff --git a/packages/ai/src/registry/tavily.ts b/packages/ai/src/registry/tavily.ts index c5dcdfa38..e315a8031 100644 --- a/packages/ai/src/registry/tavily.ts +++ b/packages/ai/src/registry/tavily.ts @@ -1,4 +1,4 @@ -import * as AIError from "../error"; +import { createApiKeyLogin } from "./api-key-login"; import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; @@ -10,32 +10,14 @@ const AUTH_URL = "https://app.tavily.com/home"; * Opens browser to API keys page and prompts user to paste their API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginTavily(options: OAuthLoginCallbacks): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("Tavily"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: "Copy your Tavily API key from the API Keys page.", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your Tavily API key", - placeholder: "tvly-...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginTavily = createApiKeyLogin({ + providerLabel: "Tavily", + authUrl: AUTH_URL, + instructions: "Copy your Tavily API key from the API Keys page.", + promptMessage: "Paste your Tavily API key", + placeholder: "tvly-...", + validation: null, +}); export const tavilyProvider = { id: "tavily", diff --git a/packages/ai/src/registry/vercel-ai-gateway.ts b/packages/ai/src/registry/vercel-ai-gateway.ts index 77af5d58b..9f005ef3b 100644 --- a/packages/ai/src/registry/vercel-ai-gateway.ts +++ b/packages/ai/src/registry/vercel-ai-gateway.ts @@ -1,35 +1,17 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; -export async function loginVercelAiGateway(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError("Vercel AI Gateway"); - } - - options.onAuth?.({ - url: AUTH_URL, - instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard", - }); - - const apiKey = await options.onPrompt({ - message: "Paste your Vercel AI Gateway API key", - placeholder: "vck_...", - }); - - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new AIError.ApiKeyRequiredError(); - } - - return trimmed; -} +export const loginVercelAiGateway = createApiKeyLogin({ + providerLabel: "Vercel AI Gateway", + authUrl: AUTH_URL, + instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard", + promptMessage: "Paste your Vercel AI Gateway API key", + placeholder: "vck_...", + validation: null, +}); export const vercelAiGatewayProvider = { id: "vercel-ai-gateway", diff --git a/packages/ai/src/registry/vllm.ts b/packages/ai/src/registry/vllm.ts index 17c80ebda..038bce0ad 100644 --- a/packages/ai/src/registry/vllm.ts +++ b/packages/ai/src/registry/vllm.ts @@ -1,5 +1,5 @@ -import * as AIError from "../error"; -import type { OAuthController, OAuthLoginCallbacks, OAuthProvider } from "./oauth/types"; +import { createApiKeyLogin } from "./api-key-login"; +import type { OAuthLoginCallbacks, OAuthProvider } from "./oauth/types"; import type { ProviderDefinition } from "./types"; const PROVIDER_ID: OAuthProvider = "vllm"; @@ -7,25 +7,15 @@ const AUTH_URL = "https://docs.vllm.ai/en/latest/serving/openai_compatible_serve const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8000/v1"; const DEFAULT_LOCAL_TOKEN = "vllm-local"; -export async function loginVllm(options: OAuthController): Promise { - if (!options.onPrompt) { - throw new AIError.OnPromptRequiredError(PROVIDER_ID); - } - options.onAuth?.({ - url: AUTH_URL, - instructions: `Paste your vLLM API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}).`, - }); - const apiKey = await options.onPrompt({ - message: "Paste your vLLM API key (optional for local no-auth)", - placeholder: DEFAULT_LOCAL_TOKEN, - allowEmpty: true, - }); - if (options.signal?.aborted) { - throw new AIError.LoginCancelledError(); - } - const trimmed = apiKey.trim(); - return trimmed || DEFAULT_LOCAL_TOKEN; -} +export const loginVllm = createApiKeyLogin({ + providerLabel: PROVIDER_ID, + authUrl: AUTH_URL, + instructions: `Paste your vLLM API key if your server requires auth. Leave empty for local no-auth mode (default base URL: ${DEFAULT_LOCAL_BASE_URL}).`, + promptMessage: "Paste your vLLM API key (optional for local no-auth)", + placeholder: DEFAULT_LOCAL_TOKEN, + validation: null, + emptyKeyFallback: DEFAULT_LOCAL_TOKEN, +}); export const vllmProvider = { id: "vllm",