fix: resolve definitivamente erro 400 de Schema no Gemini e erro de geracao de imagem no AI Studio usando generateImages
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m8s Details

This commit is contained in:
Sidney 2026-07-17 22:57:48 -03:00
parent 8d1b246f84
commit 961cbfdbdd
1 changed files with 158 additions and 124 deletions

View File

@ -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');