Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
73 KiB
JSON

{"repo": "aidyou/chatspeed", "files": {"src-tauri/src/ccproxy/handler/request_preprocessor.rs": {"6924c6a9895601d56504131669bffc4e240cdc88": {"t": 1785746256.5289693, "n": 12185, "h": "4d9d5a1cf0d1406a", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_effort(body_json: &mut Value) {\n let Some(reasoning_effort) = body_json\n .get_mut(\"reasoning_effort\")\n .and_then(|value| value.as_str().map(str::to_owned))\n else {\n return;\n };\n\n if reasoning_effort.eq_ignore_ascii_case(\"xhigh\") {\n body_json[\"reasoning_effort\"] = Value::String(\"max\".to_string());\n }\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(\n chat_protocol,\n ChatProtocol::OpenAI | ChatProtocol::HuggingFace\n )\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_effort(&mut body_json);\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(\n unified_request.tool_choice,\n Some(UnifiedToolChoice::Required)\n ) && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider_id: 1,\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_maps_xhigh_reasoning_effort_to_max() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"reasoning_effort\": \"xhigh\",\n \"messages\": [{ \"role\": \"user\", \"content\": \"start\" }]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(processed_json[\"reasoning_effort\"], \"max\");\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}, "6e42f8b7affd32ed17f4381b8f20d473bd222fb5": {"t": 1785761175.267947, "n": 10992, "h": "9ef87352be8a678a", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(\n chat_protocol,\n ChatProtocol::OpenAI | ChatProtocol::HuggingFace\n )\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(\n unified_request.tool_choice,\n Some(UnifiedToolChoice::Required)\n ) && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider_id: 1,\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}, "cc47e167c13f5121c2e5a48ee35fe8998a0648ab": {"t": 1785761176.454422, "n": 10964, "h": "774f32c433f6dc2a", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(\n chat_protocol,\n ChatProtocol::OpenAI | ChatProtocol::HuggingFace\n )\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(\n unified_request.tool_choice,\n Some(UnifiedToolChoice::Required)\n ) && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}, "4351256c219d188aff293cbd32ef264ea1534d78": {"t": 1785761177.7145498, "n": 10916, "h": "4d2c18873988c9a1", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(chat_protocol, ChatProtocol::OpenAI | ChatProtocol::HuggingFace)\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(unified_request.tool_choice, Some(UnifiedToolChoice::Required))\n && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}, "c6e3ccfdbb456498b030a7c56e046e8a2ec9635d": {"t": 1786067656.0853307, "n": 12185, "h": "4d9d5a1cf0d1406a", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_effort(body_json: &mut Value) {\n let Some(reasoning_effort) = body_json\n .get_mut(\"reasoning_effort\")\n .and_then(|value| value.as_str().map(str::to_owned))\n else {\n return;\n };\n\n if reasoning_effort.eq_ignore_ascii_case(\"xhigh\") {\n body_json[\"reasoning_effort\"] = Value::String(\"max\".to_string());\n }\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(\n chat_protocol,\n ChatProtocol::OpenAI | ChatProtocol::HuggingFace\n )\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_effort(&mut body_json);\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(\n unified_request.tool_choice,\n Some(UnifiedToolChoice::Required)\n ) && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider_id: 1,\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_maps_xhigh_reasoning_effort_to_max() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"reasoning_effort\": \"xhigh\",\n \"messages\": [{ \"role\": \"user\", \"content\": \"start\" }]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(processed_json[\"reasoning_effort\"], \"max\");\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}, "0d1458e53b286ac23e6c7c3e9dd6f17036774b7b": {"t": 1786068398.8734453, "n": 12185, "h": "4d9d5a1cf0d1406a", "c": "use bytes::Bytes;\nuse serde_json::Value;\n\nuse crate::ccproxy::adapter::unified::{UnifiedRequest, UnifiedToolChoice};\nuse crate::ccproxy::errors::CCProxyError;\nuse crate::ccproxy::types::ProxyModel;\nuse crate::ccproxy::ChatProtocol;\n\nfn should_relax_required_tool_choice(base_url: &str) -> bool {\n reqwest::Url::parse(base_url)\n .ok()\n .and_then(|url| url.host_str().map(|host| host.to_lowercase()))\n .is_some_and(|host| host == \"api.deepseek.com\" || host == \"api.deepseek.cn\")\n}\n\nfn deepseek_reasoning_enabled(body_json: &Value) -> bool {\n if let Some(thinking_enabled) = body_json\n .get(\"thinking\")\n .and_then(|thinking| thinking.get(\"type\"))\n .and_then(|value| value.as_str())\n .map(|value| value.eq_ignore_ascii_case(\"enabled\"))\n {\n return thinking_enabled;\n }\n\n if let Some(enable_thinking) = body_json\n .get(\"enable_thinking\")\n .and_then(|value| value.as_bool())\n {\n return enable_thinking;\n }\n\n if body_json\n .get(\"reasoning_effort\")\n .and_then(|value| value.as_str())\n .is_some_and(|value| !value.trim().is_empty())\n {\n return true;\n }\n\n body_json\n .get(\"thinking_budget\")\n .and_then(|value| value.as_i64())\n .is_some_and(|value| value > 0)\n}\n\nfn normalize_deepseek_reasoning_effort(body_json: &mut Value) {\n let Some(reasoning_effort) = body_json\n .get_mut(\"reasoning_effort\")\n .and_then(|value| value.as_str().map(str::to_owned))\n else {\n return;\n };\n\n if reasoning_effort.eq_ignore_ascii_case(\"xhigh\") {\n body_json[\"reasoning_effort\"] = Value::String(\"max\".to_string());\n }\n}\n\nfn normalize_deepseek_reasoning_replay(body_json: &mut Value) {\n if !deepseek_reasoning_enabled(body_json) {\n return;\n }\n\n let has_tool_round = body_json\n .get(\"messages\")\n .and_then(|messages| messages.as_array())\n .is_some_and(|messages| {\n messages.iter().any(|message| {\n message\n .get(\"role\")\n .and_then(|role| role.as_str())\n .is_some_and(|role| role == \"tool\")\n || message\n .get(\"tool_calls\")\n .and_then(|tool_calls| tool_calls.as_array())\n .is_some_and(|tool_calls| !tool_calls.is_empty())\n })\n });\n\n if !has_tool_round {\n return;\n }\n\n let Some(messages) = body_json\n .get_mut(\"messages\")\n .and_then(|messages| messages.as_array_mut())\n else {\n return;\n };\n\n for message in messages.iter_mut() {\n let Some(role) = message.get(\"role\").and_then(|role| role.as_str()) else {\n continue;\n };\n if role != \"assistant\" {\n continue;\n }\n\n let Some(message_obj) = message.as_object_mut() else {\n continue;\n };\n\n if message_obj.contains_key(\"reasoning_content\") {\n continue;\n }\n\n if let Some(thinking) = message_obj.get(\"thinking\").cloned() {\n message_obj.insert(\"reasoning_content\".to_string(), thinking);\n } else {\n message_obj.insert(\n \"reasoning_content\".to_string(),\n Value::String(String::new()),\n );\n }\n }\n}\n\npub fn preprocess_client_request_body(\n client_request_body: Bytes,\n chat_protocol: &ChatProtocol,\n proxy_model: &ProxyModel,\n) -> Result<Bytes, CCProxyError> {\n let mut body_json: Value = serde_json::from_slice(&client_request_body).map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to deserialize request body: {}\", e))\n })?;\n\n if should_relax_required_tool_choice(&proxy_model.base_url)\n && matches!(\n chat_protocol,\n ChatProtocol::OpenAI | ChatProtocol::HuggingFace\n )\n {\n let has_tools = body_json\n .get(\"tools\")\n .and_then(|tools| tools.as_array())\n .is_some_and(|tools| !tools.is_empty());\n\n if has_tools {\n if let Some(tool_choice) = body_json.get_mut(\"tool_choice\") {\n if tool_choice.as_str() == Some(\"required\") {\n *tool_choice = Value::String(\"auto\".to_string());\n }\n }\n }\n\n normalize_deepseek_reasoning_effort(&mut body_json);\n normalize_deepseek_reasoning_replay(&mut body_json);\n }\n\n serde_json::to_vec(&body_json)\n .map(Bytes::from)\n .map_err(|e| {\n CCProxyError::InternalError(format!(\"Failed to serialize preprocessed body: {}\", e))\n })\n}\n\npub fn preprocess_unified_request(unified_request: &mut UnifiedRequest, proxy_model: &ProxyModel) {\n if matches!(\n unified_request.tool_choice,\n Some(UnifiedToolChoice::Required)\n ) && should_relax_required_tool_choice(&proxy_model.base_url)\n {\n unified_request.tool_choice = Some(UnifiedToolChoice::Auto);\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::preprocess_client_request_body;\n use crate::ccproxy::{types::ProxyModel, ChatProtocol};\n use bytes::Bytes;\n use serde_json::json;\n\n fn deepseek_proxy_model() -> ProxyModel {\n ProxyModel {\n client_alias: \"deepseek-v4-flash\".to_string(),\n provider_id: 1,\n provider: \"Deepseek\".to_string(),\n chat_protocol: ChatProtocol::OpenAI,\n base_url: \"https://api.deepseek.com\".to_string(),\n model: \"deepseek-v4-flash\".to_string(),\n api_key: String::new(),\n model_metadata: None,\n custom_params: None,\n prompt_injection: \"off\".to_string(),\n prompt_injection_position: None,\n prompt_text: String::new(),\n tool_filter: Default::default(),\n prompt_replace: Vec::new(),\n temp_ratio: 1.0,\n max_tokens: None,\n temperature: None,\n presence_penalty: None,\n frequency_penalty: None,\n top_p: None,\n top_k: None,\n stop: Vec::new(),\n tool_compat_mode: None,\n }\n }\n\n #[test]\n fn preprocess_deepseek_maps_xhigh_reasoning_effort_to_max() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"reasoning_effort\": \"xhigh\",\n \"messages\": [{ \"role\": \"user\", \"content\": \"start\" }]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(processed_json[\"reasoning_effort\"], \"max\");\n }\n\n #[test]\n fn preprocess_deepseek_replays_missing_reasoning_as_empty_string() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"reasoning_effort\": \"medium\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"planning\",\n \"reasoning_content\": \"hidden plan\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"read_file\", \"arguments\": \"{}\" }\n }]\n },\n { \"role\": \"tool\", \"tool_call_id\": \"tool_1\", \"content\": \"ok\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden plan\"\n );\n assert_eq!(processed_json[\"messages\"][3][\"reasoning_content\"], \"\");\n }\n\n #[test]\n fn preprocess_deepseek_promotes_thinking_field_into_reasoning_content() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"enabled\" },\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"visible answer\",\n \"thinking\": \"hidden chain\",\n \"tool_calls\": [{\n \"id\": \"tool_1\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert_eq!(\n processed_json[\"messages\"][1][\"reasoning_content\"],\n \"hidden chain\"\n );\n }\n\n #[test]\n fn preprocess_deepseek_skips_reasoning_replay_when_thinking_is_not_enabled() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n\n #[test]\n fn preprocess_deepseek_respects_explicit_thinking_disable_over_reasoning_effort() {\n let body = json!({\n \"model\": \"deepseek-v4-flash\",\n \"thinking\": { \"type\": \"disabled\" },\n \"reasoning_effort\": \"high\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"start\" },\n {\n \"role\": \"assistant\",\n \"content\": \"final visible answer\",\n \"tool_calls\": [{\n \"id\": \"tool_2\",\n \"type\": \"function\",\n \"function\": { \"name\": \"todo_update\", \"arguments\": \"{}\" }\n }]\n }\n ]\n });\n\n let processed = preprocess_client_request_body(\n Bytes::from(body.to_string()),\n &ChatProtocol::OpenAI,\n &deepseek_proxy_model(),\n )\n .expect(\"preprocess should succeed\");\n\n let processed_json: serde_json::Value =\n serde_json::from_slice(&processed).expect(\"processed body should be valid json\");\n\n assert!(processed_json[\"messages\"][1]\n .get(\"reasoning_content\")\n .is_none());\n }\n}\n"}}}}