diff --git a/apps/api/internal/clients/clients_test.go b/apps/api/internal/clients/clients_test.go index e883ba6..b94226a 100644 --- a/apps/api/internal/clients/clients_test.go +++ b/apps/api/internal/clients/clients_test.go @@ -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) { diff --git a/apps/api/internal/clients/gemini.go b/apps/api/internal/clients/gemini.go index dc0ac50..6a8f6ed 100644 --- a/apps/api/internal/clients/gemini.go +++ b/apps/api/internal/clients/gemini.go @@ -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() diff --git a/apps/api/internal/clients/openai.go b/apps/api/internal/clients/openai.go index eaf54b4..8a579e1 100644 --- a/apps/api/internal/clients/openai.go +++ b/apps/api/internal/clients/openai.go @@ -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() diff --git a/apps/api/internal/clients/types.go b/apps/api/internal/clients/types.go index 778ae92..8d88793 100644 --- a/apps/api/internal/clients/types.go +++ b/apps/api/internal/clients/types.go @@ -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 } diff --git a/apps/api/internal/clients/volces.go b/apps/api/internal/clients/volces.go index 60ec856..b03c4d3 100644 --- a/apps/api/internal/clients/volces.go +++ b/apps/api/internal/clients/volces.go @@ -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() diff --git a/apps/web/src/pages/WorkspacePage.tsx b/apps/web/src/pages/WorkspacePage.tsx index e7e8eef..c7ff2c8 100644 --- a/apps/web/src/pages/WorkspacePage.tsx +++ b/apps/web/src/pages/WorkspacePage.tsx @@ -812,7 +812,7 @@ function TaskRecord(props: { task: GatewayTask; token: string; onCopyRequestId: {props.task.apiKeyName || props.task.apiKeyPrefix || props.task.apiKeyId || '-'} {tokenUsage} {chargeText} - {formatDuration(props.task.responseDurationMs)} + {formatDuration(taskDurationMs(props.task))} {formatDateTime(props.task.createdAt)}