fix(grok): set async video duration_ms from create accept to done discovery
Store CreatedAt on pending billing at video create and use wall-clock E2E latency when status/content first observes official done+video.url, so usage logs no longer record only the single poll hop.
This commit is contained in:
1 parent
165b072908
commit
85b65284ec
3 files changed
+81
No files matched your search
@@ -188,6 +188,10 @@ func (h *OpenAIGatewayHandler) handleGrokMedia(c *gin.Context, endpoint service.
|
||||
var oauth429FailoverState service.OpenAIOAuth429FailoverState
|
||||
mediaEligibilityRejected := false
|
||||
switchCount := 0
|
||||
videoCreateStartedAt := ""
|
||||
if isGrokVideoCreateEndpoint(endpoint) {
|
||||
videoCreateStartedAt = service.GrokVideoPendingCreatedAtNow()
|
||||
}
|
||||
maxAccountSwitches := h.maxAccountSwitches
|
||||
if maxAccountSwitches <= 0 {
|
||||
maxAccountSwitches = 3
|
||||
@@ -425,6 +429,8 @@ func (h *OpenAIGatewayHandler) handleGrokMedia(c *gin.Context, endpoint service.
|
||||
VideoResolution: result.VideoResolution,
|
||||
VideoDurationSeconds: result.VideoDurationSeconds,
|
||||
OriginalModel: clientRequestedModel(c, requestModel),
|
||||
// Wall-clock start for usage duration_ms: create accepted → first done discovery.
|
||||
CreatedAt: videoCreateStartedAt,
|
||||
}); err != nil {
|
||||
reqLog.Warn("grok_media.store_video_pending_billing_failed",
|
||||
zap.Int64("account_id", account.ID),
|
||||
@@ -587,6 +593,14 @@ func prepareGrokVideoCompletionBilling(
|
||||
merged.VideoResolution = service.NormalizeVideoBillingResolutionOrDefault(merged.VideoResolution)
|
||||
// Official default duration is 8s when neither status nor create provided it.
|
||||
merged.VideoDurationSeconds = service.NormalizeVideoBillingDurationSecondsOrDefault(merged.VideoDurationSeconds)
|
||||
// E2E latency for async video: create accept → this discovery of done+url.
|
||||
// Bill on discovery (status/content), not after further client polls; duration
|
||||
// must not be only the single discovery hop (~hundreds of ms).
|
||||
if pending != nil {
|
||||
if e2e := service.GrokVideoE2EDuration(pending.CreatedAt, time.Now()); e2e > 0 {
|
||||
merged.Duration = e2e
|
||||
}
|
||||
}
|
||||
return &merged
|
||||
}
|
||||
|
||||
|
||||
@@ -335,6 +335,43 @@ type GrokVideoPendingBilling struct {
|
||||
VideoResolution string `json:"video_resolution,omitempty"`
|
||||
VideoDurationSeconds int `json:"video_duration_seconds,omitempty"`
|
||||
OriginalModel string `json:"original_model,omitempty"`
|
||||
// CreatedAt is when the gateway accepted the async create (RFC3339Nano UTC).
|
||||
// duration_ms for deferred billing is measured from this instant until the
|
||||
// first official done+video.url observation (status poll or content download),
|
||||
// not the latency of that single discovery request alone.
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// GrokVideoPendingCreatedAtNow formats a create-accept timestamp for pending billing.
|
||||
func GrokVideoPendingCreatedAtNow() string {
|
||||
return time.Now().UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
// GrokVideoE2EDuration returns wall time from create accept to discovery of completion.
|
||||
// Returns 0 when CreatedAt is missing or unparseable (caller keeps poll-only Duration).
|
||||
func GrokVideoE2EDuration(createdAt string, discoveredAt time.Time) time.Duration {
|
||||
createdAt = strings.TrimSpace(createdAt)
|
||||
if createdAt == "" {
|
||||
return 0
|
||||
}
|
||||
if discoveredAt.IsZero() {
|
||||
discoveredAt = time.Now()
|
||||
}
|
||||
var created time.Time
|
||||
var err error
|
||||
if created, err = time.Parse(time.RFC3339Nano, createdAt); err != nil {
|
||||
if created, err = time.Parse(time.RFC3339, createdAt); err != nil {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if created.IsZero() {
|
||||
return 0
|
||||
}
|
||||
d := discoveredAt.Sub(created)
|
||||
if d < 0 {
|
||||
return 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func grokVideoPendingBillingKey(requestID string, userID, apiKeyID int64) string {
|
||||
@@ -380,6 +417,12 @@ func (s *OpenAIGatewayService) StoreGrokVideoPendingBilling(
|
||||
if pending.VideoDurationSeconds > 0 {
|
||||
pending.VideoDurationSeconds = NormalizeVideoBillingDurationSecondsOrDefault(pending.VideoDurationSeconds)
|
||||
}
|
||||
// Always stamp create-accept time when missing so deferred duration_ms is E2E.
|
||||
if strings.TrimSpace(pending.CreatedAt) == "" {
|
||||
pending.CreatedAt = GrokVideoPendingCreatedAtNow()
|
||||
} else {
|
||||
pending.CreatedAt = strings.TrimSpace(pending.CreatedAt)
|
||||
}
|
||||
payload, err := json.Marshal(pending)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,10 +2,34 @@ package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGrokVideoE2EDurationFromCreatedAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
created := time.Now().UTC().Add(-45 * time.Second)
|
||||
d := GrokVideoE2EDuration(created.Format(time.RFC3339Nano), time.Now().UTC())
|
||||
require.GreaterOrEqual(t, d, 44*time.Second)
|
||||
require.LessOrEqual(t, d, 47*time.Second)
|
||||
|
||||
require.Equal(t, time.Duration(0), GrokVideoE2EDuration("", time.Now()))
|
||||
require.Equal(t, time.Duration(0), GrokVideoE2EDuration("not-a-time", time.Now()))
|
||||
// Future CreatedAt clamps to zero (clock skew).
|
||||
require.Equal(t, time.Duration(0), GrokVideoE2EDuration(time.Now().Add(time.Hour).UTC().Format(time.RFC3339Nano), time.Now()))
|
||||
}
|
||||
|
||||
func TestGrokVideoPendingCreatedAtStampOnStoreShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
// GrokVideoPendingCreatedAtNow must be parseable by GrokVideoE2EDuration.
|
||||
stamp := GrokVideoPendingCreatedAtNow()
|
||||
require.NotEmpty(t, stamp)
|
||||
d := GrokVideoE2EDuration(stamp, time.Now().UTC().Add(2*time.Second))
|
||||
require.GreaterOrEqual(t, d, time.Second)
|
||||
require.LessOrEqual(t, d, 3*time.Second)
|
||||
}
|
||||
|
||||
func TestIsGrokVideoStatusBillable(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Official success: status=done + video.url
|
||||
|
||||
Reference in new issue
Block a user