feat: bill OpenAI cache_write_tokens at cache-creation price with zero clamp
Parse OpenAI's native cache_write_tokens (chat prompt_tokens_details / responses input_tokens_details), bill it at the cache-creation ratio, and clamp the uncached prompt remainder at zero since cached + cache-write can exceed prompt_tokens. Propagate the field through chat/responses/claude format conversions and tiered expression billing (cc variable).
This commit is contained in:
@@ -88,6 +88,7 @@ func HasOpenAIUsageTokens(usage *Usage) bool {
|
||||
}
|
||||
if usage.PromptTokensDetails.CachedTokens != 0 ||
|
||||
usage.PromptTokensDetails.CachedCreationTokens != 0 ||
|
||||
usage.PromptTokensDetails.CacheWriteTokens != 0 ||
|
||||
usage.PromptTokensDetails.TextTokens != 0 ||
|
||||
usage.PromptTokensDetails.ImageTokens != 0 ||
|
||||
usage.PromptTokensDetails.AudioTokens != 0 {
|
||||
|
||||
@@ -849,6 +849,7 @@ type OpenAIResponsesRequest struct {
|
||||
MaxOutputTokens *uint `json:"max_output_tokens,omitempty"`
|
||||
TopLogProbs *int `json:"top_logprobs,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Moderation json.RawMessage `json:"moderation,omitempty"`
|
||||
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
@@ -859,6 +860,7 @@ type OpenAIResponsesRequest struct {
|
||||
// This field is allowed by default and can be disabled via channel setting disable_store.
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
PromptCacheOptions json.RawMessage `json:"prompt_cache_options,omitempty"`
|
||||
PromptCacheRetention json.RawMessage `json:"prompt_cache_retention,omitempty"`
|
||||
// SafetyIdentifier carries client identity for policy abuse detection.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_safety_identifier.
|
||||
|
||||
+20
-3
@@ -256,9 +256,26 @@ type OpenAIVideoResponse struct {
|
||||
type InputTokenDetails struct {
|
||||
CachedTokens int `json:"cached_tokens"`
|
||||
CachedCreationTokens int `json:"cached_creation_tokens,omitempty"`
|
||||
TextTokens int `json:"text_tokens"`
|
||||
AudioTokens int `json:"audio_tokens"`
|
||||
ImageTokens int `json:"image_tokens"`
|
||||
// CacheWriteTokens is OpenAI's native cache-write count, reported as
|
||||
// prompt_tokens_details.cache_write_tokens (Chat Completions) or
|
||||
// input_tokens_details.cache_write_tokens (Responses). It is billed at the
|
||||
// cache-creation price.
|
||||
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
|
||||
TextTokens int `json:"text_tokens"`
|
||||
AudioTokens int `json:"audio_tokens"`
|
||||
ImageTokens int `json:"image_tokens"`
|
||||
}
|
||||
|
||||
// CacheCreationTokensTotal returns the cache-write token count regardless of
|
||||
// which field the upstream reported it in: Claude-derived conversions populate
|
||||
// CachedCreationTokens while OpenAI reports cache_write_tokens natively. Both
|
||||
// are billed at the cache-creation price; when both are present the larger
|
||||
// value wins so the same tokens are never double-counted.
|
||||
func (d InputTokenDetails) CacheCreationTokensTotal() int {
|
||||
if d.CacheWriteTokens > d.CachedCreationTokens {
|
||||
return d.CacheWriteTokens
|
||||
}
|
||||
return d.CachedCreationTokens
|
||||
}
|
||||
|
||||
type OutputTokenDetails struct {
|
||||
|
||||
@@ -17,12 +17,14 @@ type OpenAIResponsesCompactionRequest struct {
|
||||
// Codex compact request parity:
|
||||
// https://github.com/openai/codex/commit/53d59722268dde82fb93c1f37964ce196c2a86d7
|
||||
// https://github.com/openai/codex/commit/5d6f23a27bf9c90709af527a7108c1c2eadf5123
|
||||
Tools json.RawMessage `json:"tools,omitempty"`
|
||||
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"`
|
||||
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
PromptCacheOptions json.RawMessage `json:"prompt_cache_options,omitempty"`
|
||||
PromptCacheRetention json.RawMessage `json:"prompt_cache_retention,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OpenAIResponsesCompactionRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -51,6 +51,7 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
usage.TotalTokens = responsesResponse.Usage.TotalTokens
|
||||
if responsesResponse.Usage.InputTokensDetails != nil {
|
||||
usage.PromptTokensDetails.CachedTokens = responsesResponse.Usage.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensDetails.CacheWriteTokens = responsesResponse.Usage.InputTokensDetails.CacheWriteTokens
|
||||
}
|
||||
}
|
||||
if info == nil || info.ResponsesUsageInfo == nil || info.ResponsesUsageInfo.BuiltInTools == nil {
|
||||
@@ -104,6 +105,7 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
|
||||
}
|
||||
if streamResponse.Response.Usage.InputTokensDetails != nil {
|
||||
usage.PromptTokensDetails.CachedTokens = streamResponse.Response.Usage.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensDetails.CacheWriteTokens = streamResponse.Response.Usage.InputTokensDetails.CacheWriteTokens
|
||||
}
|
||||
}
|
||||
if streamResponse.Response.HasImageGenerationCall() {
|
||||
|
||||
@@ -37,6 +37,7 @@ func OaiResponsesCompactionHandler(c *gin.Context, resp *http.Response) (*dto.Us
|
||||
usage.TotalTokens = compactResp.Usage.TotalTokens
|
||||
if compactResp.Usage.InputTokensDetails != nil {
|
||||
usage.PromptTokensDetails.CachedTokens = compactResp.Usage.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensDetails.CacheWriteTokens = compactResp.Usage.InputTokensDetails.CacheWriteTokens
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,12 +41,14 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
|
||||
responsesReq = req
|
||||
case *dto.OpenAIResponsesCompactionRequest:
|
||||
responsesReq = &dto.OpenAIResponsesRequest{
|
||||
Model: req.Model,
|
||||
Input: req.Input,
|
||||
Instructions: req.Instructions,
|
||||
PreviousResponseID: req.PreviousResponseID,
|
||||
ParallelToolCalls: req.ParallelToolCalls,
|
||||
ServiceTier: req.ServiceTier,
|
||||
Model: req.Model,
|
||||
Input: req.Input,
|
||||
Instructions: req.Instructions,
|
||||
PreviousResponseID: req.PreviousResponseID,
|
||||
ParallelToolCalls: req.ParallelToolCalls,
|
||||
ServiceTier: req.ServiceTier,
|
||||
PromptCacheOptions: req.PromptCacheOptions,
|
||||
PromptCacheRetention: req.PromptCacheRetention,
|
||||
}
|
||||
default:
|
||||
return types.NewErrorWithStatusCode(
|
||||
|
||||
@@ -216,6 +216,9 @@ func buildOpenAIStyleUsageFromClaudeUsage(usage *dto.Usage) dto.Usage {
|
||||
usage.ClaudeCacheCreation1hTokens,
|
||||
)
|
||||
cacheCreationTokens := cacheCreationTokensForOpenAIUsage(usage)
|
||||
// Expose the standard OpenAI cache-write field alongside the legacy
|
||||
// cached_creation_tokens so OpenAI-format clients can bill cache writes.
|
||||
clone.PromptTokensDetails.CacheWriteTokens = cacheCreationTokens
|
||||
totalInputTokens := usage.PromptTokens + usage.PromptTokensDetails.CachedTokens + cacheCreationTokens
|
||||
clone.PromptTokens = totalInputTokens
|
||||
clone.InputTokens = totalInputTokens
|
||||
|
||||
@@ -39,10 +39,21 @@ func buildClaudeUsageFromOpenAIUsage(oaiUsage *dto.Usage) *dto.ClaudeUsage {
|
||||
oaiUsage.ClaudeCacheCreation5mTokens,
|
||||
oaiUsage.ClaudeCacheCreation1hTokens,
|
||||
)
|
||||
cacheCreationTokens := oaiUsage.PromptTokensDetails.CacheCreationTokensTotal()
|
||||
inputTokens := oaiUsage.PromptTokens
|
||||
if oaiUsage.PromptTokensDetails.CacheWriteTokens > 0 {
|
||||
// OpenAI native cache-write usage counts cached and cache-write tokens
|
||||
// inside prompt_tokens, while Claude semantics reports input_tokens
|
||||
// excluding both; the uncached remainder clamps at zero.
|
||||
inputTokens = oaiUsage.PromptTokens - oaiUsage.PromptTokensDetails.CachedTokens - cacheCreationTokens
|
||||
if inputTokens < 0 {
|
||||
inputTokens = 0
|
||||
}
|
||||
}
|
||||
usage := &dto.ClaudeUsage{
|
||||
InputTokens: oaiUsage.PromptTokens,
|
||||
InputTokens: inputTokens,
|
||||
OutputTokens: oaiUsage.CompletionTokens,
|
||||
CacheCreationInputTokens: oaiUsage.PromptTokensDetails.CachedCreationTokens,
|
||||
CacheCreationInputTokens: cacheCreationTokens,
|
||||
CacheReadInputTokens: oaiUsage.PromptTokensDetails.CachedTokens,
|
||||
BillingUsage: billingUsage,
|
||||
}
|
||||
|
||||
@@ -79,6 +79,30 @@ func TestResponseOpenAI2ClaudeUsageCarriesOpenAIBillingUsage(t *testing.T) {
|
||||
assert.Nil(t, resp.Usage.BillingUsage.OpenAIUsage.BillingUsage)
|
||||
}
|
||||
|
||||
func TestBuildClaudeUsageFromOpenAICacheWriteUsage(t *testing.T) {
|
||||
usage := buildClaudeUsageFromOpenAIUsage(&dto.Usage{
|
||||
PromptTokens: 3619,
|
||||
CompletionTokens: 36,
|
||||
TotalTokens: 3655,
|
||||
PromptTokensDetails: dto.InputTokenDetails{
|
||||
CachedTokens: 2921,
|
||||
CacheWriteTokens: 3616,
|
||||
},
|
||||
})
|
||||
|
||||
require.NotNil(t, usage)
|
||||
// Claude semantics reports input_tokens excluding cache read/write; the
|
||||
// remainder 3619-2921-3616 clamps to 0.
|
||||
assert.Equal(t, 0, usage.InputTokens)
|
||||
assert.Equal(t, 2921, usage.CacheReadInputTokens)
|
||||
assert.Equal(t, 3616, usage.CacheCreationInputTokens)
|
||||
assert.Equal(t, 36, usage.OutputTokens)
|
||||
require.NotNil(t, usage.BillingUsage)
|
||||
require.NotNil(t, usage.BillingUsage.OpenAIUsage)
|
||||
assert.Equal(t, dto.BillingUsageSemanticOpenAI, usage.BillingUsage.Semantic)
|
||||
assert.Equal(t, 3616, usage.BillingUsage.OpenAIUsage.PromptTokensDetails.CacheWriteTokens)
|
||||
}
|
||||
|
||||
func TestStreamResponseOpenAI2ClaudeClosesTextThinkingAndToolBlocks(t *testing.T) {
|
||||
info := &relaycommon.RelayInfo{
|
||||
ClaudeConvertInfo: &relaycommon.ClaudeConvertInfo{
|
||||
|
||||
@@ -137,6 +137,7 @@ func UsageFromChatUsage(src *dto.Usage) *dto.Usage {
|
||||
src.PromptTokensDetails.ImageTokens != 0 ||
|
||||
src.PromptTokensDetails.AudioTokens != 0 ||
|
||||
src.PromptTokensDetails.CachedCreationTokens != 0 ||
|
||||
src.PromptTokensDetails.CacheWriteTokens != 0 ||
|
||||
src.PromptTokensDetails.TextTokens != 0 {
|
||||
details := src.PromptTokensDetails
|
||||
usage.InputTokensDetails = &details
|
||||
|
||||
@@ -156,6 +156,7 @@ func UsageFromResponsesUsage(src *dto.Usage) *dto.Usage {
|
||||
if src.InputTokensDetails != nil {
|
||||
usage.PromptTokensDetails.CachedTokens = src.InputTokensDetails.CachedTokens
|
||||
usage.PromptTokensDetails.CachedCreationTokens = src.InputTokensDetails.CachedCreationTokens
|
||||
usage.PromptTokensDetails.CacheWriteTokens = src.InputTokensDetails.CacheWriteTokens
|
||||
usage.PromptTokensDetails.TextTokens = src.InputTokensDetails.TextTokens
|
||||
usage.PromptTokensDetails.ImageTokens = src.InputTokensDetails.ImageTokens
|
||||
usage.PromptTokensDetails.AudioTokens = src.InputTokensDetails.AudioTokens
|
||||
|
||||
@@ -277,6 +277,7 @@ func TestConvertResponseProviderToOAIChatUsage(t *testing.T) {
|
||||
assert.Equal(t, 22, toChat.Usage.TotalTokens)
|
||||
assert.Equal(t, 3, toChat.Usage.PromptTokensDetails.CachedTokens)
|
||||
assert.Equal(t, 4, toChat.Usage.PromptTokensDetails.CachedCreationTokens)
|
||||
assert.Equal(t, 4, toChat.Usage.PromptTokensDetails.CacheWriteTokens)
|
||||
require.NotNil(t, toChat.Usage.BillingUsage)
|
||||
require.NotNil(t, toChat.Usage.BillingUsage.ClaudeUsage)
|
||||
assert.Equal(t, dto.BillingUsageSourceClaudeMessages, toChat.Usage.BillingUsage.Source)
|
||||
@@ -539,6 +540,7 @@ func TestResponseUsageMatrixChatAndResponsesDetails(t *testing.T) {
|
||||
PromptTokensDetails: dto.InputTokenDetails{
|
||||
CachedTokens: 3,
|
||||
CachedCreationTokens: 2,
|
||||
CacheWriteTokens: 6,
|
||||
TextTokens: 4,
|
||||
AudioTokens: 1,
|
||||
ImageTokens: 5,
|
||||
@@ -558,6 +560,7 @@ func TestResponseUsageMatrixChatAndResponsesDetails(t *testing.T) {
|
||||
require.NotNil(t, result.Usage.InputTokensDetails)
|
||||
assert.Equal(t, 3, result.Usage.InputTokensDetails.CachedTokens)
|
||||
assert.Equal(t, 2, result.Usage.InputTokensDetails.CachedCreationTokens)
|
||||
assert.Equal(t, 6, result.Usage.InputTokensDetails.CacheWriteTokens)
|
||||
assert.Equal(t, 4, result.Usage.InputTokensDetails.TextTokens)
|
||||
assert.Equal(t, 1, result.Usage.InputTokensDetails.AudioTokens)
|
||||
assert.Equal(t, 5, result.Usage.InputTokensDetails.ImageTokens)
|
||||
@@ -579,6 +582,7 @@ func TestResponseUsageMatrixChatAndResponsesDetails(t *testing.T) {
|
||||
InputTokensDetails: &dto.InputTokenDetails{
|
||||
CachedTokens: 4,
|
||||
CachedCreationTokens: 1,
|
||||
CacheWriteTokens: 7,
|
||||
TextTokens: 5,
|
||||
AudioTokens: 2,
|
||||
ImageTokens: 1,
|
||||
@@ -598,6 +602,7 @@ func TestResponseUsageMatrixChatAndResponsesDetails(t *testing.T) {
|
||||
assert.Equal(t, 21, result.Usage.TotalTokens)
|
||||
assert.Equal(t, 4, result.Usage.PromptTokensDetails.CachedTokens)
|
||||
assert.Equal(t, 1, result.Usage.PromptTokensDetails.CachedCreationTokens)
|
||||
assert.Equal(t, 7, result.Usage.PromptTokensDetails.CacheWriteTokens)
|
||||
assert.Equal(t, 5, result.Usage.PromptTokensDetails.TextTokens)
|
||||
assert.Equal(t, 2, result.Usage.PromptTokensDetails.AudioTokens)
|
||||
assert.Equal(t, 1, result.Usage.PromptTokensDetails.ImageTokens)
|
||||
|
||||
@@ -208,7 +208,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
|
||||
summary.CompletionTokens = usage.CompletionTokens
|
||||
summary.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
summary.CacheTokens = usage.PromptTokensDetails.CachedTokens
|
||||
summary.CacheCreationTokens = usage.PromptTokensDetails.CachedCreationTokens
|
||||
summary.CacheCreationTokens = usage.PromptTokensDetails.CacheCreationTokensTotal()
|
||||
summary.CacheCreationTokens5m = usage.ClaudeCacheCreation5mTokens
|
||||
summary.CacheCreationTokens1h = usage.ClaudeCacheCreation1hTokens
|
||||
summary.ImageTokens = usage.PromptTokensDetails.ImageTokens
|
||||
@@ -294,6 +294,13 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI cache-write usage can report cached_tokens + cache_write_tokens
|
||||
// exceeding prompt_tokens; the uncached remainder must clamp at zero so
|
||||
// billing never subtracts more than the reported input.
|
||||
if baseTokens.IsNegative() {
|
||||
baseTokens = decimal.Zero
|
||||
}
|
||||
|
||||
promptQuota := baseTokens.Add(cachedTokensWithRatio).Add(imageTokensWithRatio).Add(cachedCreationTokensWithRatio)
|
||||
completionQuota := dCompletionTokens.Mul(dCompletionRatio)
|
||||
quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio)
|
||||
|
||||
@@ -375,6 +375,62 @@ func TestCalculateTextQuotaSummaryHandlesLegacyClaudeDerivedOpenAIUsage(t *testi
|
||||
require.Equal(t, 1624, summary.Quota)
|
||||
}
|
||||
|
||||
func TestCalculateTextQuotaSummaryBillsOpenAICacheWriteTokens(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
RelayFormat: types.RelayFormatOpenAI,
|
||||
OriginModelName: "gpt-5.1",
|
||||
PriceData: types.PriceData{
|
||||
ModelRatio: 1,
|
||||
CompletionRatio: 2,
|
||||
CacheRatio: 0.1,
|
||||
CacheCreationRatio: 1.25,
|
||||
GroupRatioInfo: types.GroupRatioInfo{GroupRatio: 1},
|
||||
},
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
t.Run("uncached remainder stays positive", func(t *testing.T) {
|
||||
usage := &dto.Usage{
|
||||
PromptTokens: 1473,
|
||||
CompletionTokens: 19,
|
||||
PromptTokensDetails: dto.InputTokenDetails{
|
||||
CacheWriteTokens: 1470,
|
||||
},
|
||||
}
|
||||
|
||||
summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
|
||||
|
||||
require.Equal(t, 1470, summary.CacheCreationTokens)
|
||||
// (1473-0-1470) + 1470*1.25 + 19*2 = 3 + 1837.5 + 38 = 1878.5 => 1879
|
||||
require.Equal(t, 1879, summary.Quota)
|
||||
})
|
||||
|
||||
t.Run("uncached remainder clamps to zero", func(t *testing.T) {
|
||||
// Real OpenAI payload shape: cached_tokens + cache_write_tokens exceeds
|
||||
// prompt_tokens, so the uncached remainder must clamp to 0 instead of
|
||||
// producing a negative charge component.
|
||||
usage := &dto.Usage{
|
||||
PromptTokens: 3619,
|
||||
CompletionTokens: 36,
|
||||
PromptTokensDetails: dto.InputTokenDetails{
|
||||
CachedTokens: 2921,
|
||||
CacheWriteTokens: 3616,
|
||||
},
|
||||
}
|
||||
|
||||
summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
|
||||
|
||||
require.Equal(t, 3619, summary.PromptTokens)
|
||||
require.Equal(t, 3616, summary.CacheCreationTokens)
|
||||
// max(3619-2921-3616, 0) + 2921*0.1 + 3616*1.25 + 36*2 = 4884.1 => 4884
|
||||
require.Equal(t, 4884, summary.Quota)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateTextQuotaSummarySeparatesOpenRouterCacheReadFromPromptBilling(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -22,7 +22,7 @@ func BuildTieredTokenParams(usage *dto.Usage, isClaudeUsageSemantic bool, usedVa
|
||||
p := float64(usage.PromptTokens)
|
||||
c := float64(usage.CompletionTokens)
|
||||
cr := float64(usage.PromptTokensDetails.CachedTokens)
|
||||
cc5m := float64(usage.PromptTokensDetails.CachedCreationTokens)
|
||||
cc5m := float64(usage.PromptTokensDetails.CacheCreationTokensTotal())
|
||||
cc1h := float64(0)
|
||||
|
||||
if usage.UsageSemantic == "anthropic" {
|
||||
|
||||
@@ -81,6 +81,9 @@ var defaultCacheRatio = map[string]float64{
|
||||
}
|
||||
|
||||
var defaultCreateCacheRatio = map[string]float64{
|
||||
"gpt-5.6-sol": 1.25,
|
||||
"gpt-5.6-terra": 1.25,
|
||||
"gpt-5.6-luna": 1.25,
|
||||
"claude-3-sonnet-20240229": 1.25,
|
||||
"claude-3-opus-20240229": 1.25,
|
||||
"claude-3-haiku-20240307": 1.25,
|
||||
|
||||
@@ -329,7 +329,7 @@ function CommonLogsCard<TData>({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-[minmax(0,1.35fr)_minmax(0,0.75fr)] gap-1.5'>
|
||||
<div className='grid grid-cols-[minmax(0,1fr)_minmax(0,0.8fr)] gap-1.5'>
|
||||
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
|
||||
<MobileLogTimeStatus
|
||||
createdAt={rowData?.created_at}
|
||||
|
||||
Reference in New Issue
Block a user