fix(secretaria): embed() cai para Gemini se OpenAI falhar (não derruba RAG)

Antes, com openai_key presente, qualquer falha (ex.: 429 insufficient_quota)
retornava null sem tentar o Gemini. Agora tenta OpenAI e, em falha, usa Gemini.
Aviso documentado: OpenAI (1536d) e Gemini (768d) são incompatíveis — trocar de
provider exige reindexar (sync-knowledge + memória de contato).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 15:23:45 +02:00
parent bfa320b280
commit f51785bace
3 changed files with 87 additions and 46 deletions
@@ -71,35 +71,55 @@ async function embed(text, cfg) {
return null; return null;
const openaiKey = cfg?.openai_key; const openaiKey = cfg?.openai_key;
const geminiKey = cfg?.gemini_key; const geminiKey = cfg?.gemini_key;
// Preferência: OpenAI; em QUALQUER falha (ex.: 429 insufficient_quota), cai para
// o Gemini — assim uma chave OpenAI sem créditos não derruba o RAG.
// ⚠️ OpenAI (1536-dim) e Gemini (768-dim) geram espaços vetoriais incompatíveis.
// Ao trocar de provider é preciso REINDEXAR (sync-knowledge + memória de contato),
// senão o cosseno mistura dimensões e o retrieval fica sem sentido.
if (openaiKey) {
const v = await embedOpenAI(input, openaiKey);
if (v)
return v;
}
if (geminiKey) {
const v = await embedGemini(input, geminiKey);
if (v)
return v;
}
return null;
}
async function embedOpenAI(input, apiKey) {
try { try {
if (openaiKey) { const res = await fetch('https://api.openai.com/v1/embeddings', {
const res = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${openaiKey}` }, body: JSON.stringify({ model: 'text-embedding-3-small', input }),
body: JSON.stringify({ model: 'text-embedding-3-small', input }), });
}); if (!res.ok)
if (!res.ok) return null;
return null; const data = await res.json();
const data = await res.json(); return data?.data?.[0]?.embedding ?? null;
return data?.data?.[0]?.embedding ?? null; }
} catch {
if (geminiKey) { return null;
const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${geminiKey}`; }
const res = await fetch(url, { }
method: 'POST', async function embedGemini(input, apiKey) {
headers: { 'Content-Type': 'application/json' }, try {
body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }), const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${apiKey}`;
}); const res = await fetch(url, {
if (!res.ok) method: 'POST',
return null; headers: { 'Content-Type': 'application/json' },
const data = await res.json(); body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }),
return data?.embedding?.values ?? null; });
} if (!res.ok)
return null;
const data = await res.json();
return data?.embedding?.values ?? null;
} }
catch { catch {
return null; return null;
} }
return null;
} }
function hasEmbeddingKey(cfg) { function hasEmbeddingKey(cfg) {
return !!(cfg?.openai_key || cfg?.gemini_key); return !!(cfg?.openai_key || cfg?.gemini_key);
@@ -0,0 +1 @@
{"version":3,"file":"embeddings.js","sourceRoot":"","sources":["embeddings.ts"],"names":[],"mappings":"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"}
@@ -49,32 +49,52 @@ export async function embed(text: string, cfg: any): Promise<number[] | null> {
if (!input.trim()) return null if (!input.trim()) return null
const openaiKey = cfg?.openai_key as string | undefined const openaiKey = cfg?.openai_key as string | undefined
const geminiKey = cfg?.gemini_key as string | undefined const geminiKey = cfg?.gemini_key as string | undefined
// Preferência: OpenAI; em QUALQUER falha (ex.: 429 insufficient_quota), cai para
// o Gemini — assim uma chave OpenAI sem créditos não derruba o RAG.
// ⚠️ OpenAI (1536-dim) e Gemini (768-dim) geram espaços vetoriais incompatíveis.
// Ao trocar de provider é preciso REINDEXAR (sync-knowledge + memória de contato),
// senão o cosseno mistura dimensões e o retrieval fica sem sentido.
if (openaiKey) {
const v = await embedOpenAI(input, openaiKey)
if (v) return v
}
if (geminiKey) {
const v = await embedGemini(input, geminiKey)
if (v) return v
}
return null
}
async function embedOpenAI(input: string, apiKey: string): Promise<number[] | null> {
try { try {
if (openaiKey) { const res = await fetch('https://api.openai.com/v1/embeddings', {
const res = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${openaiKey}` }, body: JSON.stringify({ model: 'text-embedding-3-small', input }),
body: JSON.stringify({ model: 'text-embedding-3-small', input }), })
}) if (!res.ok) return null
if (!res.ok) return null const data: any = await res.json()
const data: any = await res.json() return data?.data?.[0]?.embedding ?? null
return data?.data?.[0]?.embedding ?? null } catch {
} return null
if (geminiKey) { }
const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${geminiKey}` }
const res = await fetch(url, {
method: 'POST', async function embedGemini(input: string, apiKey: string): Promise<number[] | null> {
headers: { 'Content-Type': 'application/json' }, try {
body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }), const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${apiKey}`
}) const res = await fetch(url, {
if (!res.ok) return null method: 'POST',
const data: any = await res.json() headers: { 'Content-Type': 'application/json' },
return data?.embedding?.values ?? null body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }),
} })
if (!res.ok) return null
const data: any = await res.json()
return data?.embedding?.values ?? null
} catch { } catch {
return null return null
} }
return null
} }
export function hasEmbeddingKey(cfg: any): boolean { export function hasEmbeddingKey(cfg: any): boolean {