mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 16:20:27 +00:00
feat: add Grok media eligibility controls
This commit is contained in:
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user