sgl-project--sglang
94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
571 行
17 KiB
Rust
571 行
17 KiB
Rust
use serde_json::json;
|
|
use smg::protocols::{
|
|
chat::{ChatCompletionRequest, ChatMessage, MessageContent},
|
|
common::{
|
|
Function, FunctionCall, FunctionChoice, StreamOptions, Tool, ToolChoice, ToolChoiceValue,
|
|
ToolReference,
|
|
},
|
|
validated::Normalizable,
|
|
};
|
|
use validator::Validate;
|
|
|
|
// Deprecated fields normalization tests
|
|
|
|
#[test]
|
|
fn test_max_tokens_normalizes_to_max_completion_tokens() {
|
|
#[allow(deprecated)]
|
|
let mut req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
max_tokens: Some(100),
|
|
max_completion_tokens: None,
|
|
..Default::default()
|
|
};
|
|
|
|
req.normalize();
|
|
assert_eq!(
|
|
req.max_completion_tokens,
|
|
Some(100),
|
|
"max_tokens should be copied to max_completion_tokens"
|
|
);
|
|
#[allow(deprecated)]
|
|
{
|
|
assert!(
|
|
req.max_tokens.is_none(),
|
|
"Deprecated field should be cleared"
|
|
);
|
|
}
|
|
assert!(
|
|
req.validate().is_ok(),
|
|
"Should be valid after normalization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_max_completion_tokens_takes_precedence() {
|
|
#[allow(deprecated)]
|
|
let mut req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
max_tokens: Some(100),
|
|
max_completion_tokens: Some(200),
|
|
..Default::default()
|
|
};
|
|
|
|
req.normalize();
|
|
assert_eq!(
|
|
req.max_completion_tokens,
|
|
Some(200),
|
|
"max_completion_tokens should take precedence"
|
|
);
|
|
assert!(
|
|
req.validate().is_ok(),
|
|
"Should be valid after normalization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_functions_normalizes_to_tools() {
|
|
#[allow(deprecated)]
|
|
let mut req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
functions: Some(vec![Function {
|
|
name: "test_func".to_string(),
|
|
description: Some("Test function".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
}]),
|
|
tools: None,
|
|
..Default::default()
|
|
};
|
|
|
|
req.normalize();
|
|
assert!(req.tools.is_some(), "functions should be migrated to tools");
|
|
assert_eq!(req.tools.as_ref().unwrap().len(), 1);
|
|
assert_eq!(req.tools.as_ref().unwrap()[0].function.name, "test_func");
|
|
#[allow(deprecated)]
|
|
{
|
|
assert!(
|
|
req.functions.is_none(),
|
|
"Deprecated field should be cleared"
|
|
);
|
|
}
|
|
assert!(
|
|
req.validate().is_ok(),
|
|
"Should be valid after normalization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_call_normalizes_to_tool_choice() {
|
|
#[allow(deprecated)]
|
|
let mut req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
function_call: Some(FunctionCall::None),
|
|
tool_choice: None,
|
|
..Default::default()
|
|
};
|
|
|
|
req.normalize();
|
|
assert!(
|
|
req.tool_choice.is_some(),
|
|
"function_call should be migrated to tool_choice"
|
|
);
|
|
assert!(matches!(
|
|
req.tool_choice,
|
|
Some(ToolChoice::Value(ToolChoiceValue::None))
|
|
));
|
|
#[allow(deprecated)]
|
|
{
|
|
assert!(
|
|
req.function_call.is_none(),
|
|
"Deprecated field should be cleared"
|
|
);
|
|
}
|
|
assert!(
|
|
req.validate().is_ok(),
|
|
"Should be valid after normalization"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_call_function_variant_normalizes() {
|
|
#[allow(deprecated)]
|
|
let mut req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
function_call: Some(FunctionCall::Function {
|
|
name: "my_function".to_string(),
|
|
}),
|
|
tool_choice: None,
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "my_function".to_string(),
|
|
description: None,
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
..Default::default()
|
|
};
|
|
|
|
req.normalize();
|
|
assert!(
|
|
req.tool_choice.is_some(),
|
|
"function_call should be migrated to tool_choice"
|
|
);
|
|
match &req.tool_choice {
|
|
Some(ToolChoice::Function { function, .. }) => {
|
|
assert_eq!(function.name, "my_function");
|
|
}
|
|
_ => panic!("Expected ToolChoice::Function variant"),
|
|
}
|
|
#[allow(deprecated)]
|
|
{
|
|
assert!(
|
|
req.function_call.is_none(),
|
|
"Deprecated field should be cleared"
|
|
);
|
|
}
|
|
assert!(
|
|
req.validate().is_ok(),
|
|
"Should be valid after normalization"
|
|
);
|
|
}
|
|
|
|
// Stream options validation tests
|
|
|
|
#[test]
|
|
fn test_stream_options_requires_stream_enabled() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
stream: false,
|
|
stream_options: Some(StreamOptions {
|
|
include_usage: Some(true),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(
|
|
result.is_err(),
|
|
"Should reject stream_options when stream is false"
|
|
);
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("stream_options") && err.contains("stream") && err.contains("enabled"),
|
|
"Error should mention stream dependency: {}",
|
|
err
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stream_options_valid_when_stream_enabled() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
stream: true,
|
|
stream_options: Some(StreamOptions {
|
|
include_usage: Some(true),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(
|
|
result.is_ok(),
|
|
"Should accept stream_options when stream is true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_stream_options_valid_when_stream_disabled() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
stream: false,
|
|
stream_options: None,
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(
|
|
result.is_ok(),
|
|
"Should accept no stream_options when stream is false"
|
|
);
|
|
}
|
|
|
|
// Tool choice validation tests
|
|
#[test]
|
|
fn test_tool_choice_function_not_found() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::Function {
|
|
function: FunctionChoice {
|
|
name: "nonexistent_function".to_string(),
|
|
},
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_err(), "Should reject nonexistent function name");
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("function 'nonexistent_function' not found"),
|
|
"Error should mention the missing function: {}",
|
|
err
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_function_exists_valid() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::Function {
|
|
function: FunctionChoice {
|
|
name: "get_weather".to_string(),
|
|
},
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_ok(), "Should accept existing function name");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_invalid_mode() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "invalid_mode".to_string(),
|
|
tools: vec![ToolReference::Function {
|
|
name: "get_weather".to_string(),
|
|
}],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_err(), "Should reject invalid mode");
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("must be 'auto' or 'required'"),
|
|
"Error should mention valid modes: {}",
|
|
err
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_valid_mode_auto() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "auto".to_string(),
|
|
tools: vec![ToolReference::Function {
|
|
name: "get_weather".to_string(),
|
|
}],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_ok(), "Should accept 'auto' mode");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_valid_mode_required() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "required".to_string(),
|
|
tools: vec![ToolReference::Function {
|
|
name: "get_weather".to_string(),
|
|
}],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_ok(), "Should accept 'required' mode");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_tool_not_found() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
}]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "auto".to_string(),
|
|
tools: vec![ToolReference::Function {
|
|
name: "nonexistent_tool".to_string(),
|
|
}],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_err(), "Should reject nonexistent tool name");
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("tool 'nonexistent_tool' not found"),
|
|
"Error should mention the missing tool: {}",
|
|
err
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_multiple_tools_valid() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![
|
|
Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
},
|
|
Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_time".to_string(),
|
|
description: Some("Get time".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
},
|
|
]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "auto".to_string(),
|
|
tools: vec![
|
|
ToolReference::Function {
|
|
name: "get_weather".to_string(),
|
|
},
|
|
ToolReference::Function {
|
|
name: "get_time".to_string(),
|
|
},
|
|
],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(result.is_ok(), "Should accept all valid tool references");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_choice_allowed_tools_one_invalid_among_valid() {
|
|
let req = ChatCompletionRequest {
|
|
model: "test-model".to_string(),
|
|
messages: vec![ChatMessage::User {
|
|
content: MessageContent::Text("hello".to_string()),
|
|
name: None,
|
|
}],
|
|
tools: Some(vec![
|
|
Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_weather".to_string(),
|
|
description: Some("Get weather".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
},
|
|
Tool {
|
|
tool_type: "function".to_string(),
|
|
function: Function {
|
|
name: "get_time".to_string(),
|
|
description: Some("Get time".to_string()),
|
|
parameters: json!({}),
|
|
strict: None,
|
|
},
|
|
},
|
|
]),
|
|
tool_choice: Some(ToolChoice::AllowedTools {
|
|
mode: "auto".to_string(),
|
|
tools: vec![
|
|
ToolReference::Function {
|
|
name: "get_weather".to_string(),
|
|
},
|
|
ToolReference::Function {
|
|
name: "nonexistent_tool".to_string(),
|
|
},
|
|
],
|
|
tool_type: "function".to_string(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
let result = req.validate();
|
|
assert!(
|
|
result.is_err(),
|
|
"Should reject if any tool reference is invalid"
|
|
);
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains("tool 'nonexistent_tool' not found"),
|
|
"Error should mention the missing tool: {}",
|
|
err
|
|
);
|
|
}
|