Not a Plant Fehlermeldung
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user