Nano Banana 2.1 через Ofox: как создать, сохранить и изменить изображение
Как вызвать Nano Banana 2.1 через Ofox, сохранить изображение и продолжить редактирование: Python-код, реальные ответы API и ограничения.
Обновлено 13 октября 2026 года. Ofox — и издатель этого руководства, и API-шлюз, через который отправлялись описанные здесь запросы. При подготовке статьи использовался ИИ; сведения сверили с реальными результатами API и инструментов, а перед публикацией статья прошла независимую проверку с помощью ИИ. Все шесть описанных ниже запросов к изображениям были фактически отправлены через Ofox по нативному протоколу Gemini. Пример программы приведён, чтобы вы могли выполнить собственные запросы, а не как утверждение, что проверены все возможные маршруты и настройки.
Идентификатор модели Nano Banana 2.1 в Ofox — google/gemini-nano-banana-2.1. Для сравнения используется более старая модель google/gemini-3.1-flash-image, которую часто называют Nano Banana 2. При переносе запроса не подменяйте новый идентификатор старым. На общедоступной странице модели Ofox для версии 2.1 указаны протоколы Gemini и OpenAI, а в качестве провайдера — Vertex. Рабочий запрос из этого руководства использует нативный маршрут Gemini generateContent, а не маршрут с совместимостью с OpenAI.
Вы создадите постер, сохраните полученные байты изображения на диск и, если захотите, передадите полный ответ модели в двух следующих шагах редактирования. Отдельный вариант с загрузкой показывает, как передать существующее локальное изображение. Более подробное руководство по редактированию и выбору композиции есть в статье как редактировать постер.
Что понадобится перед первым запросом
Вам понадобятся API-ключ Ofox с доступом к модели, достаточный баланс на счёте, Python 3 и пакет requests. Установите зависимость и задайте ключ в оболочке:
python -m pip install requests
export OFOX_API_KEY="YOUR_OFOX_API_KEY"
Не храните ключ в исходных файлах, снимках экрана и общих журналах. В проверенных запросах использовалась такая конечная точка API:
POST https://api.ofox.io/gemini/v1beta/models/google/gemini-nano-banana-2.1:generateContent
x-goog-api-key: YOUR_OFOX_API_KEY
В запросе ниже указаны обе модальности ответа — текст и изображение, квадратное соотношение сторон и размер изображения 1K. В документации Google по генерации изображений описаны варианты 1K, 2K и 4K; выполнение через шлюз в рамках этой статьи проверено только для 1K. Не считайте документацию подтверждением того, что каждый размер проверен через ваш маршрут. 512 не входит в документированные варианты размера для этой модели.
Полная программа на Python
Сохраните программу под именем nano21.py. По умолчанию она отправляет один запрос на генерацию. Параметр --chain сначала отправляет этот запрос, а затем — два точных запроса на редактирование, использованных в нашем тесте. Параметр --upload PATH вместо этого начинает новый запрос с указанным локальным изображением и запросом на редактирование заголовка; чтобы этот запрос имел смысл, используйте исходный постер. Объединить эти два параметра нельзя.
import argparse
import base64
import binascii
import json
import mimetypes
import os
from pathlib import Path
import sys
import requests
URL = (
"https://api.ofox.io/gemini/v1beta/models/"
"google/gemini-nano-banana-2.1:generateContent"
)
PROMPTS = [
"Create a square minimalist event poster on warm cream paper. Large exact headline: ORCHARD MARKET. Smaller exact line: SATURDAY 10 AM. Draw three red apples in a blue ceramic bowl below the text. No other text, no logos, no border.",
"Change only the headline from ORCHARD MARKET to ORCHARD FAIR. Keep SATURDAY 10 AM exactly unchanged. Preserve the three red apples, blue bowl, cream background, composition and all other pixels as closely as possible.",
"Now change only the bowl from blue to dark green. Preserve ORCHARD FAIR, SATURDAY 10 AM, the three red apples and the layout.",
]
EXTENSIONS = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/webp": ".webp",
}
def send(contents, api_key):
body = {
"contents": contents,
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": "1:1",
"imageSize": "1K",
},
},
}
try:
# Deliberately one attempt: image requests are not automatically retried.
response = requests.post(
URL,
headers={"x-goog-api-key": api_key},
json=body,
timeout=(15, 180), # connection timeout, then read timeout
)
except requests.RequestException as exc:
raise RuntimeError(f"Request failed before a usable response: {exc}") from exc
try:
data = response.json()
except ValueError as exc:
raise RuntimeError(
f"HTTP {response.status_code}; non-JSON response: "
f"{response.text[:1000]}"
) from exc
if not response.ok:
error = data.get("error", data) if isinstance(data, dict) else data
raise RuntimeError(
f"HTTP {response.status_code}; API error: "
f"{json.dumps(error, ensure_ascii=False)[:2000]}"
)
if not isinstance(data, dict):
raise RuntimeError("Successful HTTP response was not a JSON object.")
return data
def save_response(data, output_dir, turn_number):
candidates = data.get("candidates") or []
if not candidates or not isinstance(candidates[0], dict):
raise RuntimeError(f"No candidate returned: {json.dumps(data)[:2000]}")
candidate = candidates[0]
finish_reason = candidate.get("finishReason")
if finish_reason not in (None, "STOP"):
raise RuntimeError(
f"Generation did not finish normally ({finish_reason}): "
f"{json.dumps(candidate, ensure_ascii=False)[:2000]}"
)
# Keep this original object intact for the next turn. In particular, do not
# rebuild its parts from printed text and saved image files.
content = candidate.get("content")
if not isinstance(content, dict):
raise RuntimeError("First candidate has no content object.")
output_dir.mkdir(parents=True, exist_ok=True)
image_count = 0
for part in content.get("parts", []):
if "text" in part and part["text"]:
print(f"text: {part['text']}")
inline = part.get("inlineData")
if not inline:
continue
mime = inline.get("mimeType", "").split(";", 1)[0].lower()
if not mime.startswith("image/"):
continue
encoded = inline.get("data")
if not isinstance(encoded, str):
raise RuntimeError("Image part has no base64 string.")
try:
image_bytes = base64.b64decode(encoded, validate=True)
except (binascii.Error, ValueError) as exc:
raise RuntimeError("Image part contains invalid base64.") from exc
if not image_bytes:
raise RuntimeError("Image part decoded to zero bytes.")
image_count += 1
extension = EXTENSIONS.get(mime, ".bin")
path = output_dir / f"turn-{turn_number}-image-{image_count}{extension}"
path.write_bytes(image_bytes)
print(f"saved image: {path} ({mime})")
if not image_count:
raise RuntimeError(
"Response contained no image inlineData; inspect the returned "
"candidate and any text instead of treating text as an image."
)
if "usageMetadata" in data:
print("usageMetadata:", json.dumps(data["usageMetadata"]))
return content
def uploaded_image_part(path):
mime, _ = mimetypes.guess_type(path.name)
if mime not in EXTENSIONS:
raise ValueError("Upload a .png, .jpg, .jpeg, or .webp image.")
image_bytes = path.read_bytes()
if not image_bytes:
raise ValueError("Upload file is empty.")
return {
"inlineData": {
"mimeType": mime,
"data": base64.b64encode(image_bytes).decode("ascii"),
}
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--chain", action="store_true")
parser.add_argument("--upload", type=Path)
parser.add_argument("--output-dir", type=Path, default=Path("outputs"))
args = parser.parse_args()
if args.chain and args.upload:
parser.error("--chain and --upload are separate workflows")
api_key = os.environ.get("OFOX_API_KEY")
if not api_key:
parser.error("Set OFOX_API_KEY in the environment first")
if args.upload:
if not args.upload.is_file():
parser.error(f"Image file does not exist: {args.upload}")
# A new conversation with an uploaded image, not the earlier
# generation/edit conversation.
contents = [{
"role": "user",
"parts": [
uploaded_image_part(args.upload),
{"text": PROMPTS[1]},
],
}]
result = send(contents, api_key)
save_response(result, args.output_dir, 1)
return
contents = []
selected_prompts = PROMPTS if args.chain else PROMPTS[:1]
for turn_number, prompt in enumerate(selected_prompts, start=1):
contents.append({"role": "user", "parts": [{"text": prompt}]})
result = send(contents, api_key)
model_content = save_response(result, args.output_dir, turn_number)
# Preserve the full returned content, including inline image data,
# text, and any signatures on parts, for the next request.
contents.append(model_content)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, RuntimeError) as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
Выполните один запрос командой python nano21.py. Чтобы выполнить все три шага, запустите python nano21.py --chain. Чтобы отредактировать постер с диска в новом диалоге, запустите python nano21.py --upload outputs/turn-1-image-1.png. Это примеры запуска приведённого выше файла Python, а не специальные команды CLI Ofox.
Если в ответе есть изображение, программа выведет строку вида saved image: outputs/turn-1-image-1.png (image/png), а также любой возвращённый текст и usageMetadata. Эта строка — пример формата вывода, а не стенограмма отдельного запуска скрипта. В проверенном запросе на создание постера ответ имел статус HTTP 200 и завершение STOP; в нём было изображение PNG размером 1024 × 1024, а также сведения о 54 входных токенах, 80 возвращённых токенах выходного текста, 1 120 выходных токенах изображения и 639 токенах рассуждений. В необработанном ответе не было видимой текстовой части, хотя в нём указывалось 80 токенов выходного текста. Это число — возвращённые метаданные об использовании, а не видимый текст. Ваши текст, метаданные, время выполнения и изображение могут отличаться.
Код сохраняет каждую часть inlineData с изображением из первого кандидата, а не предполагает, что изображение всегда находится в первой части. Для выбора расширения файла он использует MIME-тип из ответа; неизвестный MIME-тип изображения сохраняется с расширением .bin, а не ошибочно помечается как PNG. Ошибки HTTP по возможности разбираются как JSON, а отсутствие изображения считается ошибкой, а не успешным результатом. Автоматических повторных попыток намеренно нет: если без проверки повторить генерацию после тайм-аута, модель может создать ещё одно изображение, за которое придётся заплатить. Если запрос завершился ошибкой, прежде чем отправлять его снова, проверьте код статуса, JSON с ошибкой и состояние счёта. Руководство Google по устранению неполадок поможет понять категории ошибок на стороне Gemini, но ответ шлюза Ofox нужно оценивать отдельно.
Почему в истории редактирования нужен полный ответ
Сохранённый PNG — это результат работы. Он не заменяет историю диалога. В сценарии --chain каждый следующий запрос содержит предыдущую реплику пользователя, затем всё содержимое candidates[0].content, возвращённое моделью, и новую реплику пользователя. В этом содержимом могут быть текст, встроенные данные изображения и подписи рассуждений на уровне частей. Если собрать его заново из файла изображения и видимого текста, можно потерять данные, нужные для корректного продолжения.
Поэтому загрузка файла устроена отдельно. Параметр --upload передаёт часть inlineData с MIME-типом локального файла и его байтами в кодировке base64 вместе с инструкцией. Он не выдаёт загруженный файл за предыдущую реплику модели и не воссоздаёт подписи. Для других задач по сохранению внешнего вида товара см. процесс с маской и сохранением деталей и разбор того, когда использовать один или два референса для сцены.
Фраза «изменить только» — это строгое указание для редактирования, а не функция блокировки пикселей. В нашем тесте из трёх шагов и Nano Banana 2.1, и более старая модель добавили на первый постер запрошенный заголовок на английском языке и время. При редактировании обе изменили заголовок на ORCHARD FAIR, сохранив SATURDAY 10 AM, три красных яблока и синюю миску. На третьем шаге обе сделали миску тёмно-зелёной и сохранили указанные слова. В результатах обеих моделей не было редактируемых слоёв. Кроме того, на изображениях постер располагался на листе бумаги с внешним полем или тенью, несмотря на указание не добавлять рамку. Перед тем как принять результат редактирования, проверьте полученное изображение — особенно если важны точный текст, детали товара или композиция.
В тесте была одна трёхшаговая цепочка для каждой модели с одинаковыми запросами и настройками 1K, 1:1; начальное значение и уровень рассуждений не задавались. Все шесть запросов завершились со статусом HTTP 200 и STOP; в ответах были изображения PNG размером 1024 × 1024. Это не повторный тест надёжности и не общая оценка качества; кроме того, модели могут использовать разные настройки по умолчанию. Ниже показаны результаты этих API-запросов, а не снимки экрана из Playground:
| Шаг | Nano Banana 2.1 | Более старая Nano Banana 2 |
|---|---|---|
| Постер | ![]() | ![]() |
| Редактирование заголовка | ![]() | ![]() |
| Редактирование миски | ![]() | ![]() |
В архиве сравнения для скачивания собраны связанные файлы. Если вы переносите существующую интеграцию, в сравнении версий 2.1 и 2 наблюдаемое поведение отделено от более общих решений о переходе.
Общедоступная страница модели и запрос через API с авторизацией

Снимок экрана показывает сведения о модели, протоколы, тарифы и кнопку входа в Playground. Он не показывает поля открытого Playground и не подтверждает генерацию изображения через этот интерфейс.
На странице модели есть встроенный Playground с полем для запроса, элементами загрузки вложения и продолжения диалога. Для использования Playground нужно войти в систему. Там же указано, что соблюдение запрошенного соотношения сторон не гарантируется и обеспечивается по возможности, поэтому выбор соотношения сторон в интерфейсе не гарантирует такой результат для каждого изображения. В этом руководстве не утверждается, что проверялся интерфейс Ofox Agent после авторизации или что Agent определённо выбирает версию 2.1. Если вам нужна воспроизводимая интеграция, используйте точный идентификатор модели и проверяйте ответ API, как показано выше.
Переход на новую модель, ограничения и расходы
Замена google/gemini-3.1-flash-image на google/gemini-nano-banana-2.1 — не просто изменение написания идентификатора. Проверьте обработчик ответов, выбор размера изображения, лимиты выходных токенов и предположения о текстовом выводе и выводе рассуждений. В измеренных запросах на создание постера модель 2.1 указала токены выходного текста и токены рассуждений, тогда как ответ старой модели для постера не содержал сведений об этих категориях. Отсутствие категории в ответе не доказывает, что расхода не было, а одинаковые запросы не гарантируют одинакового расхода или настроек по умолчанию. Сохраняйте полный ответ и записывайте возвращённые сведения об использовании, не перенося на новую модель правила учёта старой.
В текущем общедоступном снимке каталога указаны такие расценки для 2.1: $1,50 за миллион входных токенов, $7,50 за миллион токенов выходного текста и $30 за миллион выходных токенов изображения; для веб-поиска отдельно указана ставка за запрос. Для старой модели в каталоге указаны другие расценки. Это данные из каталога, а не проверенный счёт и не фиксированная цена за одно созданное изображение. На странице Google с ценами независимо указаны 1 120 токенов изображения для результата 2.1 в разрешении 1K, однако опубликованные материалы Google расходятся в данных о числе токенов изображения для 4K. В этом руководстве запросы через шлюз для 4K не выполнялись. Прежде чем планировать пакетную генерацию или предполагать, что более низкая ставка за токены изображения снизит стоимость всего запроса, ознакомьтесь с анализом цен.
В документации Google по модели описаны такие возможности, как несколько референсных изображений; в руководстве по генерации изображений рассматриваются настройки изображения. Описанные там возможности не означают, что через этот маршрут Ofox были проверены все настройки, количества референсных изображений или разрешения. В частности, не добавляйте в этот запрос непроверенные инструкции по настройке seed, temperature или topP, предполагая, что они работают с этой моделью, предоставляемой через Vertex.
Наконец, ошибки генерации и права на использование изображения — разные вопросы. Если ответ не содержит изображения или запрос завершился ошибкой, разберитесь в фактическом JSON и статусе; подробнее этот случай рассмотрен в руководстве что делать, если изображение не вернулось. Для публикации или коммерческого использования убедитесь, что у вас есть права на предоставленные изображения, и соблюдайте применимые условия Gemini. Информация о SynthID не гарантирует наличие или отсутствие видимого водяного знака, право собственности, эксклюзивность, отсутствие нарушений прав или конфиденциальность данных.








