feat: add Grok media eligibility controls

This commit is contained in:
aofee
2026-09-02 11:50:33 +08:00
parent aa23648835
commit 01bd9b71a2
9 changed files with 624 additions and 3 deletions
@@ -0,0 +1,144 @@
package admin
import (
"strconv"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
const (
grokMediaEligibilityModeAuto = "auto"
grokMediaEligibilityModeEnabled = "enabled"
grokMediaEligibilityModeDisabled = "disabled"
)
type grokMediaEligibilityResponse struct {
AccountID int64 `json:"account_id"`
Mode string `json:"mode"`
Eligible bool `json:"eligible"`
Reason string `json:"reason"`
}
type updateGrokMediaEligibilityRequest struct {
Mode string `json:"mode" binding:"required"`
}
// GetGrokMediaEligibility returns the current manual override and evaluated
// media-routing decision for a Grok OAuth account.
// GET /api/v1/admin/accounts/:id/grok-media-eligibility
func (h *AccountHandler) GetGrokMediaEligibility(c *gin.Context) {
account, err := h.getGrokOAuthAccount(c)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, buildGrokMediaEligibilityResponse(account))
}
// UpdateGrokMediaEligibility updates only the manual media eligibility
// override. It deliberately uses UpdateAccountExtra so unrelated account
// extra fields are preserved.
// PUT /api/v1/admin/accounts/:id/grok-media-eligibility
func (h *AccountHandler) UpdateGrokMediaEligibility(c *gin.Context) {
account, err := h.getGrokOAuthAccount(c)
if err != nil {
response.ErrorFrom(c, err)
return
}
var req updateGrokMediaEligibilityRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
var value any
switch req.Mode {
case grokMediaEligibilityModeAuto:
value = nil
case grokMediaEligibilityModeEnabled:
value = true
case grokMediaEligibilityModeDisabled:
value = false
default:
response.ErrorFrom(c, infraerrors.BadRequest(
"GROK_MEDIA_ELIGIBILITY_INVALID_MODE",
"mode must be one of auto, enabled, disabled",
))
return
}
updates := map[string]any{service.GrokMediaEligibleExtraKey: value}
if err := service.ValidateGrokMediaEligibilityExtra(account.Platform, updates); err != nil {
response.ErrorFrom(c, err)
return
}
if err := h.adminService.UpdateAccountExtra(c.Request.Context(), account.ID, updates); err != nil {
response.ErrorFrom(c, err)
return
}
updated, err := h.adminService.GetAccount(c.Request.Context(), account.ID)
if err != nil {
response.ErrorFrom(c, err)
return
}
if err := ensureGrokOAuthAccount(updated); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, buildGrokMediaEligibilityResponse(updated))
}
func (h *AccountHandler) getGrokOAuthAccount(c *gin.Context) (*service.Account, error) {
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
return nil, infraerrors.BadRequest("INVALID_ACCOUNT_ID", "Invalid account ID")
}
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
if err != nil {
return nil, err
}
if err := ensureGrokOAuthAccount(account); err != nil {
return nil, err
}
return account, nil
}
func ensureGrokOAuthAccount(account *service.Account) error {
if account == nil {
return infraerrors.NotFound("ACCOUNT_NOT_FOUND", "account not found")
}
if account.Platform != service.PlatformGrok || account.Type != service.AccountTypeOAuth {
return infraerrors.BadRequest(
"GROK_MEDIA_ELIGIBILITY_UNSUPPORTED_ACCOUNT",
"media eligibility is only supported for Grok OAuth accounts",
)
}
return nil
}
func buildGrokMediaEligibilityResponse(account *service.Account) grokMediaEligibilityResponse {
eligible, reason := account.GrokMediaGenerationEligibility()
return grokMediaEligibilityResponse{
AccountID: account.ID,
Mode: grokMediaEligibilityModeFromExtra(account.Extra),
Eligible: eligible,
Reason: reason,
}
}
func grokMediaEligibilityModeFromExtra(extra map[string]any) string {
if extra != nil {
if enabled, ok := extra[service.GrokMediaEligibleExtraKey].(bool); ok {
if enabled {
return grokMediaEligibilityModeEnabled
}
return grokMediaEligibilityModeDisabled
}
}
return grokMediaEligibilityModeAuto
}
@@ -0,0 +1,146 @@
package admin
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type grokMediaEligibilityAdminServiceStub struct {
*stubAdminService
account *service.Account
}
func (s *grokMediaEligibilityAdminServiceStub) GetAccount(_ context.Context, _ int64) (*service.Account, error) {
return s.account, nil
}
func (s *grokMediaEligibilityAdminServiceStub) UpdateAccountExtra(_ context.Context, _ int64, updates map[string]any) error {
if s.account.Extra == nil {
s.account.Extra = map[string]any{}
}
for key, value := range updates {
if value == nil {
delete(s.account.Extra, key)
} else {
s.account.Extra[key] = value
}
}
return nil
}
func newGrokMediaEligibilityRouter(account *service.Account) *gin.Engine {
stub := &grokMediaEligibilityAdminServiceStub{
stubAdminService: newStubAdminService(),
account: account,
}
h := NewAccountHandler(stub, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router := gin.New()
router.GET("/accounts/:id/grok-media-eligibility", h.GetGrokMediaEligibility)
router.PUT("/accounts/:id/grok-media-eligibility", h.UpdateGrokMediaEligibility)
return router
}
func TestGrokMediaEligibilityHandlerGetReturnsModeAndReason(t *testing.T) {
gin.SetMode(gin.TestMode)
router := newGrokMediaEligibilityRouter(&service.Account{
ID: 12,
Platform: service.PlatformGrok,
Type: service.AccountTypeOAuth,
Extra: map[string]any{
"grok_billing_snapshot": map[string]any{"status_code": 200, "partial": true},
"unrelated_setting": "preserve",
},
})
req := httptest.NewRequest(http.MethodGet, "/accounts/12/grok-media-eligibility", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var envelope struct {
Data grokMediaEligibilityResponse `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope))
require.Equal(t, int64(12), envelope.Data.AccountID)
require.Equal(t, grokMediaEligibilityModeAuto, envelope.Data.Mode)
require.Equal(t, "billing_inconclusive", envelope.Data.Reason)
}
func TestGrokMediaEligibilityHandlerUpdateModesPreservesExtra(t *testing.T) {
gin.SetMode(gin.TestMode)
account := &service.Account{
ID: 12,
Platform: service.PlatformGrok,
Type: service.AccountTypeOAuth,
Extra: map[string]any{"unrelated_setting": "preserve"},
}
router := newGrokMediaEligibilityRouter(account)
tests := []struct {
mode string
wantMode string
wantExtra any
wantEligible bool
wantReason string
}{
{mode: "enabled", wantMode: grokMediaEligibilityModeEnabled, wantExtra: true, wantEligible: true, wantReason: "override_enabled"},
{mode: "disabled", wantMode: grokMediaEligibilityModeDisabled, wantExtra: false, wantEligible: false, wantReason: "override_disabled"},
{mode: "auto", wantMode: grokMediaEligibilityModeAuto, wantExtra: nil, wantEligible: false, wantReason: "billing_unobserved"},
}
for _, tt := range tests {
t.Run(tt.mode, func(t *testing.T) {
body, err := json.Marshal(map[string]string{"mode": tt.mode})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPut, "/accounts/12/grok-media-eligibility", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var envelope struct {
Data grokMediaEligibilityResponse `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &envelope))
require.Equal(t, tt.wantMode, envelope.Data.Mode)
require.Equal(t, tt.wantEligible, envelope.Data.Eligible)
require.Equal(t, tt.wantReason, envelope.Data.Reason)
require.Equal(t, "preserve", account.Extra["unrelated_setting"])
if tt.wantExtra == nil {
_, exists := account.Extra[service.GrokMediaEligibleExtraKey]
require.False(t, exists)
} else {
require.Equal(t, tt.wantExtra, account.Extra[service.GrokMediaEligibleExtraKey])
}
})
}
}
func TestGrokMediaEligibilityHandlerRejectsUnsupportedAndInvalidModes(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, tt := range []struct {
name string
account *service.Account
body string
}{
{name: "unsupported account", account: &service.Account{ID: 1, Platform: service.PlatformOpenAI, Type: service.AccountTypeOAuth}, body: `{"mode":"enabled"}`},
{name: "invalid mode", account: &service.Account{ID: 1, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}, body: `{"mode":"force"}`},
} {
t.Run(tt.name, func(t *testing.T) {
router := newGrokMediaEligibilityRouter(tt.account)
req := httptest.NewRequest(http.MethodPut, "/accounts/1/grok-media-eligibility", bytes.NewBufferString(tt.body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusBadRequest, rec.Code)
})
}
}
+2
View File
@@ -372,6 +372,8 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers, stepUpAu
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
accounts.POST("/sync/crs/preview", h.Admin.Account.PreviewFromCRS)
accounts.PUT("/:id", h.Admin.Account.Update)
accounts.GET("/:id/grok-media-eligibility", h.Admin.Account.GetGrokMediaEligibility)
accounts.PUT("/:id/grok-media-eligibility", h.Admin.Account.UpdateGrokMediaEligibility)
accounts.PUT("/:id/upstream-billing-probe", h.Admin.Account.SetUpstreamBillingProbeEnabled)
accounts.POST("/:id/upstream-billing-probe", h.Admin.Account.ProbeUpstreamBilling)
accounts.GET("/:id/ollama-cloud-usage", h.Admin.Account.GetOllamaCloudUsage)