fix task duration recording

This commit is contained in:
wangbo 2026-05-18 01:06:52 +08:00
parent d09a4c2e4d
commit ba419cd90a
6 changed files with 44 additions and 6 deletions

View File

@ -110,6 +110,7 @@ func TestOpenAIClientChatContract(t *testing.T) {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
time.Sleep(25 * time.Millisecond)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "chatcmpl-test",
"object": "chat.completion",
@ -145,6 +146,9 @@ func TestOpenAIClientChatContract(t *testing.T) {
if response.RequestID != "req-chat-test" || response.ResponseStartedAt.IsZero() || response.ResponseFinishedAt.IsZero() {
t.Fatalf("response metadata was not captured: %+v", response)
}
if response.ResponseDurationMS < 20 {
t.Fatalf("response duration should include upstream latency, got %dms", response.ResponseDurationMS)
}
}
func TestOpenAIClientChatStreamContract(t *testing.T) {

View File

@ -27,11 +27,11 @@ func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error
return Response{}, err
}
req.Header.Set("Content-Type", "application/json")
responseStartedAt := time.Now()
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
responseFinishedAt := time.Now()

View File

@ -33,11 +33,11 @@ func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
responseStartedAt := time.Now()
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeOpenAIResponse(resp, stream, request.StreamDelta)
responseFinishedAt := time.Now()

View File

@ -146,5 +146,8 @@ func responseDurationMS(startedAt time.Time, finishedAt time.Time) int64 {
if duration < 0 {
return 0
}
if duration == 0 && finishedAt.After(startedAt) {
return 1
}
return duration
}

View File

@ -45,11 +45,11 @@ func (c VolcesClient) runImage(ctx context.Context, request Request, apiKey stri
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
responseStartedAt := time.Now()
resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
responseFinishedAt := time.Now()

View File

@ -812,7 +812,7 @@ function TaskRecord(props: { task: GatewayTask; token: string; onCopyRequestId:
<TableCell>{props.task.apiKeyName || props.task.apiKeyPrefix || props.task.apiKeyId || '-'}</TableCell>
<TableCell className="taskRecordTokenCell">{tokenUsage}</TableCell>
<TableCell>{chargeText}</TableCell>
<TableCell>{formatDuration(props.task.responseDurationMs)}</TableCell>
<TableCell>{formatDuration(taskDurationMs(props.task))}</TableCell>
<TableCell>{formatDateTime(props.task.createdAt)}</TableCell>
<TableCell>
<Button type="button" variant="ghost" size="sm" className="taskRecordJsonButton" title={taskErrorText(props.task) || '查看原始 JSON'} onClick={() => props.onOpenJson(props.task)}>
@ -1029,7 +1029,7 @@ function taskAttemptMeta(attempt: NonNullable<GatewayTask['attempts']>[number])
attempt.providerModelName || attempt.modelName || attempt.modelAlias,
attempt.requestId ? `RequestID ${attempt.requestId}` : '',
statusCode ? `状态码 ${statusCode}` : '',
attempt.responseDurationMs ? formatDuration(attempt.responseDurationMs) : '',
formatDuration(attemptDurationMs(attempt)),
].filter(Boolean);
return values.join(' · ') || attempt.clientId || '-';
}
@ -1355,10 +1355,41 @@ function tokenValue(value: unknown) {
return Number.isFinite(numericValue) ? numericValue : null;
}
function taskDurationMs(task: GatewayTask) {
return (
positiveDurationMs(task.responseDurationMs) ??
elapsedDurationMs(task.responseStartedAt, task.responseFinishedAt) ??
elapsedDurationMs(task.createdAt, task.finishedAt)
);
}
function attemptDurationMs(attempt: NonNullable<GatewayTask['attempts']>[number]) {
return (
positiveDurationMs(attempt.responseDurationMs) ??
elapsedDurationMs(attempt.responseStartedAt, attempt.responseFinishedAt) ??
elapsedDurationMs(attempt.startedAt, attempt.finishedAt)
);
}
function positiveDurationMs(value?: number) {
if (value === undefined || value === null) return undefined;
const numericValue = Number(value);
return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : undefined;
}
function elapsedDurationMs(start?: string, end?: string) {
if (!start || !end) return undefined;
const startedAt = new Date(start).getTime();
const finishedAt = new Date(end).getTime();
if (!Number.isFinite(startedAt) || !Number.isFinite(finishedAt)) return undefined;
const elapsed = finishedAt - startedAt;
return elapsed > 0 ? Math.max(1, Math.round(elapsed)) : undefined;
}
function formatDuration(value?: number) {
if (value === undefined || value === null) return '-';
const milliseconds = Math.max(0, Math.round(value));
if (milliseconds === 0) return '0秒';
if (milliseconds === 0) return '-';
if (milliseconds < 1000) return `${milliseconds}毫秒`;
const totalSeconds = Math.round(milliseconds / 1000);
const hours = Math.floor(totalSeconds / 3600);