← Назад в блог

Лимиты API при работе с нейросетями — и как с ними справляться

Опубликовано
6 мин чтения
--- просмотров

Лимиты API при работе с нейросетями — и как с ними справляться

Работа с нейросетями сегодня невероятно актуальна. Однако если ты используешь удалённый сервис для взаимодействия с ИИ-агентами (например, OpenAI API), ты неизбежно столкнёшься с рядом ограничений, таких как:

  • максимальный размер запроса
  • максимальное количество запросов в единицу времени
  • лимиты на пропускную способность и конкурентность

Как известно, вызовы API к удалённым сервисам всегда сопровождаются лимитами. И когда ты обрабатываешь большие датасеты — особенно внутри циклов — эти ограничения быстро становятся проблемой.


Почему «просто отправить всё разом» не работает

Ты можешь сказать:

«Почему бы не объединить несколько запросов в один, а затем распарсить результат?»

На практике это не всегда работает. У OpenAI (и многих других моделей) есть особенность — при работе со списками или массивами нейросеть часто теряет, пропускает или исключает один или несколько элементов.

Поэтому самый надёжный способ — обрабатывать каждый элемент массива по отдельности. Это может быть медленнее, но гарантирует точность и согласованность результата.


Что такое rate limiting — и почему это важно

Для таких ситуаций существует механизм под названием rate limiting — способ ограничить частоту отправки запросов. Он помогает предотвратить перегрузку сервиса и обеспечивает справедливое распределение ресурсов.

Если игнорировать rate limits, ты быстро столкнёшься с ошибками вроде:

Error: 429 Too Many Requests

или даже временной приостановкой API-ключа.

Мой случай: перевод больших датасетов

OpenAi rate limiting per second

В одном из моих проектов мне нужно было переводить большие объёмы текста на русский с помощью OpenAI API. Сначала я просто проходил циклом по всему и отправлял запросы — но быстро упёрся в лимиты.

Решение было простым: добавить контроллер частоты запросов. Вот минимальный пример клиента, который соблюдает лимиты запросов в минуту.

Такой подход позволяет плавно распределять нагрузку и оставаться в пределах лимитов API — даже при обработке тысяч элементов.

# translationClient.py

import Config from "@app/config";
import Bottleneck from "bottleneck";
import { createRequire } from "node:module";
import { z } from "zod";
import zodToJsonSchema, { JsonSchema7Type } from "zod-to-json-schema";

const TranslationSchema = z.object({
  genres: z.array(z.string()).describe("Genres"),
  features: z.array(z.string()).describe("Features"),
  descriptionFull: z.string().describe("Full Description"),
  supportedLanguagesText: z
    .array(z.string())
    .describe("Supported languages text"),
  supportedLanguagesVoice: z
    .array(z.string())
    .describe("Supported languages voice"),
});

export type TranslationInput = z.infer<typeof TranslationSchema>;

const TranslationJsonSchema = zodToJsonSchema(TranslationSchema, {
  $refStrategy: "none",
}) as JsonSchema7Type;

type JsonSchemaObject = JsonSchema7Type & {
  properties?: Record<string, unknown>;
  required?: string[];
};

const schemaObject = TranslationJsonSchema as JsonSchemaObject;

if (schemaObject && schemaObject.properties) {
  schemaObject.required = Object.keys(schemaObject.properties);
}

const require = createRequire(import.meta.url);
const openAiModuleName = "openai";
const { default: OpenAI } = require(openAiModuleName) as { default: any };

const client = new OpenAI({ apiKey: Config.openAiKey });

export const openAiLimiter = new Bottleneck({
  // Minimum spacing between calls so we stay under the rate limit
  // For 500 requests per minute -> ~8.33 per second -> ~120 ms interval
  minTime: 120,

  // Use the reservoir to cap total calls per window
  reservoir: 500,                        // maximum calls per window
  reservoirRefreshInterval: 60 * 1000,  // refresh the allowance every minute
  reservoirRefreshAmount: 500
});

const limitCall = async <TRes>(call: CallableFunction, maxRetries = 5): Promise<TRes> => {
  let attempt = 0
  let backoff = 500

  while (true) {
    try {
      return await openAiLimiter.schedule(() => call());
    } catch (error: any) {
      attempt++
      if (attempt > maxRetries) {
        throw error
      }

      if (error.status == 429) {
        await new Promise(res => setTimeout(res, backoff))
        backoff *= 2
      } else {
        throw error
      }
    }
  }
}

export async function getGameTranslation(
  lang: string,
  input: TranslationInput,
) {
  return await limitCall<TranslationInput>(async () => {
    const completion = await client.chat.completions.create({
      model: "gpt-4o-mini",
      messages: [
        {
          role: "system",
          content: `You are a professional translator. Preserve meaning and tone. Return ONLY valid JSON`,
        },
        {
          role: "user",
          content: [
            `Translate this object to ${lang} language: ${JSON.stringify(input)}`,
            "Keep formatting where sensible",
            "If there are names or bock codes, do not translate them",
            "Return every property from the original object even if it should be empty.",
            "Input",
          ].join("\n\n"),
        },
      ],
      response_format: {
        type: "json_schema",
        json_schema: {
          name: "Translation",
          schema: TranslationJsonSchema,
          strict: true,
        },
      },
    });

    const raw = completion.choices[0]?.message?.content ?? "{}";
    const translation = TranslationSchema.parse(JSON.parse(raw));
    return translation;
  })
}

Улучшенная версия с добавленным мониторингом LangFuse

import Config from "@app/config";
import Bottleneck from "bottleneck";
import { OpenAI } from "openai/client";
import { z } from "zod";
import zodToJsonSchema, { JsonSchema7Type } from "zod-to-json-schema";
import { observeOpenAI } from "@langfuse/openai";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

const TranslationSchema = z.object({
  genres: z.array(z.string()).describe("Genres"),
  features: z.array(z.string()).describe("Features"),
  descriptionFull: z.string().describe("Full Description"),
  supportedLanguagesText: z
    .array(z.string())
    .describe("Supported languages text"),
  supportedLanguagesVoice: z
    .array(z.string())
    .describe("Supported languages voice"),
});

export type TranslationInput = z.infer<typeof TranslationSchema>;

const TranslationJsonSchema = zodToJsonSchema(TranslationSchema, {
  $refStrategy: "none",
}) as JsonSchema7Type;

type JsonSchemaObject = JsonSchema7Type & {
  properties?: Record<string, unknown>;
  required?: string[];
};

const schemaObject = TranslationJsonSchema as JsonSchemaObject;

if (schemaObject && schemaObject.properties) {
  schemaObject.required = Object.keys(schemaObject.properties);
}

const sdk = new NodeSDK({
  spanProcessors: [
    new LangfuseSpanProcessor({
      publicKey: Config.langfusePublicKey,
      secretKey: Config.langfuseSecretKey,
      baseUrl: Config.langfuseHost,
    }),
  ],
});

sdk.start();

// Wrap the OpenAI client with Langfuse tracing
const tracedOpenAI = observeOpenAI(new OpenAI({ apiKey: Config.openAiKey }), {
  // Configure trace-level attributes for all API calls
  traceName: "my-openai-trace", // Name for the trace
  sessionId: "user-session-123", // Track user session
  userId: "user-abc", // Track user identity
  tags: ["openai-integration"], // Add searchable tags
});

export const openAiLimiter = new Bottleneck({
  // Minimum spacing between calls so we stay under the rate limit
  // For 500 requests per minute -> ~8.33 per second -> ~120 ms interval
  minTime: 120,

  // Use the reservoir to cap total calls per window
  reservoir: 500, // maximum calls per window
  reservoirRefreshInterval: 60 * 1000, // refresh the allowance every minute
  reservoirRefreshAmount: 500,
});

const limitCall = async <TRes>(
  call: CallableFunction,
  maxRetries = 5,
): Promise<TRes> => {
  let attempt = 0;
  let backoff = 500;

  while (true) {
    try {
      return await openAiLimiter.schedule(() => call());
    } catch (error: any) {
      attempt++;
      if (attempt > maxRetries) {
        throw error;
      }

      if (error.status == 429) {
        await new Promise((res) => setTimeout(res, backoff));
        backoff *= 2;
      } else {
        throw error;
      }
    }
  }
};

export async function getGameTranslation(
  lang: string,
  input: TranslationInput,
) {
  return await limitCall<TranslationInput>(async () => {
    const completion = await tracedOpenAI.chat.completions.create({
      model: "gpt-4o-mini",
      messages: [
        {
          role: "system",
          content: `You are a professional translator. Preserve meaning and tone. Return ONLY valid JSON`,
        },
        {
          role: "user",
          content: [
            `Translate this object to ${lang} language: ${JSON.stringify(input)}`,
            "Keep formatting where sensible",
            "If there are names or bock codes, do not translate them",
            "Return every property from the original object even if it should be empty.",
            "Input",
          ].join("\n\n"),
        },
      ],
      response_format: {
        type: "json_schema",
        json_schema: {
          name: "Translation",
          schema: TranslationJsonSchema,
          strict: true,
        },
      },
    });

    const raw = completion.choices[0]?.message?.content ?? "{}";
    const translation = TranslationSchema.parse(JSON.parse(raw));
    return translation;
  });
}

Поделись своим опытом

Какие подходы используешь ты? Поделись в комментариях, как ты справляешься с rate limits, обрабатываешь очереди запросов или распределяешь нагрузку API между сервисами — было бы интересно узнать о твоих методах.

Открыт для работы по контракту

Я доступен для работы по контракту. Если у вас есть интересная идея проекта — запишитесь на звонок через Calendly.

Записаться на 30-минутный звонок