@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
1,398 lines • 51.4 kB
JavaScript
/**
* Provider Configuration Utility
* Consolidated configuration helpers for all AI providers
* Eliminates duplicate error messages and configuration logic
* Enhanced with format validation and advanced error classification
* Extended with Claude subscription OAuth support
*/
import { logger } from "./logger.js";
import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../providers/catalog/loader.js";
// Re-export subscription types for convenience
/**
* Looks up a JSON catalog entry by id and derives its ProviderConfigOptions.
* Backs the 9 catalog providers' create<Name>Config() delegations below —
* the JSON catalog (src/lib/providers/catalog/<id>.json) is their single
* source of truth.
*/
function catalogConfigOptions(id) {
const entry = getCatalogJsonEntries().find((e) => e.id === id);
if (!entry) {
throw new Error(`Unknown catalog provider id: ${id}`);
}
return buildCatalogConfigOptions(entry);
}
/**
* API key format validation patterns (extracted from advanced validation system)
* Exported for use across the codebase to replace scattered regex patterns
*/
export const API_KEY_FORMATS = {
openai: /^sk-[A-Za-z0-9]{48,}$/,
anthropic: /^sk-ant-[A-Za-z0-9\-_]{95,}$/,
"google-ai": /^AIza[A-Za-z0-9\-_]{35}$/,
huggingface: /^hf_[A-Za-z0-9]{37}$/,
mistral: /^[A-Za-z0-9]{32}$/,
azure: /^[A-Za-z0-9]{32}$/,
aws: /^[A-Z0-9]{20}$/, // Access Key ID format
bedrock: /^[A-Z0-9]{20}$/, // AWS access key ID: 20 uppercase alphanumerics
};
/**
* OAuth token format validation patterns
* These patterns are more flexible as OAuth tokens can vary by provider
*/
export const OAUTH_TOKEN_FORMATS = {
// OAuth access tokens are typically JWT or opaque tokens
// Claude OAuth access tokens: Bearer tokens with typical JWT structure or opaque format
"anthropic-access": /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$|^[A-Za-z0-9\-_]{32,}$/,
// Refresh tokens are typically longer opaque strings
"anthropic-refresh": /^[A-Za-z0-9\-_]{32,}$/,
};
/**
* API key length constants to replace scattered magic numbers
*/
export const API_KEY_LENGTHS = {
OPENAI_MIN: 48, // OpenAI API keys minimum length
ANTHROPIC_MIN: 95, // Anthropic API keys minimum length
HUGGINGFACE_EXACT: 37, // HuggingFace tokens exact length
AZURE_MIN: 32, // Azure OpenAI API keys minimum length
MISTRAL_EXACT: 32, // Mistral API keys exact length
AWS_ACCESS_KEY: 20, // AWS access key ID exact length
GOOGLE_AI_EXACT: 39, // Google AI Studio keys exact length (with AIza prefix)
};
/**
* Project ID format validation (for Google Cloud)
*/
export const PROJECT_ID_FORMAT = {
MIN_LENGTH: 6, // Minimum project ID length
MAX_LENGTH: 30, // Maximum project ID length
PATTERN: /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/, // Google Cloud project ID format
};
/**
* Validates API key format for a specific provider
* @param providerKey Provider identifier (e.g., 'openai', 'anthropic')
* @param apiKey The API key to validate
* @returns True if format is valid
*/
export function validateApiKeyFormat(providerKey, apiKey) {
const format = API_KEY_FORMATS[providerKey.toLowerCase()];
if (!format) {
// No format validation available, assume valid if not empty
return apiKey.length > 0;
}
return format.test(apiKey);
}
/**
* Enhanced validation with format checking
* @param config Provider configuration options
* @param enableFormatValidation Whether to validate API key format
* @returns Validation result with detailed information
*/
export function validateApiKeyEnhanced(config, enableFormatValidation = false) {
// Check primary environment variable
let apiKey = process.env[config.envVarName];
// Check fallback environment variables if provided
if (!apiKey && config.fallbackEnvVars) {
for (const fallbackVar of config.fallbackEnvVars) {
apiKey = process.env[fallbackVar];
if (apiKey) {
break;
}
}
}
if (!apiKey) {
if (config.optional) {
// Local providers — base URL defaulted; treat as valid with empty value.
return { isValid: true, apiKey: "" };
}
return {
isValid: false,
apiKey: "",
errorType: "missing",
error: createConfigErrorMessage(config),
};
}
// Optional format validation
if (enableFormatValidation) {
const providerKey = config.providerName.toLowerCase().replace(/\s/g, "-");
const formatValid = validateApiKeyFormat(providerKey, apiKey);
if (!formatValid) {
return {
isValid: false,
apiKey,
formatValid: false,
errorType: "format",
error: `Invalid ${config.providerName} API key format. Please check your API key.`,
};
}
}
return {
isValid: true,
apiKey,
formatValid: enableFormatValidation ? true : undefined,
};
}
/**
* Validates an API key for a provider and returns it (BACKWARD COMPATIBLE)
* Throws detailed error message if validation fails
* @param config Provider configuration options
* @returns The validated API key
*/
export function validateApiKey(config) {
// Check primary environment variable
let apiKey = process.env[config.envVarName];
// Check fallback environment variables if provided
if (!apiKey && config.fallbackEnvVars) {
for (const fallbackVar of config.fallbackEnvVars) {
apiKey = process.env[fallbackVar];
if (apiKey) {
break;
}
}
}
if (!apiKey) {
// Local providers (LM Studio, llama.cpp) treat envVarName as a base-URL
// override, not a credential. Returning "" lets callers fall back to the
// documented default URL without raising a configuration error.
if (config.optional) {
return "";
}
throw new Error(createConfigErrorMessage(config));
}
return apiKey;
}
/**
* Creates a standardized configuration error message
* @param config Provider configuration options
* @returns Formatted error message with setup instructions
*/
function createConfigErrorMessage(config) {
const envVarsList = config.fallbackEnvVars
? [config.envVarName, ...config.fallbackEnvVars].join(" or ")
: config.envVarName;
return `❌ ${config.providerName} Provider Configuration Error
Missing required environment variable: ${envVarsList}
🔧 Step 1: Get ${config.description}
${config.instructions.join("\n")}
🔧 Step 2: Set Environment Variable
Add to your .env file:
${config.envVarName}=your_key_here
🔧 Step 3: Restart Application
Restart your application to load the new environment variables.`;
}
/**
* Gets a provider model with fallback to default
* @param envVar Environment variable name for the model
* @param defaultModel Default model to use if env var not set
* @returns The model name to use
*/
export function getProviderModel(envVar, defaultModel) {
return process.env[envVar] || defaultModel;
}
/**
* Checks if provider credentials are available
* @param envVars Array of environment variable names to check
* @returns True if one of the credentials is available
*/
export function hasProviderCredentials(envVars) {
return envVars.some((envVar) => !!process.env[envVar]);
}
/**
* Evaluates a `ProviderDescriptor.envVars.extraRequiredFallbacks`-shaped
* list against an env-var source. Each entry is either a single env var
* name (satisfied on its own) or a nested array of names that must ALL be
* present together (e.g. Vertex's GOOGLE_AUTH_CLIENT_EMAIL +
* GOOGLE_AUTH_PRIVATE_KEY pair, which is only valid auth as a pair).
* Returns true when at least one entry is satisfied. The single evaluation
* site for this shape — every consumer (providerUtils.ts, providerHealth.ts,
* setup.ts, environmentManager.ts) must call this instead of re-deriving the
* same `.some()`/`.every()` logic, so they can't drift out of sync with each
* other or with the real auth gate (hasGoogleCredentials()).
* @param env Explicit env-var source (`process.env`, or a parsed .env file) —
* never hardcoded, so callers checking a file's contents (not the live
* process env) can reuse this too.
*/
export function satisfiesFallbacks(fallbacks, env) {
if (!fallbacks) {
return false;
}
return fallbacks.some((entry) => typeof entry === "string"
? !!env[entry]
: entry.every((name) => !!env[name]));
}
// =============================================================================
// PROVIDER-SPECIFIC CONFIGURATION CREATORS
// =============================================================================
/**
* Creates Anthropic provider configuration
* Supports both API key and OAuth authentication methods
*/
export function createAnthropicConfig() {
const authMethod = getAnthropicAuthMethod();
const tier = getAnthropicSubscriptionTier();
// Base instructions for API key authentication
const apiKeyInstructions = [
"🔑 Option 1: API Key Authentication (Recommended for developers)",
"1. Visit: https://console.anthropic.com/",
"2. Sign in or create an account",
"3. Go to API Keys section",
"4. Create a new API key",
"5. Set ANTHROPIC_API_KEY in your .env file",
];
// OAuth instructions for Claude subscription users
const oauthInstructions = [
"",
"🔐 Option 2: OAuth Authentication (For Claude Pro/Max subscribers)",
"1. Set ANTHROPIC_AUTH_METHOD=oauth in your .env file",
"2. Set ANTHROPIC_SUBSCRIPTION_TIER to your tier (free, pro, max)",
"3. Run the OAuth flow to obtain tokens, or set pre-configured tokens:",
" - ANTHROPIC_OAUTH_TOKEN=your_access_token",
" - ANTHROPIC_OAUTH_REFRESH_TOKEN=your_refresh_token (optional)",
"",
"📋 Available Subscription Tiers:",
" - free: Free tier with limited usage",
" - pro: Claude Pro ($20/month) - Extended usage limits",
" - max: Claude Max - Highest usage limits",
" - api: API-based access (pay-per-use)",
];
// Choose instructions based on current auth method
const instructions = authMethod === "oauth"
? [...oauthInstructions.slice(1), "", ...apiKeyInstructions]
: [...apiKeyInstructions, ...oauthInstructions];
return {
providerName: "Anthropic",
envVarName: authMethod === "oauth" ? "ANTHROPIC_OAUTH_TOKEN" : "ANTHROPIC_API_KEY",
setupUrl: authMethod === "oauth"
? "https://claude.ai/settings"
: "https://console.anthropic.com/",
description: authMethod === "oauth"
? `Anthropic OAuth Token (${tier} tier)`
: "Anthropic API Key",
instructions,
fallbackEnvVars: authMethod === "oauth"
? ["ANTHROPIC_API_KEY"] // Fall back to API key if OAuth token not present
: [
"ANTHROPIC_OAUTH_TOKEN",
"CLAUDE_OAUTH_TOKEN",
"ANTHROPIC_OAUTH_ACCESS_TOKEN",
], // Fall back to OAuth if API key not present
};
}
/**
* Creates OpenAI provider configuration
*/
export function createOpenAIConfig() {
return {
providerName: "OPENAI",
envVarName: "OPENAI_API_KEY",
setupUrl: "https://platform.openai.com/api-keys",
description: "Credentials",
instructions: [
"1. Visit: https://platform.openai.com/api-keys",
"2. Create new API key",
"3. Copy the key",
],
};
}
/**
* Creates HuggingFace provider configuration
*/
export function createHuggingFaceConfig() {
return {
providerName: "HuggingFace",
envVarName: "HUGGINGFACE_API_KEY",
setupUrl: "https://huggingface.co/settings/tokens",
description: "Credentials",
instructions: [
"1. Visit: https://huggingface.co/settings/tokens",
"2. Create new API token",
"3. Copy the token",
],
fallbackEnvVars: ["HF_TOKEN"],
};
}
/**
* Creates Mistral provider configuration
*/
export function createMistralConfig() {
return catalogConfigOptions("mistral");
}
/**
* Creates AWS Access Key configuration for Bedrock
*/
export function createAWSAccessKeyConfig() {
return {
providerName: "AWS Bedrock",
envVarName: "AWS_ACCESS_KEY_ID",
setupUrl: "https://console.aws.amazon.com/iam/",
description: "AWS Credentials",
instructions: [
"1. Visit: https://console.aws.amazon.com/iam/",
"2. Create IAM user with Bedrock permissions",
"3. Generate access key",
],
};
}
/**
* Creates AWS Secret Key configuration for Bedrock
*/
export function createAWSSecretConfig() {
return {
providerName: "AWS Bedrock",
envVarName: "AWS_SECRET_ACCESS_KEY",
setupUrl: "https://console.aws.amazon.com/iam/",
description: "AWS Credentials",
instructions: [
"1. Visit: https://console.aws.amazon.com/iam/",
"2. Create IAM user with Bedrock permissions",
"3. Generate access key",
],
};
}
/**
* Creates Azure OpenAI API Key configuration
*/
export function createAzureAPIKeyConfig() {
return {
providerName: "Azure OpenAI",
envVarName: "AZURE_OPENAI_API_KEY",
setupUrl: "https://portal.azure.com/",
description: "Azure OpenAI API Key",
instructions: [
"1. Visit: https://portal.azure.com/",
"2. Create or access Azure OpenAI resource",
"3. Get API key from Keys and Endpoint section",
],
};
}
/**
* Creates Azure OpenAI Endpoint configuration
*/
export function createAzureEndpointConfig() {
return {
providerName: "Azure OpenAI",
envVarName: "AZURE_OPENAI_ENDPOINT",
setupUrl: "https://portal.azure.com/",
description: "Azure OpenAI Endpoint",
instructions: [
"1. Visit: https://portal.azure.com/",
"2. Access your Azure OpenAI resource",
"3. Copy endpoint URL from Keys and Endpoint section",
],
};
}
/**
* Creates OpenAI Compatible provider configuration
*/
export function createOpenAICompatibleConfig() {
return {
providerName: "OpenAI Compatible",
envVarName: "OPENAI_COMPATIBLE_API_KEY",
setupUrl: "https://openrouter.ai/",
description: "OpenAI-compatible API credentials",
instructions: [
"1. Set OPENAI_COMPATIBLE_BASE_URL to your endpoint (e.g., https://api.openrouter.ai/api/v1)",
"2. Get API key from your OpenAI-compatible service:",
" • OpenRouter: https://openrouter.ai/keys",
" • vLLM: Use a random value for local deployments",
" • LiteLLM: Check your LiteLLM server configuration",
"3. Set OPENAI_COMPATIBLE_API_KEY to your API key",
"4. Optionally set OPENAI_COMPATIBLE_MODEL (will auto-discover if not set)",
],
};
}
/**
* Creates DeepSeek provider configuration
*/
export function createDeepSeekConfig() {
return {
providerName: "DeepSeek",
envVarName: "DEEPSEEK_API_KEY",
setupUrl: "https://platform.deepseek.com/api_keys",
description: "API key",
instructions: [
"1. Visit: https://platform.deepseek.com/api_keys",
"2. Create or sign in to your DeepSeek account",
"3. Generate a new API key",
"4. Set DEEPSEEK_API_KEY in your .env file",
],
};
}
/**
* Creates NVIDIA NIM provider configuration
*/
export function createNvidiaNimConfig() {
return {
providerName: "NVIDIA NIM",
envVarName: "NVIDIA_NIM_API_KEY",
setupUrl: "https://build.nvidia.com/settings/api-keys",
description: "API key",
instructions: [
"1. Visit: https://build.nvidia.com/",
"2. Sign in with your NVIDIA developer account",
"3. Open Settings → API Keys",
"4. Generate a new API key (Bearer token)",
"5. Set NVIDIA_NIM_API_KEY in your .env file",
],
};
}
/**
* Creates xAI Grok provider configuration.
*/
export function createXaiConfig() {
return catalogConfigOptions("xai");
}
/**
* Creates SambaNova provider configuration.
*/
export function createSambanovaConfig() {
return catalogConfigOptions("sambanova");
}
export function createCerebrasConfig() {
return catalogConfigOptions("cerebras");
}
/**
* Creates Groq provider configuration.
*/
export function createGroqConfig() {
return catalogConfigOptions("groq");
}
/**
* Creates Cohere provider configuration.
*/
export function createCohereConfig() {
return {
providerName: "Cohere",
envVarName: "COHERE_API_KEY",
setupUrl: "https://dashboard.cohere.com/api-keys",
description: "API key",
instructions: [
"1. Visit: https://dashboard.cohere.com/api-keys",
"2. Sign in to your Cohere account",
"3. Create a new API key",
"4. Set COHERE_API_KEY in your .env file",
],
};
}
/**
* Creates Replicate provider configuration.
*/
export function createReplicateConfig() {
return {
providerName: "Replicate",
envVarName: "REPLICATE_API_TOKEN",
setupUrl: "https://replicate.com/account/api-tokens",
description: "API token",
instructions: [
"1. Visit: https://replicate.com/account/api-tokens",
"2. Sign in to your Replicate account",
"3. Create a new API token",
"4. Set REPLICATE_API_TOKEN in your .env file",
],
};
}
/**
* Creates Together AI provider configuration.
*/
export function createTogetherAIConfig() {
return catalogConfigOptions("together-ai");
}
/**
* Creates Fireworks AI provider configuration.
*/
export function createFireworksConfig() {
return catalogConfigOptions("fireworks");
}
/**
* Creates Perplexity provider configuration.
*/
export function createPerplexityConfig() {
return catalogConfigOptions("perplexity");
}
/**
* Creates Voyage AI provider configuration (embedding-only).
*/
export function createVoyageConfig() {
return {
providerName: "Voyage AI",
envVarName: "VOYAGE_API_KEY",
setupUrl: "https://dash.voyageai.com/api-keys",
description: "API key",
instructions: [
"1. Visit: https://dash.voyageai.com/api-keys",
"2. Sign in to your Voyage AI account",
"3. Create a new API key",
"4. Set VOYAGE_API_KEY in your .env file",
],
};
}
/**
* Creates Jina AI provider configuration (embeddings + reranking).
*/
export function createJinaConfig() {
return {
providerName: "Jina AI",
envVarName: "JINA_API_KEY",
setupUrl: "https://jina.ai/?sui=apikey",
description: "API key",
instructions: [
"1. Visit: https://jina.ai/?sui=apikey",
"2. Sign in / create account",
"3. Copy your API key",
"4. Set JINA_API_KEY in your .env file",
],
};
}
/**
* Creates Stability AI provider configuration (image generation).
*/
export function createStabilityConfig() {
return {
providerName: "Stability AI",
envVarName: "STABILITY_API_KEY",
setupUrl: "https://platform.stability.ai/account/keys",
description: "API key",
instructions: [
"1. Visit: https://platform.stability.ai/account/keys",
"2. Sign in to your Stability AI account",
"3. Create a new API key",
"4. Set STABILITY_API_KEY in your .env file",
],
};
}
/**
* Creates Ideogram provider configuration (image generation).
*/
export function createIdeogramConfig() {
return {
providerName: "Ideogram",
envVarName: "IDEOGRAM_API_KEY",
setupUrl: "https://developer.ideogram.ai/",
description: "API key",
instructions: [
"1. Visit: https://developer.ideogram.ai/",
"2. Sign in / create account",
"3. Generate a new API key",
"4. Set IDEOGRAM_API_KEY in your .env file",
],
};
}
/**
* Creates Recraft provider configuration (image generation, vector focus).
*/
export function createRecraftConfig() {
return {
providerName: "Recraft",
envVarName: "RECRAFT_API_KEY",
setupUrl: "https://www.recraft.ai/api",
description: "API token",
instructions: [
"1. Visit: https://www.recraft.ai/api",
"2. Sign in / create account",
"3. Create an API token",
"4. Set RECRAFT_API_KEY in your .env file",
],
};
}
/**
* Creates Cloudflare Workers AI provider configuration.
*
* Cloudflare requires both `CLOUDFLARE_API_KEY` (workers-AI-scoped token)
* AND `CLOUDFLARE_ACCOUNT_ID` since the endpoint is per-account.
*/
export function createCloudflareConfig() {
return catalogConfigOptions("cloudflare");
}
/**
* Creates Google Vertex Project ID configuration
*/
export function createVertexProjectConfig() {
return {
providerName: "Google Vertex AI",
envVarName: "GOOGLE_CLOUD_PROJECT_ID",
setupUrl: "https://console.cloud.google.com/",
description: "Google Cloud Credentials",
instructions: [
"1. Visit: https://console.cloud.google.com/",
"2. Create or select a project",
"3. Enable Vertex AI API",
"4. Set up authentication",
],
fallbackEnvVars: [
"VERTEX_PROJECT_ID",
"GOOGLE_VERTEX_PROJECT",
"GOOGLE_CLOUD_PROJECT",
],
};
}
/**
* Creates Google Cloud Authentication configuration
*/
export function createGoogleAuthConfig() {
return {
providerName: "Google Vertex AI",
envVarName: "GOOGLE_APPLICATION_CREDENTIALS",
setupUrl: "https://console.cloud.google.com/",
description: "Google Cloud authentication",
instructions: [
"🔧 Option 1: Service Account Key File",
"GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json",
"",
"🔧 Option 2: Service Account Key (Base64)",
"GOOGLE_SERVICE_ACCOUNT_KEY=base64_encoded_key",
"",
"🔧 Option 3: Individual Credentials",
"GOOGLE_AUTH_CLIENT_EMAIL=your-service-account@project.iam.gserviceaccount.com",
"GOOGLE_AUTH_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----...",
],
fallbackEnvVars: ["GOOGLE_SERVICE_ACCOUNT_KEY"],
};
}
/**
* Creates Anthropic Base Provider configuration
*/
export function createAnthropicBaseConfig() {
return {
providerName: "ANTHROPIC",
envVarName: "ANTHROPIC_API_KEY",
setupUrl: "https://console.anthropic.com/",
description: "Credentials",
instructions: [
"Get your API key from https://console.anthropic.com/",
"",
"💡 Step 2: Add to your .env file (or export in CLI):",
],
};
}
// =============================================================================
// HELPER FUNCTIONS FOR SPECIFIC PROVIDER NEEDS
// =============================================================================
/**
* Gets AWS Region with default fallback
* Supports both AWS_REGION and AWS_DEFAULT_REGION for broader compatibility
*/
export function getAWSRegion() {
return (process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1");
}
/**
* Gets AWS Session Token if available
*/
export function getAWSSessionToken() {
return process.env.AWS_SESSION_TOKEN;
}
/**
* Checks if HuggingFace credentials are available
*/
export function hasHuggingFaceCredentials() {
return hasProviderCredentials(["HUGGINGFACE_API_KEY", "HF_TOKEN"]);
}
// =============================================================================
// ANTHROPIC/CLAUDE SUBSCRIPTION AUTH HELPERS
// =============================================================================
/**
* Gets the configured Anthropic authentication method
* Defaults to "api_key" for backward compatibility
* @returns The configured authentication method
*/
export function getAnthropicAuthMethod() {
const method = process.env.ANTHROPIC_AUTH_METHOD?.toLowerCase();
if (method === "oauth") {
return "oauth";
}
return "api_key";
}
/**
* Gets the configured Claude subscription tier
* Defaults to "api" for backward compatibility (API key users)
* @returns The configured subscription tier
*/
export function getAnthropicSubscriptionTier() {
const tier = process.env.ANTHROPIC_SUBSCRIPTION_TIER?.toLowerCase();
switch (tier) {
case "free":
return "free";
case "pro":
return "pro";
case "max":
return "max";
case "max_5":
return "max_5";
case "max_20":
return "max_20";
case "api":
default:
return "api";
}
}
/**
* Validates OAuth access token format
* @param token The token to validate
* @returns True if the token format is valid
*/
export function validateOAuthAccessToken(token) {
if (!token || token.length < 32) {
return false;
}
const format = OAUTH_TOKEN_FORMATS["anthropic-access"];
return format.test(token);
}
/**
* Validates OAuth refresh token format
* @param token The token to validate
* @returns True if the token format is valid
*/
export function validateOAuthRefreshToken(token) {
if (!token || token.length < 32) {
return false;
}
const format = OAUTH_TOKEN_FORMATS["anthropic-refresh"];
return format.test(token);
}
/**
* Detects the best available authentication method for Anthropic
* Checks environment variables and returns the most appropriate auth configuration
* @returns Complete authentication configuration
*/
export function detectAnthropicAuth() {
const explicitMethod = process.env.ANTHROPIC_AUTH_METHOD?.toLowerCase();
const tier = getAnthropicSubscriptionTier();
// Check for OAuth tokens — canonical + fallbacks for backward compatibility
const accessToken = process.env.ANTHROPIC_OAUTH_TOKEN ??
process.env.CLAUDE_OAUTH_TOKEN ??
process.env.ANTHROPIC_OAUTH_ACCESS_TOKEN;
const refreshToken = process.env.ANTHROPIC_OAUTH_REFRESH_TOKEN;
// Check for API key
const apiKey = process.env.ANTHROPIC_API_KEY;
// If explicit method is set, use it
if (explicitMethod === "oauth") {
if (accessToken) {
const isValidAccessToken = validateOAuthAccessToken(accessToken);
const isValidRefreshToken = refreshToken
? validateOAuthRefreshToken(refreshToken)
: true;
if (!isValidAccessToken) {
return {
method: "oauth",
tier,
accessToken,
refreshToken,
isConfigured: false,
error: "Invalid OAuth access token format. Token should be a valid JWT or opaque token.",
};
}
if (refreshToken && !isValidRefreshToken) {
return {
method: "oauth",
tier,
accessToken,
refreshToken,
isConfigured: false,
error: "Invalid OAuth refresh token format. Token should be at least 32 characters.",
};
}
return {
method: "oauth",
tier,
accessToken,
refreshToken,
isConfigured: true,
};
}
// OAuth method specified but no token
return {
method: "oauth",
tier,
isConfigured: false,
error: "OAuth authentication method specified but ANTHROPIC_OAUTH_TOKEN not set.",
};
}
// If explicit method is api_key or not set
if (apiKey) {
const isValidApiKey = validateApiKeyFormat("anthropic", apiKey);
if (!isValidApiKey) {
// Still return as configured but note the format issue
return {
method: "api_key",
tier: "api", // API key users are always on "api" tier
apiKey,
isConfigured: true,
error: "API key format may be invalid. Expected format: sk-ant-...",
};
}
return {
method: "api_key",
tier: "api",
apiKey,
isConfigured: true,
};
}
// Check if OAuth tokens are available without explicit method set
if (accessToken) {
const isValidAccessToken = validateOAuthAccessToken(accessToken);
return {
method: "oauth",
tier,
accessToken,
refreshToken,
isConfigured: isValidAccessToken,
error: isValidAccessToken
? undefined
: "Invalid OAuth access token format.",
};
}
// No authentication configured
return {
method: "api_key",
tier: "api",
isConfigured: false,
error: "No Anthropic authentication configured. Set ANTHROPIC_API_KEY or configure OAuth.",
};
}
/**
* Checks if Anthropic credentials are available (either API key or OAuth)
* @returns True if any valid authentication is configured
*/
export function hasAnthropicCredentials() {
const auth = detectAnthropicAuth();
return auth.isConfigured;
}
/**
* Gets the authentication token or key for Anthropic API calls
* Returns the appropriate credential based on configured auth method
* @returns The API key or OAuth access token, or undefined if not configured
*/
export function getAnthropicCredential() {
const auth = detectAnthropicAuth();
if (!auth.isConfigured) {
return undefined;
}
return auth.method === "oauth" ? auth.accessToken : auth.apiKey;
}
/**
* Checks if OAuth refresh is needed based on token state
* This is a placeholder for actual token expiration checking
* @returns True if refresh is needed
*/
export function needsOAuthRefresh() {
const auth = detectAnthropicAuth();
if (auth.method !== "oauth" || !auth.isConfigured) {
return false;
}
// In a real implementation, you would check token expiration
// For now, we just check if a refresh token is available
// The actual refresh logic would be in the OAuth client
return !!auth.refreshToken;
}
/**
* Gets subscription tier limits for informational purposes
* These are approximate limits and may change
* @param tier The subscription tier
* @returns Object with tier limit information
*/
export function getSubscriptionTierLimits(tier) {
switch (tier) {
case "free":
return {
messagesPerDay: 10,
contextWindow: 100000,
priorityAccess: false,
description: "Free tier with limited daily messages",
};
case "pro":
return {
messagesPerDay: 100,
contextWindow: 200000,
priorityAccess: true,
description: "Claude Pro subscription with extended limits",
};
case "max":
return {
messagesPerDay: "unlimited",
contextWindow: 200000,
priorityAccess: true,
description: "Claude Max subscription with highest limits",
};
case "max_5":
return {
messagesPerDay: "unlimited",
contextWindow: 200000,
priorityAccess: true,
description: "Claude Max 5x usage tier with priority processing",
};
case "max_20":
return {
messagesPerDay: "unlimited",
contextWindow: 200000,
priorityAccess: true,
description: "Claude Max 20x usage tier with maximum capacity",
};
case "api":
default:
return {
messagesPerDay: "unlimited",
contextWindow: 200000,
priorityAccess: true,
description: "API access with pay-per-use billing",
};
}
}
// =============================================================================
// ENVIRONMENT VARIABLE CONSTANTS
// =============================================================================
/**
* Environment variables for Anthropic/Claude subscription configuration
* These control authentication method, subscription tier, and feature flags
*/
export const ANTHROPIC_ENV_VARS = {
/** Authentication method: "api_key" or "oauth" */
AUTH_METHOD: "ANTHROPIC_AUTH_METHOD",
/** Subscription tier: "free", "pro", "max", "max_5", "max_20", or "api" */
SUBSCRIPTION_TIER: "ANTHROPIC_SUBSCRIPTION_TIER",
/** Enable beta features: "true" or "false" */
ENABLE_BETA_FEATURES: "ANTHROPIC_ENABLE_BETA_FEATURES",
/** API key for api_key authentication */
API_KEY: "ANTHROPIC_API_KEY",
/** OAuth access token for oauth authentication (canonical, with BC fallbacks) */
OAUTH_ACCESS_TOKEN: "ANTHROPIC_OAUTH_TOKEN",
/** OAuth refresh token for oauth authentication */
OAUTH_REFRESH_TOKEN: "ANTHROPIC_OAUTH_REFRESH_TOKEN",
/** OAuth token expiry timestamp (Unix epoch in seconds) */
OAUTH_TOKEN_EXPIRY: "ANTHROPIC_OAUTH_TOKEN_EXPIRY",
};
/**
* Valid subscription tier values for validation
*/
export const VALID_SUBSCRIPTION_TIERS = [
"free",
"pro",
"max",
"max_5",
"max_20",
"api",
];
/**
* Valid authentication method values for validation
*/
export const VALID_AUTH_METHODS = [
"api_key",
"oauth",
];
// =============================================================================
// SUBSCRIPTION TIER VALIDATION
// =============================================================================
/**
* Validates a subscription tier value
* @param tier The tier value to validate
* @returns True if the tier is valid, false otherwise
*/
export function isValidSubscriptionTier(tier) {
if (!tier) {
return false;
}
return VALID_SUBSCRIPTION_TIERS.includes(tier);
}
/**
* Validates an authentication method value
* @param method The method value to validate
* @returns True if the method is valid, false otherwise
*/
export function isValidAuthMethod(method) {
if (!method) {
return false;
}
return VALID_AUTH_METHODS.includes(method);
}
/**
* Validates subscription tier and returns a detailed result
* @param tier The tier value to validate
* @returns Validation result with error details if invalid
*/
export function validateSubscriptionTier(tier) {
if (!tier) {
return {
isValid: false,
error: "Subscription tier is required but not provided.",
};
}
const normalizedTier = tier.toLowerCase().trim();
if (isValidSubscriptionTier(normalizedTier)) {
return {
isValid: true,
tier: normalizedTier,
};
}
return {
isValid: false,
error: `Invalid subscription tier "${tier}". Valid values are: ${VALID_SUBSCRIPTION_TIERS.join(", ")}`,
};
}
// =============================================================================
// ANTHROPIC AUTH CONFIG FUNCTIONS
// =============================================================================
/**
* Gets the complete Anthropic authentication configuration
* Detects auth method from environment or config and loads appropriate credentials
*
* @returns Complete AnthropicAuthConfig with method, credentials, and tier
*/
export function getAnthropicAuthConfig() {
const method = getAnthropicAuthMethod();
const tier = detectSubscriptionTier();
if (method === "oauth") {
const accessToken = process.env[ANTHROPIC_ENV_VARS.OAUTH_ACCESS_TOKEN];
const refreshToken = process.env[ANTHROPIC_ENV_VARS.OAUTH_REFRESH_TOKEN];
const tokenExpiryStr = process.env[ANTHROPIC_ENV_VARS.OAUTH_TOKEN_EXPIRY];
// Parse token expiry if provided
let expiresAt;
if (tokenExpiryStr) {
const parsed = parseInt(tokenExpiryStr, 10);
if (!isNaN(parsed)) {
expiresAt = parsed;
}
}
// Build OAuth token object
const oauthToken = accessToken
? {
accessToken,
refreshToken,
expiresAt,
tokenType: "Bearer",
}
: undefined;
return {
method: "oauth",
oauthToken,
accessToken, // Legacy field for backward compatibility
refreshToken, // Legacy field for backward compatibility
tokenExpiry: expiresAt ? expiresAt * 1000 : undefined, // Convert to milliseconds
subscriptionTier: tier,
autoRefresh: !!refreshToken,
};
}
// API key authentication
const apiKey = process.env[ANTHROPIC_ENV_VARS.API_KEY];
return {
method: "api_key",
apiKey,
subscriptionTier: tier,
autoRefresh: false,
};
}
/**
* Detects the subscription tier from environment variables or config
* Checks environment variable first, then config, defaults to "api" if using API key
*
* @returns The detected subscription tier
*/
export function detectSubscriptionTier() {
// 1. Check environment variable first (highest priority)
const envTier = process.env[ANTHROPIC_ENV_VARS.SUBSCRIPTION_TIER];
if (envTier) {
const validation = validateSubscriptionTier(envTier);
if (validation.isValid && validation.tier) {
return validation.tier;
}
logger.warn("Invalid ANTHROPIC_SUBSCRIPTION_TIER value", {
value: envTier,
validValues: VALID_SUBSCRIPTION_TIERS,
fallback: "Defaulting based on auth method",
});
}
// 2. Check config file (could be extended to read from config file)
// For now, we check if there's a tier set via other means
// This is a placeholder for config file integration
// const configTier = loadConfigTier(); // Future: implement config file loading
// 3. Default based on authentication method
const authMethod = getAnthropicAuthMethod();
if (authMethod === "oauth") {
// OAuth users are typically subscription users, default to "pro"
// unless they've explicitly configured otherwise
logger.debug("[detectSubscriptionTier] OAuth auth detected, defaulting to pro tier");
return "pro";
}
// 4. API key users default to "api" tier
logger.debug("[detectSubscriptionTier] API key auth, defaulting to api tier");
return "api";
}
/**
* Determines whether beta features should be enabled
* Checks environment/config and defaults based on authentication method
*
* @returns True if beta features should be enabled
*/
export function shouldEnableBetaFeatures() {
// 1. Check explicit environment variable first (highest priority)
const envValue = process.env[ANTHROPIC_ENV_VARS.ENABLE_BETA_FEATURES];
if (envValue !== undefined) {
const normalized = envValue.toLowerCase().trim();
if (normalized === "true" || normalized === "1" || normalized === "yes") {
return true;
}
if (normalized === "false" || normalized === "0" || normalized === "no") {
return false;
}
logger.warn("Invalid ANTHROPIC_ENABLE_BETA_FEATURES value", {
value: envValue,
expected: ["true", "false"],
fallback: "Defaulting based on auth method",
});
}
// 2. Check config file (placeholder for future config integration)
// const configValue = loadConfigBetaFeatures();
// 3. Default based on authentication method
// OAuth users get beta features enabled by default
// API key users get beta features disabled by default for stability
const authMethod = getAnthropicAuthMethod();
return authMethod === "oauth";
}
/**
* Gets complete subscription configuration including auth, tier, and features
* Combines all configuration sources into a unified config object
*
* @returns Complete subscription configuration
*/
export function getAnthropicSubscriptionConfig() {
const auth = getAnthropicAuthConfig();
const tier = detectSubscriptionTier();
const betaFeaturesEnabled = shouldEnableBetaFeatures();
const limits = getSubscriptionTierLimits(tier);
// Determine if properly configured
let isConfigured = false;
let error;
if (auth.method === "oauth") {
if (auth.oauthToken?.accessToken || auth.accessToken) {
isConfigured = true;
}
else {
error =
"OAuth authentication method specified but no access token configured. " +
`Set ${ANTHROPIC_ENV_VARS.OAUTH_ACCESS_TOKEN} environment variable.`;
}
}
else {
if (auth.apiKey) {
isConfigured = true;
}
else {
error =
"API key authentication method specified but no API key configured. " +
`Set ${ANTHROPIC_ENV_VARS.API_KEY} environment variable.`;
}
}
return {
auth,
tier,
betaFeaturesEnabled,
limits,
isConfigured,
error,
};
}
/**
* Checks if the current subscription tier has access to a specific feature
* @param feature The feature to check
* @param currentTier The current subscription tier (optional, auto-detects if not provided)
* @returns True if the tier has access to the feature
*/
export function hasSubscriptionFeature(feature, currentTier) {
const tier = currentTier ?? detectSubscriptionTier();
const limits = getSubscriptionTierLimits(tier);
// Map features to tier capabilities
switch (feature) {
case "extended_thinking":
// Extended thinking requires pro or higher, or API tier
return tier !== "free";
case "priority_access":
return limits.priorityAccess;
case "vision":
case "file_analysis":
// Available on all tiers
return true;
case "mcp_tools":
// MCP tools require pro or higher
return tier !== "free";
case "computer_use":
// Computer use requires max tier or API
return (tier === "max" ||
tier === "max_5" ||
tier === "max_20" ||
tier === "api");
case "web_search":
// Web search available on pro or higher
return tier !== "free";
default:
return false;
}
}
/**
* Gets a human-readable description of the current authentication configuration
* Useful for debugging and user feedback
*
* @returns Human-readable configuration description
*/
export function describeAnthropicConfig() {
const config = getAnthropicSubscriptionConfig();
const lines = [];
lines.push(`Authentication Method: ${config.auth.method}`);
lines.push(`Subscription Tier: ${config.tier}`);
lines.push(`Beta Features: ${config.betaFeaturesEnabled ? "Enabled" : "Disabled"}`);
lines.push(`Configured: ${config.isConfigured ? "Yes" : "No"}`);
if (config.error) {
lines.push(`Error: ${config.error}`);
}
lines.push(`Daily Messages: ${config.limits.messagesPerDay}`);
lines.push(`Context Window: ${config.limits.contextWindow.toLocaleString()} tokens`);
lines.push(`Priority Access: ${config.limits.priorityAccess ? "Yes" : "No"}`);
return lines.join("\n");
}
/**
* Resolves the {apiKey, baseURL} pair for a config-driven OpenAI-compatible
* catalog entry (see OpenAICompatCatalogEntry in types/providers.ts).
*
* Extracted from the identical 6-line precedence block that was copy-pasted
* across groq.ts, xai.ts, togetherAi.ts, fireworks.ts, perplexity.ts, and
* mistral.ts, plus Cloudflare's accountId-computed-baseURL variant.
*
* Precedence (matches every ported subclass's original behavior exactly):
* apiKey: credentials.apiKey (trimmed, non-blank) > env var > throw
* baseURL: credentials.baseURL (trimmed, non-blank)
* > env var (if entry.baseURLEnvVar is set, trimmed, non-blank)
* > entry.defaultBaseURL
* baseURL (computedBaseURL entries, e.g. Cloudflare):
* credentials.baseURL > computedBaseURL.build(accountId), where
* accountId = credentials.accountId (trimmed) > env var (trimmed)
* > throw computedBaseURL.missingValueMessage
*/
export function resolveOpenAICompatConfig(entry, credentials) {
const overrideApiKey = credentials?.apiKey?.trim();
const apiKey = overrideApiKey && overrideApiKey.length > 0
? overrideApiKey
: validateApiKey(entry.configOptions);
if (entry.computedBaseURL) {
const { envVar, missingValueMessage, build } = entry.computedBaseURL;
// An explicit base URL is checked first and trimmed the same way the
// static branch trims it. It makes the account id irrelevant — there is
// nothing left to build — so demanding one anyway would reject a fully
// specified override.
const overrideComputedBaseURL = credentials?.baseURL?.trim();
if (overrideComputedBaseURL && overrideComputedBaseURL.length > 0) {
return { apiKey, baseURL: overrideComputedBaseURL };
}
const extraValue = (credentials?.accountId ??
process.env[envVar] ??
"").trim();
if (!extraValue) {
throw new Error(missingValueMessage);
}
return { apiKey, baseURL: build(extraValue) };
}
const overrideBaseURL = credentials?.baseURL?.trim();
const envBaseURL = entry.baseURLEnvVar
? process.env[entry.baseURLEnvVar]?.trim()
: undefined;
const baseURL = (overrideBaseURL && overrideBaseURL.length > 0
? overrideBaseURL
: undefined) ??
(envBaseURL && envBaseURL.length > 0 ? envBaseURL : undefined) ??
entry.defaultBaseURL;
if (!baseURL) {
// Reachable only for an entry that sets neither defaultBaseURL nor
// computedBaseURL. Returning "" instead would hand the SDK an empty base
// URL and surface as a confusing request failure far from the cause.
throw new Error(`${entry.providerName}: no base URL. Set one in credentials` +
(entry.baseURLEnvVar ? `, set ${entry.baseURLEnvVar}` : "") +
`, or give the catalog entry a defaultBaseURL.`);
}
return { apiKey, baseURL };
}
/**
* TypeSafe (Jev) — the `decide` inference type. Not a text provider.
*/
export function createTypeSafeConfig() {
return {
providerName: "TypeSafe (Jev)",
envVarName: "TYPESAFE_API_KEY",
setupUrl: "https://console.typesafe.ai/keys",
description: "API key",
instructions: [
"1. Visit: https://console.typesafe.ai/keys",
"2. Sign in to your TypeSafe account",
"3. Create a new API key",
"4. Set TYPESAFE_API_KEY in your .env file",
],
};
}
/**
* Laya — the `decide` inference type. Not a text provider, and it has no
* built-in endpoint: LAYA_BASE_URL is required alongside the key.
*/
export function createLayaConfig() {
return {
providerName: "Laya",
envVarName: "LAYA_API_KEY",
setupUrl: "https://github.com/NandhaKishorM/laya",
description: "API key for your Laya endpoint",
instructions: [
"1. Run a Laya server, or use a LiteLLM proxy with a pass-through route to one",
"2. Set LAYA_BASE_URL to its base URL (NeuroLink calls <LAYA_BASE_URL>/predict)",
"3. Set LAYA_API_KEY to the key it accepts (on LiteLLM, a virtual key with that route in its Allowed Routes)",
],
};
}
/**
* XOR — the `decide` inference type. Not a text provider, and it has no
* built-in endpoint: XOR_BASE_URL is required alongside the key.
*/
export function createXorConfig() {
return {
providerName: "XOR",
envVarName: "XOR_API_KEY",
setupUrl: "https://huggingface.co/juspay/xor",
description: "API key for your XOR endpoint",
instructions: [
"1. Reach an XOR deployment, or use a LiteLLM proxy with a pass-through route to one",
"2. Set XOR_BASE_URL to its origin (NeuroLink calls <XOR_BASE_URL>/v1/systemone)",
"3. Set XOR_API_KEY to the key it accepts (on LiteLLM, a virtual key whose team allows xor-1.1)",
],
};
}
/**
* Perplexity Decisions API — the `decide` inference type, not the Sonar text
* models. It reads the same PERPLEXITY_API_KEY as the `perplexity` text
* provider and has a public endpoint, so the key alone configures it.
*/
export function createPerplexityDeciderConfig() {
return {
providerName: "Perplexity Decisions",
envVarName: "PERPLEXITY_API_KEY",
setupUrl: "https://console.perplexity.ai",
description: "API key for the Perplexity Decisions API",
instructions: [
"1. Create an API key in the Perplexity console (https://console.perplexity.ai)",
"2. Set PERPLEXITY_API_KEY; it is the same key the Perplexity text provider reads, so setting it also lets decide() use Perplexity when no other decision provider is configured",
"3. Optionally set PERPLEXITY_DECIDER_BASE_URL to an origin other than https://api.perplexity.ai (NeuroLink calls <base>/v1/decisions)",
],
};
}
/**
* Cloudflare Clef — the `decide` inference type, reached through Workers AI,
* not the Workers AI text models. It reads the same CLOUDFLARE_API_KEY and
* CLOUDFLARE_ACCOUNT_ID as the `cloudflare` text provider; the endpoint is
* Cloudflare's own, so the two together configure it.
*/
export function createCloudflareClefConfig() {
return {
providerName: "Cloudflare Clef",
envVarName: "CLOUDFLARE_API_KEY",
setupUrl: "https://dash.cloudflare.com/profile/api-tokens",
description: "API token (Workers AI scope) for Cloudflare's Clef decision models",
instructions: [
"1. Create an API token with the 'Workers AI: Read + Write' permission (https://dash.cloudflare.com/profile/api-tokens)",
"2. Set CLOUDFLARE_API_KEY to it, and CLOUDFLARE_ACCOUNT_ID to your account id (in the dashboard URL, or under 'Account ID'); they are the same settings the Cloudflare text provider reads, so setting them also lets decide() use Clef when no other decision provider is configured",
"3. The Clef endpoint ignores state text past about 2,048 tokens, so use it for short decisions",
],
};
}