feat(usage): add reasoning effort column
This commit is contained in:
@@ -366,6 +366,7 @@ func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
|
|||||||
AccountID: l.AccountID,
|
AccountID: l.AccountID,
|
||||||
RequestID: l.RequestID,
|
RequestID: l.RequestID,
|
||||||
Model: l.Model,
|
Model: l.Model,
|
||||||
|
ReasoningEffort: l.ReasoningEffort,
|
||||||
GroupID: l.GroupID,
|
GroupID: l.GroupID,
|
||||||
SubscriptionID: l.SubscriptionID,
|
SubscriptionID: l.SubscriptionID,
|
||||||
InputTokens: l.InputTokens,
|
InputTokens: l.InputTokens,
|
||||||
|
|||||||
@@ -222,6 +222,9 @@ type UsageLog struct {
|
|||||||
AccountID int64 `json:"account_id"`
|
AccountID int64 `json:"account_id"`
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
|
// ReasoningEffort is the request's reasoning effort level (OpenAI Responses API).
|
||||||
|
// nil means not provided / not applicable.
|
||||||
|
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
|
||||||
|
|
||||||
GroupID *int64 `json:"group_id"`
|
GroupID *int64 `json:"group_id"`
|
||||||
SubscriptionID *int64 `json:"subscription_id"`
|
SubscriptionID *int64 `json:"subscription_id"`
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, created_at"
|
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, reasoning_effort, created_at"
|
||||||
|
|
||||||
type usageLogRepository struct {
|
type usageLogRepository struct {
|
||||||
client *dbent.Client
|
client *dbent.Client
|
||||||
@@ -111,21 +111,22 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
duration_ms,
|
duration_ms,
|
||||||
first_token_ms,
|
first_token_ms,
|
||||||
user_agent,
|
user_agent,
|
||||||
ip_address,
|
ip_address,
|
||||||
image_count,
|
image_count,
|
||||||
image_size,
|
image_size,
|
||||||
created_at
|
reasoning_effort,
|
||||||
) VALUES (
|
created_at
|
||||||
$1, $2, $3, $4, $5,
|
) VALUES (
|
||||||
$6, $7,
|
$1, $2, $3, $4, $5,
|
||||||
$8, $9, $10, $11,
|
$6, $7,
|
||||||
$12, $13,
|
$8, $9, $10, $11,
|
||||||
$14, $15, $16, $17, $18, $19,
|
$12, $13,
|
||||||
$20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30
|
$14, $15, $16, $17, $18, $19,
|
||||||
)
|
$20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31
|
||||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
)
|
||||||
RETURNING id, created_at
|
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||||
`
|
RETURNING id, created_at
|
||||||
|
`
|
||||||
|
|
||||||
groupID := nullInt64(log.GroupID)
|
groupID := nullInt64(log.GroupID)
|
||||||
subscriptionID := nullInt64(log.SubscriptionID)
|
subscriptionID := nullInt64(log.SubscriptionID)
|
||||||
@@ -134,6 +135,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
userAgent := nullString(log.UserAgent)
|
userAgent := nullString(log.UserAgent)
|
||||||
ipAddress := nullString(log.IPAddress)
|
ipAddress := nullString(log.IPAddress)
|
||||||
imageSize := nullString(log.ImageSize)
|
imageSize := nullString(log.ImageSize)
|
||||||
|
reasoningEffort := nullString(log.ReasoningEffort)
|
||||||
|
|
||||||
var requestIDArg any
|
var requestIDArg any
|
||||||
if requestID != "" {
|
if requestID != "" {
|
||||||
@@ -170,6 +172,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
ipAddress,
|
ipAddress,
|
||||||
log.ImageCount,
|
log.ImageCount,
|
||||||
imageSize,
|
imageSize,
|
||||||
|
reasoningEffort,
|
||||||
createdAt,
|
createdAt,
|
||||||
}
|
}
|
||||||
if err := scanSingleRow(ctx, sqlq, query, args, &log.ID, &log.CreatedAt); err != nil {
|
if err := scanSingleRow(ctx, sqlq, query, args, &log.ID, &log.CreatedAt); err != nil {
|
||||||
@@ -2090,6 +2093,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
ipAddress sql.NullString
|
ipAddress sql.NullString
|
||||||
imageCount int
|
imageCount int
|
||||||
imageSize sql.NullString
|
imageSize sql.NullString
|
||||||
|
reasoningEffort sql.NullString
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2124,6 +2128,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
&ipAddress,
|
&ipAddress,
|
||||||
&imageCount,
|
&imageCount,
|
||||||
&imageSize,
|
&imageSize,
|
||||||
|
&reasoningEffort,
|
||||||
&createdAt,
|
&createdAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2183,6 +2188,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
if imageSize.Valid {
|
if imageSize.Valid {
|
||||||
log.ImageSize = &imageSize.String
|
log.ImageSize = &imageSize.String
|
||||||
}
|
}
|
||||||
|
if reasoningEffort.Valid {
|
||||||
|
log.ReasoningEffort = &reasoningEffort.String
|
||||||
|
}
|
||||||
|
|
||||||
return log, nil
|
return log, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,12 +156,15 @@ type OpenAIUsage struct {
|
|||||||
|
|
||||||
// OpenAIForwardResult represents the result of forwarding
|
// OpenAIForwardResult represents the result of forwarding
|
||||||
type OpenAIForwardResult struct {
|
type OpenAIForwardResult struct {
|
||||||
RequestID string
|
RequestID string
|
||||||
Usage OpenAIUsage
|
Usage OpenAIUsage
|
||||||
Model string
|
Model string
|
||||||
Stream bool
|
// ReasoningEffort is extracted from request body (reasoning.effort) or derived from model suffix.
|
||||||
Duration time.Duration
|
// Stored for usage records display; nil means not provided / not applicable.
|
||||||
FirstTokenMs *int
|
ReasoningEffort *string
|
||||||
|
Stream bool
|
||||||
|
Duration time.Duration
|
||||||
|
FirstTokenMs *int
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAIGatewayService handles OpenAI API gateway operations
|
// OpenAIGatewayService handles OpenAI API gateway operations
|
||||||
@@ -958,13 +961,16 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reasoningEffort := extractOpenAIReasoningEffort(reqBody, originalModel)
|
||||||
|
|
||||||
return &OpenAIForwardResult{
|
return &OpenAIForwardResult{
|
||||||
RequestID: resp.Header.Get("x-request-id"),
|
RequestID: resp.Header.Get("x-request-id"),
|
||||||
Usage: *usage,
|
Usage: *usage,
|
||||||
Model: originalModel,
|
Model: originalModel,
|
||||||
Stream: reqStream,
|
ReasoningEffort: reasoningEffort,
|
||||||
Duration: time.Since(startTime),
|
Stream: reqStream,
|
||||||
FirstTokenMs: firstTokenMs,
|
Duration: time.Since(startTime),
|
||||||
|
FirstTokenMs: firstTokenMs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1687,6 +1693,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
RequestID: result.RequestID,
|
RequestID: result.RequestID,
|
||||||
Model: result.Model,
|
Model: result.Model,
|
||||||
|
ReasoningEffort: result.ReasoningEffort,
|
||||||
InputTokens: actualInputTokens,
|
InputTokens: actualInputTokens,
|
||||||
OutputTokens: result.Usage.OutputTokens,
|
OutputTokens: result.Usage.OutputTokens,
|
||||||
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
||||||
@@ -1881,3 +1888,86 @@ func (s *OpenAIGatewayService) updateCodexUsageSnapshot(ctx context.Context, acc
|
|||||||
_ = s.accountRepo.UpdateExtra(updateCtx, accountID, updates)
|
_ = s.accountRepo.UpdateExtra(updateCtx, accountID, updates)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getOpenAIReasoningEffortFromReqBody(reqBody map[string]any) (value string, present bool) {
|
||||||
|
if reqBody == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary: reasoning.effort
|
||||||
|
if reasoning, ok := reqBody["reasoning"].(map[string]any); ok {
|
||||||
|
if effort, ok := reasoning["effort"].(string); ok {
|
||||||
|
return normalizeOpenAIReasoningEffort(effort), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: some clients may use a flat field.
|
||||||
|
if effort, ok := reqBody["reasoning_effort"].(string); ok {
|
||||||
|
return normalizeOpenAIReasoningEffort(effort), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveOpenAIReasoningEffortFromModel(model string) string {
|
||||||
|
if strings.TrimSpace(model) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
modelID := strings.TrimSpace(model)
|
||||||
|
if strings.Contains(modelID, "/") {
|
||||||
|
parts := strings.Split(modelID, "/")
|
||||||
|
modelID = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.FieldsFunc(strings.ToLower(modelID), func(r rune) bool {
|
||||||
|
switch r {
|
||||||
|
case '-', '_', ' ':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeOpenAIReasoningEffort(parts[len(parts)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOpenAIReasoningEffort(reqBody map[string]any, requestedModel string) *string {
|
||||||
|
if value, present := getOpenAIReasoningEffortFromReqBody(reqBody); present {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
value := deriveOpenAIReasoningEffortFromModel(requestedModel)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeOpenAIReasoningEffort(raw string) string {
|
||||||
|
value := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize separators for "x-high"/"x_high" variants.
|
||||||
|
value = strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case "none", "minimal":
|
||||||
|
return ""
|
||||||
|
case "low", "medium", "high":
|
||||||
|
return value
|
||||||
|
case "xhigh", "extrahigh":
|
||||||
|
return "xhigh"
|
||||||
|
default:
|
||||||
|
// Only store known effort levels for now to keep UI consistent.
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ type UsageLog struct {
|
|||||||
AccountID int64
|
AccountID int64
|
||||||
RequestID string
|
RequestID string
|
||||||
Model string
|
Model string
|
||||||
|
// ReasoningEffort is the request's reasoning effort level (OpenAI Responses API),
|
||||||
|
// e.g. "low" / "medium" / "high" / "xhigh". Nil means not provided / not applicable.
|
||||||
|
ReasoningEffort *string
|
||||||
|
|
||||||
GroupID *int64
|
GroupID *int64
|
||||||
SubscriptionID *int64
|
SubscriptionID *int64
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add reasoning_effort field to usage_logs for OpenAI/Codex requests.
|
||||||
|
-- This stores the request's reasoning effort level (e.g. low/medium/high/xhigh).
|
||||||
|
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS reasoning_effort VARCHAR(20);
|
||||||
|
|
||||||
@@ -21,6 +21,12 @@
|
|||||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-reasoning_effort="{ row }">
|
||||||
|
<span class="text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ formatReasoningEffort(row.reasoning_effort) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-group="{ row }">
|
<template #cell-group="{ row }">
|
||||||
<span v-if="row.group" class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
|
<span v-if="row.group" class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200">
|
||||||
{{ row.group.name }}
|
{{ row.group.name }}
|
||||||
@@ -232,14 +238,14 @@
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime, formatReasoningEffort } from '@/utils/format'
|
||||||
import DataTable from '@/components/common/DataTable.vue'
|
import DataTable from '@/components/common/DataTable.vue'
|
||||||
import EmptyState from '@/components/common/EmptyState.vue'
|
import EmptyState from '@/components/common/EmptyState.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import type { AdminUsageLog } from '@/types'
|
import type { AdminUsageLog } from '@/types'
|
||||||
|
|
||||||
defineProps(['data', 'loading'])
|
defineProps(['data', 'loading'])
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -259,6 +265,7 @@ const cols = computed(() => [
|
|||||||
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
|
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
|
||||||
{ key: 'account', label: t('admin.usage.account'), sortable: false },
|
{ key: 'account', label: t('admin.usage.account'), sortable: false },
|
||||||
{ key: 'model', label: t('usage.model'), sortable: true },
|
{ key: 'model', label: t('usage.model'), sortable: true },
|
||||||
|
{ key: 'reasoning_effort', label: t('usage.reasoningEffort'), sortable: false },
|
||||||
{ key: 'group', label: t('admin.usage.group'), sortable: false },
|
{ key: 'group', label: t('admin.usage.group'), sortable: false },
|
||||||
{ key: 'stream', label: t('usage.type'), sortable: false },
|
{ key: 'stream', label: t('usage.type'), sortable: false },
|
||||||
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ export default {
|
|||||||
exporting: 'Exporting...',
|
exporting: 'Exporting...',
|
||||||
preparingExport: 'Preparing export...',
|
preparingExport: 'Preparing export...',
|
||||||
model: 'Model',
|
model: 'Model',
|
||||||
|
reasoningEffort: 'Reasoning Effort',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
tokens: 'Tokens',
|
tokens: 'Tokens',
|
||||||
cost: 'Cost',
|
cost: 'Cost',
|
||||||
|
|||||||
@@ -491,6 +491,7 @@ export default {
|
|||||||
exporting: '导出中...',
|
exporting: '导出中...',
|
||||||
preparingExport: '正在准备导出...',
|
preparingExport: '正在准备导出...',
|
||||||
model: '模型',
|
model: '模型',
|
||||||
|
reasoningEffort: '推理强度',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
tokens: 'Token',
|
tokens: 'Token',
|
||||||
cost: '费用',
|
cost: '费用',
|
||||||
|
|||||||
@@ -710,6 +710,7 @@ export interface UsageLog {
|
|||||||
account_id: number | null
|
account_id: number | null
|
||||||
request_id: string
|
request_id: string
|
||||||
model: string
|
model: string
|
||||||
|
reasoning_effort?: string | null
|
||||||
|
|
||||||
group_id: number | null
|
group_id: number | null
|
||||||
subscription_id: number | null
|
subscription_id: number | null
|
||||||
|
|||||||
@@ -174,6 +174,35 @@ export function parseDateTimeLocalInput(value: string): number | null {
|
|||||||
return Math.floor(date.getTime() / 1000)
|
return Math.floor(date.getTime() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 OpenAI reasoning effort(用于使用记录展示)
|
||||||
|
* @param effort 原始 effort(如 "low" / "medium" / "high" / "xhigh")
|
||||||
|
* @returns 格式化后的字符串(Low / Medium / High / Xhigh),无值返回 "-"
|
||||||
|
*/
|
||||||
|
export function formatReasoningEffort(effort: string | null | undefined): string {
|
||||||
|
const raw = (effort ?? '').toString().trim()
|
||||||
|
if (!raw) return '-'
|
||||||
|
|
||||||
|
const normalized = raw.toLowerCase().replace(/[-_\s]/g, '')
|
||||||
|
switch (normalized) {
|
||||||
|
case 'low':
|
||||||
|
return 'Low'
|
||||||
|
case 'medium':
|
||||||
|
return 'Medium'
|
||||||
|
case 'high':
|
||||||
|
return 'High'
|
||||||
|
case 'xhigh':
|
||||||
|
case 'extrahigh':
|
||||||
|
return 'Xhigh'
|
||||||
|
case 'none':
|
||||||
|
case 'minimal':
|
||||||
|
return '-'
|
||||||
|
default:
|
||||||
|
// best-effort: Title-case first letter
|
||||||
|
return raw.length > 1 ? raw[0].toUpperCase() + raw.slice(1) : raw.toUpperCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间(只显示时分)
|
* 格式化时间(只显示时分)
|
||||||
* @param date 日期字符串或 Date 对象
|
* @param date 日期字符串或 Date 对象
|
||||||
|
|||||||
@@ -35,12 +35,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
|
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'
|
import { formatReasoningEffort } from '@/utils/format'
|
||||||
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'
|
||||||
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
|
import UsageStatsCards from '@/components/admin/usage/UsageStatsCards.vue'; import UsageFilters from '@/components/admin/usage/UsageFilters.vue'
|
||||||
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
|
import UsageTable from '@/components/admin/usage/UsageTable.vue'; import UsageExportProgress from '@/components/admin/usage/UsageExportProgress.vue'
|
||||||
|
import UsageCleanupDialog from '@/components/admin/usage/UsageCleanupDialog.vue'
|
||||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'; import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||||
import type { AdminUsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
import type { AdminUsageLog, TrendDataPoint, ModelStat } from '@/types'; import type { AdminUsageStatsResponse, AdminUsageQueryParams } from '@/api/admin/usage'
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ const exportToExcel = async () => {
|
|||||||
const XLSX = await import('xlsx')
|
const XLSX = await import('xlsx')
|
||||||
const headers = [
|
const headers = [
|
||||||
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
||||||
t('admin.usage.account'), t('usage.model'), t('admin.usage.group'),
|
t('admin.usage.account'), t('usage.model'), t('usage.reasoningEffort'), t('admin.usage.group'),
|
||||||
t('usage.type'),
|
t('usage.type'),
|
||||||
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
||||||
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
||||||
@@ -120,6 +121,7 @@ const exportToExcel = async () => {
|
|||||||
log.api_key?.name || '',
|
log.api_key?.name || '',
|
||||||
log.account?.name || '',
|
log.account?.name || '',
|
||||||
log.model,
|
log.model,
|
||||||
|
formatReasoningEffort(log.reasoning_effort),
|
||||||
log.group?.name || '',
|
log.group?.name || '',
|
||||||
log.stream ? t('usage.stream') : t('usage.sync'),
|
log.stream ? t('usage.stream') : t('usage.sync'),
|
||||||
log.input_tokens,
|
log.input_tokens,
|
||||||
|
|||||||
@@ -157,6 +157,12 @@
|
|||||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #cell-reasoning_effort="{ row }">
|
||||||
|
<span class="text-sm text-gray-900 dark:text-white">
|
||||||
|
{{ formatReasoningEffort(row.reasoning_effort) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #cell-stream="{ row }">
|
<template #cell-stream="{ row }">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
|
class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium"
|
||||||
@@ -438,12 +444,12 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
|||||||
import DataTable from '@/components/common/DataTable.vue'
|
import DataTable from '@/components/common/DataTable.vue'
|
||||||
import Pagination from '@/components/common/Pagination.vue'
|
import Pagination from '@/components/common/Pagination.vue'
|
||||||
import EmptyState from '@/components/common/EmptyState.vue'
|
import EmptyState from '@/components/common/EmptyState.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import DateRangePicker from '@/components/common/DateRangePicker.vue'
|
import DateRangePicker from '@/components/common/DateRangePicker.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types'
|
import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types'
|
||||||
import type { Column } from '@/components/common/types'
|
import type { Column } from '@/components/common/types'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime, formatReasoningEffort } from '@/utils/format'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -466,6 +472,7 @@ const usageStats = ref<UsageStatsResponse | null>(null)
|
|||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
|
{ key: 'api_key', label: t('usage.apiKeyFilter'), sortable: false },
|
||||||
{ key: 'model', label: t('usage.model'), sortable: true },
|
{ key: 'model', label: t('usage.model'), sortable: true },
|
||||||
|
{ key: 'reasoning_effort', label: t('usage.reasoningEffort'), sortable: false },
|
||||||
{ key: 'stream', label: t('usage.type'), sortable: false },
|
{ key: 'stream', label: t('usage.type'), sortable: false },
|
||||||
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
{ key: 'tokens', label: t('usage.tokens'), sortable: false },
|
||||||
{ key: 'cost', label: t('usage.cost'), sortable: false },
|
{ key: 'cost', label: t('usage.cost'), sortable: false },
|
||||||
@@ -723,6 +730,7 @@ const exportToCSV = async () => {
|
|||||||
'Time',
|
'Time',
|
||||||
'API Key Name',
|
'API Key Name',
|
||||||
'Model',
|
'Model',
|
||||||
|
'Reasoning Effort',
|
||||||
'Type',
|
'Type',
|
||||||
'Input Tokens',
|
'Input Tokens',
|
||||||
'Output Tokens',
|
'Output Tokens',
|
||||||
@@ -739,6 +747,7 @@ const exportToCSV = async () => {
|
|||||||
log.created_at,
|
log.created_at,
|
||||||
log.api_key?.name || '',
|
log.api_key?.name || '',
|
||||||
log.model,
|
log.model,
|
||||||
|
formatReasoningEffort(log.reasoning_effort),
|
||||||
log.stream ? 'Stream' : 'Sync',
|
log.stream ? 'Stream' : 'Sync',
|
||||||
log.input_tokens,
|
log.input_tokens,
|
||||||
log.output_tokens,
|
log.output_tokens,
|
||||||
|
|||||||
Reference in New Issue
Block a user