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

@@ -31,35 +31,35 @@ export interface EntitlementState {
renewsAt: string | null;
}
export interface BillingSummary {
entitlement: EntitlementState;
credits: CreditState;
availableProducts: PurchaseProductId[];
}
export interface RevenueCatEntitlementInfo {
productIdentifier?: string;
expirationDate?: string | null;
expiresDate?: string | null;
}
export interface RevenueCatNonSubscriptionTransaction {
productIdentifier?: string;
transactionIdentifier?: string;
transactionId?: string;
purchaseDate?: string | null;
}
export interface RevenueCatCustomerInfo {
appUserId?: string | null;
originalAppUserId?: string | null;
entitlements: {
active: Record<string, RevenueCatEntitlementInfo>;
};
nonSubscriptions?: Record<string, RevenueCatNonSubscriptionTransaction[]>;
allPurchasedProductIdentifiers?: string[];
latestExpirationDate?: string | null;
}
export interface BillingSummary {
entitlement: EntitlementState;
credits: CreditState;
availableProducts: PurchaseProductId[];
}
export interface RevenueCatEntitlementInfo {
productIdentifier?: string;
expirationDate?: string | null;
expiresDate?: string | null;
}
export interface RevenueCatNonSubscriptionTransaction {
productIdentifier?: string;
transactionIdentifier?: string;
transactionId?: string;
purchaseDate?: string | null;
}
export interface RevenueCatCustomerInfo {
appUserId?: string | null;
originalAppUserId?: string | null;
entitlements: {
active: Record<string, RevenueCatEntitlementInfo>;
};
nonSubscriptions?: Record<string, RevenueCatNonSubscriptionTransaction[]>;
allPurchasedProductIdentifiers?: string[];
latestExpirationDate?: string | null;
}
export interface ScanPlantRequest {
userId: string;
@@ -112,16 +112,16 @@ export interface HealthCheckResponse {
billing: BillingSummary;
}
export interface ServiceHealthResponse {
ok: boolean;
uptimeSec: number;
timestamp: string;
openAiConfigured: boolean;
dbReady?: boolean;
dbPath?: string;
scanModel?: string;
healthModel?: string;
}
export interface ServiceHealthResponse {
ok: boolean;
uptimeSec: number;
timestamp: string;
openAiConfigured: boolean;
dbReady?: boolean;
dbPath?: string;
scanModel?: string;
healthModel?: string;
}
export interface SimulatePurchaseRequest {
userId: string;
@@ -143,21 +143,21 @@ export interface SimulateWebhookRequest {
};
}
export interface SimulateWebhookResponse {
event: SimulatedWebhookEvent;
billing: BillingSummary;
}
export type RevenueCatSyncSource =
| 'app_init'
| 'subscription_purchase'
| 'topup_purchase'
| 'restore';
export interface SyncRevenueCatStateResponse {
billing: BillingSummary;
syncedAt: string;
}
export interface SimulateWebhookResponse {
event: SimulatedWebhookEvent;
billing: BillingSummary;
}
export type RevenueCatSyncSource =
| 'app_init'
| 'subscription_purchase'
| 'topup_purchase'
| 'restore';
export interface SyncRevenueCatStateResponse {
billing: BillingSummary;
syncedAt: string;
}
export type BackendErrorCode =
| 'INSUFFICIENT_CREDITS'
@@ -165,7 +165,8 @@ export type BackendErrorCode =
| 'TIMEOUT'
| 'NETWORK_ERROR'
| 'PROVIDER_ERROR'
| 'BAD_REQUEST';
| 'BAD_REQUEST'
| 'NOT_A_PLANT';
export class BackendApiError extends Error {
public readonly code: BackendErrorCode;

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