Not a Plant Fehlermeldung
This commit is contained in:
@@ -27,13 +27,18 @@ const OPENAI_SCAN_MODEL_CHAIN = parseModelChain(OPENAI_SCAN_MODEL, OPENAI_SCAN_F
|
||||
const OPENAI_SCAN_MODEL_CHAIN_PRO = parseModelChain(OPENAI_SCAN_MODEL_PRO, OPENAI_SCAN_FALLBACK_MODELS_PRO);
|
||||
const OPENAI_HEALTH_MODEL_CHAIN = parseModelChain(OPENAI_HEALTH_MODEL, OPENAI_HEALTH_FALLBACK_MODELS);
|
||||
|
||||
const getScanModelChain = (plan) => {
|
||||
return plan === 'pro' ? OPENAI_SCAN_MODEL_CHAIN_PRO : OPENAI_SCAN_MODEL_CHAIN;
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
const getScanModelChain = (plan) => {
|
||||
return plan === 'pro' ? OPENAI_SCAN_MODEL_CHAIN_PRO : OPENAI_SCAN_MODEL_CHAIN;
|
||||
};
|
||||
|
||||
const isReasoningModel = (model) => {
|
||||
const normalized = String(model || '').toLowerCase();
|
||||
return normalized.startsWith('gpt-5') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4');
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
|
||||
const toErrorMessage = (error) => {
|
||||
if (error instanceof Error) return error.message;
|
||||
@@ -215,11 +220,12 @@ const buildIdentifyPrompt = (language, mode) => {
|
||||
? '- "name" must be an English common name only. Never return a German or other non-English common name. If no reliable English common name is known, use "botanicalName" as "name" instead of inventing or translating.'
|
||||
: `- "name" must be strictly written in ${getLanguageLabel(language)}. If a reliable common name in that language is not known, use "botanicalName" as "name" instead of inventing a localized name.`;
|
||||
|
||||
return [
|
||||
`${reviewInstruction}`,
|
||||
'Return strict JSON only in this shape:',
|
||||
'{"name":"...","botanicalName":"...","confidence":0.0,"description":"...","careInfo":{"waterIntervalDays":7,"light":"...","temp":"..."}}',
|
||||
'Rules:',
|
||||
return [
|
||||
`${reviewInstruction}`,
|
||||
'If the image does not clearly show a plant (for example a person, animal, room, furniture, or no identifiable foliage), return {"notAPlant":true} and nothing else.',
|
||||
'Return strict JSON only in this shape:',
|
||||
'{"name":"...","botanicalName":"...","confidence":0.0,"description":"...","careInfo":{"waterIntervalDays":7,"light":"...","temp":"..."}}',
|
||||
'Rules:',
|
||||
nameLanguageInstruction,
|
||||
`- "description" and "careInfo.light" must be written in ${getLanguageLabel(language)}.`,
|
||||
`- "careInfo.light": short light requirement in ${getLanguageLabel(language)} (e.g. "bright indirect light", "full sun", "partial shade"). Must always be a real value, never "Unknown".`,
|
||||
@@ -279,14 +285,33 @@ const extractMessageContent = (payload) => {
|
||||
.join('')
|
||||
.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const postChatCompletion = async ({ modelChain, messages, imageUri, temperature }) => {
|
||||
if (!OPENAI_API_KEY) return null;
|
||||
if (typeof fetch !== 'function') {
|
||||
throw new Error('Global fetch is not available in this Node runtime.');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const buildRequestBody = ({ model, messages, temperature, maxCompletionTokens }) => {
|
||||
const body = {
|
||||
model,
|
||||
response_format: { type: 'json_object' },
|
||||
messages,
|
||||
};
|
||||
|
||||
if (typeof temperature === 'number') body.temperature = temperature;
|
||||
|
||||
if (isReasoningModel(model)) {
|
||||
body.reasoning_effort = 'minimal';
|
||||
body.max_completion_tokens = maxCompletionTokens;
|
||||
} else {
|
||||
body.max_tokens = maxCompletionTokens;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const postChatCompletion = async ({ modelChain, messages, imageUri, temperature, maxCompletionTokens = 600 }) => {
|
||||
if (!OPENAI_API_KEY) return null;
|
||||
if (typeof fetch !== 'function') {
|
||||
throw new Error('Global fetch is not available in this Node runtime.');
|
||||
}
|
||||
|
||||
const attemptedModels = [];
|
||||
|
||||
@@ -295,18 +320,13 @@ const postChatCompletion = async ({ modelChain, messages, imageUri, temperature
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OPENAI_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const body = {
|
||||
model,
|
||||
response_format: { type: 'json_object' },
|
||||
messages,
|
||||
};
|
||||
if (typeof temperature === 'number') body.temperature = temperature;
|
||||
|
||||
const response = await fetch(OPENAI_CHAT_COMPLETIONS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
try {
|
||||
const body = buildRequestBody({ model, messages, temperature, maxCompletionTokens });
|
||||
|
||||
const response = await fetch(OPENAI_CHAT_COMPLETIONS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
},
|
||||
@@ -362,15 +382,16 @@ const identifyPlant = async ({ imageUri, language, mode = 'primary', plan = 'fre
|
||||
role: 'system',
|
||||
content: 'You are a plant identification assistant. Return strict JSON only.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: buildIdentifyPrompt(language, mode) },
|
||||
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: buildIdentifyPrompt(language, mode) },
|
||||
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
maxCompletionTokens: 600,
|
||||
});
|
||||
|
||||
if (!completion?.payload) {
|
||||
return {
|
||||
@@ -391,19 +412,25 @@ const identifyPlant = async ({ imageUri, language, mode = 'primary', plan = 'fre
|
||||
}
|
||||
|
||||
const parsed = parseContentToJson(content);
|
||||
if (!parsed) {
|
||||
console.warn('OpenAI identify returned non-JSON content.', {
|
||||
model: completion.modelUsed || modelChain[0],
|
||||
mode,
|
||||
preview: content.slice(0, 220),
|
||||
});
|
||||
return { result: null, modelUsed: completion.modelUsed, attemptedModels: completion.attemptedModels };
|
||||
}
|
||||
|
||||
const normalized = normalizeIdentifyResult(parsed, language);
|
||||
if (!normalized) {
|
||||
console.warn('OpenAI identify JSON did not match schema.', {
|
||||
model: completion.modelUsed || modelChain[0],
|
||||
if (!parsed) {
|
||||
console.warn('OpenAI identify returned non-JSON content.', {
|
||||
model: completion.modelUsed || modelChain[0],
|
||||
mode,
|
||||
preview: content.slice(0, 220),
|
||||
});
|
||||
return { result: null, modelUsed: completion.modelUsed, attemptedModels: completion.attemptedModels };
|
||||
}
|
||||
|
||||
if (parsed.notAPlant === true) {
|
||||
const error = new Error('Image does not contain a plant.');
|
||||
error.code = 'NOT_A_PLANT';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalized = normalizeIdentifyResult(parsed, language);
|
||||
if (!normalized) {
|
||||
console.warn('OpenAI identify JSON did not match schema.', {
|
||||
model: completion.modelUsed || modelChain[0],
|
||||
mode,
|
||||
keys: Object.keys(parsed),
|
||||
});
|
||||
@@ -422,15 +449,16 @@ const analyzePlantHealth = async ({ imageUri, language, plantContext }) => {
|
||||
role: 'system',
|
||||
content: 'You are a plant health diagnosis assistant. Return strict JSON only.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: buildHealthPrompt(language, plantContext) },
|
||||
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: buildHealthPrompt(language, plantContext) },
|
||||
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
maxCompletionTokens: 800,
|
||||
});
|
||||
|
||||
if (!completion?.payload) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user