Как объединить личные AI подписки в единый пул

от автора

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

Возьмём типичные ситуации для разных должностей:

  • Василий, технический писатель. Долгое время ему хватало подписки за 20$. Но то ли вендор лимиты втихую урезал, толи эффективность команды повысилась, но хватать ему 20$ подписки перестало. Хочет купить подписку за 100$, но понимает, что большая часть квоты у него всегда будет неиспользованной

  • Артём, QA тестировщик. У него подписка за 100$, но распределение потребления неравномерное. Когда существовали пятичасовые лимиты — было особенно тяжко. Перед релизом всё съедается в ноль, а в остальное время он едва ли 50% использует

  • Аня, Senior backend-разработчица. Обожает оставлять агентов на ночь в режиме цели (goal), работает много и активно. Подписки за 200$ перестало хватать. Пришлось покупать вторую. Но на практике она не используется большую часть времени. Расход квоты всегда в районе 25%

Кто-то может возразить: «Но ведь есть же официальные Business/Enterprise-тарифы! Зачем изобретать велосипед?». Проблема особенно актуальна для IT в России, когда внезапно оказывается, что велосипеды не продают российским юрлицам, а даже если продали, в любой момент могут превратить велосипед в тыкву.

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

Оказывается, мы далеко не первые, кто додумался до такой архитектуры, и умные люди уже давно наклепали опенсорс-проектов похожей направленности. Самым популярным на данный момент является codex-lb, который набрал уже почти 3к звёзд на гитхабе.

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

Developer A ─┐Developer B ─┤Developer C ─┤...          ├──► codex-lb ───► ChatGPT account #1 ($20)Developer O ─┘       │        ├─► ChatGPT account #2 ($100)                     │        ├─► ChatGPT account #3 ($200)                     │        ├─► ...                     │        └─► ChatGPT account #15                     │                     ├── routing                     ├── quotas                     ├── sticky sessions                     ├── API keys                     └── usage accounting

Для разработчика архитектура превращается в blackbox. У разработчика есть только ключ: он не выбирает чью подписку использовать, он не знает ничего про лимиты, текущее потребление и нагруженность. Оркестратор сам за него выбирает на какой аккаунт послать запрос, у кого осталось больше квоты, когда у какого аккаунт произойдет reset и какие модели доступны.

Для начала администратору необходимо добавить аккаунты в пул подписок. Делает он это через OAuth-инфраструктуру Codex/OpenAI и хранит access/refresh/id токены аккаунтов. Токены шифруются; в коде есть отдельный механизм обновления refresh-токенов и даже механизмы для горизонтального масштабирования.

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

Хорошо, с сетапом разобрались. Но теперь возникает вопрос — а как внутри системы балансируются запросы между подписками? Механизм оказывается сложнее базового round robin. Нам предлагается на выбор несколько стратегий:

Capacity weighted

Account A      20% remainingAccount B      70% remainingAccount C      90% remaining                  ↓             больше traffic                  │           ┌──────┴──────┐           B             C

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

Relative availability

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

остаток квоты / количество секунд до reset

В таком формате больший приоритет получают аккаунты, которые уже подходят к reset, но всё ещё имеют достаточный лимит:

Account Aосталось 40%reset через 2 дняAccount Bосталось 40%reset через 6 дней

Аккаунт A получает более высокий приоритет: его оставшуюся квоту нужно быстрее использовать, иначе она пропадёт при reset.

Для команды, где люди работают в разное время суток, это уже довольно умно.

Reset drain

Тут мы уже пытаемся выжать подписку на максимум. На вероятность выбора теперь влияют только секунды до reset, без учёта оставшейся квоты:

A: 46% left → reset tomorrowB: 79% left → reset in 4 daysC: 65% left → reset in 6 daystraffic   ↓   A   ↓после него B/C

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


А теперь перейдём к самому проблемному моменту всей архитектуры.

Пользователь работал через аккаунт А, разрабатывал какую-то фичу. Квота аккаунта А закончилась, фича еще не доделана. Как нам безопасно и эффективно продолжить на аккаунте Б и можно ли в принципе это сделать?

Для этого вводим два понятия: Sticky routing и Hard continuation.

Это не настройки-переключатели, а состояния, которые оркестратор определяет автоматически и от которых зависит, можем ли мы продолжить текущую сессию в другой подписке, или нет?

Возьмём для примера стандартный флоу работы с агентом

THREADВесь чат на несколько часов│├── TURN 1│   User: "Исследуй auth и исправь баг"│   ││   ├── inference #1│   ├── tool call│   ├── inference #2│   ├── tool call│   ├── inference #3│   └── Assistant: "Готово"│├── TURN 2│   User: "Теперь добавь тесты"│   ├── inference #1│   ├── ...│   └── Assistant: "Готово"│└── TURN 3    User: "А теперь отрефактори"

Особенности кодекса состоят в том, что между вызовами внутри одного turn он не передаёт каждый раз полную историю, вместо повторной передачи всего контекста используется продолжение через previous_response_id. Этот идентификатор ссылается на состояние, доступное в рамках соответствующего аккаунта. Между turns агента же, отправляется полная дельта текущего контекстного окна.

Зачем всё это было нужно? Чтобы понять простую вещь: если внутри одного turn закончился лимит, например после inference #2, то мы не сможем безопасно продолжить работу через другой аккаунт, так как находимся в процессе исполнения. То есть для типичного сценария: закончилась подписка А -> перенаправляем на подписку Б, такой кейс не подходит. И нам приходится либо создавать новую сессию, либо ждать, пока сбросятся лимиты. Это и есть Hard continuation.

Если же квота аккаунта A закончилась уже после завершения TURN 1., то мы спокойно передаём контекст через аккаунт Б и продолжаем работу там. Это как раз механизм Sticky routing

Подводя итог, можно отметить, что reset drain стратегия хоть и является наиболее экономически выгодной, тем не менее сильно уступает в удобстве для рядового разработчика.


Но тут все замечают слона в комнате. Официальной политикой OpenAI запрещена передача доступа к аккаунту третьим лицам.

Данная статья не является рекомендацией по обходу официальных правил или нарушению принятых Terms of Use и Account sharing policy.

Все персонажи и события вымышлены, любые совпадения с реальными людьми или событиями случайны

ссылка на оригинал статьи https://habr.com/ru/articles/1073270/