fix(usage): bound quota HTTP requests

Merged
DN
7d ago
+223-9

Why

amp, claude, codex, copilot, kimi, minimax, minimax_tokenplan and zai build their HTTP client from tokscale_core::http::client(), which sets no timeout. A server that stalls before or during the response can hang a usage refresh indefinitely.

What changed

  • Add helpers::http_client(), which bounds each request, including the body read, to 30 seconds, and route those eight adapters through it.
  • 30s matches the adapters that already set a bound (opencode_go, sakana and cursor; grok uses 12s). Those are left unchanged.
  • A refresh and the retry after it each get their own 30s deadline.

Tradeoffs

Two requests change server state, and a timeout does not undo them:

  • Codex consume_reset_credit is a POST. If it times out, the server may still have redeemed the credit. A retry from the TUI uses a fresh redeem_request_id, so it can redeem a second credit.
  • Codex and Kimi send OAuth refreshes through this client. If the provider rotates the refresh token and the client times out after the server issued the new pair, the new refresh token is lost.

Without a timeout the same ambiguity exists; it shows up as a hang that the user ends by killing the process or retrying. A longer timeout narrows the window but does not close it. Closing it means reusing redeem_request_id across retries, which this PR does not attempt.

Blast Radius

Usage quota fetches for the eight adapters above. Requests that used to hang now fail after 30s with a timeout error. No other HTTP callers change.

Verification

  • helpers.rs tests: a loopback server that stalls before the headers, and one that stalls after one byte of a ten-byte body. A client with an injected 100ms timeout fails with is_timeout() inside a 2s watchdog. Negative control: on the same fixture, client_builder() without a timeout is still waiting when a 300ms watchdog fires.
  • tests/usage_http_policy.rs scans non-test code under commands/usage/ for direct tokscale_core::http::client() calls, in the style of tests/tls_policy.rs. Restoring upstream amp.rs makes it fail. A second test checks that the scan flags shipped calls and skips #[cfg(test)] mod tests.
  • cargo fmt --check and cargo clippy -p tokscale-cli --all-targets -- -D warnings are clean.

šŸ¤– Generated with Claude Code


Summary by cubic

Bounds usage quota HTTP requests to 30 seconds so a stalled server can no longer hang a usage refresh indefinitely.

amp, claude, codex, copilot, kimi, minimax, minimax_tokenplan, and zai now build their client through a shared http_client() helper that sets a request timeout covering the body read; opencode_go, sakana, cursor, and grok already bounded requests and are unchanged. A refresh and its retry each get their own deadline.

Side effects

  • Codex's consume_reset_credit POST may still redeem a credit server-side after a timeout; a retry uses a fresh redeem_request_id, so it can redeem a second credit.
  • Codex and Kimi OAuth refreshes may lose a rotated refresh token if the timeout hits after the provider issued the new pair.
  • Adds a source-scan test, tests/usage_http_policy.rs, that fails if an adapter calls the unbounded shared client directly.