Seedance 2.5 is live — 30-second cinematic video with native audio & real-person references
Генерация изображений для игр: кэширование для экономии
2026/09/03

Генерация изображений для игр: кэширование для экономии

Постройте конвейер генерации изображений с кэшем, семантическими ключами, одиночными запросами, асинхронным опросом, лимитами бюджета и долговечным хранилищем.

Кэширование может снизить среднюю стоимость вызова API генерации изображений на действие игрока до $0.002, но оно не может удешевить действительно новое изображение. При текущей ставке Z-Image на reAPI $0.005 за завершённую генерацию действует правило доля платных промахов × завершённых попыток на принятый ассет ≤ 0.4. Доля промахов 40% достигает потолка только при условии, что каждый новый ассет проходит с первой попытки; оставаться ниже $0.002 требует результата ниже этого предела.[1]

Кэшируйте смысл элемента, а не просто его подсказку. Объединяйте одновременные промахи, предварительно генерируйте только вероятные элементы и измеряйте стоимость на принятый игровой ассет, а не на один ответ API.

TL;DR

  • При $0.005 за завершённую генерацию Z-Image игра, нацеленная на эффективную стоимость не более $0.002 на создание, нуждается в доля платных промахов × завершённых попыток на принятый ассет ≤ 0.4. Ровно одна попытка, 40% соответствует потолку; строго ниже означает менее 40%.[1]
  • Ведите ключ рецепта для того, что комбинировал игрок, и ключ семантического ассета для объекта результата и версии арта.
  • Обеспечьте одно активное задание ассета с ограничением уникальности в общей базе данных. Карта Promise в памяти процесса может объединить вызовы, но это не блокировка, защищающая несколько рабочих.
  • Генерация изображений асинхронна. Отправьте один раз, сохраните ID задачи, опрашивайте конечную точку задачи и скопируйте завершённый вывод в контролируемое вами хранилище. [2][3]
  • Дымовой тест из трёх задач завершил три исходных кандидата с первой отправки за 15 кредитов, или $0.015 всего. Они не были визуально проверены, поэтому это кандидаты, а не принятые ассеты.[4]
  • Если 3000 ежедневных изображений уже действительно новые семантические ассеты после кэширования, обычный кэш не спасёт экономику. Игра должна переиспользовать арт, сузить генеративную поверхность или поддержать более высокую стоимость.

Реальная проблема в стоимости создания, не в стоимости изображения

Представим игру с бесконечным крафтом: она объединяет предметы вроде железа и огня, а затем создаёт новый результат и его изображение. Как удержать расходы на медиа ниже $0.002 за один крафт, если игроки совершают 3000 действий в день и многие комбинации появляются впервые? Здесь проявляется главное ограничение: игра способна создавать новые запросы быстрее, чем выручка позволяет их оплачивать.

Начните с этой формулы:

дневная стоимость генерации =
  события создания
  × доля платных промахов
  × завершённые попытки на принятый ассет
  × цена за завершённую генерацию

При $0.005 за завершённую задачу Z-Image потолок составляет:

$0.005 × доля платных промахов × завершённые попытки на принятый ассет <= $0.002
доля платных промахов × завершённые попытки на принятый ассет <= 0.4

Для 3000 событий создания в день за 30 дней сценарии с одной попыткой выглядят так:

Доля платных промаховНовых генераций/деньСтоимость API за 30 дней
100%3,000$450
40%1,200$180
20%600$90
10%300$45

Это сценарии, а не предсказанные коэффициенты попадания, и каждая строка предполагает одну завершённую попытку на принятый ассет. При 1.5 попытках, например, доля промахов должна быть не более 26.7%, чтобы сохранить тот же потолок $0.002. Если все 3000 событий остаются уникальными после семантической дедупликации, применяется первая строка.

Ключи рецептов и ключи семантических ассетов решают разные проблемы

Ключ рецепта представляет ввод игрока. Если порядок не имеет значения в ваших правилах, отсортируйте точные канонические ID элементов перед хэшированием. Нехэшированная идентичность легко проверяется:

идентичность рецепта: v3 | fire | iron

Это делает так, что iron + fire и fire + iron разрешают через одну строку рецепта, принадлежащую серверу. Включите версию правил, потому что более позднее изменение баланса может изменить результат. Не ленивьте ID для ключа: потеря нормализации может объединить два разных элемента.

Ключ семантического ассета представляет то, что в итоге отображает игра. Его нехэшированная идентичность может оставаться столь же ясной:

идентичность ассета: ember-shield | inventory-icon-v1 | z-image | 1:1 | filtered

Несколько рецептов могут разрешить в ember-shield; переводы также могут называть его по-другому. Эти пути всё ещё могут переиспользовать одно одобренное визуальное.

Необработанные подсказки делают плохие ключи: изменения формулировки фрагментируют кэш, и идентичные подсказки устаревают после изменения направления арта. Сопоставьте каждый рецепт со стабильным ID результата, затем ключируйте арт по ID результата, модели, соотношению сторон, режиму безопасности и версии стиля. Увеличьте версию, когда хотите нового арта.

Поток запроса, ориентированный на кэш

Рассматривайте генерацию как фоновое производство ассетов, даже если игрок её инициирует. Практичный поток:

  1. Разрешите входящий рецепт в семантический ID результата.
  2. Найдите ключ семантического ассета в долговечном хранилище.
  3. При попадании верните немедленно сохранённый URL.
  4. При промахе атомарно вставьте одну строку общего задания, ключированную семантическим ассетом.
  5. В той же транзакции базы данных зарезервируйте один слот в общем дневном бюджете подачи. Другие рабочие возвращают существующее задание.
  6. Зафиксируйте строку как submitting перед одним POST, затем сохраните ID задачи сразу же после ответа.
  7. Если подача неоднозначна, отметьте submit_uncertain и остановитесь. Опрашивайте известный ID задачи с GET, пока он не станет completed или failed.
  8. Запишите статус терминала и урегулированные кредиты. Скопируйте завершённое изображение в собственное хранилище и переместите его в pending_review.
  9. Только проверка приёма может переместить pending_review в ready.

После шага шесть верните заполнитель и состояние pending. Позвольте рабочему опрашивать, пока клиент проверяет конечную точку вашей игры. Никогда не предоставляйте ключ reAPI клиенту.

Опрос бесплатен, и конечная точка задачи в полёте кэшируется пять секунд, поэтому опрашивайте примерно каждые пять секунд.[3] Уважайте Retry-After на 429. Не слепо переотправляйте POST: reAPI не дедупликирует запросы генерации по Idempotency-Key, поэтому потерянный ответ может скрыть принятую задачу и второй платёж.[5]

Запускаемый модуль Node 20 с общей блокировкой

Наименьший пример, защищающий несколько рабочих, нуждается в общем состоянии. Модуль ниже — чистый .mjs, использует PostgreSQL через пакет postgres и возвращается после подачи вместо того, чтобы держать запрос игрока открытым.

npm install postgres

Создайте таблицы один раз. Заполните game_recipes из собственных данных правил игры; браузер отправляет два ID элемента, никогда не ID результата или отображаемое имя.

CREATE TABLE game_recipes (
  recipe_key text PRIMARY KEY,
  result_item_id text NOT NULL,
  result_display_name text NOT NULL
);

CREATE TABLE game_asset_jobs (
  asset_key text PRIMARY KEY,
  origin_recipe_key text NOT NULL REFERENCES game_recipes(recipe_key),
  result_item_id text NOT NULL,
  state text NOT NULL CHECK (state IN (
    'submitting', 'submit_uncertain', 'processing', 'completed',
    'pending_review', 'ready', 'failed', 'rejected'
  )),
  submit_owner uuid NOT NULL,
  prompt text NOT NULL,
  task_id text UNIQUE,
  source_url text,
  durable_url text,
  usage_credits integer,
  error_message text,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE game_generation_budgets (
  budget_day date PRIMARY KEY,
  submissions integer NOT NULL CHECK (submissions >= 0),
  submission_limit integer NOT NULL CHECK (submission_limit > 0)
);

Сохраните это как cache-first.mjs. Полные ключи SHA-256 хэшируют точные канонические ID; нет шага ленивизации, который может свернуть два разных ID.

import { createHash, randomUUID } from 'node:crypto';
import postgres from 'postgres';

const API_BASE = 'https://reapi.ai/api/v1';
const apiKey = process.env.REAPI_API_KEY;
const databaseUrl = process.env.DATABASE_URL;
const dailyLimit = Number(process.env.DAILY_NEW_ASSET_LIMIT ?? '100');

if (!apiKey) throw new Error('Set REAPI_API_KEY on the server');
if (!databaseUrl) throw new Error('Set DATABASE_URL on the server');
if (!Number.isInteger(dailyLimit) || dailyLimit < 1) {
  throw new Error('DAILY_NEW_ASSET_LIMIT must be a positive integer');
}

const sql = postgres(databaseUrl);
const localFlights = new Map(); // L1 only; PostgreSQL owns correctness.

function hashIdentity(value) {
  return createHash('sha256')
    .update(JSON.stringify(value), 'utf8')
    .digest('hex');
}

function canonicalId(value) {
  if (typeof value !== 'string' || value.length === 0 || value.length > 200) {
    throw new Error('Item IDs must be non-empty canonical server IDs');
  }
  return value; // Preserve the exact string; do not trim, slugify, or case-fold.
}

export function recipeKey(leftItemId, rightItemId) {
  const pair = [canonicalId(leftItemId), canonicalId(rightItemId)].sort(
    (a, b) => (a < b ? -1 : a > b ? 1 : 0),
  );
  return `recipe:v3:${hashIdentity({ rulesVersion: 'v3', itemIds: pair })}`;
}

function assetKey(resultItemId) {
  return `asset:${hashIdentity({
    resultItemId,
    styleVersion: 'inventory-icon-v1',
    model: 'z-image',
    aspectRatio: '1:1',
    contentFilter: true,
  })}`;
}

async function resolveRecipe(leftItemId, rightItemId) {
  const key = recipeKey(leftItemId, rightItemId);
  const [recipe] = await sql`
    SELECT result_item_id, result_display_name
    FROM game_recipes
    WHERE recipe_key = ${key}
  `;
  if (!recipe) throw new Error('Unknown recipe');
  if (recipe.result_display_name.length > 200) {
    throw new Error('Canonical display name is too long');
  }
  return { ...recipe, recipeKey: key };
}

function publicJob(job) {
  return {
    assetKey: job.asset_key,
    state: job.state,
    taskId: job.task_id ?? null,
    url: job.state === 'ready' ? job.durable_url : null,
    usageCredits: job.usage_credits ?? null,
  };
}

async function claimSharedJob(recipe, key, prompt) {
  const owner = randomUUID();

  return sql.begin(async (tx) => {
    const created = await tx`
      INSERT INTO game_asset_jobs (
        asset_key, origin_recipe_key, result_item_id,
        state, submit_owner, prompt
      ) VALUES (
        ${key}, ${recipe.recipeKey}, ${recipe.result_item_id},
        'submitting', ${owner}, ${prompt}
      )
      ON CONFLICT (asset_key) DO NOTHING
      RETURNING *
    `;

    if (created.length === 0) {
      const [existing] = await tx`
        SELECT * FROM game_asset_jobs WHERE asset_key = ${key}
      `;
      if (!existing) throw new Error('Concurrent job was not readable');
      return { ownsSubmit: false, job: existing };
    }

    const budget = await tx`
      INSERT INTO game_generation_budgets (
        budget_day, submissions, submission_limit
      ) VALUES (
        (now() AT TIME ZONE 'UTC')::date, 1, ${dailyLimit}
      )
      ON CONFLICT (budget_day) DO UPDATE SET
        submissions = game_generation_budgets.submissions + 1,
        submission_limit = EXCLUDED.submission_limit
      WHERE game_generation_budgets.submissions < EXCLUDED.submission_limit
      RETURNING submissions
    `;
    if (budget.length === 0) throw new Error('Shared daily budget reached');

    return { ownsSubmit: true, owner, job: created[0] };
  });
}

async function markSubmitUncertain(key, owner, message) {
  const [job] = await sql`
    UPDATE game_asset_jobs
    SET state = 'submit_uncertain', error_message = ${message},
        updated_at = now()
    WHERE asset_key = ${key} AND submit_owner = ${owner}
      AND state = 'submitting'
    RETURNING *
  `;
  return job;
}

async function submitClaimedJob(claim, key, prompt) {
  let response;
  let body = '';

  try {
    response = await fetch(`${API_BASE}/images/generations`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'z-image',
        prompt,
        aspect_ratio: '1:1',
        content_filter: true,
      }),
    });
    body = await response.text();
  } catch (error) {
    const job = await markSubmitUncertain(
      key,
      claim.owner,
      error instanceof Error ? error.message : String(error),
    );
    return publicJob(job ?? claim.job);
  }

  let task;
  try {
    task = JSON.parse(body);
  } catch {
    task = null;
  }

  if (!response.ok || !task?.id) {
    const message = `Unconfirmed submit response: HTTP ${response.status}`;
    const job = await markSubmitUncertain(key, claim.owner, message);
    return publicJob(job ?? claim.job);
  }

  const [saved] = await sql`
    UPDATE game_asset_jobs
    SET state = 'processing', task_id = ${task.id}, updated_at = now()
    WHERE asset_key = ${key} AND submit_owner = ${claim.owner}
      AND state = 'submitting'
    RETURNING *
  `;
  if (!saved) {
    throw new Error(`Task ${task.id} was accepted but its ID was not persisted; do not resubmit`);
  }
  return publicJob(saved);
}

async function startOrRead(recipe, key) {
  const prompt = [
    `A clean 3D inventory icon of ${recipe.result_display_name}.`,
    'One centered object, readable silhouette, plain warm background,',
    'soft studio light, no text, no logo, no existing game character.',
  ].join(' ');

  const claim = await claimSharedJob(recipe, key, prompt);
  if (!claim.ownsSubmit) return publicJob(claim.job);
  return submitClaimedJob(claim, key, prompt);
}

export async function getOrCreateCraftAsset({ leftItemId, rightItemId }) {
  const recipe = await resolveRecipe(leftItemId, rightItemId);
  const key = assetKey(recipe.result_item_id);
  const existing = localFlights.get(key);
  if (existing) return existing;

  const work = startOrRead(recipe, key).finally(() => {
    if (localFlights.get(key) === work) localFlights.delete(key);
  });
  localFlights.set(key, work);
  return work;
}

export async function pollAsset(key) {
  const [job] = await sql`
    SELECT * FROM game_asset_jobs WHERE asset_key = ${key}
  `;
  if (!job) throw new Error('Unknown asset job');
  if (job.state !== 'processing' || !job.task_id) return publicJob(job);

  const response = await fetch(`${API_BASE}/tasks/${job.task_id}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (response.status === 429) {
    const parsed = Number.parseInt(response.headers.get('retry-after') ?? '', 10);
    return {
      ...publicJob(job),
      retryAfterSeconds: Number.isFinite(parsed) ? Math.max(parsed, 1) : 5,
    };
  }
  if (!response.ok) {
    throw new Error(`GET task failed with HTTP ${response.status}; retry this GET, not the POST`);
  }

  const task = await response.json();
  if (task.status === 'processing') return publicJob(job);
  const credits = Number.isInteger(task.usage?.credits)
    ? task.usage.credits
    : null;

  if (task.status === 'failed') {
    const [failed] = await sql`
      UPDATE game_asset_jobs
      SET state = 'failed', usage_credits = ${credits},
          error_message = ${task.error?.message ?? 'Generation failed'},
          updated_at = now()
      WHERE asset_key = ${key} AND task_id = ${job.task_id}
      RETURNING *
    `;
    return publicJob(failed);
  }

  if (task.status !== 'completed') {
    throw new Error(`Unexpected task status: ${task.status}`);
  }

  const sourceUrl = task.output?.image_urls?.[0] ?? null;
  const [completed] = await sql`
    UPDATE game_asset_jobs
    SET state = 'completed', source_url = ${sourceUrl},
        usage_credits = ${credits},
        error_message = ${sourceUrl ? null : 'Completed task returned no image URL'},
        updated_at = now()
    WHERE asset_key = ${key} AND task_id = ${job.task_id}
    RETURNING *
  `;
  return publicJob(completed);
}

export async function stageForReview(key, durableUrl) {
  const [job] = await sql`
    UPDATE game_asset_jobs
    SET state = 'pending_review', durable_url = ${durableUrl},
        updated_at = now()
    WHERE asset_key = ${key} AND state = 'completed'
      AND source_url IS NOT NULL
    RETURNING *
  `;
  if (!job) throw new Error('Only an archived completed asset can enter review');
  return publicJob(job);
}

export async function reviewAsset(key, accepted, reason = null) {
  const nextState = accepted ? 'ready' : 'rejected';
  const [job] = await sql`
    UPDATE game_asset_jobs
    SET state = ${nextState}, error_message = ${reason}, updated_at = now()
    WHERE asset_key = ${key} AND state = 'pending_review'
    RETURNING *
  `;
  if (!job) throw new Error('Asset is not pending review');
  return publicJob(job);
}

Строка базы данных, а не localFlights, предотвращает отправку одного ассета вторым рабочим. Процесс, который умирает во время POST, оставляет submitting; рассматривайте устаревшую строку как submit_uncertain и исследуйте её, а не переиспользуйте. После существования известного ID задачи переходящие сбои при опросе переиспользуют только GET.

Общий счётчик — консервативный лимит подачи. Неподтвёрждённая или отклонённая POST всё равно потребляет свой слот, что безопаснее, чем молчаливо превышать лимит. Если терминальная задача не удаётся, её статус и usage.credits остаются в строке. Этот пример не повторно пытается неудачные или отклонённые ассеты; добавьте отдельный реестр попыток и явное, бюджетируемое действие оператора перед поддержкой повторных попыток.

Рабочий вашего хранилища должен загрузить source_url, загрузить его в ваше хранилище объектов и передать этот долговечный URL в stageForReview. Только reviewAsset(..., true) создаёт кэшируемый ready ассет.

Что доказывает дымовой тест из трёх задач

3 сентября 2026 г. мы отправили три исходных подсказки значка инвентаря 1:1 с content_filter: true: мшистый фонарь, компас из стекла и щит углей. Каждая задача завершилась с первой отправки и сообщила 5 кредитов. [4]

КандидатID задачиНаблюдаемое время завершенияКредиты
Мшистый фонарьtask_01a065490250739d893a1efcd83f4d1d25.1 s5
Компас из стеклаtask_01a0654995df7265b44b132dce34a91425.0 s5
Щит углейtask_01a0654995fd73e28e7c68217066407018.2 s5
Всего3 задачи15 / $0.015

Общественная запись дымового теста содержит параметры запроса, ID задач, прошедшее время и урегулированные кредиты. Она намеренно опускает временные URL вывода.[4]

Это дымовой тест, а не тест задержки; он ничего не говорит о p95, всплесках или долговременной надёжности. Протокол записал результаты задач и урегулирование, но не включал проверку арта. completed означает, что прибыли три кандидата, а не то, что рецензент арта их принял.

Поэтому полезная метрика создания:

стоимость на принятый ассет =
  общие урегулированные кредиты генерации × $0.001 / принятые ассеты

Если три попытки дают один полезный значок, его эффективная стоимость составляет $0.015. Записывайте технический статус и творческую приёмку отдельно.

Предварительно генерируйте для задержки, а не воображаемой экономии

Предварительно генерируйте только предсказуемые запросы, например 20 лучших рецептов, достижимых из инвентаря игрока. Используйте тот же семантический кэш и путь одиночного полёта с отдельным дневным лимитом.

Не генерируйте теоретическое пространство комбинаций. Неиспользуемые ассеты стоят деньги, и открытое создание перерастает грубую очередь. Ранжируйте по наблюдаемому спросу и остановитесь на лимите.

Гибрид обычно ощущается лучше всего:

  • одобренные семантические ассеты возвращаются немедленно;
  • вероятные следующие элементы предварительно генерируются;
  • долгохвостные промахи показывают заполнитель, пока работает одна фоновая задача;
  • непопулярные или оскорбительные запросы попадают в квоту вместо бесконечного цикла повторных попыток.

Безопасность, хранилище и операционные границы

Сохраняйте content_filter: true для генерации, ориентированной на игроков. Z-Image оставляет этот параметр отключённым по умолчанию, поэтому полагаться на значение по умолчанию недостаточно. [2] Модерируйте или ограничивайте текст игроков перед тем, как он станет подсказкой, блокируйте попытки подражать защищённым персонажам или реальным людям и ограничивайте скорость по учётной записи и устройству. Вышестоящий фильтр — это один уровень, а не полная политика вашей игры.

Сохраняйте ключ на сервере. Логируйте ключи рецепта и ассета, ID задачи, статус, кредиты и приём без сохранения ненужных данных игроков.

Наконец, скопируйте завершённые файлы в долговечное хранилище перед публикацией их URL для игровых клиентов. URL вывода задачи не являются постоянным контрактом ассета. [3] Сохраняйте контрольную сумму и тип контента рядом с записью ассета, чтобы повторная попытка не смогла тихо заменить одобренный арт.

Заключение: когда кэширования недостаточно

Кэширование работает только тогда, когда игра допускает переиспользование. Если доля платных промахов × завершённые попытки на принятый ассет превышает 0.4, потолок $0.002 не может удерживаться при цене за единицу $0.005. Если 3000 ежедневных запросов в исходном примере уже уникальные семантические ассеты после кэширования, изменение программного обеспечения кэша не решит проблему.

Затем измените ограничение продукта: поделитесь артом между похожими элементами, используйте процедурное перекрашивание или наложения, генерируйте только популярные элементы, ограничьте открытия, взимайте плату за создание долгохвостя или примите более крупный медиа-бюджет.

Проверьте живую страницу модели Z-Image перед установкой бюджета, затем используйте руководство API Z-Image и справочник API Tasks для реализации отправки и опроса. Цены и поведение модели могут меняться; ваши собственные урегулированные кредиты остаются источником истины для счёта продакшена.

FAQ

Снижает ли кэширование цену нового AI-изображения?

Нет. Оно снижает, как часто вы его покупаете. Текущий платёж Z-Image остаётся пять кредитов за завершённую генерацию; попадание в кэш не стоит новых кредитов генерации.

Должна ли сама подсказка быть ключом кэша?

Обычно нет. Разрешите рецепты в стабильный ID результата, затем версируйте его арт отдельно.

Что если порядок элементов имеет значение?

Не сортируйте входы рецепта. water + fire и fire + water должны иметь разные ключи рецепта всякий раз, когда ваши правила дают им разные результаты.

Должна ли игра предварительно генерировать ассеты или генерировать их в реальном времени?

Используйте оба: предварительно генерируйте бюджетируемый вероятный набор, обслуживайте попадания немедленно и генерируйте непредсказуемые долгохвостые элементы в фоне.

Как часто я должен опрашивать задачу изображения reAPI?

Примерно каждые пять секунд — практичное начальное место. Более частый опрос не заставляет генерацию завершаться быстрее и может тратить бюджет лимита скорости.

Взимается ли плата за неудачные генерации?

Документация Z-Image говорит, что неудачные и отклонённые запросы не взимаются. Всё же осмотрите status вместе с usage.credits: завершённый, но визуально неиспользуемый результат отличается от неудачной задачи и учитывается в творческую стоимость.[2][3]

Могу ли я пообещать среднее значение ниже $0.002 на создание?

Только после измерения вашей доли платных промахов и завершённых попыток на принятый ассет. При $0.005 за изображение, оставаться строго ниже $0.002 требует их произведения оставаться ниже 0.4. Доля промахов 40% просто встречает потолок, когда каждый ассет проходит с первой завершённой попытки.

Может ли игровой клиент вызывать конечную точку изображения напрямую?

Он не должен. Учётные данные на стороне клиента могут быть извлечены и злоупотреблены. Маршрутизируйте генерацию через ваш серверную часть, обеспечивайте там квоты и позвольте клиентам запрашивать задание ожидающей работы, принадлежащей игре.

Справочные материалы

  1. reAPI. Z-Image model page and live pricing. Retrieved September 3, 2026 from reapi.ai/models/z-image
  2. reAPI. Z-Image API documentation: inputs, asynchronous response, pricing, filters, and output retention. Retrieved September 3, 2026 from reapi.ai/docs/z-image
  3. reAPI. Tasks API: status, settled usage, polling, rate-limit behavior, and output storage. Retrieved September 3, 2026 from reapi.ai/docs/api/tasks
  4. reAPI. Three-task Z-Image smoke-test record. Run September 3, 2026. Temporary output URLs are omitted; no visual acceptance review was performed.
  5. reAPI. API overview: idempotency behavior. Retrieved September 3, 2026.

Автор

avatar for reAPI Team
reAPI Team

Категории

TL;DRРеальная проблема в стоимости создания, не в стоимости изображенияКлючи рецептов и ключи семантических ассетов решают разные проблемыПоток запроса, ориентированный на кэшЗапускаемый модуль Node 20 с общей блокировкойЧто доказывает дымовой тест из трёх задачПредварительно генерируйте для задержки, а не воображаемой экономииБезопасность, хранилище и операционные границыЗаключение: когда кэширования недостаточноFAQСнижает ли кэширование цену нового AI-изображения?Должна ли сама подсказка быть ключом кэша?Что если порядок элементов имеет значение?Должна ли игра предварительно генерировать ассеты или генерировать их в реальном времени?Как часто я должен опрашивать задачу изображения reAPI?Взимается ли плата за неудачные генерации?Могу ли я пообещать среднее значение ниже $0.002 на создание?Может ли игровой клиент вызывать конечную точку изображения напрямую?Справочные материалы