import json import os import requests from flask import Flask, jsonify, request try: from recipe_service.metrics import initialize, record, request_context, usage_json except ModuleNotFoundError: from metrics import initialize, record, request_context, usage_json app = Flask(__name__) MAX_IMAGE_BYTES = 10 * 1024 * 1024 ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"} PROXY_URL = "http://127.0.0.1:8768/gemini" initialize() def settings() -> dict: try: config = json.loads(os.environ.get("GEMINI_GENERATION_CONFIG", "{}")) except json.JSONDecodeError as exc: raise RuntimeError("GEMINI_GENERATION_CONFIG is invalid") from exc if not isinstance(config, dict): raise RuntimeError("GEMINI_GENERATION_CONFIG must be an object") return config def authorized() -> bool: expected = os.environ.get("RECIPE_API_TOKEN") authorization = request.headers.get("Authorization", "") return bool(expected and authorization == f"Bearer {expected}") @app.get("/health") @app.get("/receipt/health") def health(): return jsonify(status="ok") @app.post("/receipt") @app.post("/receipt/") @app.post("/recipe") @app.post("/recipe/") def recipe(): request_id, started_at, started_monotonic = request_context() image_mime = None image_bytes = None prompt_chars = None status_code = 500 response_bytes = None usage = {} error = None if not authorized(): status_code = 401 error = "unauthorized" response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars, status_code=status_code, duration_ms=int((__import__('time').monotonic() - started_monotonic) * 1000), response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code image = request.files.get("image") prompt = request.form.get("prompt") if image is None or not prompt: status_code = 400 error = "image and prompt are required" response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code if image.mimetype not in ALLOWED_TYPES: status_code = 415 error = "unsupported image type" response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image.mimetype, prompt_chars=len(prompt), status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code image_data = image.read(MAX_IMAGE_BYTES + 1) if len(image_data) > MAX_IMAGE_BYTES: status_code = 413 error = "image is too large" response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image.mimetype, image_bytes=len(image_data), prompt_chars=len(prompt), status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code image_mime = image.mimetype image_bytes = len(image_data) prompt_chars = len(prompt) try: config = settings() response = requests.post( os.environ.get("GEMINI_PROXY_URL", PROXY_URL), files={"image": (image.filename or "image", image_data, image.mimetype)}, data={"prompt": prompt, "generation_config": json.dumps(config)}, timeout=180, ) except (requests.RequestException, RuntimeError) as exc: error = str(exc) if isinstance(exc, RuntimeError) else "Gemini unavailable" status_code = 503 response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars, status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code if response.status_code != 200: try: detail = response.json().get("error", {}).get("message", "Gemini request failed") except ValueError: detail = "Gemini request failed" error = detail status_code = 502 response = jsonify(error=error) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars, status_code=status_code, duration_ms=0, response_bytes=len(response.get_data()), usage_json=usage_json(usage), error=error) return response, status_code data = response.json() usage = data.get("usage", {}) result = jsonify(text=data.get("text"), usage=usage) record(request_id=request_id, started_at=started_at, client_ip=request.remote_addr, user_agent=request.user_agent.string, method=request.method, path=request.path, image_mime=image_mime, image_bytes=image_bytes, prompt_chars=prompt_chars, status_code=200, duration_ms=0, response_bytes=len(result.get_data()), usage_json=usage_json(usage), error=None) return result