diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go
index 886a5535..22d3f1f0 100644
--- a/backend/internal/handler/dto/mappers.go
+++ b/backend/internal/handler/dto/mappers.go
@@ -366,6 +366,7 @@ func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
AccountID: l.AccountID,
RequestID: l.RequestID,
Model: l.Model,
+ ReasoningEffort: l.ReasoningEffort,
GroupID: l.GroupID,
SubscriptionID: l.SubscriptionID,
InputTokens: l.InputTokens,
diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go
index 4cfaef5f..64979189 100644
--- a/backend/internal/handler/dto/types.go
+++ b/backend/internal/handler/dto/types.go
@@ -222,6 +222,9 @@ type UsageLog struct {
AccountID int64 `json:"account_id"`
RequestID string `json:"request_id"`
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"`
SubscriptionID *int64 `json:"subscription_id"`
diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go
index 963db7ba..dc8f1460 100644
--- a/backend/internal/repository/usage_log_repo.go
+++ b/backend/internal/repository/usage_log_repo.go
@@ -22,7 +22,7 @@ import (
"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 {
client *dbent.Client
@@ -111,21 +111,22 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
duration_ms,
first_token_ms,
user_agent,
- ip_address,
- image_count,
- image_size,
- created_at
- ) VALUES (
- $1, $2, $3, $4, $5,
- $6, $7,
- $8, $9, $10, $11,
- $12, $13,
- $14, $15, $16, $17, $18, $19,
- $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30
- )
- ON CONFLICT (request_id, api_key_id) DO NOTHING
- RETURNING id, created_at
- `
+ ip_address,
+ image_count,
+ image_size,
+ reasoning_effort,
+ created_at
+ ) VALUES (
+ $1, $2, $3, $4, $5,
+ $6, $7,
+ $8, $9, $10, $11,
+ $12, $13,
+ $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
+ `
groupID := nullInt64(log.GroupID)
subscriptionID := nullInt64(log.SubscriptionID)
@@ -134,6 +135,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
userAgent := nullString(log.UserAgent)
ipAddress := nullString(log.IPAddress)
imageSize := nullString(log.ImageSize)
+ reasoningEffort := nullString(log.ReasoningEffort)
var requestIDArg any
if requestID != "" {
@@ -170,6 +172,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
ipAddress,
log.ImageCount,
imageSize,
+ reasoningEffort,
createdAt,
}
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
imageCount int
imageSize sql.NullString
+ reasoningEffort sql.NullString
createdAt time.Time
)
@@ -2124,6 +2128,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
&ipAddress,
&imageCount,
&imageSize,
+ &reasoningEffort,
&createdAt,
); err != nil {
return nil, err
@@ -2183,6 +2188,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
if imageSize.Valid {
log.ImageSize = &imageSize.String
}
+ if reasoningEffort.Valid {
+ log.ReasoningEffort = &reasoningEffort.String
+ }
return log, nil
}
diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go
index b1866dee..04ea2930 100644
--- a/backend/internal/service/openai_gateway_service.go
+++ b/backend/internal/service/openai_gateway_service.go
@@ -156,12 +156,15 @@ type OpenAIUsage struct {
// OpenAIForwardResult represents the result of forwarding
type OpenAIForwardResult struct {
- RequestID string
- Usage OpenAIUsage
- Model string
- Stream bool
- Duration time.Duration
- FirstTokenMs *int
+ RequestID string
+ Usage OpenAIUsage
+ Model string
+ // ReasoningEffort is extracted from request body (reasoning.effort) or derived from model suffix.
+ // Stored for usage records display; nil means not provided / not applicable.
+ ReasoningEffort *string
+ Stream bool
+ Duration time.Duration
+ FirstTokenMs *int
}
// 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{
- RequestID: resp.Header.Get("x-request-id"),
- Usage: *usage,
- Model: originalModel,
- Stream: reqStream,
- Duration: time.Since(startTime),
- FirstTokenMs: firstTokenMs,
+ RequestID: resp.Header.Get("x-request-id"),
+ Usage: *usage,
+ Model: originalModel,
+ ReasoningEffort: reasoningEffort,
+ Stream: reqStream,
+ Duration: time.Since(startTime),
+ FirstTokenMs: firstTokenMs,
}, nil
}
@@ -1728,6 +1734,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
AccountID: account.ID,
RequestID: result.RequestID,
Model: result.Model,
+ ReasoningEffort: result.ReasoningEffort,
InputTokens: actualInputTokens,
OutputTokens: result.Usage.OutputTokens,
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
@@ -1922,3 +1929,86 @@ func (s *OpenAIGatewayService) updateCodexUsageSnapshot(ctx context.Context, acc
_ = 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 ""
+ }
+}
diff --git a/backend/internal/service/usage_log.go b/backend/internal/service/usage_log.go
index 3b0e934f..a9721d7f 100644
--- a/backend/internal/service/usage_log.go
+++ b/backend/internal/service/usage_log.go
@@ -14,6 +14,9 @@ type UsageLog struct {
AccountID int64
RequestID 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
SubscriptionID *int64
diff --git a/backend/migrations/046_add_usage_log_reasoning_effort.sql b/backend/migrations/046_add_usage_log_reasoning_effort.sql
new file mode 100644
index 00000000..f6572d1d
--- /dev/null
+++ b/backend/migrations/046_add_usage_log_reasoning_effort.sql
@@ -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);
+
diff --git a/frontend/src/components/admin/usage/UsageTable.vue b/frontend/src/components/admin/usage/UsageTable.vue
index f6d1b1be..5edbd3b6 100644
--- a/frontend/src/components/admin/usage/UsageTable.vue
+++ b/frontend/src/components/admin/usage/UsageTable.vue
@@ -21,6 +21,12 @@
{{ value }}
+
+
+ {{ formatReasoningEffort(row.reasoning_effort) }}
+
+
+
{{ row.group.name }}
@@ -232,14 +238,14 @@
-