# Provider-specific integration notes (verified 2026-07-05)

Compact knowledge bank for the two providers built and shipped on 2026-07-05. Each section is enough to write a fresh `fetch_<provider>` function without scraping upstream docs.

## MiniMax Coding Plan

**Auth:** `Authorization: Bearer sk-cp-...` (note the `sk-cp-` prefix; not all `sk-` strings are MiniMax keys — `sk-DW...` here turned out to be **OpenCode API key** not cookie, see pitfalls).

**Endpoint:** `GET https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains`

**Status codes:**
- 0 → success
- 1004 → invalid api_key (return as `success=False, error="API Key 无效"`)
- any other → use `status_msg` as error string

**Response shape (verbatim from a real call):**

```json
{
  "base_resp": { "status_code": 0, "status_msg": "success" },
  "model_remains": [
    {
      "model_name": "general",
      "current_interval_total_count": 0,
      "current_interval_usage_count": 0,
      "current_interval_remaining_percent": 75,
      "current_weekly_total_count": 0,
      "current_weekly_usage_count": 0,
      "current_weekly_remaining_percent": 82,
      "remains_time": 6358271,
      "weekly_remains_time": 56758271
    },
    {
      "model_name": "video",
      "current_interval_total_count": 3,
      "current_interval_usage_count": 3,
      "current_interval_remaining_percent": 100,
      "current_weekly_total_count": 21,
      "current_weekly_usage_count": 21,
      "current_weekly_remaining_percent": 100
    }
  ]
}
```

**Critical field-naming gotcha:**
The names `current_interval_usage_count` and `current_weekly_usage_count` are MISLEADING. In a quota-of-3-with-used-3 account, both fields return `3`. Treat them as noise. The authoritative number is **`*_remaining_percent`**: integer 0-100, where 100 = fully unused, 0 = fully consumed. Compute `used_pct = 100 - remaining_pct`.

**Do NOT use:**
- `minimax-usage`'s `Remains` model (TypeScript, in https://github.com/Eyozy/minimax-usage) — it also misinterprets `*_usage_count` as "remaining" and produces wrong numbers. If you port the upstream code blindly you'll see `0 / 3 video` instead of `100% video remaining`.
- The `*_total_count` field for denominator — it's a quota cap, not a denominator for percentage.

**Display strategy:**
- If `remaining_pct == 0` AND `total > 0`: render as absolute (`used 3 of 3 requests`). Otherwise render as percent.

**Quota reset:** `remains_time` is **milliseconds**, not seconds. Divide by 1000 for `reset_in_sec`. The other window (`weekly_remains_time`) is also ms.

**Iteration:** loop over `model_remains`. For each entry produce (a) `Window` for the window state and (b) `Window` labeled `"Weekly"` if weekly data exists. Don't dedupe across the list — each model has its own per-window tracking.

---

## OpenCode Go (dashboard scrape)

**Auth:** Cookie `auth=Fe26.2**...` — this is a JWT-format cookie set by opencode.ai's session. **NOT** an API key (`sk-...` strings — even if labeled "opencode" — almost certainly won't work as a cookie).

If you receive `sk-PX3C6...` / `sk-DWcV...` from a user labeled "opencode", those are likely **OpenCode API keys** used for the OpenAI-compatible API. They are NOT cookies and will result in 302 → `/auth/authorize`. Ask the user to verify by opening DevTools → Network → any dashboard request → Cookie header. Confirm shape `auth=Fe26.2**...` before treating as cookie.

**No public API.** The Dashboard at `https://opencode.ai/workspace/<wrk_id>/go` is a server-rendered HTML page (with RSC stream) that embeds the quota values in a JS-friendly payload. You scrape that HTML and regex out the values.

**Discovery flow when the user gives a cookie but no workspace_id:**

1. `GET https://opencode.ai/go` with the cookie header.
2. Response body contains a `wrk_<id>` token. Extract with `re.findall(r"wrk_[A-Za-z0-9]+\", body)` and pick one (often the only one — but if there are multiple, no clear "primary" indicator, so take the first sorted).
3. Then `GET https://opencode.ai/workspace/<wrk_id>/go` for the actual quota values.

If the user gives both cookie AND explicit workspace name like "Default", step 2 still yields the real `wrk_` ID regardless of what name the user said.

**RSC payload format (this can change without notice — verify before relying on it):**

```text
... useBalance:!1, region:$R[30]=["us","eu","sg","cn"],
    rollingUsage:$R[31]={status:"ok",resetInSec:18000,usagePercent:0},
    weeklyUsage:$R[32]={status:"ok",resetInSec:92598,usagePercent:23},
    monthlyUsage:$R[33]={status:"ok",resetInSec:1342783,usagePercent:30} ...
```

`resetInSec` and `usagePercent` are in **either order** within the `{ ... }` block, depending on how the React Server Components renderer emitted the properties. You need TWO regex patterns per window:

- **Primary regex** (assumes usagePercent first): `usagePercent\s*:\s*(-?\d+(?:\.\d+)?)[^}]*resetInSec\s*:\s*(-?\d+(?:\.\d+)?)`
- **Alt regex** (assumes resetInSec first): `resetInSec\s*:\s*(-?\d+(?:\.\d+)?)[^}]*usagePercent\s*:\s*(-?\d+(?:\.\d+)?)`

**The bug we hit (2026-07-05):** A list comprehension generated the alt pattern with `"5h Rolling".split()[0].lower()` → `"5h"` as the keyword, so it became `5hUsage:...`, which never matches the actual `rollingUsage:` prefix. Fix: write each alt pattern explicitly, do not derive the keyword from the label.

**Decode groups:**
- If primary matched: `g1 = pct, g2 = reset`.
- If alt matched: `g1 = reset, g2 = pct`.

Track which one matched with a flag — don't try/except on `int(float(...))`.

**The "silent always 100%" decoder-swap bug (2026-07-05, observed in production):** Even with the dual-regex pattern in place, accidentally using the primary-order decoder regardless of which regex matched produces a silent failure that looks fine end-to-end. Symptom: every account / window returns `used: 100.0, remaining: 0.0` with `reset_at` still pointing to a sensible future time. The decode-swapped reset value (e.g. 85268s, way beyond a 5h window) makes the `reset_in_sec` field uncharacteristically large — the **only** diagnostic signal that something is wrong. Three detections during testing:

1. **Look at the JSON response** as raw text and check whether `reset_in_sec` is roughly sane for each window. A 5h Rolling window showing `reset_in_sec: 85268` is wrong (should max out around 18000). A weekly window with `reset_in_sec: 1234573` is similarly off.
2. **Compare against a known-good OpenAI call** — if you can call the upstream via the `sk-PX...` API key path (different endpoint, but same underlying numbers), use that as ground truth.
3. **Pick a window where the user knows the real value** (e.g. "I just used 30% of my weekly budget last night"). If the UI shows 100%, the decoder swapped.

Code reference (the only way to ship it right):

```python
for (label, primary_re), (_, alt_re) in zip(RE_WINDOWS, RE_WINDOWS_ALT):
    m = primary_re.search(html)
    used_alt = False
    if not m:
        m = alt_re.search(html)
        used_alt = True
    if not m:
        continue
    g1, g2 = m.group(1), m.group(2)
    # Do NOT do try/except on int(float(...)) — it silently swaps the
    # wrong way and you get "always 100%" output.
    if used_alt:
        reset = int(float(g1))
        pct = float(g2)
    else:
        pct = float(g1)
        reset = int(float(g2))
    pct = max(0.0, min(100.0, pct))
```

**Streaming RSC:** OpenCode dashboard is *streaming* RSC — the HTTP response may chunk and the first read may not contain all of `rollingUsage/weeklyUsage/monthlyUsage`. If you see only Weekly + Monthly parse but Rolling missing, re-scrape. On a normal good fetch all three should appear together within the first 16 KB of HTML.

**Reset time conversion:** `resetInSec` is in **seconds**, not milliseconds (unlike MiniMax's `remains_time`). Convert directly.

**Auth failure:** when cookie is invalid or expired, the dashboard returns a 302 redirect to `/auth/authorize`. Treat as auth failure: `success=False, error="认证失败"`.

**A note about field drift:** OpenCode's RSC payload format is undocumented private API. If the dashboard shape changes (e.g. they rename `usagePercent`, add `usagePercent:null` for unset windows, or wrap each usage in a nested object), every regex above stops matching. The diagnostic is the same — `No windows in response` from `parse_quota_html`, after `cookies + workspace_id + 200 status` are all green. Re-fetch a fresh dashboard page in a browser, view-source, and diff against the example block above.

---

## Cross-provider conventions

**Dataclass shape (used for both):**

```python
from dataclasses import asdict, dataclass, field

@dataclass
class Window:
    label: str           # e.g. "5h Rolling" | "general" | "Weekly"
    used: float
    remaining: float
    total: float
    unit: str            # "%" or "requests"
    reset_at: str        # ISO8601 with Z suffix, server clock (UTC)
    reset_in_sec: int    # for `setInterval` countdown in UI

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class AccountResult:
    index: int
    name: str
    success: bool
    updated_at: str
    windows: list[Window] = field(default_factory=list)
    error: str = ""
    workspace: str = ""   # for OpenCode Go (the wrk_id)

    def to_dict(self) -> dict:
        d = {
            "index": self.index,
            "name": self.name,
            "success": self.success,
            "updated_at": self.updated_at,
        }
        if self.workspace:
            d["workspace"] = self.workspace
        if self.error:
            d["error"] = self.error
        if self.windows:
            d["windows"] = [w.to_dict() for w in self.windows]
        return d
```

`asdict()` from `dataclasses` handles the Window conversion cleanly. **Crucially: FastAPI's response-model validation fails for `@dataclass` lists**, so always call `.to_dict()` and return `list[dict]` (NOT `list[AccountResult]`).

```python
@app.get("/api/<provider>/quota")
async def quota() -> list[dict]:
    cfg = load_config()
    results: list[dict] = []
    for i, account in enumerate(getattr(cfg, f"{provider}_accounts")):
        r = await fetch_<provider>(account, i)
        results.append(r.to_dict())
    return results
```
