api-secrets-parser.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. /**
  2. * API Secrets Parser Module
  3. *
  4. * Parses the ApiHandlerSecrets TypeScript interface from src/shared/api.ts
  5. * to automatically discover API key fields for all providers.
  6. *
  7. * This eliminates the need for manual maintenance of provider-to-API-key mappings.
  8. */
  9. /**
  10. * Parses the ApiHandlerSecrets interface from api.ts content
  11. *
  12. * @param {string} content - Content of api.ts file
  13. * @returns {Object} Parsed API key fields with metadata
  14. * @returns {Object.fields} - Map of field names to their metadata
  15. * @returns {Object.fieldNames} - Array of all field names
  16. */
  17. export function parseApiHandlerSecrets(content) {
  18. // Find the ApiHandlerSecrets interface definition
  19. const interfaceMatch = content.match(/export interface ApiHandlerSecrets \{([\s\S]*?)\}/m)
  20. if (!interfaceMatch) {
  21. throw new Error("Could not find ApiHandlerSecrets interface definition")
  22. }
  23. const interfaceContent = interfaceMatch[1]
  24. const fields = {}
  25. const fieldNames = []
  26. // Match field definitions like: fieldName?: string // comment
  27. const fieldMatches = interfaceContent.matchAll(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\?\s*:\s*([^/\n]+)(?:\/\/\s*(.*))?$/gm)
  28. for (const match of fieldMatches) {
  29. const [, name, type, comment] = match
  30. fields[name] = {
  31. name,
  32. type: type.trim(),
  33. comment: comment?.trim() || "",
  34. isSecret: true, // All fields in ApiHandlerSecrets are secrets
  35. }
  36. fieldNames.push(name)
  37. }
  38. return { fields, fieldNames }
  39. }
  40. /**
  41. * Maps provider IDs to their required API key fields
  42. *
  43. * @param {Array<string>} providerIds - List of provider IDs from ApiProvider type
  44. * @param {Object} apiSecretsFields - Parsed fields from ApiHandlerSecrets
  45. * @returns {Object} Map of provider ID to array of API key field names
  46. *
  47. * Example output:
  48. * {
  49. * "anthropic": ["apiKey"],
  50. * "bedrock": ["awsAccessKey", "awsSecretKey"],
  51. * "cerebras": ["cerebrasApiKey"],
  52. * ...
  53. * }
  54. */
  55. export function mapProviderToApiKeys(providerIds, apiSecretsFields) {
  56. const providerApiKeyMap = {}
  57. // Track which fields have been assigned to prevent duplicates
  58. const assignedFields = new Set()
  59. // First pass: Map provider-specific API key fields
  60. for (const providerId of providerIds) {
  61. const apiKeyFields = []
  62. for (const fieldName of apiSecretsFields.fieldNames) {
  63. if (assignedFields.has(fieldName)) {
  64. continue
  65. }
  66. const providerFromField = extractProviderFromFieldName(fieldName)
  67. if (providerFromField === providerId) {
  68. apiKeyFields.push(fieldName)
  69. assignedFields.add(fieldName)
  70. }
  71. }
  72. if (apiKeyFields.length > 0) {
  73. providerApiKeyMap[providerId] = apiKeyFields
  74. }
  75. }
  76. // Second pass: Handle special cases and multi-key providers
  77. applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields)
  78. return providerApiKeyMap
  79. }
  80. /**
  81. * Determines the provider ID from an API key field name
  82. * Uses pattern matching on common naming conventions
  83. *
  84. * @param {string} fieldName - API key field name (e.g., "cerebrasApiKey")
  85. * @returns {string|null} Provider ID or null if not a provider-specific key
  86. */
  87. export function extractProviderFromFieldName(fieldName) {
  88. // Normalize field name to lowercase for matching
  89. const lowerFieldName = fieldName.toLowerCase()
  90. // SPECIAL CASES FIRST (before pattern matching)
  91. // Special case: "apiKey" alone maps to "anthropic" (primary provider)
  92. if (fieldName === "apiKey") {
  93. return "anthropic"
  94. }
  95. // Special case: clineAccountId maps to "cline"
  96. if (lowerFieldName === "clineaccountid") {
  97. return "cline"
  98. }
  99. // Special case: authNonce is not provider-specific
  100. if (lowerFieldName === "authnonce") {
  101. return null
  102. }
  103. // Special case: Vertex fields (not in ApiHandlerSecrets but in ApiHandlerOptions)
  104. if (lowerFieldName === "vertexprojectid" || lowerFieldName === "vertexregion") {
  105. return "vertex"
  106. }
  107. // Pattern 1: AWS-specific fields (check before generic pattern to avoid false positives)
  108. if (lowerFieldName.startsWith("aws")) {
  109. // awsAccessKey, awsSecretKey, awsSessionToken, awsRegion -> bedrock
  110. if (
  111. lowerFieldName.includes("accesskey") ||
  112. lowerFieldName.includes("secretkey") ||
  113. lowerFieldName.includes("sessiontoken") ||
  114. lowerFieldName.includes("region")
  115. ) {
  116. return "bedrock"
  117. }
  118. // awsBedrockApiKey is explicitly bedrock
  119. if (lowerFieldName.includes("bedrock")) {
  120. return "bedrock"
  121. }
  122. }
  123. // Pattern 2: Vertex-specific fields
  124. if (lowerFieldName.startsWith("vertex")) {
  125. return "vertex"
  126. }
  127. // Pattern 3: SAP AI Core fields
  128. if (lowerFieldName.startsWith("sapaicore") || lowerFieldName.startsWith("sapai")) {
  129. return "sapaicore"
  130. }
  131. // Pattern 4: Provider name in the middle (e.g., openAiNativeApiKey) - check before generic pattern
  132. const providerPatterns = [
  133. { pattern: "openainative", providerId: "openai-native" },
  134. { pattern: "openrouter", providerId: "openrouter" },
  135. { pattern: "openai", providerId: "openai" },
  136. { pattern: "gemini", providerId: "gemini" },
  137. { pattern: "deepseek", providerId: "deepseek" },
  138. { pattern: "ollama", providerId: "ollama" },
  139. { pattern: "lmstudio", providerId: "lmstudio" },
  140. { pattern: "litellm", providerId: "litellm" },
  141. { pattern: "qwen", providerId: "qwen" },
  142. { pattern: "doubao", providerId: "doubao" },
  143. { pattern: "mistral", providerId: "mistral" },
  144. { pattern: "fireworks", providerId: "fireworks" },
  145. { pattern: "asksage", providerId: "asksage" },
  146. { pattern: "xai", providerId: "xai" },
  147. { pattern: "moonshot", providerId: "moonshot" },
  148. { pattern: "sambanova", providerId: "sambanova" },
  149. { pattern: "cerebras", providerId: "cerebras" },
  150. { pattern: "groq", providerId: "groq" },
  151. { pattern: "huggingface", providerId: "huggingface" },
  152. { pattern: "huawei", providerId: "huawei-cloud-maas" },
  153. { pattern: "baseten", providerId: "baseten" },
  154. { pattern: "vercel", providerId: "vercel-ai-gateway" },
  155. { pattern: "zai", providerId: "zai" },
  156. { pattern: "requesty", providerId: "requesty" },
  157. { pattern: "together", providerId: "together" },
  158. { pattern: "dify", providerId: "dify" },
  159. ]
  160. for (const { pattern, providerId } of providerPatterns) {
  161. if (lowerFieldName.includes(pattern)) {
  162. return providerId
  163. }
  164. }
  165. // Pattern 5: <provider>ApiKey format (most common) - checked LAST to avoid false positives
  166. if (lowerFieldName.endsWith("apikey")) {
  167. // Extract from ORIGINAL fieldName to preserve camelCase for normalization
  168. const providerPart = fieldName.slice(0, -6) // Remove "ApiKey"
  169. return normalizeProviderName(providerPart)
  170. }
  171. return null
  172. }
  173. /**
  174. * Normalizes provider name extracted from field name to match provider ID format
  175. *
  176. * @param {string} providerPart - Provider part extracted from field name
  177. * @returns {string} Normalized provider ID
  178. */
  179. function normalizeProviderName(providerPart) {
  180. // Handle camelCase to kebab-case conversion
  181. const normalized = providerPart
  182. .replace(/([A-Z])/g, "-$1")
  183. .toLowerCase()
  184. .replace(/^-/, "")
  185. // Handle special cases
  186. const specialCases = {
  187. "open-router": "openrouter",
  188. "open-ai-native": "openai-native",
  189. "open-ai": "openai",
  190. "lite-llm": "litellm",
  191. "deep-seek": "deepseek",
  192. "ask-sage": "asksage",
  193. "hugging-face": "huggingface",
  194. "huawei-cloud-maas": "huawei-cloud-maas",
  195. "sap-ai-core": "sapaicore",
  196. "vercel-ai-gateway": "vercel-ai-gateway",
  197. }
  198. return specialCases[normalized] || normalized
  199. }
  200. /**
  201. * Applies special case mappings for complex provider relationships
  202. *
  203. * @param {Object} providerApiKeyMap - Current map being built
  204. * @param {Object} apiSecretsFields - Parsed API secrets fields
  205. * @param {Set<string>} assignedFields - Set of already assigned field names
  206. */
  207. function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) {
  208. // Special case 1: Bedrock needs AWS fields (if not already assigned)
  209. const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"]
  210. const bedrockFields = providerApiKeyMap["bedrock"] || []
  211. for (const field of awsFields) {
  212. if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) {
  213. bedrockFields.push(field)
  214. assignedFields.add(field)
  215. }
  216. }
  217. // Optional: awsSessionToken for temporary credentials
  218. if (apiSecretsFields.fieldNames.includes("awsSessionToken") && !bedrockFields.includes("awsSessionToken")) {
  219. bedrockFields.push("awsSessionToken")
  220. assignedFields.add("awsSessionToken")
  221. }
  222. if (bedrockFields.length > 0) {
  223. providerApiKeyMap["bedrock"] = bedrockFields
  224. }
  225. // Special case 2: Vertex needs project ID and region
  226. if (providerApiKeyMap["vertex"]) {
  227. // Vertex typically uses application default credentials,
  228. // but requires project ID and region configuration
  229. // These are already captured if they exist in ApiHandlerSecrets
  230. }
  231. // Special case 3: SAP AI Core multi-key authentication
  232. if (providerApiKeyMap["sapaicore"]) {
  233. const sapFields = providerApiKeyMap["sapaicore"]
  234. const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"]
  235. for (const field of requiredSapFields) {
  236. if (apiSecretsFields.fieldNames.includes(field) && !sapFields.includes(field)) {
  237. sapFields.push(field)
  238. assignedFields.add(field)
  239. }
  240. }
  241. }
  242. }
  243. /**
  244. * Generates display name for an API key field
  245. * Converts camelCase to Title Case with proper spacing
  246. *
  247. * @param {string} fieldName - API key field name
  248. * @returns {string} Human-readable display name
  249. */
  250. export function generateApiKeyDisplayName(fieldName) {
  251. // Special cases for known abbreviations
  252. const specialCases = {
  253. apiKey: "API Key",
  254. awsAccessKey: "AWS Access Key",
  255. awsSecretKey: "AWS Secret Key",
  256. awsSessionToken: "AWS Session Token",
  257. awsRegion: "AWS Region",
  258. awsBedrockApiKey: "AWS Bedrock API Key",
  259. openRouterApiKey: "OpenRouter API Key",
  260. openAiApiKey: "OpenAI API Key",
  261. openAiNativeApiKey: "OpenAI Native API Key",
  262. geminiApiKey: "Gemini API Key",
  263. ollamaApiKey: "Ollama API Key",
  264. deepSeekApiKey: "DeepSeek API Key",
  265. liteLlmApiKey: "LiteLLM API Key",
  266. qwenApiKey: "Qwen API Key",
  267. doubaoApiKey: "Doubao API Key",
  268. mistralApiKey: "Mistral API Key",
  269. fireworksApiKey: "Fireworks API Key",
  270. asksageApiKey: "AskSage API Key",
  271. xaiApiKey: "X AI API Key",
  272. moonshotApiKey: "Moonshot API Key",
  273. sambanovaApiKey: "SambaNova API Key",
  274. cerebrasApiKey: "Cerebras API Key",
  275. groqApiKey: "Groq API Key",
  276. huggingFaceApiKey: "Hugging Face API Key",
  277. nebiusApiKey: "Nebius API Key",
  278. basetenApiKey: "Baseten API Key",
  279. vercelAiGatewayApiKey: "Vercel AI Gateway API Key",
  280. zaiApiKey: "Z AI API Key",
  281. requestyApiKey: "Requesty API Key",
  282. togetherApiKey: "Together AI API Key",
  283. difyApiKey: "Dify API Key",
  284. clineAccountId: "Cline Account ID",
  285. vertexProjectId: "Vertex Project ID",
  286. vertexRegion: "Vertex Region",
  287. sapAiCoreClientId: "SAP AI Core Client ID",
  288. sapAiCoreClientSecret: "SAP AI Core Client Secret",
  289. huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key",
  290. hicapApiKey: "Hicap API Key",
  291. }
  292. if (specialCases[fieldName]) {
  293. return specialCases[fieldName]
  294. }
  295. // Generic conversion: camelCase -> Title Case
  296. return fieldName
  297. .replace(/([A-Z])/g, " $1")
  298. .replace(/^./, (str) => str.toUpperCase())
  299. .trim()
  300. }
  301. /**
  302. * Validates that all providers have at least one API key field mapped
  303. *
  304. * @param {Array<string>} providerIds - All provider IDs
  305. * @param {Object} providerApiKeyMap - Generated mapping
  306. * @returns {Object} Validation result with warnings for unmapped providers
  307. */
  308. export function validateApiKeyMappings(providerIds, providerApiKeyMap) {
  309. const unmappedProviders = []
  310. const warnings = []
  311. for (const providerId of providerIds) {
  312. if (!providerApiKeyMap[providerId] || providerApiKeyMap[providerId].length === 0) {
  313. // Some providers don't require API keys - they use alternative authentication:
  314. const noKeyProviders = ["vscode-lm", "ollama", "lmstudio", "claude-code", "oca", "vertex", "qwen-code"]
  315. if (!noKeyProviders.includes(providerId)) {
  316. unmappedProviders.push(providerId)
  317. warnings.push(`WARNING: Provider "${providerId}" has no API key fields mapped`)
  318. }
  319. }
  320. }
  321. return {
  322. valid: unmappedProviders.length === 0,
  323. unmappedProviders,
  324. warnings,
  325. totalProviders: providerIds.length,
  326. mappedProviders: Object.keys(providerApiKeyMap).length,
  327. }
  328. }