140 lines
6.2 KiB
Python
140 lines
6.2 KiB
Python
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 |