{"repo": "aurovexlabs/dzidzaal", "files": {"backend/src/config/aiProvider.js": {"14bdf9666330bb75d652c83cc51992680291840c": {"t": 1785745543.6782424, "n": 9517, "h": "c9bd55a05c4e6a4d", "c": "\"use strict\";\n/**\n * DzidzaAI — Multi-Provider AI Abstraction Layer\n *\n * Supports: OpenAI (GPT-4o-mini), Google Gemini (1.5 Flash), DeepSeek (Chat)\n *\n * Priority:\n * 1. Env var AI_PROVIDER if explicitly set\n * 2. OPENAI_API_KEY → OpenAI\n * 3. GEMINI_API_KEY → Gemini\n * 4. DEEPSEEK_API_KEY → DeepSeek\n *\n * All providers expose the same interface:\n * chat(messages, opts) → { content, tokensUsed, provider }\n * json(prompt, opts) → parsed object (retries on bad JSON)\n *\n * Failover: if the primary provider throws, the next available is tried once.\n */\n\nconst NodeCache = require(\"node-cache\");\nconst responseCache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); // 5-min cache\n\nconst LOCAL_VECTOR_DIMENSIONS = 64;\n\nconst localEmbedding = (text) => {\n const crypto = require(\"crypto\");\n const normalized = String(text || \"\")\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n const vector = new Array(LOCAL_VECTOR_DIMENSIONS).fill(0);\n for (const token of normalized.split(\" \").filter(Boolean)) {\n const hash = crypto.createHash(\"sha256\").update(token).digest();\n const index = hash.readUInt32BE(0) % LOCAL_VECTOR_DIMENSIONS;\n vector[index] += 1;\n }\n\n const magnitude =\n Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)) || 1;\n return vector.map((value) => value / magnitude);\n};\n\n// ─── Determine provider priority ─────────────────────────────────────────────\nconst buildProviderList = () => {\n const explicit = process.env.AI_PROVIDER?.toLowerCase();\n const all = [];\n\n if (explicit) {\n // If explicitly set, put it first — still include others for failover\n const map = { openai: \"openai\", gemini: \"gemini\", deepseek: \"deepseek\" };\n if (map[explicit]) all.push(map[explicit]);\n }\n\n if (!all.includes(\"openai\") && process.env.OPENAI_API_KEY) all.push(\"openai\");\n if (!all.includes(\"gemini\") && process.env.GEMINI_API_KEY) all.push(\"gemini\");\n if (!all.includes(\"deepseek\") && process.env.DEEPSEEK_API_KEY)\n all.push(\"deepseek\");\n\n return all;\n};\n\n// ─── Provider adapters ────────────────────────────────────────────────────────\n\n// OpenAI (also used for DeepSeek which is OpenAI-compatible)\nconst openAICompletions = async (\n apiKey,\n baseURL,\n model,\n messages,\n opts = {},\n) => {\n const { default: OpenAI } = await Promise.resolve().then(() =>\n require(\"openai\"),\n );\n const client = new OpenAI({ apiKey, baseURL });\n\n const params = {\n model,\n messages,\n max_tokens: opts.maxTokens || 1200,\n temperature: opts.temperature ?? 0.7,\n };\n if (opts.json) params.response_format = { type: \"json_object\" };\n\n const res = await client.chat.completions.create(params);\n return {\n content: res.choices[0].message.content,\n tokensUsed: res.usage?.total_tokens || 0,\n };\n};\n\nconst openAIEmbeddings = async (apiKey, baseURL, model, input) => {\n const { default: OpenAI } = await Promise.resolve().then(() =>\n require(\"openai\"),\n );\n const client = new OpenAI({ apiKey, baseURL });\n const res = await client.embeddings.create({\n model,\n input,\n });\n\n return {\n vectors: res.data.map((item) => item.embedding),\n tokensUsed: res.usage?.total_tokens || 0,\n };\n};\n\n// Google Gemini\nconst geminiCompletions = async (messages, opts = {}) => {\n const { GoogleGenerativeAI } = require(\"@google/generative-ai\");\n const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const model = genAI.getGenerativeModel({ model: \"gemini-2.5-flash\" });\n\n // Convert OpenAI message format to Gemini format\n const systemMsg = messages.find((m) => m.role === \"system\");\n const chatHistory = messages\n .filter((m) => m.role !== \"system\")\n .slice(0, -1)\n .map((m) => ({\n role: m.role === \"assistant\" ? \"model\" : \"user\",\n parts: [{ text: m.content }],\n }));\n const lastMsg = messages.filter((m) => m.role !== \"system\").at(-1);\n\n let prompt = lastMsg?.content || \"\";\n if (systemMsg) prompt = `${systemMsg.content}\\n\\n${prompt}`;\n if (opts.json)\n prompt +=\n \"\\n\\nIMPORTANT: Respond with ONLY valid JSON. No markdown, no explanation.\";\n\n const chat = model.startChat({\n history: chatHistory,\n generationConfig: {\n maxOutputTokens: opts.maxTokens || 1200,\n temperature: opts.temperature ?? 0.7,\n },\n });\n\n const result = await chat.sendMessage(prompt);\n const text = result.response.text();\n\n return {\n content: text,\n tokensUsed: result.response.usageMetadata?.totalTokenCount || 0,\n };\n};\n\n// ─── Core completion function with failover ───────────────────────────────────\nconst complete = async (messages, opts = {}) => {\n const providers = buildProviderList();\n\n if (providers.length === 0) {\n throw new Error(\n \"No AI engine configured. Set at least one of: OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY in your .env file.\",\n );\n }\n\n let lastError;\n\n for (const provider of providers) {\n try {\n let result;\n\n if (provider === \"openai\") {\n result = await openAICompletions(\n process.env.OPENAI_API_KEY,\n undefined,\n \"gpt-4o-mini\",\n messages,\n opts,\n );\n } else if (provider === \"gemini\") {\n result = await geminiCompletions(messages, opts);\n } else if (provider === \"deepseek\") {\n result = await openAICompletions(\n process.env.DEEPSEEK_API_KEY,\n \"https://api.deepseek.com\",\n \"deepseek-chat\",\n messages,\n opts,\n );\n }\n\n return { ...result, provider };\n } catch (err) {\n console.error(`[AI] Provider \"${provider}\" failed: ${err.message}`);\n lastError = err;\n // Continue to next provider\n }\n }\n\n throw new Error(`All AI engines failed. Last error: ${lastError?.message}`);\n};\n\n// ─── JSON helper with retry + repair ─────────────────────────────────────────\nconst completeJSON = async (prompt, opts = {}) => {\n const MAX_RETRIES = 2;\n let lastError;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const result = await complete([{ role: \"user\", content: prompt }], {\n ...opts,\n json: true,\n });\n\n let text = result.content.trim();\n\n // Strip markdown code fences if present (Gemini sometimes adds them)\n text = text.replace(/^```(?:json)?\\s*/i, \"\").replace(/\\s*```$/i, \"\");\n\n // Find first { and last } to extract JSON\n const start = text.indexOf(\"{\");\n const end = text.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1) {\n text = text.slice(start, end + 1);\n }\n\n try {\n const parsed = JSON.parse(text);\n return {\n data: parsed,\n provider: result.provider,\n tokensUsed: result.tokensUsed,\n };\n } catch (parseErr) {\n console.error(\n `[AI] JSON parse failed (attempt ${attempt + 1}):`,\n parseErr.message,\n );\n lastError = parseErr;\n\n if (attempt < MAX_RETRIES) {\n // Ask the model to fix its output\n prompt = `${prompt}\\n\\nYour previous response could not be parsed as JSON. Return ONLY raw JSON with no markdown or explanations.`;\n }\n }\n }\n\n throw new Error(\n `AI returned invalid JSON after ${MAX_RETRIES + 1} attempts: ${lastError?.message}`,\n );\n};\n\n// ─── Cache wrapper ────────────────────────────────────────────────────────────\nconst completeWithCache = async (cacheKey, messages, opts = {}) => {\n if (opts.noCache) return complete(messages, opts);\n const cached = responseCache.get(cacheKey);\n if (cached) return { ...cached, fromCache: true };\n const result = await complete(messages, opts);\n responseCache.set(cacheKey, result);\n return result;\n};\n\nconst embedMany = async (texts, opts = {}) => {\n const inputs = Array.isArray(texts) ? texts : [texts];\n const cleaned = inputs.map((text) => String(text || \"\").trim());\n\n if (!cleaned.length) {\n return { vectors: [], provider: null, model: null, tokensUsed: 0 };\n }\n\n const provider = (\n opts.provider ||\n process.env.EMBEDDING_PROVIDER ||\n \"\"\n ).toLowerCase();\n const model =\n opts.model || process.env.EMBEDDING_MODEL || \"text-embedding-3-small\";\n\n if (\n (provider === \"openai\" || provider === \"\") &&\n process.env.OPENAI_API_KEY\n ) {\n const result = await openAIEmbeddings(\n process.env.OPENAI_API_KEY,\n undefined,\n model,\n cleaned,\n );\n return {\n vectors: result.vectors,\n provider: \"openai\",\n model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n if (\n (provider === \"deepseek\" || provider === \"\") &&\n process.env.DEEPSEEK_API_KEY\n ) {\n const result = await openAIEmbeddings(\n process.env.DEEPSEEK_API_KEY,\n \"https://api.deepseek.com\",\n model,\n cleaned,\n );\n return {\n vectors: result.vectors,\n provider: \"deepseek\",\n model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n return {\n vectors: cleaned.map(localEmbedding),\n provider: \"local-hash\",\n model: \"hash-64\",\n tokensUsed: 0,\n };\n};\n\nconst embedText = async (text, opts = {}) => {\n const result = await embedMany([text], opts);\n if (result.vectors.length && result.vectors[0]) {\n return {\n vector: result.vectors[0],\n provider: result.provider,\n model: result.model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n return {\n vector: localEmbedding(text),\n provider: \"local-hash\",\n model: \"hash-64\",\n tokensUsed: 0,\n };\n};\n\nmodule.exports = {\n complete,\n completeJSON,\n completeWithCache,\n embedMany,\n embedText,\n};\n"}, "ba81ae500f221be17b2f2863a49c67082a35183c": {"t": 1785754063.1805475, "n": 9517, "h": "c9bd55a05c4e6a4d", "c": "\"use strict\";\n/**\n * DzidzaAI — Multi-Provider AI Abstraction Layer\n *\n * Supports: OpenAI (GPT-4o-mini), Google Gemini (1.5 Flash), DeepSeek (Chat)\n *\n * Priority:\n * 1. Env var AI_PROVIDER if explicitly set\n * 2. OPENAI_API_KEY → OpenAI\n * 3. GEMINI_API_KEY → Gemini\n * 4. DEEPSEEK_API_KEY → DeepSeek\n *\n * All providers expose the same interface:\n * chat(messages, opts) → { content, tokensUsed, provider }\n * json(prompt, opts) → parsed object (retries on bad JSON)\n *\n * Failover: if the primary provider throws, the next available is tried once.\n */\n\nconst NodeCache = require(\"node-cache\");\nconst responseCache = new NodeCache({ stdTTL: 300, checkperiod: 60 }); // 5-min cache\n\nconst LOCAL_VECTOR_DIMENSIONS = 64;\n\nconst localEmbedding = (text) => {\n const crypto = require(\"crypto\");\n const normalized = String(text || \"\")\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n\n const vector = new Array(LOCAL_VECTOR_DIMENSIONS).fill(0);\n for (const token of normalized.split(\" \").filter(Boolean)) {\n const hash = crypto.createHash(\"sha256\").update(token).digest();\n const index = hash.readUInt32BE(0) % LOCAL_VECTOR_DIMENSIONS;\n vector[index] += 1;\n }\n\n const magnitude =\n Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0)) || 1;\n return vector.map((value) => value / magnitude);\n};\n\n// ─── Determine provider priority ─────────────────────────────────────────────\nconst buildProviderList = () => {\n const explicit = process.env.AI_PROVIDER?.toLowerCase();\n const all = [];\n\n if (explicit) {\n // If explicitly set, put it first — still include others for failover\n const map = { openai: \"openai\", gemini: \"gemini\", deepseek: \"deepseek\" };\n if (map[explicit]) all.push(map[explicit]);\n }\n\n if (!all.includes(\"openai\") && process.env.OPENAI_API_KEY) all.push(\"openai\");\n if (!all.includes(\"gemini\") && process.env.GEMINI_API_KEY) all.push(\"gemini\");\n if (!all.includes(\"deepseek\") && process.env.DEEPSEEK_API_KEY)\n all.push(\"deepseek\");\n\n return all;\n};\n\n// ─── Provider adapters ────────────────────────────────────────────────────────\n\n// OpenAI (also used for DeepSeek which is OpenAI-compatible)\nconst openAICompletions = async (\n apiKey,\n baseURL,\n model,\n messages,\n opts = {},\n) => {\n const { default: OpenAI } = await Promise.resolve().then(() =>\n require(\"openai\"),\n );\n const client = new OpenAI({ apiKey, baseURL });\n\n const params = {\n model,\n messages,\n max_tokens: opts.maxTokens || 1200,\n temperature: opts.temperature ?? 0.7,\n };\n if (opts.json) params.response_format = { type: \"json_object\" };\n\n const res = await client.chat.completions.create(params);\n return {\n content: res.choices[0].message.content,\n tokensUsed: res.usage?.total_tokens || 0,\n };\n};\n\nconst openAIEmbeddings = async (apiKey, baseURL, model, input) => {\n const { default: OpenAI } = await Promise.resolve().then(() =>\n require(\"openai\"),\n );\n const client = new OpenAI({ apiKey, baseURL });\n const res = await client.embeddings.create({\n model,\n input,\n });\n\n return {\n vectors: res.data.map((item) => item.embedding),\n tokensUsed: res.usage?.total_tokens || 0,\n };\n};\n\n// Google Gemini\nconst geminiCompletions = async (messages, opts = {}) => {\n const { GoogleGenerativeAI } = require(\"@google/generative-ai\");\n const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const model = genAI.getGenerativeModel({ model: \"gemini-2.5-flash\" });\n\n // Convert OpenAI message format to Gemini format\n const systemMsg = messages.find((m) => m.role === \"system\");\n const chatHistory = messages\n .filter((m) => m.role !== \"system\")\n .slice(0, -1)\n .map((m) => ({\n role: m.role === \"assistant\" ? \"model\" : \"user\",\n parts: [{ text: m.content }],\n }));\n const lastMsg = messages.filter((m) => m.role !== \"system\").at(-1);\n\n let prompt = lastMsg?.content || \"\";\n if (systemMsg) prompt = `${systemMsg.content}\\n\\n${prompt}`;\n if (opts.json)\n prompt +=\n \"\\n\\nIMPORTANT: Respond with ONLY valid JSON. No markdown, no explanation.\";\n\n const chat = model.startChat({\n history: chatHistory,\n generationConfig: {\n maxOutputTokens: opts.maxTokens || 1200,\n temperature: opts.temperature ?? 0.7,\n },\n });\n\n const result = await chat.sendMessage(prompt);\n const text = result.response.text();\n\n return {\n content: text,\n tokensUsed: result.response.usageMetadata?.totalTokenCount || 0,\n };\n};\n\n// ─── Core completion function with failover ───────────────────────────────────\nconst complete = async (messages, opts = {}) => {\n const providers = buildProviderList();\n\n if (providers.length === 0) {\n throw new Error(\n \"No AI engine configured. Set at least one of: OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY in your .env file.\",\n );\n }\n\n let lastError;\n\n for (const provider of providers) {\n try {\n let result;\n\n if (provider === \"openai\") {\n result = await openAICompletions(\n process.env.OPENAI_API_KEY,\n undefined,\n \"gpt-4o-mini\",\n messages,\n opts,\n );\n } else if (provider === \"gemini\") {\n result = await geminiCompletions(messages, opts);\n } else if (provider === \"deepseek\") {\n result = await openAICompletions(\n process.env.DEEPSEEK_API_KEY,\n \"https://api.deepseek.com\",\n \"deepseek-chat\",\n messages,\n opts,\n );\n }\n\n return { ...result, provider };\n } catch (err) {\n console.error(`[AI] Provider \"${provider}\" failed: ${err.message}`);\n lastError = err;\n // Continue to next provider\n }\n }\n\n throw new Error(`All AI engines failed. Last error: ${lastError?.message}`);\n};\n\n// ─── JSON helper with retry + repair ─────────────────────────────────────────\nconst completeJSON = async (prompt, opts = {}) => {\n const MAX_RETRIES = 2;\n let lastError;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n const result = await complete([{ role: \"user\", content: prompt }], {\n ...opts,\n json: true,\n });\n\n let text = result.content.trim();\n\n // Strip markdown code fences if present (Gemini sometimes adds them)\n text = text.replace(/^```(?:json)?\\s*/i, \"\").replace(/\\s*```$/i, \"\");\n\n // Find first { and last } to extract JSON\n const start = text.indexOf(\"{\");\n const end = text.lastIndexOf(\"}\");\n if (start !== -1 && end !== -1) {\n text = text.slice(start, end + 1);\n }\n\n try {\n const parsed = JSON.parse(text);\n return {\n data: parsed,\n provider: result.provider,\n tokensUsed: result.tokensUsed,\n };\n } catch (parseErr) {\n console.error(\n `[AI] JSON parse failed (attempt ${attempt + 1}):`,\n parseErr.message,\n );\n lastError = parseErr;\n\n if (attempt < MAX_RETRIES) {\n // Ask the model to fix its output\n prompt = `${prompt}\\n\\nYour previous response could not be parsed as JSON. Return ONLY raw JSON with no markdown or explanations.`;\n }\n }\n }\n\n throw new Error(\n `AI returned invalid JSON after ${MAX_RETRIES + 1} attempts: ${lastError?.message}`,\n );\n};\n\n// ─── Cache wrapper ────────────────────────────────────────────────────────────\nconst completeWithCache = async (cacheKey, messages, opts = {}) => {\n if (opts.noCache) return complete(messages, opts);\n const cached = responseCache.get(cacheKey);\n if (cached) return { ...cached, fromCache: true };\n const result = await complete(messages, opts);\n responseCache.set(cacheKey, result);\n return result;\n};\n\nconst embedMany = async (texts, opts = {}) => {\n const inputs = Array.isArray(texts) ? texts : [texts];\n const cleaned = inputs.map((text) => String(text || \"\").trim());\n\n if (!cleaned.length) {\n return { vectors: [], provider: null, model: null, tokensUsed: 0 };\n }\n\n const provider = (\n opts.provider ||\n process.env.EMBEDDING_PROVIDER ||\n \"\"\n ).toLowerCase();\n const model =\n opts.model || process.env.EMBEDDING_MODEL || \"text-embedding-3-small\";\n\n if (\n (provider === \"openai\" || provider === \"\") &&\n process.env.OPENAI_API_KEY\n ) {\n const result = await openAIEmbeddings(\n process.env.OPENAI_API_KEY,\n undefined,\n model,\n cleaned,\n );\n return {\n vectors: result.vectors,\n provider: \"openai\",\n model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n if (\n (provider === \"deepseek\" || provider === \"\") &&\n process.env.DEEPSEEK_API_KEY\n ) {\n const result = await openAIEmbeddings(\n process.env.DEEPSEEK_API_KEY,\n \"https://api.deepseek.com\",\n model,\n cleaned,\n );\n return {\n vectors: result.vectors,\n provider: \"deepseek\",\n model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n return {\n vectors: cleaned.map(localEmbedding),\n provider: \"local-hash\",\n model: \"hash-64\",\n tokensUsed: 0,\n };\n};\n\nconst embedText = async (text, opts = {}) => {\n const result = await embedMany([text], opts);\n if (result.vectors.length && result.vectors[0]) {\n return {\n vector: result.vectors[0],\n provider: result.provider,\n model: result.model,\n tokensUsed: result.tokensUsed,\n };\n }\n\n return {\n vector: localEmbedding(text),\n provider: \"local-hash\",\n model: \"hash-64\",\n tokensUsed: 0,\n };\n};\n\nmodule.exports = {\n complete,\n completeJSON,\n completeWithCache,\n embedMany,\n embedText,\n};\n"}}}}