Skip to content

Commit f342ef9

Browse files
committed
fix: _extract_usage_from_response and CreateJobForm
1 parent 97d8081 commit f342ef9

File tree

4 files changed

+164
-64
lines changed

4 files changed

+164
-64
lines changed

frontend/public/locales/en/translation.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -405,25 +405,25 @@
405405
"userSpawnRate": "User Spawn Rate",
406406
"userSpawnRateTooltip": "Number of new virtual users started per second during ramp-up",
407407
"requestFieldMapping": "Request Field Mapping",
408-
"promptFieldPath": "Prompt Field Path",
408+
"promptFieldPath": "Prompt",
409409
"promptFieldPathTooltip": "The key in your request payload that contains the user prompt (needed for performance metrics calculation)",
410-
"streamingResponseConfiguration": "Streaming Response Configuration",
410+
"streamingResponseConfiguration": "Response Field Mapping",
411411
"streamLinePrefix": "Stream Line Prefix",
412412
"streamLinePrefixTooltip": "Text that appears at the beginning of each data line (e.g., \"data:\", \"event:\")",
413413
"dataFormatTooltip": "Format of the streaming data after removing the prefix",
414414
"jsonFormat": "JSON Format",
415415
"plainText": "Plain Text",
416-
"contentFieldPath": "Content Field Path",
416+
"contentFieldPath": "Content",
417417
"contentFieldPathTooltip": "Dot-notation path to the main content in each JSON chunk (e.g., choices.0.delta.content)",
418-
"reasoningFieldPath": "Reasoning Field Path",
418+
"reasoningFieldPath": "Reasoning Content",
419419
"reasoningFieldPathTooltip": "Dot-notation path to reasoning content in JSON (optional, for models that support reasoning)",
420420
"usageFieldPath": "Token Usage Field",
421421
"usageFieldPathTooltip": "Field path for token usage statistics (for token throughput calculation, if not filled or filled incorrectly, the built-in tokenizer will be used to estimate)",
422-
"promptTokensFieldPath": "Prompt Tokens Field Path",
422+
"promptTokensFieldPath": "Prompt Tokens",
423423
"promptTokensFieldPathTooltip": "Dot-notation path to prompt tokens count field (e.g., usage.prompt_tokens)",
424-
"completionTokensFieldPath": "Completion Tokens Field Path",
424+
"completionTokensFieldPath": "Completion Tokens",
425425
"completionTokensFieldPathTooltip": "Dot-notation path to completion tokens count field (e.g., usage.completion_tokens)",
426-
"totalTokensFieldPath": "Total Tokens Field Path",
426+
"totalTokensFieldPath": "Total Tokens",
427427
"totalTokensFieldPathTooltip": "Dot-notation path to total tokens count field (e.g., usage.total_tokens)",
428428
"streamTerminationConfiguration": "Stream Termination Configuration",
429429
"endLinePrefix": "End Line Prefix",
@@ -432,7 +432,7 @@
432432
"stopSignalTooltip": "Text content that indicates the stream has ended (e.g., [DONE], STOP, finished)",
433433
"endFieldPath": "End Field Path",
434434
"endFieldPathTooltip": "JSON path to a field that indicates completion (optional, e.g., choices.0.finish_reason)",
435-
"nonStreamingResponseConfiguration": "Non-Streaming Response Configuration",
435+
"nonStreamingResponseConfiguration": "Response Field Mapping",
436436
"nonStreamingContentFieldPathTooltip": "Dot-notation path to the main content in the response JSON (e.g., choices.0.message.content)",
437437
"nonStreamingReasoningFieldPathTooltip": "Dot-notation path to reasoning content (optional, for models with reasoning capabilities)",
438438
"fieldMappingDescription": "⚠️ Please accurately configure the mapping between the prompt and the response data field. This mapping directly affects the accuracy of data set replacement, load test execution, and performance metrics (such as response latency and token throughput). If the response does not contain the usage field, the built-in tokenizer will be used to estimate the token count.",

frontend/public/locales/zh/translation.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@
407407
"requestFieldMapping": "请求字段映射",
408408
"promptFieldPath": "提示词字段路径",
409409
"promptFieldPathTooltip": "请求参数中包含用户提示词的键(性能指标计算需要)",
410-
"streamingResponseConfiguration": "流式响应配置",
410+
"streamingResponseConfiguration": "响应字段映射",
411411
"streamLinePrefix": "数据行流前缀",
412412
"streamLinePrefixTooltip": "出现在每个数据行开头的文本,如:data:、event:",
413413
"dataFormatTooltip": "移除前缀后的数据格式",
@@ -432,7 +432,7 @@
432432
"stopSignalTooltip": "流式输出结束的标志,如:[DONE]、STOP、finished",
433433
"endFieldPath": "结束字段路径",
434434
"endFieldPathTooltip": "指示完成的字段的JSON路径,如: choices.0.finish_reason",
435-
"nonStreamingResponseConfiguration": "非流式响应配置",
435+
"nonStreamingResponseConfiguration": "响应字段映射",
436436
"nonStreamingContentFieldPathTooltip": "content字段路径,使用点分割,如:choices.0.delta.content",
437437
"nonStreamingReasoningFieldPathTooltip": "reasoning_content字段路径,使用点分割,如:choices.0.delta.reasoning_content",
438438
"fieldMappingDescription": "⚠️ 请准确配置提示词(prompt)与响应数据字段的映射关系。该映射直接影响数据集替换、压测执行及性能指标(如 响应时延、token吞吐量)的统计准确性。若响应中未包含 usage 字段,系统将回退至内置 tokenizer 估算 token 数量。",

frontend/src/components/CreateJobForm.tsx

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,17 @@ const CreateJobFormContent: React.FC<CreateJobFormProps> = ({
343343

344344
// Always preserve original values when copying
345345
dataToFill.field_mapping = originalFieldMapping || {
346-
prompt: '',
347-
stream_prefix: '',
346+
prompt: 'messages.0.content',
347+
stream_prefix: 'data:',
348348
data_format: 'json',
349-
content: '',
350-
reasoning_content: '',
351-
usage: '',
352-
end_prefix: '',
353-
stop_flag: '',
349+
content: 'choices.0.message.content',
350+
reasoning_content: 'choices.0.message.reasoning_content',
351+
prompt_tokens: 'usage.prompt_tokens',
352+
completion_tokens: 'usage.completion_tokens',
353+
total_tokens: 'usage.total_tokens',
354+
end_prefix: 'data:',
354355
end_field: '',
356+
stop_flag: '[DONE]',
355357
};
356358
dataToFill.request_payload = originalRequestPayload;
357359

@@ -2232,27 +2234,6 @@ const CreateJobFormContent: React.FC<CreateJobFormProps> = ({
22322234
</Form.Item>
22332235
</Col>
22342236
</Row>
2235-
{/* <Col span={12}>
2236-
<Form.Item
2237-
name={['field_mapping', 'usage']}
2238-
label={
2239-
<span>
2240-
{t('components.createJobForm.usageFieldPath')}
2241-
<Tooltip
2242-
title={t(
2243-
'components.createJobForm.usageFieldPathTooltip'
2244-
)}
2245-
>
2246-
<InfoCircleOutlined
2247-
style={{ marginLeft: 5 }}
2248-
/>
2249-
</Tooltip>
2250-
</span>
2251-
}
2252-
>
2253-
<Input placeholder='usage' />
2254-
</Form.Item>
2255-
</Col> */}
22562237
</>
22572238
)
22582239
);
@@ -2416,8 +2397,67 @@ const CreateJobFormContent: React.FC<CreateJobFormProps> = ({
24162397
</Form.Item>
24172398
</Col>
24182399
</Row>
2400+
<Row gutter={16} style={{ marginTop: 16 }}>
2401+
<Col span={8}>
2402+
<Form.Item
2403+
name={['field_mapping', 'prompt_tokens']}
2404+
label={
2405+
<span>
2406+
{t('components.createJobForm.promptTokensFieldPath')}
2407+
<Tooltip
2408+
title={t(
2409+
'components.createJobForm.promptTokensFieldPathTooltip'
2410+
)}
2411+
>
2412+
<InfoCircleOutlined style={{ marginLeft: 5 }} />
2413+
</Tooltip>
2414+
</span>
2415+
}
2416+
>
2417+
<Input placeholder='usage.prompt_tokens' />
2418+
</Form.Item>
2419+
</Col>
24192420

2420-
<Row gutter={24} style={{ marginTop: 16 }}>
2421+
<Col span={8}>
2422+
<Form.Item
2423+
name={['field_mapping', 'completion_tokens']}
2424+
label={
2425+
<span>
2426+
{t('components.createJobForm.completionTokensFieldPath')}
2427+
<Tooltip
2428+
title={t(
2429+
'components.createJobForm.completionTokensFieldPathTooltip'
2430+
)}
2431+
>
2432+
<InfoCircleOutlined style={{ marginLeft: 5 }} />
2433+
</Tooltip>
2434+
</span>
2435+
}
2436+
>
2437+
<Input placeholder='usage.completion_tokens' />
2438+
</Form.Item>
2439+
</Col>
2440+
<Col span={8}>
2441+
<Form.Item
2442+
name={['field_mapping', 'total_tokens']}
2443+
label={
2444+
<span>
2445+
{t('components.createJobForm.totalTokensFieldPath')}
2446+
<Tooltip
2447+
title={t(
2448+
'components.createJobForm.totalTokensFieldPathTooltip'
2449+
)}
2450+
>
2451+
<InfoCircleOutlined style={{ marginLeft: 5 }} />
2452+
</Tooltip>
2453+
</span>
2454+
}
2455+
>
2456+
<Input placeholder='usage.total_tokens' />
2457+
</Form.Item>
2458+
</Col>
2459+
</Row>
2460+
{/* <Row gutter={24} style={{ marginTop: 16 }}>
24212461
<Col span={12}>
24222462
<Form.Item
24232463
name={['field_mapping', 'usage']}
@@ -2437,7 +2477,7 @@ const CreateJobFormContent: React.FC<CreateJobFormProps> = ({
24372477
<Input placeholder='usage' />
24382478
</Form.Item>
24392479
</Col>
2440-
</Row>
2480+
</Row> */}
24412481
</div>
24422482
)}
24432483
</div>

st_engine/engine/request_processor.py

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def process_stream_chunk(
316316
EventManager.fire_metric_event(
317317
"Time_to_first_output_token", ttfot, 0
318318
)
319-
return False, None, metrics # Continue processing
319+
return False, None, metrics
320320

321321

322322
# === REQUEST HANDLERS ===
@@ -735,6 +735,81 @@ def handle_stream_request(
735735
return "", "", usage
736736
return metrics.reasoning_content, metrics.content, metrics.usage
737737

738+
@staticmethod
739+
def _extract_usage_from_response(
740+
resp_json: Dict[str, Any], field_mapping: FieldMapping
741+
) -> Dict[str, Optional[int]]:
742+
"""
743+
Extract usage from response JSON using FieldMapping.
744+
Similar to extract_metrics_from_chunk but for non-streaming responses.
745+
"""
746+
usage: Dict[str, Optional[int]] = {
747+
"prompt_tokens": 0,
748+
"completion_tokens": 0,
749+
"total_tokens": 0,
750+
}
751+
752+
# Update prompt tokens if field mapping exists
753+
if field_mapping.prompt_tokens:
754+
prompt_tokens_value = safe_int_convert(
755+
StreamProcessor.get_field_value(resp_json, field_mapping.prompt_tokens)
756+
)
757+
if prompt_tokens_value > 0:
758+
usage["prompt_tokens"] = prompt_tokens_value
759+
760+
# Update completion tokens if field mapping exists
761+
if field_mapping.completion_tokens:
762+
completion_tokens_value = safe_int_convert(
763+
StreamProcessor.get_field_value(
764+
resp_json, field_mapping.completion_tokens
765+
)
766+
)
767+
if completion_tokens_value > 0:
768+
usage["completion_tokens"] = completion_tokens_value
769+
770+
# Update total tokens if field mapping exists
771+
if field_mapping.total_tokens:
772+
total_tokens_value = safe_int_convert(
773+
StreamProcessor.get_field_value(resp_json, field_mapping.total_tokens)
774+
)
775+
if total_tokens_value > 0:
776+
usage["total_tokens"] = total_tokens_value
777+
778+
# Fallback: try to extract from usage field if mappings are not provided
779+
if (
780+
usage["prompt_tokens"] == 0
781+
and usage["completion_tokens"] == 0
782+
and usage["total_tokens"] == 0
783+
):
784+
if "usage" in resp_json and isinstance(resp_json["usage"], dict):
785+
response_usage = resp_json["usage"]
786+
if "prompt_tokens" in response_usage:
787+
usage["prompt_tokens"] = safe_int_convert(
788+
response_usage["prompt_tokens"]
789+
)
790+
if "input_tokens" in response_usage:
791+
usage["prompt_tokens"] = safe_int_convert(
792+
response_usage["input_tokens"]
793+
)
794+
if "completion_tokens" in response_usage:
795+
usage["completion_tokens"] = safe_int_convert(
796+
response_usage["completion_tokens"]
797+
)
798+
if "output_tokens" in response_usage:
799+
usage["completion_tokens"] = safe_int_convert(
800+
response_usage["output_tokens"]
801+
)
802+
if "total_tokens" in response_usage:
803+
usage["total_tokens"] = safe_int_convert(
804+
response_usage["total_tokens"]
805+
)
806+
if "all_tokens" in response_usage:
807+
usage["total_tokens"] = safe_int_convert(
808+
response_usage["all_tokens"]
809+
)
810+
811+
return usage
812+
738813
def handle_non_stream_request(
739814
self, client, base_request_kwargs: Dict[str, Any], start_time: float
740815
) -> Tuple[str, str, Dict[str, Optional[int]]]:
@@ -767,7 +842,6 @@ def handle_non_stream_request(
767842
"total_tokens": 0,
768843
},
769844
)
770-
self.task_logger.info(f"base_request_kwargs: {base_request_kwargs}")
771845

772846
request_kwargs = {**base_request_kwargs, "stream": False}
773847
content, reasoning_content = "", ""
@@ -827,30 +901,16 @@ def handle_non_stream_request(
827901
0,
828902
)
829903

830-
# Extract token counts from usage field if available
831-
if "usage" in resp_json and isinstance(resp_json["usage"], dict):
832-
usage = resp_json["usage"]
833-
self.task_logger.debug(f"usage: {usage}")
904+
# Extract usage and content using FieldMapping
905+
usage = self._extract_usage_from_response(resp_json, field_mapping)
834906

835-
if usage["total_tokens"] is None:
836-
content = (
837-
StreamProcessor.get_field_value(
838-
resp_json, field_mapping.content
839-
)
840-
if field_mapping.content
841-
else ""
842-
)
843-
content = str(content) if content else ""
844-
845-
reasoning_content = (
846-
StreamProcessor.get_field_value(
847-
resp_json, field_mapping.reasoning_content
848-
)
849-
if field_mapping.reasoning_content
850-
else ""
907+
if field_mapping.content:
908+
content = StreamProcessor.get_field_value(
909+
resp_json, field_mapping.content
851910
)
852-
reasoning_content = (
853-
str(reasoning_content) if reasoning_content else ""
911+
if field_mapping.reasoning_content:
912+
reasoning_content = StreamProcessor.get_field_value(
913+
resp_json, field_mapping.reasoning_content
854914
)
855915
response.success()
856916
return reasoning_content, content, usage

0 commit comments

Comments
 (0)