Route recipe responses through Gemini proxy
This commit is contained in:
@@ -496,3 +496,36 @@ HISTORY. Добавлен `recipe_service/.gitignore` для `.env`, `*.env`,
|
|||||||
Авторизованный запрос дошёл до Gemini, но получил HTTP 502: `API key not valid`.
|
Авторизованный запрос дошёл до Gemini, но получил HTTP 502: `API key not valid`.
|
||||||
Следовательно, Bearer-защита работает, а `GEMINI_API_KEY` в серверном env-файле
|
Следовательно, 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/`; их изменение без новой подтверждённой проблемы не
|
||||||
|
выполнялось.
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
GEMINI_API_KEY=replace-with-secret
|
GEMINI_PROXY_URL=http://95.179.252.111:8768/gemini
|
||||||
GEMINI_MODEL=gemini-3.6-flash
|
|
||||||
GEMINI_GENERATION_CONFIG={"temperature":0,"maxOutputTokens":300,"thinkingConfig":{"thinkingLevel":"minimal"}}
|
GEMINI_GENERATION_CONFIG={"temperature":0,"maxOutputTokens":300,"thinkingConfig":{"thinkingLevel":"minimal"}}
|
||||||
RECIPE_API_TOKEN=replace-with-random-token
|
RECIPE_API_TOKEN=replace-with-random-token
|
||||||
@@ -6,9 +6,11 @@
|
|||||||
`prompt`. Требуется заголовок `Authorization: Bearer <RECIPE_API_TOKEN>`.
|
`prompt`. Требуется заголовок `Authorization: Bearer <RECIPE_API_TOKEN>`.
|
||||||
Изображение: JPEG/PNG/WEBP, не более 10 MB.
|
Изображение: JPEG/PNG/WEBP, не более 10 MB.
|
||||||
|
|
||||||
Ключ Gemini и JSON-строка `GEMINI_GENERATION_CONFIG` находятся только в
|
URL немецкого Gemini proxy и JSON-строка `GEMINI_GENERATION_CONFIG` находятся
|
||||||
серверном EnvironmentFile. Сервис передаёт изображение, prompt и настройки в
|
только в серверном EnvironmentFile. Сервис передаёт изображение, prompt и
|
||||||
Gemini и возвращает `text` и `usage`; разбор рецепта выполняется позднее.
|
настройки в proxy и возвращает `text` и `usage`; разбор рецепта выполняется
|
||||||
|
позднее. API-ключ Gemini остаётся только на немецкой ВМ и в recipe service не
|
||||||
|
передаётся.
|
||||||
Токен `RECIPE_API_TOKEN` также хранится только в EnvironmentFile и не
|
Токен `RECIPE_API_TOKEN` также хранится только в EnvironmentFile и не
|
||||||
передаётся в Gemini.
|
передаётся в Gemini.
|
||||||
|
|
||||||
|
|||||||
+9
-19
@@ -1,4 +1,3 @@
|
|||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -9,20 +8,17 @@ from flask import Flask, jsonify, request
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
MAX_IMAGE_BYTES = 10 * 1024 * 1024
|
||||||
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
|
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]:
|
def settings() -> dict:
|
||||||
key = os.environ.get("GEMINI_API_KEY")
|
|
||||||
if not key:
|
|
||||||
raise RuntimeError("GEMINI_API_KEY is not configured")
|
|
||||||
try:
|
try:
|
||||||
config = json.loads(os.environ.get("GEMINI_GENERATION_CONFIG", "{}"))
|
config = json.loads(os.environ.get("GEMINI_GENERATION_CONFIG", "{}"))
|
||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
raise RuntimeError("GEMINI_GENERATION_CONFIG is invalid") from exc
|
raise RuntimeError("GEMINI_GENERATION_CONFIG is invalid") from exc
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise RuntimeError("GEMINI_GENERATION_CONFIG must be an object")
|
raise RuntimeError("GEMINI_GENERATION_CONFIG must be an object")
|
||||||
return key, config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def authorized() -> bool:
|
def authorized() -> bool:
|
||||||
@@ -50,17 +46,11 @@ def recipe():
|
|||||||
if len(image_data) > MAX_IMAGE_BYTES:
|
if len(image_data) > MAX_IMAGE_BYTES:
|
||||||
return jsonify(error="image is too large"), 413
|
return jsonify(error="image is too large"), 413
|
||||||
try:
|
try:
|
||||||
key, config = settings()
|
config = settings()
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{GEMINI_URL}/{os.environ.get('GEMINI_MODEL', 'gemini-3.6-flash')}:generateContent",
|
os.environ.get("GEMINI_PROXY_URL", PROXY_URL),
|
||||||
params={"key": key},
|
files={"image": (image.filename or "image", image_data, image.mimetype)},
|
||||||
json={
|
data={"prompt": prompt, "generation_config": json.dumps(config)},
|
||||||
"contents": [{"parts": [{"text": prompt}, {"inline_data": {
|
|
||||||
"mime_type": image.mimetype,
|
|
||||||
"data": base64.b64encode(image_data).decode("ascii"),
|
|
||||||
}}]}],
|
|
||||||
"generationConfig": config,
|
|
||||||
},
|
|
||||||
timeout=180,
|
timeout=180,
|
||||||
)
|
)
|
||||||
except (requests.RequestException, RuntimeError) as exc:
|
except (requests.RequestException, RuntimeError) as exc:
|
||||||
@@ -73,6 +63,6 @@ def recipe():
|
|||||||
return jsonify(error=detail), 502
|
return jsonify(error=detail), 502
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return jsonify(
|
return jsonify(
|
||||||
text=data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text"),
|
text=data.get("text"),
|
||||||
usage=data.get("usageMetadata", {}),
|
usage=data.get("usage", {}),
|
||||||
)
|
)
|
||||||
@@ -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/ {
|
location /recipe/ {
|
||||||
proxy_pass http://127.0.0.1:8770/;
|
proxy_pass http://127.0.0.1:8770/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
Reference in New Issue
Block a user