diff --git a/manager/server.selfhosted.js b/manager/server.selfhosted.js index 3ca75f4..5c30da1 100644 --- a/manager/server.selfhosted.js +++ b/manager/server.selfhosted.js @@ -1477,13 +1477,15 @@ Exemplo de formato esperado: } else { // Provedor padrão: Google Gemini - let geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`; - const payload = { - contents: [ - { - parts: [ - { - text: `${userContent} + const tentarChamadaGemini = async (model, usarSchema) => { + const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; + + const payload = { + contents: [ + { + parts: [ + { + text: `${userContent} Apenas retorne o JSON estruturado contendo a lista de questões no seguinte formato de array: [ { @@ -1492,102 +1494,101 @@ Apenas retorne o JSON estruturado contendo a lista de questões no seguinte form "correctOptionIndex": 0 } ]` - } - ] - } - ], - generationConfig: { - responseMimeType: 'application/json', - responseSchema: { - type: 'ARRAY', - items: { - type: 'OBJECT', - properties: { - text: { type: 'STRING' }, - options: { - type: 'ARRAY', - items: { type: 'STRING' }, - minItems: 4, - maxItems: 4 - }, - correctOptionIndex: { type: 'INTEGER' } - }, - required: ['text', 'options', 'correctOptionIndex'] + } + ] } - } + ] + }; + + if (usarSchema) { + payload.generationConfig = { + responseMimeType: 'application/json', + responseSchema: { + type: 'ARRAY', + items: { + type: 'OBJECT', + properties: { + text: { type: 'STRING' }, + options: { + type: 'ARRAY', + items: { type: 'STRING' }, + minItems: 4, + maxItems: 4 + }, + correctOptionIndex: { type: 'INTEGER' } + }, + required: ['text', 'options', 'correctOptionIndex'] + } + } + }; } + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + return { response, modelUsed: model, schemaUsed: usarSchema }; }; - let response = await fetch(geminiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); + let resObj = null; + let modelTentado = modelName; - // Se der 404, tentar API v1 estável - if (!response.ok && response.status === 404) { - console.warn(`[Gemini] v1beta retornou 404 para o modelo ${modelName}. Tentando endpoint /v1/...`); - geminiUrl = `https://generativelanguage.googleapis.com/v1/models/${modelName}:generateContent?key=${apiKey}`; - response = await fetch(geminiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); + // Tentativa 1: Modelo selecionado com Schema estruturado + try { + console.log(`[Gemini] Tentando modelo selecionado com Schema: ${modelTentado}`); + resObj = await tentarChamadaGemini(modelTentado, true); + + // Se der erro 400 de Schema/MimeType, re-tentamos o mesmo modelo sem Schema imediatamente + if (!resObj.response.ok && resObj.response.status === 400) { + const errText = await resObj.response.clone().text(); + if (errText.includes('responseMimeType') || errText.includes('responseSchema')) { + console.warn(`[Gemini] Modelo ${modelTentado} rejeitou Schema. Re-tentando sem Schema...`); + resObj = await tentarChamadaGemini(modelTentado, false); + } + } + } catch (err) { + console.warn('[Gemini] Erro na primeira tentativa:', err.message); } - // Se ainda assim der 404 e o modelo for diferente de gemini-1.5-flash, tentar com gemini-1.5-flash de fallback universal - if (!response.ok && response.status === 404 && modelName !== 'gemini-1.5-flash') { - console.warn(`[Gemini] Modelo ${modelName} não encontrado no AI Studio. Tentando com fallback universal gemini-1.5-flash no endpoint v1...`); - geminiUrl = `https://generativelanguage.googleapis.com/v1/models/gemini-1.5-flash:generateContent?key=${apiKey}`; - response = await fetch(geminiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); + // Tentativa 2 (Fallback): Se a tentativa 1 deu 404 (modelo inválido/descontinuado) ou outro erro crítico, tentamos gemini-1.5-flash com Schema + if (!resObj || !resObj.response.ok) { + try { + modelTentado = 'gemini-1.5-flash'; + console.log(`[Gemini] Tentando fallback para gemini-1.5-flash com Schema...`); + resObj = await tentarChamadaGemini(modelTentado, true); - if (!response.ok && response.status === 404) { - // Última tentativa: gemini-1.5-flash no endpoint v1beta - console.warn(`[Gemini] Tentando gemini-1.5-flash no endpoint v1beta...`); - geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`; - response = await fetch(geminiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }); + if (!resObj.response.ok && resObj.response.status === 400) { + const errText = await resObj.response.clone().text(); + if (errText.includes('responseMimeType') || errText.includes('responseSchema')) { + console.warn('[Gemini] gemini-1.5-flash rejeitou Schema. Re-tentando sem Schema...'); + resObj = await tentarChamadaGemini(modelTentado, false); + } + } + } catch (err) { + console.warn('[Gemini] Erro no fallback gemini-1.5-flash:', err.message); } } - // Se der erro 400 por conta de Structured Outputs incompatíveis com o modelo selecionado, re-tentamos sem eles - let errText = ''; - if (!response.ok && response.status === 400) { - errText = await response.text(); - if (errText.includes('responseMimeType') || errText.includes('responseSchema')) { - console.warn(`[Gemini] Modelo ${modelName} não aceita Structured Outputs. Re-tentando chamada sem responseMimeType/Schema...`); - const simplePayload = { - contents: payload.contents - }; - response = await fetch(geminiUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(simplePayload) - }); + // Tentativa 3 (Último recurso): Se ainda falhar, tentamos gemini-1.5-flash sem Schema diretamente + if (!resObj || !resObj.response.ok) { + try { + modelTentado = 'gemini-1.5-flash'; + console.log(`[Gemini] Tentando fallback final para gemini-1.5-flash sem Schema...`); + resObj = await tentarChamadaGemini(modelTentado, false); + } catch (err) { + console.warn('[Gemini] Erro no fallback final:', err.message); } } - if (!response.ok) { - const errorText = errText || await response.text(); - throw new Error(`Gemini API retornou erro: ${response.status} - ${errorText}`); + if (!resObj || !resObj.response.ok) { + const status = resObj ? resObj.response.status : 500; + const errText = resObj ? await resObj.response.text() : 'Erro de rede ou timeout.'; + throw new Error(`Gemini API retornou erro: ${status} - ${errText}`); } - const resJson = await response.json(); + const resJson = await resObj.response.json(); const textResponse = resJson.candidates?.[0]?.content?.parts?.[0]?.text; if (!textResponse) { throw new Error('Retorno vazio da API do Gemini'); @@ -1690,19 +1691,40 @@ app.post('/api/ai/generate-image', async (req, res) => { imageUrl = resJson.data?.[0]?.url; } } else { - // Usar Google Gemini Imagen via API Nativa generateContent com responseModalities + // Usar Google Gemini Imagen via API Nativa const geminiImageModel = aiConfig.geminiImageModel || 'gemini-2.5-flash-image'; - const tentarGerarGemini = async (modelName) => { - const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`; + const tentarGerarImagen3 = async (model, promptText) => { + const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateImages?key=${apiKey}`; + const payload = { + prompt: `Ilustração educativa estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${promptText}`, + numberOfImages: 1, + outputMimeType: 'image/jpeg', + aspectRatio: '1:1' + }; + + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`Erro Imagen 3 HTTP ${response.status}: ${errText}`); + } + + const resJson = await response.json(); + const base64 = resJson.generatedImages?.[0]?.image?.imageBytes; + return base64; + }; + + const tentarGerarMultimodal = async (model, promptText) => { + const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; const payload = { contents: [ { - parts: [ - { - text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${prompt}` - } - ] + parts: [{ text: `Gere uma ilustração educativa, estilo flat vector minimalista, limpa e com fundo neutro sobre o tema: ${promptText}` }] } ], generationConfig: { @@ -1718,11 +1740,10 @@ app.post('/api/ai/generate-image', async (req, res) => { if (!response.ok) { const errText = await response.text(); - throw new Error(`Erro HTTP ${response.status}: ${errText}`); + throw new Error(`Erro Multimodal HTTP ${response.status}: ${errText}`); } const resJson = await response.json(); - let base64 = null; if (resJson.candidates?.[0]?.content?.parts) { for (const part of resJson.candidates[0].content.parts) { @@ -1736,50 +1757,63 @@ app.post('/api/ai/generate-image', async (req, res) => { }; let base64ImageBytes = null; + let ultimoErro = ''; let modelUsed = geminiImageModel; - try { - console.log(`[Gemini Imagen] Tentando gerar com o modelo selecionado: ${modelUsed}`); - base64ImageBytes = await tentarGerarGemini(modelUsed); - } catch (err) { - console.warn(`[Gemini Imagen] Falha no modelo principal ${modelUsed}:`, err.message); + // Tentativa 1: Modelo selecionado + if (modelUsed.startsWith('imagen-')) { + try { + console.log(`[Gemini Imagen] Tentando gerar com o modelo Imagen 3: ${modelUsed}`); + base64ImageBytes = await tentarGerarImagen3(modelUsed, prompt); + } catch (err) { + ultimoErro = err.message; + console.warn(`[Gemini Imagen] Falha no Imagen 3 (${modelUsed}):`, ultimoErro); + } + } else { + try { + console.log(`[Gemini Imagen] Tentando gerar com o modelo Multimodal: ${modelUsed}`); + base64ImageBytes = await tentarGerarMultimodal(modelUsed, prompt); + } catch (err) { + ultimoErro = err.message; + console.warn(`[Gemini Imagen] Falha no Multimodal (${modelUsed}):`, ultimoErro); + } } - // Fallback 1: gemini-2.5-flash-image + // Fallback 1: imagen-3.0-generate-002 estável + if (!base64ImageBytes && modelUsed !== 'imagen-3.0-generate-002') { + try { + console.log('[Gemini Imagen] Tentando fallback para imagen-3.0-generate-002 estável...'); + base64ImageBytes = await tentarGerarImagen3('imagen-3.0-generate-002', prompt); + } catch (err) { + ultimoErro = err.message; + console.warn('[Gemini Imagen] Falha no fallback imagen-3.0-generate-002:', ultimoErro); + } + } + + // Fallback 2: gemini-2.5-flash-image if (!base64ImageBytes && modelUsed !== 'gemini-2.5-flash-image') { try { - modelUsed = 'gemini-2.5-flash-image'; - console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); - base64ImageBytes = await tentarGerarGemini(modelUsed); + console.log('[Gemini Imagen] Tentando fallback para gemini-2.5-flash-image...'); + base64ImageBytes = await tentarGerarMultimodal('gemini-2.5-flash-image', prompt); } catch (err) { - console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); + ultimoErro = err.message; + console.warn('[Gemini Imagen] Falha no fallback gemini-2.5-flash-image:', ultimoErro); } } - // Fallback 2: gemini-3.1-flash-image + // Fallback 3: gemini-3.1-flash-image if (!base64ImageBytes && modelUsed !== 'gemini-3.1-flash-image') { try { - modelUsed = 'gemini-3.1-flash-image'; - console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); - base64ImageBytes = await tentarGerarGemini(modelUsed); + console.log('[Gemini Imagen] Tentando fallback para gemini-3.1-flash-image...'); + base64ImageBytes = await tentarGerarMultimodal('gemini-3.1-flash-image', prompt); } catch (err) { - console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); - } - } - - // Fallback 3: gemini-2.0-flash-exp (experimental) - if (!base64ImageBytes && modelUsed !== 'gemini-2.0-flash-exp') { - try { - modelUsed = 'gemini-2.0-flash-exp'; - console.log(`[Gemini Imagen] Tentando fallback para: ${modelUsed}`); - base64ImageBytes = await tentarGerarGemini(modelUsed); - } catch (err) { - console.warn(`[Gemini Imagen] Falha no fallback ${modelUsed}:`, err.message); + ultimoErro = err.message; + console.warn('[Gemini Imagen] Falha no fallback gemini-3.1-flash-image:', ultimoErro); } } if (!base64ImageBytes) { - throw new Error('Não foi possível gerar a imagem em nenhum dos modelos Gemini disponíveis. Verifique sua chave de API ou se a cota foi atingida.'); + throw new Error(`Não foi possível gerar a imagem em nenhum dos modelos Gemini/Imagen disponíveis. Detalhes do último erro: ${ultimoErro}`); } const imageBuffer = Buffer.from(base64ImageBytes, 'base64');