The 403 detection PR changed the 401 handler condition from
`account.Type == AccountTypeOAuth` to
`account.Type == AccountTypeOAuth && account.Platform == PlatformOpenAI`,
which accidentally excluded Gemini OAuth from the temp-unschedulable path.
Fix: use `!= PlatformAntigravity` instead, preserving Gemini behavior
while correctly excluding Antigravity (whose 401 is handled by
applyErrorPolicy's temp_unschedulable_rules).
Update tests to reflect Antigravity's new 401 semantics:
- HandleUpstreamError: Antigravity OAuth 401 now uses SetError
- CheckErrorPolicy: Antigravity 401 second hit stays TempUnscheduled
- DB fallback: split into Gemini (escalates) and Antigravity (stays temp)
Backend:
- Detect and classify 403 responses into three types:
validation (account needs Google verification),
violation (terms of service / banned),
forbidden (generic 403)
- Extract verification/appeal URLs from 403 response body
(structured JSON parsing with regex fallback)
- Add needs_verify, is_banned, needs_reauth, error_code fields
to UsageInfo (omitempty for zero impact on other platforms)
- Handle 403 in request path: classify and permanently set account error
- Save validation_url in error_message for degraded path recovery
- Enrich usage with account error on both success and degraded paths
- Add singleflight dedup for usage requests with independent context
- Differentiate cache TTL: success/403 → 3min, errors → 1min
- Return degraded UsageInfo instead of HTTP 500 on quota fetch errors
Frontend:
- Display forbidden status badges with color coding (red for banned,
amber for needs verification, gray for generic)
- Show clickable verification/appeal URL links
- Display needs_reauth and degraded error states in usage cell
- Add Antigravity tier label badge next to platform type
Tests:
- Comprehensive unit tests for classifyForbiddenType (7 cases)
- Unit tests for extractValidationURL (8 cases including unicode escapes)
- Integration test for FetchQuota forbidden path
Add a dedicated modal in group management for viewing, adding, editing,
and deleting per-user rate multipliers within a group.
Backend:
- GET /admin/groups/:id/rate-multipliers - list entries with user details
- PUT /admin/groups/:id/rate-multipliers - batch sync (full replace)
- DELETE /admin/groups/:id/rate-multipliers - clear all entries
- Repository: GetByGroupID, SyncGroupRateMultipliers methods on
user_group_rate_multipliers table (same table as user-side rates)
Frontend:
- New GroupRateMultipliersModal component with:
- User search and add with email autocomplete
- Editable rate column with local edit mode (cancel/save)
- Batch adjust: multiply all rates by a factor
- Clear all (local operation, requires save to persist)
- Pagination (10/20/50 per page)
- Platform icon with brand colors in group info bar
- Unsaved changes indicator with revert option
- Unit tests for all three backend endpoints
The ChatGPT backend-api codex/responses endpoint requires `input` to be
an array, but the OpenAI Responses API spec allows it to be a plain string.
When a client sends a string input, sub2api now converts it to the expected
message array format. Empty/whitespace-only strings become an empty array
to avoid triggering a 400 "Input must be a list" error.
- Add AdminResetQuota service method to reset daily/weekly usage windows
- Add POST /api/v1/admin/subscriptions/:id/reset-quota handler and route
- Add resetQuota API function in frontend subscriptions client
- Add reset quota button, confirmation dialog, and handlers in SubscriptionsView
- Add i18n keys for reset quota feature in zh and en locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two SSE scanners in openai_gateway_messages.go were hardcoded to 1MB
while all other scanners use defaultMaxLineSize (500MB) with config
override. This caused Responses API streams to fail on large payloads.