Not a Plant Fehlermeldung

This commit is contained in:
2026-04-17 13:12:36 +02:00
parent 383d8484a6
commit 77b98a3ebf
12 changed files with 831 additions and 195 deletions

View File

@@ -1,4 +1,5 @@
import { CareInfo, IdentificationResult, Language } from '../../types';
import { BackendApiError } from './contracts';
type OpenAiScanMode = 'primary' | 'review';
@@ -164,7 +165,8 @@ const buildPrompt = (language: Language, mode: OpenAiScanMode): string => {
return [
`${reviewInstruction}`,
`Return strict JSON only in this shape:`,
`If the image does not clearly show a plant (e.g. it shows a person, animal, object, or has no identifiable plant), return {"notAPlant":true} and nothing else.`,
`Otherwise return strict JSON only in this shape:`,
`{"name":"...","botanicalName":"...","confidence":0.0,"description":"...","careInfo":{"waterIntervalDays":7,"light":"...","temp":"..."}}`,
`Rules:`,
nameLanguageInstruction,
@@ -389,10 +391,37 @@ const extractMessageContent = (payload: unknown): string => {
return '';
};
const isReasoningModel = (model: string): boolean => {
const normalized = model.toLowerCase();
return normalized.startsWith('gpt-5') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4');
};
const buildRequestBody = (
model: string,
messages: Array<Record<string, unknown>>,
maxCompletionTokens: number,
): Record<string, unknown> => {
const body: Record<string, unknown> = {
model,
response_format: { type: 'json_object' },
messages,
};
if (isReasoningModel(model)) {
body.reasoning_effort = 'minimal';
body.max_completion_tokens = maxCompletionTokens;
} else {
body.max_tokens = maxCompletionTokens;
}
return body;
};
const postChatCompletion = async (
modelChain: string[],
imageUri: string,
messages: Array<Record<string, unknown>>,
maxCompletionTokens = 600,
): Promise<{ payload: Record<string, unknown> | null; modelUsed: string | null; attemptedModels: string[] }> => {
const attemptedModels: string[] = [];
@@ -408,11 +437,7 @@ const postChatCompletion = async (
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model,
response_format: { type: 'json_object' },
messages,
}),
body: JSON.stringify(buildRequestBody(model, messages, maxCompletionTokens)),
signal: controller.signal,
});
@@ -470,10 +495,11 @@ export const openAiScanService = {
role: 'user',
content: [
{ type: 'text', text: buildPrompt(language, mode) },
{ type: 'image_url', image_url: { url: imageUri } },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
600,
);
if (!completion.payload) return null;
@@ -498,6 +524,10 @@ export const openAiScanService = {
return null;
}
if (parsed.notAPlant === true) {
throw new BackendApiError('NOT_A_PLANT', 'Image does not contain a plant.', 422);
}
const normalized = normalizeResult(parsed, language);
if (!normalized) {
console.warn('OpenAI plant scan JSON did not match required schema.', {
@@ -533,10 +563,11 @@ export const openAiScanService = {
role: 'user',
content: [
{ type: 'text', text: buildHealthPrompt(language, plantContext) },
{ type: 'image_url', image_url: { url: imageUri } },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
800,
);
if (!completion.payload) return buildFallbackHealthAnalysis(language, plantContext);