mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 00:21:34 +00:00
fix(usage): hide mapped reasoning effort from users
Users only see the reasoning effort they requested. Admins still see the requested value plus the forwarded mapped value, matching the model column's requested vs upstream split.
This commit is contained in:
@@ -3,6 +3,7 @@ package dto
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
@@ -643,8 +644,7 @@ func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
|
||||
RequestID: l.RequestID,
|
||||
Model: requestedModel,
|
||||
ServiceTier: l.ServiceTier,
|
||||
ReasoningEffort: l.ReasoningEffort,
|
||||
RequestedReasoningEffort: l.RequestedReasoningEffort,
|
||||
ReasoningEffort: userFacingReasoningEffort(l),
|
||||
InboundEndpoint: l.InboundEndpoint,
|
||||
GroupID: l.GroupID,
|
||||
SubscriptionID: l.SubscriptionID,
|
||||
@@ -711,20 +711,53 @@ func UsageLogFromServiceAdmin(l *service.UsageLog) *AdminUsageLog {
|
||||
usageLog := usageLogFromServiceUser(l)
|
||||
usageLog.UpstreamEndpoint = l.UpstreamEndpoint
|
||||
return &AdminUsageLog{
|
||||
UsageLog: usageLog,
|
||||
UpstreamModel: l.UpstreamModel,
|
||||
UpstreamResponseModel: l.UpstreamResponseModel,
|
||||
UpstreamModelMismatch: l.UpstreamModelMismatch,
|
||||
ChannelID: l.ChannelID,
|
||||
ModelMappingChain: l.ModelMappingChain,
|
||||
BillingTier: l.BillingTier,
|
||||
AccountRateMultiplier: l.AccountRateMultiplier,
|
||||
AccountStatsCost: l.AccountStatsCost,
|
||||
IPAddress: l.IPAddress,
|
||||
Account: AccountSummaryFromService(l.Account),
|
||||
UsageLog: usageLog,
|
||||
UpstreamModel: l.UpstreamModel,
|
||||
UpstreamReasoningEffort: adminUpstreamReasoningEffort(l),
|
||||
UpstreamResponseModel: l.UpstreamResponseModel,
|
||||
UpstreamModelMismatch: l.UpstreamModelMismatch,
|
||||
ChannelID: l.ChannelID,
|
||||
ModelMappingChain: l.ModelMappingChain,
|
||||
BillingTier: l.BillingTier,
|
||||
AccountRateMultiplier: l.AccountRateMultiplier,
|
||||
AccountStatsCost: l.AccountStatsCost,
|
||||
IPAddress: l.IPAddress,
|
||||
Account: AccountSummaryFromService(l.Account),
|
||||
}
|
||||
}
|
||||
|
||||
func userFacingReasoningEffort(l *service.UsageLog) *string {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
if requested := strings.TrimSpace(derefString(l.RequestedReasoningEffort)); requested != "" {
|
||||
return &requested
|
||||
}
|
||||
return l.ReasoningEffort
|
||||
}
|
||||
|
||||
func adminUpstreamReasoningEffort(l *service.UsageLog) *string {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
forwarded := strings.TrimSpace(derefString(l.ReasoningEffort))
|
||||
if forwarded == "" {
|
||||
return nil
|
||||
}
|
||||
requested := userFacingReasoningEffort(l)
|
||||
if requested != nil && service.NormalizeMaxReasoningEffort(*requested) == service.NormalizeMaxReasoningEffort(forwarded) {
|
||||
return nil
|
||||
}
|
||||
return &forwarded
|
||||
}
|
||||
|
||||
func derefString(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func UsageCleanupTaskFromService(task *service.UsageCleanupTask) *UsageCleanupTask {
|
||||
if task == nil {
|
||||
return nil
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestUsageLogFromService_KeepsUserBillingAndIPWithoutAdminCostFields(t *test
|
||||
require.NotContains(t, string(userJSON), "account_cost")
|
||||
}
|
||||
|
||||
func TestUsageLogFromService_IncludesRequestedReasoningEffort(t *testing.T) {
|
||||
func TestUsageLogFromService_UsersSeeRequestedReasoningEffortOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
requested := "max"
|
||||
@@ -195,13 +195,43 @@ func TestUsageLogFromService_IncludesRequestedReasoningEffort(t *testing.T) {
|
||||
adminDTO := UsageLogFromServiceAdmin(log)
|
||||
|
||||
require.NotNil(t, userDTO.ReasoningEffort)
|
||||
require.Equal(t, forwarded, *userDTO.ReasoningEffort)
|
||||
require.NotNil(t, userDTO.RequestedReasoningEffort)
|
||||
require.Equal(t, requested, *userDTO.RequestedReasoningEffort)
|
||||
require.Equal(t, requested, *userDTO.ReasoningEffort)
|
||||
require.NotNil(t, adminDTO.ReasoningEffort)
|
||||
require.Equal(t, forwarded, *adminDTO.ReasoningEffort)
|
||||
require.NotNil(t, adminDTO.RequestedReasoningEffort)
|
||||
require.Equal(t, requested, *adminDTO.RequestedReasoningEffort)
|
||||
require.Equal(t, requested, *adminDTO.ReasoningEffort)
|
||||
require.NotNil(t, adminDTO.UpstreamReasoningEffort)
|
||||
require.Equal(t, forwarded, *adminDTO.UpstreamReasoningEffort)
|
||||
|
||||
userJSON, err := json.Marshal(userDTO)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(userJSON), `"reasoning_effort":"max"`)
|
||||
require.NotContains(t, string(userJSON), "upstream_reasoning_effort")
|
||||
require.NotContains(t, string(userJSON), "requested_reasoning_effort")
|
||||
|
||||
adminJSON, err := json.Marshal(adminDTO)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(adminJSON), `"reasoning_effort":"max"`)
|
||||
require.Contains(t, string(adminJSON), `"upstream_reasoning_effort":"xhigh"`)
|
||||
}
|
||||
|
||||
func TestUsageLogFromService_OmitsUpstreamReasoningEffortWhenUnmapped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
effort := "high"
|
||||
log := &service.UsageLog{
|
||||
RequestID: "req_effort_same",
|
||||
Model: "gpt-5.4",
|
||||
ReasoningEffort: &effort,
|
||||
RequestedReasoningEffort: &effort,
|
||||
}
|
||||
|
||||
adminDTO := UsageLogFromServiceAdmin(log)
|
||||
require.NotNil(t, adminDTO.ReasoningEffort)
|
||||
require.Equal(t, effort, *adminDTO.ReasoningEffort)
|
||||
require.Nil(t, adminDTO.UpstreamReasoningEffort)
|
||||
|
||||
adminJSON, err := json.Marshal(adminDTO)
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, string(adminJSON), "upstream_reasoning_effort")
|
||||
}
|
||||
|
||||
func TestUsageLogFromService_FallsBackToLegacyModelWhenRequestedModelMissing(t *testing.T) {
|
||||
|
||||
@@ -486,11 +486,10 @@ type UsageLog struct {
|
||||
Model string `json:"model"`
|
||||
// ServiceTier records the OpenAI service tier used for billing, e.g. "priority" / "flex".
|
||||
ServiceTier *string `json:"service_tier,omitempty"`
|
||||
// ReasoningEffort is the effective effort after group policy / model-family remapping.
|
||||
// ReasoningEffort is the client-requested effort (mapping-hidden, like Model).
|
||||
// OpenAI: "low"/"medium"/"high"/"xhigh"; Claude: "low"/"medium"/"high"/"max".
|
||||
// Historical rows without requested_reasoning_effort fall back to the stored effective value.
|
||||
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
|
||||
// RequestedReasoningEffort is the client-requested effort before mapping.
|
||||
RequestedReasoningEffort *string `json:"requested_reasoning_effort,omitempty"`
|
||||
// InboundEndpoint is the client-facing API endpoint path, e.g. /v1/chat/completions.
|
||||
InboundEndpoint *string `json:"inbound_endpoint,omitempty"`
|
||||
// UpstreamEndpoint is the normalized upstream endpoint path, e.g. /v1/responses.
|
||||
@@ -565,6 +564,9 @@ type AdminUsageLog struct {
|
||||
// UpstreamModel is the actual model sent to the upstream provider after mapping.
|
||||
// Omitted when no mapping was applied (requested model was used as-is).
|
||||
UpstreamModel *string `json:"upstream_model,omitempty"`
|
||||
// UpstreamReasoningEffort is the effort actually forwarded after group policy /
|
||||
// model-family remapping. Omitted when it matches the client-requested value.
|
||||
UpstreamReasoningEffort *string `json:"upstream_reasoning_effort,omitempty"`
|
||||
// UpstreamResponseModel is the raw model declared by the upstream response.
|
||||
UpstreamResponseModel *string `json:"upstream_response_model,omitempty"`
|
||||
// UpstreamModelMismatch is nil when the upstream did not declare a model.
|
||||
|
||||
Reference in New Issue
Block a user