Repository navigation
AI: status, subscriber gating and daily-quota surfacing #10
Description
Activity
- addedparityWeb/API feature-parity workWeb/API feature-parity workP0Must have for a credible parity claimMust have for a credible parity claimarea:aiArea: aiArea: ai
on Sep 15, 2026 Live-verified corrections from the service layer (#9 / PR #137) — read before building this
Five things contradict what the AI epic and these issues assume.
1.
quotais not the same object on both endpoints.
GET /api/ai/status→{usedToday, dailyLimit, remaining}.POST /api/ai/suggest→{usedToday, dailyLimit}with noremaining(verified twice). SoAiQuota.Remainingisint?, and a post-suggest quota chip cannot read.Remaining— bindRemainingOrComputedinstead. #10's "remaining quota is visible wherever an AI action is offered" has to account for that.2. There is no provider picker to build.
AI is Anthropic-only, app-wide; per-user provider keys were removed server-side 2026-09-05. Theproviderfield on/generateis audit-ledger-only and does not select anything, and theopenai/geminientries indefaultModelsare dormant and unreachable. Don't surface a provider choice.3.
crossPosthas six fields, not two —crossPostToBluesky,selectedMastodonIds,crossPostToLinkedIn,selectedLinkedInTargets,crossPostToTwitter,linkedInLinkAsFirstComment. BothcrossPostandscheduleImmediatelyaremessage_series-only (relevant to #12).4. A sixth
422code exists that the epic didn't list:refused. Treat it likeinvalid_ai_output. Modelled and marked retryable.5. Quota accounting is asymmetric, and it shapes the UI.
Input-validation422s cost nothing (usedTodaystayed 0 across two), but any call that reaches the model costs a unit — including failures. So client-side pre-flight is load-bearing, not politeness: enforce the word caps, the 10-word series gate (#12/#13), a missingpowered_documentsource ref (#15), and the non-persistable-artifact rule (#11) before calling. And no auto-retry — a retry burns another unit.Also:
context.spacingMinutesis accepted but ignored (series rows always land 4 minutes apart), so it's deliberately not exposed.Note on the exception type
The
codeis surfaced on a newAiApiException, not onInterlinedApiException— the latter issealedand was being rewritten in #130. Folding them together is flagged as a follow-up in the code. CatchAiApiExceptionin these UI issues.Unverified
POST /api/ai/generatewas never called (it persists content to a shared test account). Its envelope is contract-transcribed, flagged unverified in doc comments, and keeps the rawcreatedelement for read-after-write. Whichever of #12–#15 lands first should verify it against a real account and tighten the model.Implemented in #173 — work continues in the PR from here.
The shared
AiAvailabilityServicefetchesGET /api/ai/statusonce per session (single-flighted, reset on account change with a generation guard so a stale response can't reopen the gate for a signed-out user) and exposes the oneIsAiAvailablegate — subscriber AND providers non-empty, with unknown counting as false so a failed status fetch hides AI rather than guessing. The gate deliberately excludes quota: a subscriber who has spent today's 50 keeps the controls and gets an in-place amber notice, since hiding them would read as a bug.AiQuotaChipis the "45 of 50 left today" surface as a one-tag drop-in bound toRemainingOrComputed, andAiNotice/AiNoticeBarkeepquota_exceededandrate_limitedvisually distinct from each other and from a generic red failure, with no dialog anywhere in the path.One live finding is worth carrying forward: an unrecognized
context.modeis not cheap-rejected — it reaches the model and bills a unit asinvalid_ai_output— while every genuine input-validation 422 costs nothing. Client-side pre-flight therefore has to be exhaustive rather than best-effort, which is why all AI traffic funnels through oneRunAiAsyncthat never auto-retries.
AI controls must hide themselves rather than fail, matching the web app's documented behaviour.
Rules from the product docs
providers: []→ the site has noANTHROPIC_API_KEY; hide all AI controls (calls would409 no_provider_configured).subscriber: false→ free account; do not show AI controls at all.429 quota_exceeded→ tell the user to retry tomorrow;429 rate_limited→ honorRetry-After.Acceptance criteria
GET /api/ai/statusis fetched once per session (and on demand) and cached on a shared service.IsAiAvailablegate (subscriber AND providers non-empty) drives visibility of every AI affordance.42 of 50 left today).quota_exceededandrate_limitedrender as distinct, non-blocking in-place messages — not a generic error toast, and never a modal that blocks the dispatcher.