From 9f0974aaa927dc351e447581beb2f7822bb9fd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 29 Aug 2026 02:45:44 +0300 Subject: [PATCH] Route recipe responses through Gemini proxy --- HISTORY/2026-08-28-obdai-receipt.md | 33 +++++++++++++++++++++++++++++ recipe_service/.env.example | 3 +-- recipe_service/README.md | 8 ++++--- recipe_service/app.py | 28 ++++++++---------------- recipe_service/nginx-recipe.conf | 13 ++++++++++++ 5 files changed, 61 insertions(+), 24 deletions(-) diff --git a/HISTORY/2026-08-28-obdai-receipt.md b/HISTORY/2026-08-28-obdai-receipt.md index 47dcc72..e3e21bb 100644 --- a/HISTORY/2026-08-28-obdai-receipt.md +++ b/HISTORY/2026-08-28-obdai-receipt.md @@ -496,3 +496,36 @@ HISTORY. Добавлен `recipe_service/.gitignore` для `.env`, `*.env`, Авторизованный запрос дошёл до Gemini, но получил HTTP 502: `API key not valid`. Следовательно, Bearer-защита работает, а `GEMINI_API_KEY` в серверном env-файле недействителен и требует замены. + +## Вопрос Sonnet о доступности немецкого proxy + +Sonnet запросил подтверждение доступности `95.179.252.111:8768` с ВМ 213. +Ответ: в имеющейся проверке такой прямой запрос не выполнялся, поэтому факт +доступности этого адреса не подтверждён. Подтверждено только, что ранее proxy +работал на немецкой ВМ на `127.0.0.1:8768`, то есть bind-адрес сам по себе не +доказывает доступность с ВМ 213. Для выбора транспорта требуется отдельная +проверка соединения с таймаутом; до неё нельзя утверждать, что прямой маршрут +или SSH-туннель уже работает. + +Проверка выполнена с ВМ 213 командой `curl --max-time 5` к +`http://95.179.252.111:8768/health`: соединение отклонено, получен +`http_code=000` и ошибка `Failed to connect`. Прямой маршрут между ВМ не +доступен; для продолжения требуется SSH-туннель или изменение bind/firewall +немецкого proxy. + +## Проверка плана Sonnet и транспорт + +План Sonnet проверен по фактическим конфигурациям. На ВМ 213 в +`/etc/recipe/recipe.env` уже отсутствует `GEMINI_API_KEY`, присутствуют +`RECIPE_API_TOKEN` и `GEMINI_PROXY_URL`; `recipe.service` активен. В +задеплоенном `/opt/recipe/app.py` обнаружен дефект: ответ proxy разбирался как +Gemini-native (`candidates`/`usageMetadata`), хотя proxy возвращает поля +`text`/`usage`. В workspace исправлен разбор этих полей; локальные `py_compile` +и `git diff --check` прошли. + +Дополнительная проверка SSH-туннеля показала, что на ВМ 213 нет ключа, дающего +вход на немецкую ВМ: российский ключ получил `Permission denied`, а `vultr.ppk` +не распознан как пригодный OpenSSH-ключ. Создание туннеля без нового +разрешённого ключа невозможно. Nginx на ВМ 213 уже содержит отдельные location +для `/recipe` и `/recipe/`; их изменение без новой подтверждённой проблемы не +выполнялось. diff --git a/recipe_service/.env.example b/recipe_service/.env.example index 8048363..a3518ab 100644 --- a/recipe_service/.env.example +++ b/recipe_service/.env.example @@ -1,4 +1,3 @@ -GEMINI_API_KEY=replace-with-secret -GEMINI_MODEL=gemini-3.6-flash +GEMINI_PROXY_URL=http://95.179.252.111:8768/gemini GEMINI_GENERATION_CONFIG={"temperature":0,"maxOutputTokens":300,"thinkingConfig":{"thinkingLevel":"minimal"}} RECIPE_API_TOKEN=replace-with-random-token \ No newline at end of file diff --git a/recipe_service/README.md b/recipe_service/README.md index 95c1f24..e9c9cb4 100644 --- a/recipe_service/README.md +++ b/recipe_service/README.md @@ -6,9 +6,11 @@ `prompt`. Требуется заголовок `Authorization: Bearer `. Изображение: JPEG/PNG/WEBP, не более 10 MB. -Ключ Gemini и JSON-строка `GEMINI_GENERATION_CONFIG` находятся только в -серверном EnvironmentFile. Сервис передаёт изображение, prompt и настройки в -Gemini и возвращает `text` и `usage`; разбор рецепта выполняется позднее. +URL немецкого Gemini proxy и JSON-строка `GEMINI_GENERATION_CONFIG` находятся +только в серверном EnvironmentFile. Сервис передаёт изображение, prompt и +настройки в proxy и возвращает `text` и `usage`; разбор рецепта выполняется +позднее. API-ключ Gemini остаётся только на немецкой ВМ и в recipe service не +передаётся. Токен `RECIPE_API_TOKEN` также хранится только в EnvironmentFile и не передаётся в Gemini. diff --git a/recipe_service/app.py b/recipe_service/app.py index d4effdd..15027bf 100644 --- a/recipe_service/app.py +++ b/recipe_service/app.py @@ -1,4 +1,3 @@ -import base64 import json import os @@ -9,20 +8,17 @@ from flask import Flask, jsonify, request app = Flask(__name__) MAX_IMAGE_BYTES = 10 * 1024 * 1024 ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"} -GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models" +PROXY_URL = "http://95.179.252.111:8768/gemini" -def settings() -> tuple[str, dict]: - key = os.environ.get("GEMINI_API_KEY") - if not key: - raise RuntimeError("GEMINI_API_KEY is not configured") +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 key, config + return config def authorized() -> bool: @@ -50,17 +46,11 @@ def recipe(): if len(image_data) > MAX_IMAGE_BYTES: return jsonify(error="image is too large"), 413 try: - key, config = settings() + config = settings() response = requests.post( - f"{GEMINI_URL}/{os.environ.get('GEMINI_MODEL', 'gemini-3.6-flash')}:generateContent", - params={"key": key}, - json={ - "contents": [{"parts": [{"text": prompt}, {"inline_data": { - "mime_type": image.mimetype, - "data": base64.b64encode(image_data).decode("ascii"), - }}]}], - "generationConfig": config, - }, + 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: @@ -73,6 +63,6 @@ def recipe(): return jsonify(error=detail), 502 data = response.json() return jsonify( - text=data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text"), - usage=data.get("usageMetadata", {}), + text=data.get("text"), + usage=data.get("usage", {}), ) \ No newline at end of file diff --git a/recipe_service/nginx-recipe.conf b/recipe_service/nginx-recipe.conf index f8294f5..432c7b2 100644 --- a/recipe_service/nginx-recipe.conf +++ b/recipe_service/nginx-recipe.conf @@ -1,3 +1,16 @@ +location = /recipe { + proxy_pass http://127.0.0.1:8770/recipe; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 10m; + proxy_connect_timeout 10s; + proxy_send_timeout 200s; + proxy_read_timeout 200s; +} + location /recipe/ { proxy_pass http://127.0.0.1:8770/; proxy_http_version 1.1;