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)}