188 lines
4.9 KiB
Markdown
188 lines
4.9 KiB
Markdown
# Plan: rewrite token selection around a simple disk-first state model
|
|
|
|
## Goal
|
|
|
|
Throw away the current layered selection/cooldown/state model and replace it with a small implementation that:
|
|
|
|
- reads the main JSON file on every `/token` request
|
|
- keeps only the minimum necessary account fields on disk
|
|
- decides from file state first
|
|
- refreshes usage only when missing or stale
|
|
- validates the selected token before returning it
|
|
- moves invalid accounts to `failed.json`
|
|
- does not touch the helper scripts in this pass
|
|
|
|
## Required file model
|
|
|
|
### Main state file
|
|
|
|
`accounts.json`
|
|
|
|
```json
|
|
{
|
|
"active_account": "user@example.com",
|
|
"accounts": [
|
|
{
|
|
"email": "user@example.com",
|
|
"access_token": "...",
|
|
"refresh_token": "...",
|
|
"token_refresh_at": 1710000000,
|
|
"usage": {
|
|
"primary": {
|
|
"used_percent": 72,
|
|
"reset_at": 1710018000
|
|
},
|
|
"secondary": {
|
|
"used_percent": 18,
|
|
"reset_at": 1710600000
|
|
}
|
|
},
|
|
"usage_checked_at": 1710000000,
|
|
"disabled": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Only these fields should exist for account state.
|
|
|
|
### Failed state file
|
|
|
|
`failed.json`
|
|
|
|
```json
|
|
{
|
|
"accounts": [
|
|
{
|
|
"email": "bad@example.com",
|
|
"access_token": "...",
|
|
"refresh_token": "...",
|
|
"token_refresh_at": 1710000000,
|
|
"usage": {
|
|
"primary": {
|
|
"used_percent": 100,
|
|
"reset_at": 1710018000
|
|
},
|
|
"secondary": {
|
|
"used_percent": 100,
|
|
"reset_at": 1710600000
|
|
}
|
|
},
|
|
"usage_checked_at": 1710000000,
|
|
"disabled": false
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Top-level must contain only `accounts`.
|
|
|
|
## Selection rules
|
|
|
|
### Active account first
|
|
|
|
For each `/token` request:
|
|
|
|
1. Read `accounts.json` fresh from disk.
|
|
2. Resolve `active_account` by email.
|
|
3. Evaluate active first.
|
|
|
|
### When an account is usable
|
|
|
|
An account is usable when:
|
|
|
|
- `disabled == false`
|
|
- `secondary.used_percent < 100`
|
|
- `primary.used_percent < GIBBY_EXHAUSTED_USAGE_THRESHOLD`
|
|
|
|
Default threshold remains `95`.
|
|
|
|
### Usage freshness
|
|
|
|
Usage must be refreshed only when missing or stale.
|
|
|
|
Add env:
|
|
|
|
- `GIBBY_USAGE_STALE_SECONDS`, default `3600`
|
|
|
|
Usage is stale when:
|
|
|
|
- `usage` is missing
|
|
- `usage_checked_at` is missing
|
|
- `now - usage_checked_at > GIBBY_USAGE_STALE_SECONDS`
|
|
|
|
If active account usage is stale or missing, refresh usage for that account before deciding if it is usable.
|
|
|
|
### Fallback selection
|
|
|
|
If active account cannot be used, choose the next account by:
|
|
|
|
- filtering to usable accounts
|
|
- sorting by highest primary `used_percent`
|
|
- using file order as the tie-breaker
|
|
|
|
If a new account is chosen, write its email into `active_account` in `accounts.json`.
|
|
|
|
## Token flow
|
|
|
|
For the chosen account:
|
|
|
|
1. Ensure token is fresh enough.
|
|
2. If `token_refresh_at` says refresh is needed, refresh token and persist new values.
|
|
3. After selection decisions are finished and the token is ready, validate it by calling:
|
|
|
|
`https://chatgpt.com/backend-api/codex/models`
|
|
|
|
4. Only return the token if validation returns `200`.
|
|
|
|
## Invalid account handling
|
|
|
|
If refresh, usage auth, or final validation shows the token/account is invalid:
|
|
|
|
1. Read current main state.
|
|
2. Remove that full account object from `accounts.json`.
|
|
3. Append the same full account object to `failed.json.accounts`.
|
|
4. If it was the active account, clear `active_account` before reselection.
|
|
5. Persist both files atomically.
|
|
|
|
No `failed.txt` in the rewritten core flow.
|
|
|
|
## Files to rewrite
|
|
|
|
- `/home/wzray/AI/gibby/src/gibby/settings.py`
|
|
- keep only env needed for the new flow
|
|
- `/home/wzray/AI/gibby/src/gibby/store.py`
|
|
- rewrite as simple JSON read/write helpers for `accounts.json` and `failed.json`
|
|
- `/home/wzray/AI/gibby/src/gibby/client.py`
|
|
- keep only token refresh, usage fetch, and token validation calls
|
|
- `/home/wzray/AI/gibby/src/gibby/manager.py`
|
|
- rewrite into one small service for `/token`
|
|
- `/home/wzray/AI/gibby/src/gibby/app.py`
|
|
- keep thin FastAPI wiring for `/health` and `/token`
|
|
|
|
## Files to remove or stop using
|
|
|
|
- `/home/wzray/AI/gibby/src/gibby/models.py`
|
|
- `/home/wzray/AI/gibby/src/gibby/account_ops.py`
|
|
|
|
Their logic should be folded into the new minimal data model and service flow instead of preserved.
|
|
|
|
## Out of scope for this pass
|
|
|
|
- do not touch `scripts/oauth_helper.py`
|
|
- do not touch `scripts/refresh_limits.py`
|
|
- do not preserve old cooldown, failed.txt, dual-state, or derived snapshot machinery unless absolutely required to keep app booting during rewrite
|
|
|
|
## Verification
|
|
|
|
- `uv run pytest -q`
|
|
- API tests for:
|
|
- `/health` returns `ok`
|
|
- `/token` returns `503` when file has no usable accounts
|
|
- `/token` prefers active account when usable
|
|
- `/token` rereads the file between requests
|
|
- stale usage triggers a refresh before decision
|
|
- fresh usage skips refresh
|
|
- invalid token moves full account object to `failed.json`
|
|
- fallback chooses highest primary usage among usable non-disabled accounts
|
|
- direct file tests for exact `accounts.json` and `failed.json` schema
|