376 lines
12 KiB
JavaScript
376 lines
12 KiB
JavaScript
/**
|
|
* API Secrets Parser Module
|
|
*
|
|
* Parses the ApiHandlerSecrets TypeScript interface from src/shared/api.ts
|
|
* to automatically discover API key fields for all providers.
|
|
*
|
|
* This eliminates the need for manual maintenance of provider-to-API-key mappings.
|
|
*/
|
|
|
|
/**
|
|
* Parses the ApiHandlerSecrets interface from api.ts content
|
|
*
|
|
* @param {string} content - Content of api.ts file
|
|
* @returns {Object} Parsed API key fields with metadata
|
|
* @returns {Object.fields} - Map of field names to their metadata
|
|
* @returns {Object.fieldNames} - Array of all field names
|
|
*/
|
|
export function parseApiHandlerSecrets(content) {
|
|
// Find the ApiHandlerSecrets interface definition
|
|
const interfaceMatch = content.match(/export interface ApiHandlerSecrets \{([\s\S]*?)\}/m)
|
|
|
|
if (!interfaceMatch) {
|
|
throw new Error("Could not find ApiHandlerSecrets interface definition")
|
|
}
|
|
|
|
const interfaceContent = interfaceMatch[1]
|
|
const fields = {}
|
|
const fieldNames = []
|
|
|
|
// Match field definitions like: fieldName?: string // comment
|
|
const fieldMatches = interfaceContent.matchAll(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\?\s*:\s*([^/\n]+)(?:\/\/\s*(.*))?$/gm)
|
|
|
|
for (const match of fieldMatches) {
|
|
const [, name, type, comment] = match
|
|
|
|
fields[name] = {
|
|
name,
|
|
type: type.trim(),
|
|
comment: comment?.trim() || "",
|
|
isSecret: true, // All fields in ApiHandlerSecrets are secrets
|
|
}
|
|
|
|
fieldNames.push(name)
|
|
}
|
|
|
|
return { fields, fieldNames }
|
|
}
|
|
|
|
/**
|
|
* Maps provider IDs to their required API key fields
|
|
*
|
|
* @param {Array<string>} providerIds - List of provider IDs from ApiProvider type
|
|
* @param {Object} apiSecretsFields - Parsed fields from ApiHandlerSecrets
|
|
* @returns {Object} Map of provider ID to array of API key field names
|
|
*
|
|
* Example output:
|
|
* {
|
|
* "anthropic": ["apiKey"],
|
|
* "bedrock": ["awsAccessKey", "awsSecretKey"],
|
|
* "cerebras": ["cerebrasApiKey"],
|
|
* ...
|
|
* }
|
|
*/
|
|
export function mapProviderToApiKeys(providerIds, apiSecretsFields) {
|
|
const providerApiKeyMap = {}
|
|
|
|
// Track which fields have been assigned to prevent duplicates
|
|
const assignedFields = new Set()
|
|
|
|
// First pass: Map provider-specific API key fields
|
|
for (const providerId of providerIds) {
|
|
const apiKeyFields = []
|
|
|
|
for (const fieldName of apiSecretsFields.fieldNames) {
|
|
if (assignedFields.has(fieldName)) {
|
|
continue
|
|
}
|
|
|
|
const providerFromField = extractProviderFromFieldName(fieldName)
|
|
|
|
if (providerFromField === providerId) {
|
|
apiKeyFields.push(fieldName)
|
|
assignedFields.add(fieldName)
|
|
}
|
|
}
|
|
|
|
if (apiKeyFields.length > 0) {
|
|
providerApiKeyMap[providerId] = apiKeyFields
|
|
}
|
|
}
|
|
|
|
// Second pass: Handle special cases and multi-key providers
|
|
applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields)
|
|
|
|
return providerApiKeyMap
|
|
}
|
|
|
|
/**
|
|
* Determines the provider ID from an API key field name
|
|
* Uses pattern matching on common naming conventions
|
|
*
|
|
* @param {string} fieldName - API key field name (e.g., "cerebrasApiKey")
|
|
* @returns {string|null} Provider ID or null if not a provider-specific key
|
|
*/
|
|
export function extractProviderFromFieldName(fieldName) {
|
|
// Normalize field name to lowercase for matching
|
|
const lowerFieldName = fieldName.toLowerCase()
|
|
|
|
// SPECIAL CASES FIRST (before pattern matching)
|
|
|
|
// Special case: "apiKey" alone maps to "anthropic" (primary provider)
|
|
if (fieldName === "apiKey") {
|
|
return "anthropic"
|
|
}
|
|
|
|
// Special case: clineAccountId maps to "cline"
|
|
if (lowerFieldName === "clineaccountid") {
|
|
return "cline"
|
|
}
|
|
|
|
// Special case: authNonce is not provider-specific
|
|
if (lowerFieldName === "authnonce") {
|
|
return null
|
|
}
|
|
|
|
// Special case: Vertex fields (not in ApiHandlerSecrets but in ApiHandlerOptions)
|
|
if (lowerFieldName === "vertexprojectid" || lowerFieldName === "vertexregion") {
|
|
return "vertex"
|
|
}
|
|
|
|
// Pattern 1: AWS-specific fields (check before generic pattern to avoid false positives)
|
|
if (lowerFieldName.startsWith("aws")) {
|
|
// awsAccessKey, awsSecretKey, awsSessionToken, awsRegion -> bedrock
|
|
if (
|
|
lowerFieldName.includes("accesskey") ||
|
|
lowerFieldName.includes("secretkey") ||
|
|
lowerFieldName.includes("sessiontoken") ||
|
|
lowerFieldName.includes("region")
|
|
) {
|
|
return "bedrock"
|
|
}
|
|
// awsBedrockApiKey is explicitly bedrock
|
|
if (lowerFieldName.includes("bedrock")) {
|
|
return "bedrock"
|
|
}
|
|
}
|
|
|
|
// Pattern 2: Vertex-specific fields
|
|
if (lowerFieldName.startsWith("vertex")) {
|
|
return "vertex"
|
|
}
|
|
|
|
// Pattern 3: SAP AI Core fields
|
|
if (lowerFieldName.startsWith("sapaicore") || lowerFieldName.startsWith("sapai")) {
|
|
return "sapaicore"
|
|
}
|
|
|
|
// Pattern 4: Provider name in the middle (e.g., openAiNativeApiKey) - check before generic pattern
|
|
const providerPatterns = [
|
|
{ pattern: "openainative", providerId: "openai-native" },
|
|
{ pattern: "openrouter", providerId: "openrouter" },
|
|
{ pattern: "openai", providerId: "openai" },
|
|
{ pattern: "gemini", providerId: "gemini" },
|
|
{ pattern: "deepseek", providerId: "deepseek" },
|
|
{ pattern: "ollama", providerId: "ollama" },
|
|
{ pattern: "lmstudio", providerId: "lmstudio" },
|
|
{ pattern: "litellm", providerId: "litellm" },
|
|
{ pattern: "qwen", providerId: "qwen" },
|
|
{ pattern: "doubao", providerId: "doubao" },
|
|
{ pattern: "mistral", providerId: "mistral" },
|
|
{ pattern: "fireworks", providerId: "fireworks" },
|
|
{ pattern: "asksage", providerId: "asksage" },
|
|
{ pattern: "xai", providerId: "xai" },
|
|
{ pattern: "moonshot", providerId: "moonshot" },
|
|
{ pattern: "sambanova", providerId: "sambanova" },
|
|
{ pattern: "cerebras", providerId: "cerebras" },
|
|
{ pattern: "groq", providerId: "groq" },
|
|
{ pattern: "huggingface", providerId: "huggingface" },
|
|
{ pattern: "huawei", providerId: "huawei-cloud-maas" },
|
|
{ pattern: "baseten", providerId: "baseten" },
|
|
{ pattern: "vercel", providerId: "vercel-ai-gateway" },
|
|
{ pattern: "zai", providerId: "zai" },
|
|
{ pattern: "requesty", providerId: "requesty" },
|
|
{ pattern: "together", providerId: "together" },
|
|
{ pattern: "dify", providerId: "dify" },
|
|
]
|
|
|
|
for (const { pattern, providerId } of providerPatterns) {
|
|
if (lowerFieldName.includes(pattern)) {
|
|
return providerId
|
|
}
|
|
}
|
|
|
|
// Pattern 5: <provider>ApiKey format (most common) - checked LAST to avoid false positives
|
|
if (lowerFieldName.endsWith("apikey")) {
|
|
// Extract from ORIGINAL fieldName to preserve camelCase for normalization
|
|
const providerPart = fieldName.slice(0, -6) // Remove "ApiKey"
|
|
return normalizeProviderName(providerPart)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Normalizes provider name extracted from field name to match provider ID format
|
|
*
|
|
* @param {string} providerPart - Provider part extracted from field name
|
|
* @returns {string} Normalized provider ID
|
|
*/
|
|
function normalizeProviderName(providerPart) {
|
|
// Handle camelCase to kebab-case conversion
|
|
const normalized = providerPart
|
|
.replace(/([A-Z])/g, "-$1")
|
|
.toLowerCase()
|
|
.replace(/^-/, "")
|
|
|
|
// Handle special cases
|
|
const specialCases = {
|
|
"open-router": "openrouter",
|
|
"open-ai-native": "openai-native",
|
|
"open-ai": "openai",
|
|
"lite-llm": "litellm",
|
|
"deep-seek": "deepseek",
|
|
"ask-sage": "asksage",
|
|
"hugging-face": "huggingface",
|
|
"huawei-cloud-maas": "huawei-cloud-maas",
|
|
"sap-ai-core": "sapaicore",
|
|
"vercel-ai-gateway": "vercel-ai-gateway",
|
|
}
|
|
|
|
return specialCases[normalized] || normalized
|
|
}
|
|
|
|
/**
|
|
* Applies special case mappings for complex provider relationships
|
|
*
|
|
* @param {Object} providerApiKeyMap - Current map being built
|
|
* @param {Object} apiSecretsFields - Parsed API secrets fields
|
|
* @param {Set<string>} assignedFields - Set of already assigned field names
|
|
*/
|
|
function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) {
|
|
// Special case 1: Bedrock needs AWS fields (if not already assigned)
|
|
const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"]
|
|
const bedrockFields = providerApiKeyMap["bedrock"] || []
|
|
|
|
for (const field of awsFields) {
|
|
if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) {
|
|
bedrockFields.push(field)
|
|
assignedFields.add(field)
|
|
}
|
|
}
|
|
|
|
// Optional: awsSessionToken for temporary credentials
|
|
if (apiSecretsFields.fieldNames.includes("awsSessionToken") && !bedrockFields.includes("awsSessionToken")) {
|
|
bedrockFields.push("awsSessionToken")
|
|
assignedFields.add("awsSessionToken")
|
|
}
|
|
|
|
if (bedrockFields.length > 0) {
|
|
providerApiKeyMap["bedrock"] = bedrockFields
|
|
}
|
|
|
|
// Special case 2: Vertex needs project ID and region
|
|
if (providerApiKeyMap["vertex"]) {
|
|
// Vertex typically uses application default credentials,
|
|
// but requires project ID and region configuration
|
|
// These are already captured if they exist in ApiHandlerSecrets
|
|
}
|
|
|
|
// Special case 3: SAP AI Core multi-key authentication
|
|
if (providerApiKeyMap["sapaicore"]) {
|
|
const sapFields = providerApiKeyMap["sapaicore"]
|
|
const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"]
|
|
|
|
for (const field of requiredSapFields) {
|
|
if (apiSecretsFields.fieldNames.includes(field) && !sapFields.includes(field)) {
|
|
sapFields.push(field)
|
|
assignedFields.add(field)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates display name for an API key field
|
|
* Converts camelCase to Title Case with proper spacing
|
|
*
|
|
* @param {string} fieldName - API key field name
|
|
* @returns {string} Human-readable display name
|
|
*/
|
|
export function generateApiKeyDisplayName(fieldName) {
|
|
// Special cases for known abbreviations
|
|
const specialCases = {
|
|
apiKey: "API Key",
|
|
awsAccessKey: "AWS Access Key",
|
|
awsSecretKey: "AWS Secret Key",
|
|
awsSessionToken: "AWS Session Token",
|
|
awsRegion: "AWS Region",
|
|
awsBedrockApiKey: "AWS Bedrock API Key",
|
|
openRouterApiKey: "OpenRouter API Key",
|
|
openAiApiKey: "OpenAI API Key",
|
|
openAiNativeApiKey: "OpenAI Native API Key",
|
|
geminiApiKey: "Gemini API Key",
|
|
ollamaApiKey: "Ollama API Key",
|
|
deepSeekApiKey: "DeepSeek API Key",
|
|
liteLlmApiKey: "LiteLLM API Key",
|
|
qwenApiKey: "Qwen API Key",
|
|
doubaoApiKey: "Doubao API Key",
|
|
mistralApiKey: "Mistral API Key",
|
|
fireworksApiKey: "Fireworks API Key",
|
|
asksageApiKey: "AskSage API Key",
|
|
xaiApiKey: "X AI API Key",
|
|
moonshotApiKey: "Moonshot API Key",
|
|
sambanovaApiKey: "SambaNova API Key",
|
|
cerebrasApiKey: "Cerebras API Key",
|
|
groqApiKey: "Groq API Key",
|
|
huggingFaceApiKey: "Hugging Face API Key",
|
|
nebiusApiKey: "Nebius API Key",
|
|
basetenApiKey: "Baseten API Key",
|
|
vercelAiGatewayApiKey: "Vercel AI Gateway API Key",
|
|
zaiApiKey: "Z AI API Key",
|
|
requestyApiKey: "Requesty API Key",
|
|
togetherApiKey: "Together AI API Key",
|
|
difyApiKey: "Dify API Key",
|
|
clineAccountId: "Cline Account ID",
|
|
vertexProjectId: "Vertex Project ID",
|
|
vertexRegion: "Vertex Region",
|
|
sapAiCoreClientId: "SAP AI Core Client ID",
|
|
sapAiCoreClientSecret: "SAP AI Core Client Secret",
|
|
huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key",
|
|
hicapApiKey: "Hicap API Key",
|
|
}
|
|
|
|
if (specialCases[fieldName]) {
|
|
return specialCases[fieldName]
|
|
}
|
|
|
|
// Generic conversion: camelCase -> Title Case
|
|
return fieldName
|
|
.replace(/([A-Z])/g, " $1")
|
|
.replace(/^./, (str) => str.toUpperCase())
|
|
.trim()
|
|
}
|
|
|
|
/**
|
|
* Validates that all providers have at least one API key field mapped
|
|
*
|
|
* @param {Array<string>} providerIds - All provider IDs
|
|
* @param {Object} providerApiKeyMap - Generated mapping
|
|
* @returns {Object} Validation result with warnings for unmapped providers
|
|
*/
|
|
export function validateApiKeyMappings(providerIds, providerApiKeyMap) {
|
|
const unmappedProviders = []
|
|
const warnings = []
|
|
|
|
for (const providerId of providerIds) {
|
|
if (!providerApiKeyMap[providerId] || providerApiKeyMap[providerId].length === 0) {
|
|
// Some providers don't require API keys - they use alternative authentication:
|
|
const noKeyProviders = ["vscode-lm", "ollama", "lmstudio", "claude-code", "oca", "vertex", "qwen-code"]
|
|
|
|
if (!noKeyProviders.includes(providerId)) {
|
|
unmappedProviders.push(providerId)
|
|
warnings.push(`WARNING: Provider "${providerId}" has no API key fields mapped`)
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: unmappedProviders.length === 0,
|
|
unmappedProviders,
|
|
warnings,
|
|
totalProviders: providerIds.length,
|
|
mappedProviders: Object.keys(providerApiKeyMap).length,
|
|
}
|
|
}
|