diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go
index b71ef953d561..18698c95c642 100644
--- a/backend/internal/service/account_test_service.go
+++ b/backend/internal/service/account_test_service.go
@@ -47,6 +47,7 @@ const (
// TestEvent represents a SSE event for account testing
type TestEvent struct {
OutputLimited bool `json:"output_limited,omitempty"`
+ ReasoningTokens *int `json:"reasoning_tokens,omitempty"` // OpenAI Responses test_complete; absent when unreported
RequestedReasoningEffort string `json:"requested_reasoning_effort,omitempty"`
EffectiveReasoningEffort string `json:"effective_reasoning_effort,omitempty"`
Type string `json:"type"`
diff --git a/backend/internal/service/account_test_stream.go b/backend/internal/service/account_test_stream.go
index a7ddf5591986..c0dded7f19a7 100644
--- a/backend/internal/service/account_test_stream.go
+++ b/backend/internal/service/account_test_stream.go
@@ -369,12 +369,32 @@ func parseAccountConnectionStream(protocol string, body io.Reader, allowMedia bo
return false, "", p.incomplete()
}
+// responsesReasoningTokens reads response.usage.output_tokens_details.reasoning_tokens
+// from a Responses stream event.
+func responsesReasoningTokens(data map[string]any) (int, bool) {
+ response, _ := data["response"].(map[string]any)
+ usage, _ := response["usage"].(map[string]any)
+ details, _ := usage["output_tokens_details"].(map[string]any)
+ tokens, ok := details["reasoning_tokens"].(float64)
+ if !ok || tokens < 0 {
+ return 0, false
+ }
+ return int(tokens), true
+}
+
func (s *AccountTestService) processConnectionStream(c *gin.Context, body io.Reader, protocol string, account *Account) error {
+ var reasoningTokens *int
limited, upstreamModel, err := parseAccountConnectionStream(protocol, body, c.GetBool("account_test_allow_media"), func(event TestEvent) { s.sendEvent(c, event) }, func(data map[string]any) {
if account != nil && (data["error"] != nil || data["type"] == "response.failed" || data["type"] == "error") {
raw, _ := json.Marshal(data)
s.markOpenAIBudgetExceededFromTest(c.Request.Context(), account, http.StatusOK, raw)
}
+ // OpenAI accounts only: the admin quality check reads this count.
+ if protocol == "responses" && account.IsOpenAI() {
+ if tokens, ok := responsesReasoningTokens(data); ok {
+ reasoningTokens = &tokens
+ }
+ }
})
if err != nil {
return s.sendTestFailure(c, err)
@@ -385,6 +405,6 @@ func (s *AccountTestService) processConnectionStream(c *gin.Context, body io.Rea
if upstreamModel != "" {
s.sendEvent(c, TestEvent{Type: "upstream_model", UpstreamModel: upstreamModel})
}
- s.sendEvent(c, TestEvent{Type: "test_complete", Success: true, OutputLimited: limited})
+ s.sendEvent(c, TestEvent{Type: "test_complete", Success: true, OutputLimited: limited, ReasoningTokens: reasoningTokens})
return nil
}
diff --git a/backend/internal/service/account_test_stream_contract_test.go b/backend/internal/service/account_test_stream_contract_test.go
index 8bc88ae501fa..0e95c010f47e 100644
--- a/backend/internal/service/account_test_stream_contract_test.go
+++ b/backend/internal/service/account_test_stream_contract_test.go
@@ -97,6 +97,40 @@ func TestAccountConnectionStreamContract(t *testing.T) {
}
}
+func TestAccountConnectionStreamReportsOpenAIReasoningTokens(t *testing.T) {
+ frame := func(data string) string { return "data: " + data + "\n\n" }
+ run := func(account *Account, wire string) (string, map[string]TestEvent) {
+ c, recorder := newTestContext()
+ require.NoError(t, (&AccountTestService{}).processConnectionStream(c, strings.NewReader(wire), "responses", account))
+ events := map[string]TestEvent{}
+ for _, line := range strings.Split(recorder.Body.String(), "\n") {
+ if strings.HasPrefix(line, "data: ") {
+ var event TestEvent
+ require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &event))
+ events[event.Type] = event
+ }
+ }
+ return recorder.Body.String(), events
+ }
+ wire := frame(`{"type":"response.created","response":{"model":"gpt-6-astra","usage":null}}`) +
+ frame(`{"type":"response.output_text.delta","delta":"最少需要取出 21 颗。"}`) +
+ frame(`{"type":"response.completed","response":{"status":"completed","model":"gpt-6-astra","usage":{"input_tokens":40,"output_tokens":900,"output_tokens_details":{"reasoning_tokens":516}}}}`)
+
+ _, events := run(&Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}, wire)
+ require.Equal(t, "gpt-6-astra", events["upstream_model"].UpstreamModel)
+ require.True(t, events["test_complete"].Success)
+ require.NotNil(t, events["test_complete"].ReasoningTokens)
+ require.Equal(t, 516, *events["test_complete"].ReasoningTokens)
+
+ // Other platforms and streams without usage keep the previous event shape.
+ body, _ := run(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth}, wire)
+ require.NotContains(t, body, "reasoning_tokens")
+ body, events = run(&Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
+ frame(`{"type":"response.output_text.delta","delta":"OK"}`)+frame(`{"type":"response.completed","response":{"status":"completed"}}`))
+ require.True(t, events["test_complete"].Success)
+ require.NotContains(t, body, "reasoning_tokens")
+}
+
func TestAccountJobTerminalAndRetryContract(t *testing.T) {
require.Equal(t, AccountJobStatusFailed, AccountJobTerminalStatus(false, "cancel_check_failed", 1, 0, 0))
require.Equal(t, AccountJobStatusCanceled, AccountJobTerminalStatus(true, "", 1, 1, 2))
diff --git a/frontend/src/components/admin/account/AccountTestModal.vue b/frontend/src/components/admin/account/AccountTestModal.vue
index 27e891de400f..44f73ab342a4 100644
--- a/frontend/src/components/admin/account/AccountTestModal.vue
+++ b/frontend/src/components/admin/account/AccountTestModal.vue
@@ -75,15 +75,27 @@
{{ t('admin.accounts.openai.testMode') }}
+ {{ t('admin.accounts.qualityCheck.questionSummary', { answer: CANDY_QUALITY_ANSWER }) }}
+
+