const QWEN_CLI_VERSION: &str = "0.14.0";
fn node_arch() -> &'static str {
match std::env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
"arm" => "arm",
"x86" => "ia32",
other => other,
}
}
fn user_agent_platform() -> String {
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
other => other,
};
format!("{}; {}", os, node_arch())
}
pub fn qwen_extra_headers() -> Vec<(String, String)> {
let ua = format!("QwenCode/{} ({})", QWEN_CLI_VERSION, user_agent_platform());
vec![
("User-Agent".to_string(), ua.clone()),
("X-DashScope-CacheControl".to_string(), "enable".to_string()),
("X-DashScope-UserAgent".to_string(), ua),
("X-DashScope-AuthType".to_string(), "qwen-oauth".to_string()),
]
}
pub(crate) fn qwen_session_id() -> &'static str {
use std::sync::OnceLock;
static SESSION: OnceLock<String> = OnceLock::new();
SESSION.get_or_init(|| uuid::Uuid::new_v4().to_string())
}
pub(crate) fn qwen_prompt_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut x = nanos as u64 ^ 0x9E37_79B9_7F4A_7C15;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
format!("{:013x}", x & 0x000F_FFFF_FFFF_FFFF)
}
pub(crate) fn is_vision_model(model: &str) -> bool {
let m = model.to_ascii_lowercase();
if m == "coder-model" {
return true;
}
for prefix in ["qwen-vl", "qwen3-vl-plus", "qwen3.5-plus"] {
if m.starts_with(prefix) {
return true;
}
}
false
}
fn normalize_content_to_array(content: &serde_json::Value) -> Vec<serde_json::Value> {
match content {
serde_json::Value::String(s) => {
vec![serde_json::json!({ "type": "text", "text": s })]
}
serde_json::Value::Array(arr) => arr.clone(),
_ => Vec::new(),
}
}
pub fn looks_like_qwen_target(base_url: &str, model: &str) -> bool {
let url = base_url.to_ascii_lowercase();
let model_lower = model.to_ascii_lowercase();
let url_match = url.contains("dashscope")
|| url.contains("aliyun")
|| url.contains("aliyuncs")
|| url.contains("dialagram");
let model_match = model_lower.starts_with("qwen");
url_match || model_match
}
fn add_cache_control_to_content(content: &serde_json::Value) -> serde_json::Value {
let mut arr = normalize_content_to_array(content);
if let Some(last) = arr.last_mut()
&& let Some(obj) = last.as_object_mut()
{
obj.insert(
"cache_control".to_string(),
serde_json::json!({ "type": "ephemeral" }),
);
}
serde_json::Value::Array(arr)
}
pub fn qwen_body_transform(mut body: serde_json::Value) -> serde_json::Value {
let obj = match body.as_object_mut() {
Some(o) => o,
None => return body,
};
let is_streaming = obj.get("stream").and_then(|v| v.as_bool()).unwrap_or(false);
if let Some(serde_json::Value::Array(messages)) = obj.get_mut("messages") {
let msg_count = messages.len();
if msg_count > 0 {
let system_idx = messages
.iter()
.position(|m| m.get("role").and_then(|r| r.as_str()) == Some("system"));
let last_idx = msg_count - 1;
for (i, msg) in messages.iter_mut().enumerate() {
let should_cache = (Some(i) == system_idx) || (is_streaming && i == last_idx);
if !should_cache {
continue;
}
let Some(msg_obj) = msg.as_object_mut() else {
continue;
};
let content = match msg_obj.get("content") {
Some(c) if !c.is_null() => c.clone(),
_ => continue,
};
msg_obj.insert(
"content".to_string(),
add_cache_control_to_content(&content),
);
}
}
}
obj.insert(
"metadata".to_string(),
serde_json::json!({
"sessionId": qwen_session_id(),
"promptId": qwen_prompt_id(),
}),
);
let model = obj
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if is_vision_model(&model) {
obj.insert(
"vl_high_resolution_images".to_string(),
serde_json::Value::Bool(true),
);
}
if is_streaming
&& let Some(serde_json::Value::Array(tools)) = obj.get_mut("tools")
&& let Some(last) = tools.last_mut()
&& let Some(tool_obj) = last.as_object_mut()
{
tool_obj.insert(
"cache_control".to_string(),
serde_json::json!({ "type": "ephemeral" }),
);
}
body
}