forked from dirac-run/dirac
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi-secrets-parser.mjs
More file actions
373 lines (326 loc) · 11.8 KB
/
api-secrets-parser.mjs
File metadata and controls
373 lines (326 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**
* 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: diracAccountId maps to "dirac"
if (lowerFieldName === "diracaccountid") {
return "dirac"
}
// 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: "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",
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",
diracAccountId: "Dirac 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", "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,
}
}