refactor: add staged next-account rotation and clarify ChatGPT config
This commit is contained in:
parent
ccd4d82194
commit
d6396e4050
5 changed files with 253 additions and 100 deletions
|
|
@ -1,8 +1,11 @@
|
||||||
# HTTP server port
|
# HTTP server port
|
||||||
PORT=80
|
PORT=80
|
||||||
|
|
||||||
# Trigger background token refresh when usage reaches threshold percent
|
# Prepare next ChatGPT account when active usage reaches threshold percent
|
||||||
USAGE_REFRESH_THRESHOLD=85
|
CHATGPT_PREPARE_THRESHOLD=85
|
||||||
|
|
||||||
|
# Switch active ChatGPT account when usage reaches threshold percent
|
||||||
|
CHATGPT_SWITCH_THRESHOLD=95
|
||||||
|
|
||||||
# Persistent data directory (tokens, screenshots)
|
# Persistent data directory (tokens, screenshots)
|
||||||
DATA_DIR=/data
|
DATA_DIR=/data
|
||||||
|
|
|
||||||
101
README.md
101
README.md
|
|
@ -1,24 +1,17 @@
|
||||||
# megapt
|
# megapt
|
||||||
|
|
||||||
HTTP service that returns an active ChatGPT access token.
|
Service for issuing ChatGPT OAuth tokens via browser automation with disposable email.
|
||||||
|
|
||||||
The service can:
|
|
||||||
- restore/refresh a saved token from `/data`
|
|
||||||
- auto-register a new ChatGPT account when needed
|
|
||||||
- get verification email from a disposable mail provider (`temp-mail.org`)
|
|
||||||
- expose token and usage info via HTTP endpoint
|
|
||||||
|
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
- `GET /token` - legacy route (defaults to `chatgpt` provider)
|
- `GET /chatgpt/token`
|
||||||
- `GET /chatgpt/token` - explicit provider route
|
- `GET /token` (legacy alias, same as chatgpt)
|
||||||
|
|
||||||
Example response:
|
Response shape:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "<access_token>",
|
"token": "...",
|
||||||
"limit": {
|
"limit": {
|
||||||
"used_percent": 0,
|
"used_percent": 0,
|
||||||
"remaining_percent": 100,
|
"remaining_percent": 100,
|
||||||
|
|
@ -37,67 +30,47 @@ Example response:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
## Environment variables
|
- `PORT` - HTTP server port (default: `8080`)
|
||||||
|
- `DATA_DIR` - persistent data directory for tokens/screenshots (default: `./data`)
|
||||||
|
- `CHATGPT_PREPARE_THRESHOLD` - usage threshold to prepare `next_account` (default: `85`)
|
||||||
|
- `CHATGPT_SWITCH_THRESHOLD` - usage threshold to switch active account to `next_account` (default: `95`)
|
||||||
|
|
||||||
See `.env.example`.
|
Example config is in `.env.example`.
|
||||||
|
|
||||||
- `PORT` - HTTP port for the service
|
## Token Lifecycle
|
||||||
- `USAGE_REFRESH_THRESHOLD` - percent threshold to trigger background token rotation
|
|
||||||
- `DATA_DIR` - directory for persistent data (`chatgpt_tokens.json`, screenshots, etc.)
|
|
||||||
|
|
||||||
|
- **active account** - currently served token.
|
||||||
|
- **next account** - pre-created account/token stored for fast switch.
|
||||||
|
|
||||||
## Local run
|
Behavior:
|
||||||
|
|
||||||
Requirements:
|
1. If active token is valid, service returns it immediately.
|
||||||
- Python 3.14+
|
2. If active token is expired, service tries refresh under a single write lock.
|
||||||
- Playwright Chromium dependencies
|
3. If refresh fails or token is missing, service registers a new account (up to 4 attempts).
|
||||||
|
4. When usage reaches `CHATGPT_PREPARE_THRESHOLD`, service prepares `next_account`.
|
||||||
|
5. When usage reaches `CHATGPT_SWITCH_THRESHOLD`, service switches active account to `next_account`.
|
||||||
|
|
||||||
Install and run:
|
## Startup Behavior
|
||||||
|
|
||||||
|
On startup, service:
|
||||||
|
|
||||||
|
1. Ensures active token exists and is usable.
|
||||||
|
2. Ensures `next_account` is prepared for ChatGPT.
|
||||||
|
|
||||||
|
## Data Files
|
||||||
|
|
||||||
|
- `DATA_DIR/chatgpt_tokens.json` - token state with `active` and `next_account`.
|
||||||
|
- `DATA_DIR/screenshots/` - automation failure screenshots.
|
||||||
|
|
||||||
|
## Run Locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync --frozen --no-dev
|
PYTHONPATH=./src python src/server.py
|
||||||
./.venv/bin/python -m playwright install --with-deps chromium
|
|
||||||
PYTHONPATH=./src ./.venv/bin/python src/server.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then request token:
|
## Docker Notes
|
||||||
|
|
||||||
```bash
|
- Dockerfile sets `DATA_DIR=/data`.
|
||||||
curl http://127.0.0.1:8080/chatgpt/token
|
- `entrypoint.sh` starts Xvfb and runs `server.py`.
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Docker deployment
|
|
||||||
|
|
||||||
Build image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t megapt:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
Run container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name megapt \
|
|
||||||
--restart unless-stopped \
|
|
||||||
--env-file .env \
|
|
||||||
-v ./data:/data \
|
|
||||||
-p 80:80 \
|
|
||||||
megapt:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Check logs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f megapt
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Service performs a startup token check and tries to recover token automatically.
|
|
||||||
- Token write path is synchronized (single-writer lock) to avoid parallel re-registration.
|
|
||||||
- Browser runs in virtual display (`Xvfb`) inside container.
|
|
||||||
- Keep `/data` persistent between restarts.
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,30 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from playwright.async_api import BrowserContext
|
from playwright.async_api import BrowserContext
|
||||||
|
|
||||||
from providers.base import Provider, ProviderTokens
|
|
||||||
from email_providers import BaseProvider
|
from email_providers import BaseProvider
|
||||||
from email_providers import TempMailOrgProvider
|
from email_providers import TempMailOrgProvider
|
||||||
from .tokens import load_tokens, save_tokens, refresh_tokens
|
from providers.base import Provider, ProviderTokens
|
||||||
|
from .tokens import (
|
||||||
|
clear_next_tokens,
|
||||||
|
load_next_tokens,
|
||||||
|
load_state,
|
||||||
|
load_tokens,
|
||||||
|
promote_next_tokens,
|
||||||
|
refresh_tokens,
|
||||||
|
save_next_tokens,
|
||||||
|
save_tokens,
|
||||||
|
)
|
||||||
from .usage import get_usage_data
|
from .usage import get_usage_data
|
||||||
from .registration import register_chatgpt_account
|
from .registration import register_chatgpt_account
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
MAX_REGISTRATION_ATTEMPTS = 4
|
CHATGPT_REGISTRATION_MAX_ATTEMPTS = 4
|
||||||
|
CHATGPT_SWITCH_THRESHOLD = int(os.environ.get("CHATGPT_SWITCH_THRESHOLD", "95"))
|
||||||
|
|
||||||
|
|
||||||
class ChatGPTProvider(Provider):
|
class ChatGPTProvider(Provider):
|
||||||
|
|
@ -27,11 +38,11 @@ class ChatGPTProvider(Provider):
|
||||||
self._token_write_lock = asyncio.Lock()
|
self._token_write_lock = asyncio.Lock()
|
||||||
|
|
||||||
async def _register_with_retries(self) -> bool:
|
async def _register_with_retries(self) -> bool:
|
||||||
for attempt in range(1, MAX_REGISTRATION_ATTEMPTS + 1):
|
for attempt in range(1, CHATGPT_REGISTRATION_MAX_ATTEMPTS + 1):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Registration attempt %s/%s",
|
"Registration attempt %s/%s",
|
||||||
attempt,
|
attempt,
|
||||||
MAX_REGISTRATION_ATTEMPTS,
|
CHATGPT_REGISTRATION_MAX_ATTEMPTS,
|
||||||
)
|
)
|
||||||
success = await self.register_new_account()
|
success = await self.register_new_account()
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -39,16 +50,75 @@ class ChatGPTProvider(Provider):
|
||||||
logger.warning("Registration attempt %s failed", attempt)
|
logger.warning("Registration attempt %s failed", attempt)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _create_next_account_under_lock(self) -> bool:
|
||||||
|
active_before, next_before = load_state()
|
||||||
|
if next_before:
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info("Creating next account")
|
||||||
|
success = await self._register_with_retries()
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
|
||||||
|
generated_active = load_tokens()
|
||||||
|
if not generated_active:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Registration writes new tokens as active; restore old active and keep
|
||||||
|
# generated account as next.
|
||||||
|
if active_before:
|
||||||
|
save_tokens(active_before)
|
||||||
|
else:
|
||||||
|
clear_next_tokens()
|
||||||
|
save_next_tokens(generated_active)
|
||||||
|
logger.info("Next account is ready")
|
||||||
|
return True
|
||||||
|
|
||||||
async def force_recreate_token(self) -> str | None:
|
async def force_recreate_token(self) -> str | None:
|
||||||
async with self._token_write_lock:
|
async with self._token_write_lock:
|
||||||
success = await self._register_with_retries()
|
success = await self._register_with_retries()
|
||||||
if not success:
|
if not success:
|
||||||
return None
|
return None
|
||||||
|
clear_next_tokens()
|
||||||
tokens = load_tokens()
|
tokens = load_tokens()
|
||||||
if not tokens:
|
if not tokens:
|
||||||
return None
|
return None
|
||||||
return tokens.access_token
|
return tokens.access_token
|
||||||
|
|
||||||
|
async def ensure_next_account(self) -> bool:
|
||||||
|
next_tokens = load_next_tokens()
|
||||||
|
if next_tokens and not next_tokens.is_expired:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async with self._token_write_lock:
|
||||||
|
next_tokens = load_next_tokens()
|
||||||
|
if next_tokens and not next_tokens.is_expired:
|
||||||
|
return True
|
||||||
|
return await self._create_next_account_under_lock()
|
||||||
|
|
||||||
|
async def maybe_switch_active_account(self, usage_percent: int) -> bool:
|
||||||
|
if usage_percent < CHATGPT_SWITCH_THRESHOLD:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._token_write_lock:
|
||||||
|
next_tokens = load_next_tokens()
|
||||||
|
if not next_tokens or next_tokens.is_expired:
|
||||||
|
logger.info(
|
||||||
|
"Active usage >= %s%% and next account missing",
|
||||||
|
CHATGPT_SWITCH_THRESHOLD,
|
||||||
|
)
|
||||||
|
created = await self._create_next_account_under_lock()
|
||||||
|
if not created:
|
||||||
|
return False
|
||||||
|
|
||||||
|
switched = promote_next_tokens()
|
||||||
|
if switched:
|
||||||
|
logger.info(
|
||||||
|
"Switched active account (usage >= %s%%)",
|
||||||
|
CHATGPT_SWITCH_THRESHOLD,
|
||||||
|
)
|
||||||
|
return switched
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return "chatgpt"
|
return "chatgpt"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import aiohttp
|
|
||||||
from pathlib import Path
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from providers.base import ProviderTokens
|
from providers.base import ProviderTokens
|
||||||
|
|
||||||
|
|
@ -16,33 +18,108 @@ CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||||
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
TOKEN_URL = "https://auth.openai.com/oauth/token"
|
||||||
|
|
||||||
|
|
||||||
def load_tokens() -> ProviderTokens | None:
|
def _tokens_to_dict(tokens: ProviderTokens) -> dict[str, Any]:
|
||||||
if not TOKENS_FILE.exists():
|
return {
|
||||||
|
"access_token": tokens.access_token,
|
||||||
|
"refresh_token": tokens.refresh_token,
|
||||||
|
"expires_at": tokens.expires_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_to_tokens(data: dict[str, Any] | None) -> ProviderTokens | None:
|
||||||
|
if not isinstance(data, dict):
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
with open(TOKENS_FILE) as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return ProviderTokens(
|
return ProviderTokens(
|
||||||
access_token=data["access_token"],
|
access_token=data["access_token"],
|
||||||
refresh_token=data["refresh_token"],
|
refresh_token=data["refresh_token"],
|
||||||
expires_at=data["expires_at"],
|
expires_at=data["expires_at"],
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError, KeyError:
|
except KeyError, TypeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_tokens(tokens: ProviderTokens):
|
def _load_raw() -> dict[str, Any] | None:
|
||||||
|
if not TOKENS_FILE.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(TOKENS_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _save_raw(data: dict[str, Any]) -> None:
|
||||||
TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
TOKENS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(TOKENS_FILE, "w") as f:
|
with open(TOKENS_FILE, "w") as f:
|
||||||
json.dump(
|
json.dump(data, f, indent=2)
|
||||||
{
|
|
||||||
"access_token": tokens.access_token,
|
|
||||||
"refresh_token": tokens.refresh_token,
|
def _normalize_state(data: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
"expires_at": tokens.expires_at,
|
if not data:
|
||||||
},
|
return {"active": None, "next_account": None}
|
||||||
f,
|
|
||||||
indent=2,
|
if "active" in data or "next_account" in data:
|
||||||
)
|
return {
|
||||||
|
"active": data.get("active"),
|
||||||
|
"next_account": data.get("next_account"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backward compatibility with old flat schema
|
||||||
|
return {"active": data, "next_account": None}
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> tuple[ProviderTokens | None, ProviderTokens | None]:
|
||||||
|
normalized = _normalize_state(_load_raw())
|
||||||
|
active = _dict_to_tokens(normalized.get("active"))
|
||||||
|
next_account = _dict_to_tokens(normalized.get("next_account"))
|
||||||
|
return active, next_account
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(
|
||||||
|
active: ProviderTokens | None, next_account: ProviderTokens | None
|
||||||
|
) -> None:
|
||||||
|
payload = {
|
||||||
|
"active": _tokens_to_dict(active) if active else None,
|
||||||
|
"next_account": _tokens_to_dict(next_account) if next_account else None,
|
||||||
|
}
|
||||||
|
_save_raw(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def load_tokens() -> ProviderTokens | None:
|
||||||
|
active, _ = load_state()
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
def load_next_tokens() -> ProviderTokens | None:
|
||||||
|
_, next_account = load_state()
|
||||||
|
return next_account
|
||||||
|
|
||||||
|
|
||||||
|
def save_tokens(tokens: ProviderTokens):
|
||||||
|
_, next_account = load_state()
|
||||||
|
save_state(tokens, next_account)
|
||||||
|
|
||||||
|
|
||||||
|
def save_next_tokens(tokens: ProviderTokens):
|
||||||
|
active, _ = load_state()
|
||||||
|
save_state(active, tokens)
|
||||||
|
|
||||||
|
|
||||||
|
def promote_next_tokens() -> bool:
|
||||||
|
active, next_account = load_state()
|
||||||
|
if not next_account:
|
||||||
|
return False
|
||||||
|
save_state(next_account, None)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clear_next_tokens():
|
||||||
|
active, _ = load_state()
|
||||||
|
save_state(active, None)
|
||||||
|
|
||||||
|
|
||||||
async def refresh_tokens(refresh_token: str) -> ProviderTokens | None:
|
async def refresh_tokens(refresh_token: str) -> ProviderTokens | None:
|
||||||
|
|
@ -55,7 +132,7 @@ async def refresh_tokens(refresh_token: str) -> ProviderTokens | None:
|
||||||
async with session.post(TOKEN_URL, data=data) as resp:
|
async with session.post(TOKEN_URL, data=data) as resp:
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
print(f"Token refresh failed: {resp.status} {text}")
|
logger.warning("Token refresh failed: %s %s", resp.status, text)
|
||||||
return None
|
return None
|
||||||
json_resp = await resp.json()
|
json_resp = await resp.json()
|
||||||
expires_in = json_resp["expires_in"]
|
expires_in = json_resp["expires_in"]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from aiohttp import web
|
||||||
from providers.chatgpt import ChatGPTProvider
|
from providers.chatgpt import ChatGPTProvider
|
||||||
|
|
||||||
PORT = int(os.environ.get("PORT", "8080"))
|
PORT = int(os.environ.get("PORT", "8080"))
|
||||||
USAGE_REFRESH_THRESHOLD = int(os.environ.get("USAGE_REFRESH_THRESHOLD", "85"))
|
CHATGPT_PREPARE_THRESHOLD = int(os.environ.get("CHATGPT_PREPARE_THRESHOLD", "85"))
|
||||||
LIMIT_EXHAUSTED_PERCENT = 100
|
LIMIT_EXHAUSTED_PERCENT = 100
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
@ -37,7 +37,7 @@ def build_limit(usage_percent: int) -> dict[str, int | bool]:
|
||||||
"used_percent": usage_percent,
|
"used_percent": usage_percent,
|
||||||
"remaining_percent": remaining,
|
"remaining_percent": remaining,
|
||||||
"exhausted": usage_percent >= LIMIT_EXHAUSTED_PERCENT,
|
"exhausted": usage_percent >= LIMIT_EXHAUSTED_PERCENT,
|
||||||
"needs_refresh": usage_percent >= USAGE_REFRESH_THRESHOLD,
|
"needs_refresh": usage_percent >= CHATGPT_PREPARE_THRESHOLD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,6 +59,9 @@ async def ensure_provider_token_ready(provider_name: str):
|
||||||
logger.error("[%s] Could not prepare token at startup", provider_name)
|
logger.error("[%s] Could not prepare token at startup", provider_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if isinstance(provider, ChatGPTProvider):
|
||||||
|
await provider.ensure_next_account()
|
||||||
|
|
||||||
usage_info = await provider.get_usage_info(token)
|
usage_info = await provider.get_usage_info(token)
|
||||||
if "error" not in usage_info:
|
if "error" not in usage_info:
|
||||||
logger.info("[%s] Startup token is ready", provider_name)
|
logger.info("[%s] Startup token is ready", provider_name)
|
||||||
|
|
@ -157,6 +160,27 @@ async def token_handler(request: web.Request) -> web.Response:
|
||||||
usage_percent = usage_info.get("used_percent", 0)
|
usage_percent = usage_info.get("used_percent", 0)
|
||||||
remaining_percent = usage_info.get("remaining_percent", max(0, 100 - usage_percent))
|
remaining_percent = usage_info.get("remaining_percent", max(0, 100 - usage_percent))
|
||||||
|
|
||||||
|
if isinstance(provider, ChatGPTProvider):
|
||||||
|
switched = await provider.maybe_switch_active_account(usage_percent)
|
||||||
|
if switched:
|
||||||
|
token = await provider.get_token()
|
||||||
|
if not token:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Failed to get active token after account switch"},
|
||||||
|
status=503,
|
||||||
|
)
|
||||||
|
usage_info = await provider.get_usage_info(token)
|
||||||
|
if "error" in usage_info:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": usage_info["error"]},
|
||||||
|
status=503,
|
||||||
|
)
|
||||||
|
usage_percent = usage_info.get("used_percent", 0)
|
||||||
|
remaining_percent = usage_info.get(
|
||||||
|
"remaining_percent", max(0, 100 - usage_percent)
|
||||||
|
)
|
||||||
|
logger.info("[%s] Active account switched before response", provider_name)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[%s] token issued, used=%s%% remaining=%s%%",
|
"[%s] token issued, used=%s%% remaining=%s%%",
|
||||||
provider_name,
|
provider_name,
|
||||||
|
|
@ -184,11 +208,14 @@ async def token_handler(request: web.Request) -> web.Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Trigger background refresh if needed
|
# Trigger background refresh if needed
|
||||||
if usage_percent >= USAGE_REFRESH_THRESHOLD:
|
if usage_percent >= CHATGPT_PREPARE_THRESHOLD:
|
||||||
trigger_background_refresh(
|
if isinstance(provider, ChatGPTProvider):
|
||||||
provider_name,
|
await provider.ensure_next_account()
|
||||||
f"usage {usage_percent}% >= threshold {USAGE_REFRESH_THRESHOLD}%",
|
else:
|
||||||
)
|
trigger_background_refresh(
|
||||||
|
provider_name,
|
||||||
|
f"usage {usage_percent}% >= threshold {CHATGPT_PREPARE_THRESHOLD}%",
|
||||||
|
)
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
|
|
@ -214,7 +241,10 @@ def create_app() -> web.Application:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger.info("Starting token service on port %s", PORT)
|
logger.info("Starting token service on port %s", PORT)
|
||||||
logger.info("Usage refresh threshold: %s%%", USAGE_REFRESH_THRESHOLD)
|
logger.info(
|
||||||
|
"ChatGPT prepare-next threshold: %s%%",
|
||||||
|
CHATGPT_PREPARE_THRESHOLD,
|
||||||
|
)
|
||||||
logger.info("Available providers: %s", ", ".join(PROVIDERS.keys()))
|
logger.info("Available providers: %s", ", ".join(PROVIDERS.keys()))
|
||||||
app = create_app()
|
app = create_app()
|
||||||
web.run_app(app, host="0.0.0.0", port=PORT)
|
web.run_app(app, host="0.0.0.0", port=PORT)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue