Skip to content

feat: add live gRPC-web quota fetcher for grok-cli (#6844)#7714

Open
diegosouzapw wants to merge 2 commits into
release/v3.8.49from
feat/6844-grok-cli-quota
Open

feat: add live gRPC-web quota fetcher for grok-cli (#6844)#7714
diegosouzapw wants to merge 2 commits into
release/v3.8.49from
feat/6844-grok-cli-quota

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Adds a live quota fetcher for the grok-cli (Grok Build) provider, replacing sole reliance on the static 864 req/day / 18M tokens/day plan (src/lib/quota/planRegistry.ts) with a poll of xAI's actual shared weekly credit pool.

Scoped to the primary source only per the implementation plan's "2-3 PRs" split — the unauthenticated gRPC-web POST to grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig using the connection's existing bearer token. The optional local-CLI ACP secondary path, dashboard UI card, and reset-aware routing integration are explicitly deferred to follow-up PRs (see Non-Goals in the plan).

  • open-sse/services/grokCliQuotaFrame.ts (new): defensive, dependency-free varint/length-delimited protobuf decoder for the gRPC-web response — handles both framed (5-byte header) and raw response shapes, treats an omitted credit_usage_percent field as 0% used (proto3 default), and never throws (malformed/truncated buffers return null).
  • open-sse/services/grokCliQuotaFetcher.ts (new): fetchGrokCliQuota() / invalidateGrokCliQuotaCache() / registerGrokCliQuotaFetcher(), mirroring the v0QuotaFetcher.ts / agentrouterQuotaFetcher.ts pattern (60s in-memory TTL cache, throttleQuotaFetch(), fail-open on 401/5xx — no proactive refreshCredentials() call in this PR).
  • open-sse/services/quotaTrackersBatch.ts: wired the new fetcher into the existing batch-registration module — zero changes to the frozen src/sse/handlers/chat.ts chokepoint file.
  • src/lib/quota/planRegistry.ts: comment-only edit noting the static grok-cli plan is now the explicit fallback used only when the live fetch returns null.

How it was validated

TDD (Hard Rule #18): both new test files fail before this change (no grokCliQuotaFetcher.ts/grokCliQuotaFrame.ts module exists to import) and pass after:

node --import tsx/esm --test tests/unit/grok-cli-quota-frame.test.ts tests/unit/grok-cli-quota-fetcher.test.ts
# 16 pass, 0 fail

Test coverage:

  • Frame parser: framed buffer decode, raw/unframed fallback, omitted-field-as-0%, malformed/truncated → null, empty buffer → null, frame-header probe rejection cases.
  • Fetcher: missing credentials → null, correct headers + URL, happy-path percent/limitReached mapping, 401/5xx fail-open (no throw), unparseable body → null, TTL cache hit, cache invalidation, registerGrokCliQuotaFetcher()preflightQuota() integration.

Gates run (all green):

  • node scripts/check/check-test-discovery.mjs
  • npm run typecheck:core
  • npm run typecheck:noimplicit:core (pre-existing unrelated errors in combo.ts/usageTracking.ts/cliRuntime.ts confirmed untouched by this diff)
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <changed files>
  • npm run check:complexity-ratchets (2059/890, matches baseline)
  • node scripts/check/check-file-size.mjs (no on touched files)
  • npm run check:cycles

npm run test:coverage was intentionally not run in this session (shared/loaded box) — this PR is additive-only (2 new service files, 2 new test files, a 1-line import in the batch registration file, a comment-only edit to planRegistry.ts); CI runs the coverage gate authoritatively.

Closes #6844

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@diegosouzapw

Copy link
Copy Markdown
Owner Author

Live validation against a real Grok Build account — the fetcher never returns quota

I ran this branch against the real grok.com endpoint today with a live tier-4 token. Good news first: your one stated assumption is proven correct. grok.com does not block a plain fetch() — HTTP 200, content-type: application/grpc-web+proto, Cloudflare present but no challenge. No native-TLS/IPv4 client needed here, unlike the chat path. You can drop that caveat comment.

Three defects, though, and the first one means the feature is currently a silent no-op.

1. The request has no body → upstream rejects it

open-sse/services/grokCliQuotaFetcher.ts:125 POSTs with headers only, no body. gRPC-web requires a request frame:

no body            -> HTTP 200, 0 bytes, grpc-status: 13, grpc-message: "Missing request message."
5-byte empty frame -> HTTP 200, 119 bytes of real protobuf

Since the body comes back empty, decodeGrokCreditsFrame() returns null, fetchGrokCliQuota() returns null — on every call. The result is invisible: it just always falls back to the static 864/day estimate.

Minimum fix: body: Buffer.from([0, 0, 0, 0, 0]) (compressed-flag 0 + uint32 BE length 0).

2. The field mapping doesn't match the real response

With a valid request frame, decodeGrokCreditsFrame(<the real 119 bytes>) still returns null. The schema from the third-party doc doesn't hold. Actual wire structure:

field 1 (length-delimited, 92B)        <- nested message, NOT a fixed64 double
   field 1 (fixed32/float) = 1.0       <- the ratio/percentage lives here
   field 4 {1:1784221140, 2:867850000} <- Timestamp (seconds, nanos)
   field 5 {1:1784825940, 2:867850000} <- Timestamp (reset?)
   field 7 {1:4, 2:1.0} / {1:2} / {1:5}
   field 8 {1:2, 2:Timestamp, 3:Timestamp}
   field 11=1  field 13=1

So the percentage is a fixed32 inside a nested message, and the reset is a Timestamp{seconds,nanos} — not a top-level double and a string. Worth noting the failure mode is safe: it fails closed (null) rather than reporting a wrong percentage.

3. The trailer frame isn't handled

The response carries a second frame with flag 0x80 + grpc-status:0\r\n. The decoder should skip it.

Full hex of the real response, usable directly as a fixture:

000000005e0a5c0d0000803f12001a00220c08d49be4d2061090aee99d032a0c08d49089d3061090aee99d03
3a070804150000803f3a0208023a020805421e0802120c08d49be4d2061090aee99d031a0c08d49089d30610
90aee99d03580162006801800000000f677270632d7374617475733a300d0a

Why CI stayed green

The 16 tests pass because they build their buffers with the test's own mirrored encoder — the request side and the real schema are never exercised. Replacing the synthetic fixtures with the bytes above would have caught all three.

Verdict moved from needs-vps to fix-in-place. The approach is sound and the caching/throttling/bounds-checking around it is good work — it's the wire contract that needs correcting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(providers): Grok Build (grok-cli) quota tracking

1 participant