Unverified Commit 186e3675 authored by Wesley Liddick's avatar Wesley Liddick Committed by GitHub
Browse files

Merge pull request #1194 from Ethan0x0000/feat/requested-upstream-model-semantics

feat(usage): 统一使用记录中的请求模型与上游模型语义
parents 421728a9 27948c77
...@@ -1028,14 +1028,15 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex ...@@ -1028,14 +1028,15 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
} }
return &ForwardResult{ return &ForwardResult{
RequestID: requestID, RequestID: requestID,
Usage: *usage, Usage: *usage,
Model: originalModel, Model: originalModel,
Stream: req.Stream, UpstreamModel: mappedModel,
Duration: time.Since(startTime), Stream: req.Stream,
FirstTokenMs: firstTokenMs, Duration: time.Since(startTime),
ImageCount: imageCount, FirstTokenMs: firstTokenMs,
ImageSize: imageSize, ImageCount: imageCount,
ImageSize: imageSize,
}, nil }, nil
} }
...@@ -1241,12 +1242,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. ...@@ -1241,12 +1242,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
estimated := estimateGeminiCountTokens(body) estimated := estimateGeminiCountTokens(body)
c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated}) c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
return &ForwardResult{ return &ForwardResult{
RequestID: "", RequestID: "",
Usage: ClaudeUsage{}, Usage: ClaudeUsage{},
Model: originalModel, Model: originalModel,
Stream: false, UpstreamModel: mappedModel,
Duration: time.Since(startTime), Stream: false,
FirstTokenMs: nil, Duration: time.Since(startTime),
FirstTokenMs: nil,
}, nil }, nil
} }
setOpsUpstreamError(c, 0, safeErr, "") setOpsUpstreamError(c, 0, safeErr, "")
...@@ -1310,12 +1312,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. ...@@ -1310,12 +1312,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
estimated := estimateGeminiCountTokens(body) estimated := estimateGeminiCountTokens(body)
c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated}) c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
return &ForwardResult{ return &ForwardResult{
RequestID: "", RequestID: "",
Usage: ClaudeUsage{}, Usage: ClaudeUsage{},
Model: originalModel, Model: originalModel,
Stream: false, UpstreamModel: mappedModel,
Duration: time.Since(startTime), Stream: false,
FirstTokenMs: nil, Duration: time.Since(startTime),
FirstTokenMs: nil,
}, nil }, nil
} }
// Final attempt: surface the upstream error body (passed through below) instead of a generic retry error. // Final attempt: surface the upstream error body (passed through below) instead of a generic retry error.
...@@ -1350,12 +1353,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. ...@@ -1350,12 +1353,13 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
estimated := estimateGeminiCountTokens(body) estimated := estimateGeminiCountTokens(body)
c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated}) c.JSON(http.StatusOK, map[string]any{"totalTokens": estimated})
return &ForwardResult{ return &ForwardResult{
RequestID: requestID, RequestID: requestID,
Usage: ClaudeUsage{}, Usage: ClaudeUsage{},
Model: originalModel, Model: originalModel,
Stream: false, UpstreamModel: mappedModel,
Duration: time.Since(startTime), Stream: false,
FirstTokenMs: nil, Duration: time.Since(startTime),
FirstTokenMs: nil,
}, nil }, nil
} }
...@@ -1527,14 +1531,15 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin. ...@@ -1527,14 +1531,15 @@ func (s *GeminiMessagesCompatService) ForwardNative(ctx context.Context, c *gin.
} }
return &ForwardResult{ return &ForwardResult{
RequestID: requestID, RequestID: requestID,
Usage: *usage, Usage: *usage,
Model: originalModel, Model: originalModel,
Stream: stream, UpstreamModel: mappedModel,
Duration: time.Since(startTime), Stream: stream,
FirstTokenMs: firstTokenMs, Duration: time.Since(startTime),
ImageCount: imageCount, FirstTokenMs: firstTokenMs,
ImageSize: imageSize, ImageCount: imageCount,
ImageSize: imageSize,
}, nil }, nil
} }
......
package service package service
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
...@@ -15,6 +16,30 @@ import ( ...@@ -15,6 +16,30 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type geminiCompatHTTPUpstreamStub struct {
response *http.Response
err error
calls int
lastReq *http.Request
}
func (s *geminiCompatHTTPUpstreamStub) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
s.calls++
s.lastReq = req
if s.err != nil {
return nil, s.err
}
if s.response == nil {
return nil, fmt.Errorf("missing stub response")
}
resp := *s.response
return &resp, nil
}
func (s *geminiCompatHTTPUpstreamStub) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {
return s.Do(req, proxyURL, accountID, accountConcurrency)
}
// TestConvertClaudeToolsToGeminiTools_CustomType 测试custom类型工具转换 // TestConvertClaudeToolsToGeminiTools_CustomType 测试custom类型工具转换
func TestConvertClaudeToolsToGeminiTools_CustomType(t *testing.T) { func TestConvertClaudeToolsToGeminiTools_CustomType(t *testing.T) {
tests := []struct { tests := []struct {
...@@ -170,6 +195,42 @@ func TestGeminiHandleNativeNonStreamingResponse_DebugDisabledDoesNotEmitHeaderLo ...@@ -170,6 +195,42 @@ func TestGeminiHandleNativeNonStreamingResponse_DebugDisabledDoesNotEmitHeaderLo
require.False(t, logSink.ContainsMessage("[GeminiAPI]"), "debug 关闭时不应输出 Gemini 响应头日志") require.False(t, logSink.ContainsMessage("[GeminiAPI]"), "debug 关闭时不应输出 Gemini 响应头日志")
} }
func TestGeminiMessagesCompatServiceForward_PreservesRequestedModelAndMappedUpstreamModel(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
httpStub := &geminiCompatHTTPUpstreamStub{
response: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"x-request-id": []string{"gemini-req-1"}},
Body: io.NopCloser(strings.NewReader(`{"candidates":[{"content":{"parts":[{"text":"hello"}]}}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":5}}`)),
},
}
svc := &GeminiMessagesCompatService{httpUpstream: httpStub, cfg: &config.Config{}}
account := &Account{
ID: 1,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "test-key",
"model_mapping": map[string]any{
"claude-sonnet-4": "claude-sonnet-4-20250514",
},
},
}
body := []byte(`{"model":"claude-sonnet-4","max_tokens":16,"messages":[{"role":"user","content":"hello"}]}`)
result, err := svc.Forward(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "claude-sonnet-4", result.Model)
require.Equal(t, "claude-sonnet-4-20250514", result.UpstreamModel)
require.Equal(t, 1, httpStub.calls)
require.NotNil(t, httpStub.lastReq)
require.Contains(t, httpStub.lastReq.URL.String(), "/models/claude-sonnet-4-20250514:")
}
func TestConvertClaudeMessagesToGeminiGenerateContent_AddsThoughtSignatureForToolUse(t *testing.T) { func TestConvertClaudeMessagesToGeminiGenerateContent_AddsThoughtSignatureForToolUse(t *testing.T) {
claudeReq := map[string]any{ claudeReq := map[string]any{
"model": "claude-haiku-4-5-20251001", "model": "claude-haiku-4-5-20251001",
......
...@@ -879,6 +879,7 @@ func TestOpenAIGatewayServiceRecordUsage_UsesRequestedModelAndUpstreamModelMetad ...@@ -879,6 +879,7 @@ func TestOpenAIGatewayServiceRecordUsage_UsesRequestedModelAndUpstreamModelMetad
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog) require.NotNil(t, usageRepo.lastLog)
require.Equal(t, "gpt-5.1", usageRepo.lastLog.Model) require.Equal(t, "gpt-5.1", usageRepo.lastLog.Model)
require.Equal(t, "gpt-5.1", usageRepo.lastLog.RequestedModel)
require.NotNil(t, usageRepo.lastLog.UpstreamModel) require.NotNil(t, usageRepo.lastLog.UpstreamModel)
require.Equal(t, "gpt-5.1-codex", *usageRepo.lastLog.UpstreamModel) require.Equal(t, "gpt-5.1-codex", *usageRepo.lastLog.UpstreamModel)
require.NotNil(t, usageRepo.lastLog.ServiceTier) require.NotNil(t, usageRepo.lastLog.ServiceTier)
...@@ -894,6 +895,40 @@ func TestOpenAIGatewayServiceRecordUsage_UsesRequestedModelAndUpstreamModelMetad ...@@ -894,6 +895,40 @@ func TestOpenAIGatewayServiceRecordUsage_UsesRequestedModelAndUpstreamModelMetad
require.Equal(t, 1, userRepo.deductCalls) require.Equal(t, 1, userRepo.deductCalls)
} }
func TestOpenAIGatewayServiceRecordUsage_BillsMappedRequestsUsingUpstreamModelFallback(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
subRepo := &openAIRecordUsageSubRepoStub{}
svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil)
usage := OpenAIUsage{InputTokens: 20, OutputTokens: 10}
expectedCost, err := svc.billingService.CalculateCost("gpt-5.1-codex", UsageTokens{
InputTokens: 20,
OutputTokens: 10,
}, 1.1)
require.NoError(t, err)
err = svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "resp_upstream_model_billing_fallback",
Model: "gpt-5.1",
UpstreamModel: "gpt-5.1-codex",
Usage: usage,
Duration: time.Second,
},
APIKey: &APIKey{ID: 10},
User: &User{ID: 20},
Account: &Account{ID: 30},
})
require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog)
require.Equal(t, "gpt-5.1", usageRepo.lastLog.Model)
require.Equal(t, expectedCost.ActualCost, usageRepo.lastLog.ActualCost)
require.Equal(t, expectedCost.TotalCost, usageRepo.lastLog.TotalCost)
require.Equal(t, expectedCost.ActualCost, userRepo.lastAmount)
}
func TestOpenAIGatewayServiceRecordUsage_SubscriptionBillingSetsSubscriptionFields(t *testing.T) { func TestOpenAIGatewayServiceRecordUsage_SubscriptionBillingSetsSubscriptionFields(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{} userRepo := &openAIRecordUsageUserRepoStub{}
......
...@@ -4110,9 +4110,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec ...@@ -4110,9 +4110,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
multiplier = resolver.Resolve(ctx, user.ID, *apiKey.GroupID, apiKey.Group.RateMultiplier) multiplier = resolver.Resolve(ctx, user.ID, *apiKey.GroupID, apiKey.Group.RateMultiplier)
} }
billingModel := result.Model billingModel := forwardResultBillingModel(result.Model, result.UpstreamModel)
if result.BillingModel != "" { if result.BillingModel != "" {
billingModel = result.BillingModel billingModel = strings.TrimSpace(result.BillingModel)
} }
serviceTier := "" serviceTier := ""
if result.ServiceTier != nil { if result.ServiceTier != nil {
...@@ -4140,6 +4140,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec ...@@ -4140,6 +4140,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
AccountID: account.ID, AccountID: account.ID,
RequestID: requestID, RequestID: requestID,
Model: result.Model, Model: result.Model,
RequestedModel: result.Model,
UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model), UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, result.Model),
ServiceTier: result.ServiceTier, ServiceTier: result.ServiceTier,
ReasoningEffort: result.ReasoningEffort, ReasoningEffort: result.ReasoningEffort,
......
...@@ -2328,6 +2328,7 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2( ...@@ -2328,6 +2328,7 @@ func (s *OpenAIGatewayService) forwardOpenAIWSV2(
RequestID: responseID, RequestID: responseID,
Usage: *usage, Usage: *usage,
Model: originalModel, Model: originalModel,
UpstreamModel: mappedModel,
ServiceTier: extractOpenAIServiceTier(reqBody), ServiceTier: extractOpenAIServiceTier(reqBody),
ReasoningEffort: extractOpenAIReasoningEffort(reqBody, originalModel), ReasoningEffort: extractOpenAIReasoningEffort(reqBody, originalModel),
Stream: reqStream, Stream: reqStream,
...@@ -2945,6 +2946,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient( ...@@ -2945,6 +2946,7 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
RequestID: responseID, RequestID: responseID,
Usage: usage, Usage: usage,
Model: originalModel, Model: originalModel,
UpstreamModel: mappedModel,
ServiceTier: extractOpenAIServiceTierFromBody(payload), ServiceTier: extractOpenAIServiceTierFromBody(payload),
ReasoningEffort: extractOpenAIReasoningEffortFromBody(payload, originalModel), ReasoningEffort: extractOpenAIReasoningEffortFromBody(payload, originalModel),
Stream: reqStream, Stream: reqStream,
......
...@@ -148,10 +148,13 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun ...@@ -148,10 +148,13 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun
s.writeSoraError(c, http.StatusBadRequest, "invalid_request_error", "model is required", clientStream) s.writeSoraError(c, http.StatusBadRequest, "invalid_request_error", "model is required", clientStream)
return nil, errors.New("model is required") return nil, errors.New("model is required")
} }
originalModel := reqModel
mappedModel := account.GetMappedModel(reqModel) mappedModel := account.GetMappedModel(reqModel)
var upstreamModel string
if mappedModel != "" && mappedModel != reqModel { if mappedModel != "" && mappedModel != reqModel {
reqModel = mappedModel reqModel = mappedModel
upstreamModel = mappedModel
} }
modelCfg, ok := GetSoraModelConfig(reqModel) modelCfg, ok := GetSoraModelConfig(reqModel)
...@@ -213,13 +216,14 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun ...@@ -213,13 +216,14 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun
c.JSON(http.StatusOK, buildSoraNonStreamResponse(content, reqModel)) c.JSON(http.StatusOK, buildSoraNonStreamResponse(content, reqModel))
} }
return &ForwardResult{ return &ForwardResult{
RequestID: "", RequestID: "",
Model: reqModel, Model: originalModel,
Stream: clientStream, UpstreamModel: upstreamModel,
Duration: time.Since(startTime), Stream: clientStream,
FirstTokenMs: firstTokenMs, Duration: time.Since(startTime),
Usage: ClaudeUsage{}, FirstTokenMs: firstTokenMs,
MediaType: "prompt", Usage: ClaudeUsage{},
MediaType: "prompt",
}, nil }, nil
} }
...@@ -269,13 +273,14 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun ...@@ -269,13 +273,14 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun
c.JSON(http.StatusOK, resp) c.JSON(http.StatusOK, resp)
} }
return &ForwardResult{ return &ForwardResult{
RequestID: "", RequestID: "",
Model: reqModel, Model: originalModel,
Stream: clientStream, UpstreamModel: upstreamModel,
Duration: time.Since(startTime), Stream: clientStream,
FirstTokenMs: firstTokenMs, Duration: time.Since(startTime),
Usage: ClaudeUsage{}, FirstTokenMs: firstTokenMs,
MediaType: "prompt", Usage: ClaudeUsage{},
MediaType: "prompt",
}, nil }, nil
} }
if characterResult != nil && strings.TrimSpace(characterResult.Username) != "" { if characterResult != nil && strings.TrimSpace(characterResult.Username) != "" {
...@@ -419,16 +424,17 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun ...@@ -419,16 +424,17 @@ func (s *SoraGatewayService) Forward(ctx context.Context, c *gin.Context, accoun
} }
return &ForwardResult{ return &ForwardResult{
RequestID: taskID, RequestID: taskID,
Model: reqModel, Model: originalModel,
Stream: clientStream, UpstreamModel: upstreamModel,
Duration: time.Since(startTime), Stream: clientStream,
FirstTokenMs: firstTokenMs, Duration: time.Since(startTime),
Usage: ClaudeUsage{}, FirstTokenMs: firstTokenMs,
MediaType: mediaType, Usage: ClaudeUsage{},
MediaURL: firstMediaURL(finalURLs), MediaType: mediaType,
ImageCount: imageCount, MediaURL: firstMediaURL(finalURLs),
ImageSize: imageSize, ImageCount: imageCount,
ImageSize: imageSize,
}, nil }, nil
} }
......
...@@ -144,6 +144,11 @@ func TestSoraGatewayService_ForwardPromptEnhance(t *testing.T) { ...@@ -144,6 +144,11 @@ func TestSoraGatewayService_ForwardPromptEnhance(t *testing.T) {
ID: 1, ID: 1,
Platform: PlatformSora, Platform: PlatformSora,
Status: StatusActive, Status: StatusActive,
Credentials: map[string]any{
"model_mapping": map[string]any{
"prompt-enhance-short-10s": "prompt-enhance-short-15s",
},
},
} }
body := []byte(`{"model":"prompt-enhance-short-10s","messages":[{"role":"user","content":"cat running"}],"stream":false}`) body := []byte(`{"model":"prompt-enhance-short-10s","messages":[{"role":"user","content":"cat running"}],"stream":false}`)
...@@ -152,6 +157,7 @@ func TestSoraGatewayService_ForwardPromptEnhance(t *testing.T) { ...@@ -152,6 +157,7 @@ func TestSoraGatewayService_ForwardPromptEnhance(t *testing.T) {
require.NotNil(t, result) require.NotNil(t, result)
require.Equal(t, "prompt", result.MediaType) require.Equal(t, "prompt", result.MediaType)
require.Equal(t, "prompt-enhance-short-10s", result.Model) require.Equal(t, "prompt-enhance-short-10s", result.Model)
require.Equal(t, "prompt-enhance-short-15s", result.UpstreamModel)
} }
func TestSoraGatewayService_ForwardStoryboardPrompt(t *testing.T) { func TestSoraGatewayService_ForwardStoryboardPrompt(t *testing.T) {
......
...@@ -98,6 +98,9 @@ type UsageLog struct { ...@@ -98,6 +98,9 @@ type UsageLog struct {
AccountID int64 AccountID int64
RequestID string RequestID string
Model string Model string
// RequestedModel is the client-requested model name recorded for stable user/admin display.
// Empty should be treated as Model for backward compatibility with historical rows.
RequestedModel string
// UpstreamModel is the actual model sent to the upstream provider after mapping. // UpstreamModel is the actual model sent to the upstream provider after mapping.
// Nil means no mapping was applied (requested model was used as-is). // Nil means no mapping was applied (requested model was used as-is).
UpstreamModel *string UpstreamModel *string
......
...@@ -19,3 +19,10 @@ func optionalNonEqualStringPtr(value, compare string) *string { ...@@ -19,3 +19,10 @@ func optionalNonEqualStringPtr(value, compare string) *string {
} }
return &value return &value
} }
func forwardResultBillingModel(requestedModel, upstreamModel string) string {
if trimmedUpstream := strings.TrimSpace(upstreamModel); trimmedUpstream != "" {
return trimmedUpstream
}
return strings.TrimSpace(requestedModel)
}
-- Add requested_model field to usage_logs for normalized request/upstream model tracking.
-- NULL means historical rows written before requested_model dual-write was introduced.
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS requested_model VARCHAR(100);
-- Support requested_model / upstream_model aggregations with time-range filters.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_usage_logs_created_requested_model_upstream_model
ON usage_logs (created_at, requested_model, upstream_model);
...@@ -39,6 +39,7 @@ const DataTableStub = { ...@@ -39,6 +39,7 @@ const DataTableStub = {
template: ` template: `
<div> <div>
<div v-for="row in data" :key="row.request_id"> <div v-for="row in data" :key="row.request_id">
<slot name="cell-model" :row="row" :value="row.model" />
<slot name="cell-cost" :row="row" /> <slot name="cell-cost" :row="row" />
</div> </div>
</div> </div>
...@@ -108,4 +109,42 @@ describe('admin UsageTable tooltip', () => { ...@@ -108,4 +109,42 @@ describe('admin UsageTable tooltip', () => {
expect(text).toContain('$30.0000 / 1M tokens') expect(text).toContain('$30.0000 / 1M tokens')
expect(text).toContain('$0.069568') expect(text).toContain('$0.069568')
}) })
it('shows requested and upstream models separately for admin rows', () => {
const row = {
request_id: 'req-admin-model-1',
model: 'claude-sonnet-4',
upstream_model: 'claude-sonnet-4-20250514',
actual_cost: 0,
total_cost: 0,
account_rate_multiplier: 1,
rate_multiplier: 1,
input_cost: 0,
output_cost: 0,
cache_creation_cost: 0,
cache_read_cost: 0,
input_tokens: 0,
output_tokens: 0,
}
const wrapper = mount(UsageTable, {
props: {
data: [row],
loading: false,
columns: [],
},
global: {
stubs: {
DataTable: DataTableStub,
EmptyState: true,
Icon: true,
Teleport: true,
},
},
})
const text = wrapper.text()
expect(text).toContain('claude-sonnet-4')
expect(text).toContain('claude-sonnet-4-20250514')
})
}) })
...@@ -978,7 +978,6 @@ export interface UsageLog { ...@@ -978,7 +978,6 @@ export interface UsageLog {
account_id: number | null account_id: number | null
request_id: string request_id: string
model: string model: string
upstream_model?: string | null
service_tier?: string | null service_tier?: string | null
reasoning_effort?: string | null reasoning_effort?: string | null
inbound_endpoint?: string | null inbound_endpoint?: string | null
...@@ -1033,6 +1032,8 @@ export interface UsageLogAccountSummary { ...@@ -1033,6 +1032,8 @@ export interface UsageLogAccountSummary {
} }
export interface AdminUsageLog extends UsageLog { export interface AdminUsageLog extends UsageLog {
upstream_model?: string | null
// 账号计费倍率(仅管理员可见) // 账号计费倍率(仅管理员可见)
account_rate_multiplier?: number | null account_rate_multiplier?: number | null
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment