--- name: openai-team-proxy description: Set up, run, rotate, and debug OpenAI Team OAuth proxy — round-robin balancer for opencode/Hermes with 67+ accounts, auto-refresh, and billing diagnosis. --- # OpenAI Team Proxy Round-robin балансировщик OpenAI Team OAuth-аккаунтов. Прокси на `:20130`, совместим с OpenAI API (opencode, Kilo, Hermes, любой OpenAI-compatible клиент). ## Триггеры - Пользователь прислал ZIP/JSON с team-аккаунтами OpenAI - Нужно поднять прокси для opencode/Kilo/Hermes - Ключи протухли — нужна ротация - `billing_not_active` / `429` на team-аккаунтах ## Быстрый старт ```bash cd openai-team pip install -r requirements.txt cp /path/to/batch.json current_batch.json python -u proxy.py # -u обязательно на Windows для HTTP серверов ``` Проверка: ```bash curl http://127.0.0.1:20130/health # → {"status":"ok","accounts":67,"concurrency":670} ``` ## Архитектура - **proxy.py** — FastAPI + httpx, round-robin по аккаунтам, semaphore на concurrency - **current_batch.json** — файл с OAuth-токенами (gitignore) - **rotate.sh** — копирует новый батч + шлёт POST /reload - Порт: `20130` (настраивается через `PORT` env) - Прокси: `PROXY=socks5://user:pass@host:port` для исходящих ## Модели Team-аккаунты дают доступ к: `gpt-5.2`, `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-image-2`. ## Интеграция с opencode/Kilo ```jsonc "openai_team": { "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "http://127.0.0.1:20130/v1", "apiKey": "sk-bypass" }, "models": { "gpt-5.6-terra": { "name": "gpt-5.6-terra" }, "gpt-5.4-mini": { "name": "gpt-5.4-mini" } } } ``` ## Ротация ключей (~10 дней) ```bash ./rotate.sh /path/to/new_batch.json # или вручную: cp new.json current_batch.json && curl -X POST http://127.0.0.1:20130/reload ``` ## Питфолы ### BILLING_NOT_ACTIVE (HTTP 429 body) **ВСЕ 67 аккаунтов под одним workspace → биллинг общий.** Если workspace без карты — блокировка на всех. HTTP статус приходит 429, но тело содержит `billing_not_active`. Не путать с рейт-лимитом. **Проверка:** прямой запрос к `https://api.openai.com/v1/chat/completions` с токеном и org_id. Если 429 + billing_not_active на нескольких раскиданных аккаунтах (напр. [0, 10, 20, 30, 40, 50, 60]) — проблема в workspace, не в отдельных токенах. **Решение:** зайти в workspace на chatgpt.com → Billing → добавить карту. ### Refresh tokens — одноразовые Refresh-токены в батче ОДНОРАЗОВЫЕ. После первого использования (`POST https://auth.openai.com/oauth/token`) — сгорают с ошибкой `refresh_token_reused`. Прокси использует их при 401, после чего полагается только на access_token. ### OAuth access_token — реальный JWT, не обрезан В батчах `verify-jwt` access_token выглядит как `eyJhbG...bLiA` в read_file, но это артефакт отображения — реальный токен 1700+ символов, полный JWT. Проверять через `len(token)` в Python, не через read_file. ### Windows: python -u На Windows HTTP-серверы в фоне需要通过 `python -u` (unbuffered). Без `-u` вывод буферизируется и сервер кажется мёртвым. ### concurrency = сумма Каждый аккаунт имеет `concurrency: 10`. Общий semaphore = сумма всех concurrency (670 для 67 аккаунтов). Не путать с числом аккаунтов. ## Диагностика ```bash # Статус curl http://127.0.0.1:20130/health # Модели curl http://127.0.0.1:20130/v1/models # Тестовый запрос curl -s http://127.0.0.1:20130/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":5}' # Прямая проверка токенов (Python) python -c " import json, httpx, asyncio async def test(): with open('current_batch.json') as f: data = json.load(f) for idx in [0, 10, 20, 30, 40, 50, 60, 66]: a = data['accounts'][idx] async with httpx.AsyncClient(timeout=30) as c: r = await c.post('https://api.openai.com/v1/chat/completions', headers={'Authorization': f'Bearer {a[\"credentials\"][\"access_token\"]}', 'OpenAI-Organization': a['credentials']['organization_id'], 'Content-Type': 'application/json'}, json={'model':'gpt-5.4-mini','messages':[{'role':'user','content':'hi'}],'max_tokens':5}) print(f'[{idx}] {a[\"name\"]:20s} | HTTP {r.status_code} | {r.json().get(\"error\",{}).get(\"message\",\"N/A\")}') asyncio.run(test()) " ```