diff --git a/plugins/wasm-rust/Dockerfile b/plugins/wasm-rust/Dockerfile index ffdea8c53..c4ef13f26 100644 --- a/plugins/wasm-rust/Dockerfile +++ b/plugins/wasm-rust/Dockerfile @@ -8,7 +8,8 @@ COPY . . WORKDIR /workspace/extensions/$PLUGIN_NAME RUN if [ -f $PREBUILD ]; then sh $PREBUILD; fi RUN cargo build --target wasm32-wasip1 $BUILD_OPTS \ - && cp target/wasm32-wasip1/release/*.wasm /main.wasm + && cp target/wasm32-wasip1/release/*.wasm /main.wasm \ + && cargo clean FROM scratch COPY --from=builder /main.wasm plugin.wasm diff --git a/plugins/wasm-rust/Makefile b/plugins/wasm-rust/Makefile index 8d331f4ef..3d18e3d6c 100644 --- a/plugins/wasm-rust/Makefile +++ b/plugins/wasm-rust/Makefile @@ -23,15 +23,19 @@ build: lint-base: cargo fmt --all --check cargo clippy --workspace --all-features --all-targets + cargo clean lint: cargo fmt --all --check --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml cargo clippy --workspace --all-features --all-targets --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml + cargo clean --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml test-base: cargo test --lib + cargo clean test: cargo test --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml + cargo clean --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml builder: DOCKER_BUILDKIT=1 docker build \ diff --git a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock index d91ea7a87..119458688 100644 --- a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock +++ b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock @@ -36,10 +36,10 @@ dependencies = [ "fancy-regex", "grok", "higress-wasm-rust", + "hmac-sha256", "jieba-rs", "jsonpath-rust", "lazy_static", - "md5", "proxy-wasm", "rust-embed", "serde", @@ -295,6 +295,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" + [[package]] name = "http" version = "1.2.0" @@ -559,12 +565,6 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - [[package]] name = "memchr" version = "2.7.4" diff --git a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml index aa7372fb2..897d4663e 100644 --- a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml +++ b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.toml @@ -14,7 +14,7 @@ proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branc serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" fancy-regex = "0" -md5 = "0" +hmac-sha256 = "1" grok = "2" lazy_static = "1" jieba-rs = "0" diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/ai_data_masking.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/ai_data_masking.rs new file mode 100644 index 000000000..4ad0aa658 --- /dev/null +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/ai_data_masking.rs @@ -0,0 +1,736 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::deny_word::DenyWord; +use crate::msg_win_openai::MsgWindow; +use fancy_regex::Regex; +use grok::patterns; +use higress_wasm_rust::log::Log; +use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper}; +use higress_wasm_rust::request_wrapper::has_request_body; +use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher}; +use jsonpath_rust::{JsonPath, JsonPathValue}; +use lazy_static::lazy_static; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel}; +use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use serde_json::{json, Value}; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fmt::Write; +use std::ops::DerefMut; +use std::rc::{Rc, Weak}; +use std::str::FromStr; +use std::vec; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_|Box::new(AiDataMaskingRoot::new())); +}} + +const PLUGIN_NAME: &str = "ai-data-masking"; +const GROK_PATTERN: &str = r"%\{(?(?[A-z0-9]+)(?::(?[A-z0-9_:;\/\s\.]+))?)\}"; + +struct System { + deny_word: DenyWord, + grok_regex: Regex, + grok_patterns: BTreeMap, +} +lazy_static! { + static ref SYSTEM: System = System::new(); +} + +struct AiDataMaskingRoot { + log: Log, + rule_matcher: SharedRuleMatcher, +} +struct AiDataMasking { + weak: Weak>>>, + config: Option>, + mask_map: HashMap>, + is_openai: bool, + is_openai_stream: Option, + stream: bool, + log: Log, + msg_window: MsgWindow, + char_window_size: usize, + byte_window_size: usize, +} +fn deserialize_regexp<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + if let Some(pattern) = value.as_str() { + let (p, _) = SYSTEM.grok_to_pattern(pattern); + if let Ok(reg) = Regex::new(&p) { + Ok(reg) + } else if let Ok(reg) = Regex::new(pattern) { + Ok(reg) + } else { + Err(Error::custom(format!("regexp error field {}", pattern))) + } + } else { + Err(Error::custom("regexp error not string".to_string())) + } +} + +fn deserialize_type<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + if let Some(t) = value.as_str() { + if t == "replace" { + Ok(Type::Replace) + } else if t == "hash" { + Ok(Type::Hash) + } else { + Err(Error::custom(format!("regexp error value {}", t))) + } + } else { + Err(Error::custom("type error not string".to_string())) + } +} + +fn deserialize_denyword<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let value: Vec = Deserialize::deserialize(deserializer)?; + Ok(DenyWord::from_iter(value)) +} + +fn deserialize_jsonpath<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value: Vec = Deserialize::deserialize(deserializer)?; + let mut ret = Vec::new(); + for v in value { + if v.is_empty() { + continue; + } + match JsonPath::from_str(&v) { + Ok(jp) => ret.push(jp), + Err(_) => return Err(Error::custom(format!("jsonpath error value {}", v))), + } + } + Ok(ret) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Type { + Replace, + Hash, +} + +#[derive(Debug, Deserialize, Clone)] +struct Rule { + #[serde(deserialize_with = "deserialize_regexp")] + regex: Regex, + #[serde(deserialize_with = "deserialize_type", alias = "type")] + type_: Type, + #[serde(default)] + restore: bool, + #[serde(default)] + value: String, +} +fn default_deny_openai() -> bool { + true +} +fn default_deny_raw() -> bool { + false +} +fn default_system_deny() -> bool { + false +} +fn default_deny_code() -> u16 { + 200 +} +fn default_deny_content_type() -> String { + "application/json".to_string() +} +fn default_deny_raw_message() -> String { + "{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}".to_string() +} +fn default_deny_message() -> String { + "提问或回答中包含敏感词,已被屏蔽".to_string() +} +#[derive(Default, Debug, Deserialize, Clone)] +pub struct AiDataMaskingConfig { + #[serde(default = "default_deny_openai")] + deny_openai: bool, + #[serde(default = "default_deny_raw")] + deny_raw: bool, + #[serde(default, deserialize_with = "deserialize_jsonpath")] + deny_jsonpath: Vec, + #[serde(default = "default_system_deny")] + system_deny: bool, + #[serde(default = "default_deny_code")] + deny_code: u16, + #[serde(default = "default_deny_message")] + deny_message: String, + #[serde(default = "default_deny_raw_message")] + deny_raw_message: String, + #[serde(default = "default_deny_content_type")] + deny_content_type: String, + #[serde(default)] + replace_roles: Vec, + #[serde(deserialize_with = "deserialize_denyword", default = "DenyWord::empty")] + deny_words: DenyWord, +} + +impl AiDataMaskingConfig { + fn check_message(&self, message: &str, log: &Log) -> bool { + if let Some(word) = self.deny_words.check(message) { + log.warn(&format!( + "custom deny word {} matched from {}", + word, message + )); + return true; + } else if self.system_deny { + if let Some(word) = SYSTEM.deny_word.check(message) { + log.warn(&format!( + "system deny word {} matched from {}", + word, message + )); + return true; + } + } + false + } +} +#[derive(Debug, Deserialize, Clone)] +struct Message { + #[serde(default)] + content: String, + #[serde(default)] + reasoning_content: String, +} +#[derive(Debug, Deserialize, Clone)] +struct Req { + #[serde(default)] + stream: bool, + messages: Vec, +} + +#[derive(Default, Debug, Deserialize)] +struct ResMessage { + #[serde(default)] + message: Option, +} + +#[derive(Default, Debug, Deserialize)] +struct Res { + #[serde(default)] + choices: Vec, +} + +static SYSTEM_PATTERNS: &[(&str, &str)] = &[ + ("MOBILE", r#"\d{8,11}"#), + ("IDCARD", r#"\d{17}[0-9xX]|\d{15}"#), +]; + +impl System { + fn new() -> Self { + let grok_regex = Regex::new(GROK_PATTERN).unwrap(); + let grok_patterns = BTreeMap::new(); + let mut system = System { + deny_word: DenyWord::system(), + grok_regex, + grok_patterns, + }; + system.init(); + system + } + fn init(&mut self) { + let mut grok_temp_patterns = VecDeque::new(); + for patterns in [patterns(), SYSTEM_PATTERNS] { + for &(key, value) in patterns { + if self.grok_regex.is_match(value).is_ok_and(|r| r) { + grok_temp_patterns.push_back((String::from(key), String::from(value))); + } else { + self.grok_patterns + .insert(String::from(key), String::from(value)); + } + } + } + let mut last_ok: Option = None; + + while let Some((key, value)) = grok_temp_patterns.pop_front() { + if let Some(k) = &last_ok { + if k == &key { + break; + } + } + let (v, ok) = self.grok_to_pattern(&value); + if ok { + self.grok_patterns.insert(key, v); + last_ok = None; + } else { + if last_ok.is_none() { + last_ok = Some(key.clone()); + } + grok_temp_patterns.push_back((key, v)); + } + } + } + fn grok_to_pattern(&self, pattern: &str) -> (String, bool) { + let mut ok = true; + let mut ret = pattern.to_string(); + for capture in self.grok_regex.captures_iter(pattern) { + if capture.is_err() { + ok = false; + continue; + } + let c = capture.unwrap(); + if let (Some(full), Some(name)) = (c.get(0), c.name("pattern")) { + if let Some(p) = self.grok_patterns.get(name.as_str()) { + if let Some(alias) = c.name("alias") { + ret = ret.replace(full.as_str(), &format!("(?P<{}>{})", alias.as_str(), p)); + } else { + ret = ret.replace(full.as_str(), p); + } + } else { + ok = false; + } + } + } + (ret, ok) + } +} + +impl AiDataMaskingRoot { + fn new() -> Self { + AiDataMaskingRoot { + log: Log::new(PLUGIN_NAME.to_string()), + rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())), + } + } +} + +impl Context for AiDataMaskingRoot {} + +impl RootContext for AiDataMaskingRoot { + fn on_configure(&mut self, plugin_configuration_size: usize) -> bool { + on_configure( + self, + plugin_configuration_size, + self.rule_matcher.borrow_mut().deref_mut(), + &self.log, + ) + } + fn create_http_context(&self, context_id: u32) -> Option> { + self.create_http_context_use_wrapper(context_id) + } + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +impl RootContextWrapper for AiDataMaskingRoot { + fn rule_matcher(&self) -> &SharedRuleMatcher { + &self.rule_matcher + } + + fn create_http_context_wrapper( + &self, + _context_id: u32, + ) -> Option>> { + Some(Box::new(AiDataMasking { + weak: Weak::default(), + mask_map: HashMap::new(), + config: None, + is_openai: false, + is_openai_stream: None, + stream: false, + msg_window: MsgWindow::default(), + log: Log::new(PLUGIN_NAME.to_string()), + char_window_size: 0, + byte_window_size: 0, + })) + } +} + +impl AiDataMasking { + fn check_message(&self, message: &str) -> bool { + if let Some(config) = &self.config { + config.check_message(message, self.log()) + } else { + false + } + } + fn msg_to_response(&self, msg: &str, raw_msg: &str, content_type: &str) -> (String, String) { + if !self.is_openai { + (raw_msg.to_string(), content_type.to_string()) + } else if self.stream { + ( + format!( + "data:{}\n\n", + json!({"choices": [{"index": 0, "delta": {"role": "assistant", "content": msg}}], "usage": {}}) + ), + "text/event-stream;charset=UTF-8".to_string(), + ) + } else { + ( + json!({"choices": [{"index": 0, "message": {"role": "assistant", "content": msg}}], "usage": {}}).to_string(), + "application/json".to_string() + ) + } + } + fn deny(&mut self, in_response: bool) -> DataAction { + if in_response && self.stream { + self.replace_http_response_body(&[]); + return DataAction::Continue; + } + let (deny_code, (deny_message, content_type)) = if let Some(config) = &self.config { + ( + config.deny_code, + self.msg_to_response( + &config.deny_message, + &config.deny_raw_message, + &config.deny_content_type, + ), + ) + } else { + ( + default_deny_code(), + self.msg_to_response( + &default_deny_message(), + &default_deny_raw_message(), + &default_deny_content_type(), + ), + ) + }; + if in_response { + self.replace_http_response_body(deny_message.as_bytes()); + return DataAction::Continue; + } + self.send_http_response( + deny_code as u32, + vec![("Content-Type", &content_type)], + Some(deny_message.as_bytes()), + ); + DataAction::StopIterationAndBuffer + } + + fn replace_request_msg(&mut self, message: &str) -> String { + let config = self.config.as_ref().unwrap(); + let mut msg = message.to_string(); + for rule in &config.replace_roles { + let mut replace_pair = Vec::new(); + if rule.type_ == Type::Replace && !rule.restore { + msg = rule.regex.replace_all(&msg, &rule.value).to_string(); + } else { + for mc in rule.regex.find_iter(&msg) { + if mc.is_err() { + continue; + } + let m = mc.unwrap(); + let from_word = m.as_str(); + + let to_word = match rule.type_ { + Type::Hash => { + let digest = hmac_sha256::Hash::hash(from_word.as_bytes()); + digest.iter().fold(String::new(), |mut output, b| { + let _ = write!(output, "{b:02x}"); + output + }) + } + Type::Replace => rule.regex.replace(from_word, &rule.value).to_string(), + }; + if to_word.len() > self.byte_window_size { + self.byte_window_size = to_word.len(); + } + if to_word.chars().count() > self.char_window_size { + self.char_window_size = to_word.chars().count(); + } + + replace_pair.push((from_word.to_string(), to_word.clone())); + + if rule.restore && !to_word.is_empty() { + match self.mask_map.entry(to_word) { + std::collections::hash_map::Entry::Occupied(mut e) => { + e.insert(None); + } + std::collections::hash_map::Entry::Vacant(e) => { + e.insert(Some(from_word.to_string())); + } + } + } + } + for (from_word, to_word) in replace_pair { + msg = msg.replace(&from_word, &to_word); + } + } + } + if msg != message { + self.log() + .debug(&format!("replace_request_msg from {} to {}", message, msg)); + } + msg + } +} + +impl Context for AiDataMasking {} + +impl HttpContext for AiDataMasking { + fn on_http_request_headers( + &mut self, + _num_headers: usize, + _end_of_stream: bool, + ) -> HeaderAction { + if has_request_body() { + self.set_http_request_header("Content-Length", None); + HeaderAction::StopIteration + } else { + HeaderAction::Continue + } + } + fn on_http_response_headers( + &mut self, + _num_headers: usize, + _end_of_stream: bool, + ) -> HeaderAction { + self.set_http_response_header("Content-Length", None); + HeaderAction::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction { + if !self.stream { + return DataAction::Continue; + } + if body_size > 0 { + if let Some(body) = self.get_http_response_body(0, body_size) { + if self.is_openai && self.is_openai_stream.is_none() { + self.is_openai_stream = Some(body.starts_with(b"data:")); + } + self.msg_window + .push(&body, self.is_openai_stream.unwrap_or_default()); + let mut deny = false; + let log = Log::new(PLUGIN_NAME.to_string()); + for message in self.msg_window.messages_iter_mut() { + if let Ok(mut msg) = String::from_utf8(message.clone()) { + if let Some(config) = &self.config { + if config.check_message(&msg, &log) { + deny = true; + break; + } + } + if !self.mask_map.is_empty() { + for (from_word, to_word) in self.mask_map.iter() { + if let Some(to) = to_word { + msg = msg.replace(from_word, to); + } + } + } + message.clear(); + message.extend_from_slice(msg.as_bytes()); + } + } + if deny { + return self.deny(true); + } + } + } + let new_body = if end_of_stream { + self.msg_window.finish(self.is_openai_stream.unwrap()) + } else { + self.msg_window.pop( + self.char_window_size * 2, + self.byte_window_size * 2, + self.is_openai_stream.unwrap(), + ) + }; + self.replace_http_response_body(&new_body); + DataAction::Continue + } +} + +impl HttpContextWrapper for AiDataMasking { + fn init_self_weak( + &mut self, + self_weak: std::rc::Weak>>>, + ) { + self.weak = self_weak; + } + fn log(&self) -> &Log { + &self.log + } + fn on_config(&mut self, config: Rc) { + self.config = Some(config.clone()); + } + fn cache_request_body(&self) -> bool { + true + } + fn cache_response_body(&self) -> bool { + !self.stream + } + fn on_http_request_complete_body(&mut self, req_body: &Bytes) -> DataAction { + if self.config.is_none() { + return DataAction::Continue; + } + let config = self.config.as_ref().unwrap(); + let mut req_body = match serde_json::from_slice::(req_body) { + Ok(r) => r.to_string(), + Err(_) => { + if let Ok(r) = String::from_utf8(req_body.clone()) { + r + } else { + return DataAction::Continue; + } + } + }; + if config.deny_openai { + if let Ok(req) = serde_json::from_str::(req_body.as_str()) { + self.is_openai = true; + self.stream = req.stream; + for msg in req.messages { + if self.check_message(&msg.content) + || self.check_message(&msg.reasoning_content) + { + return self.deny(false); + } + let new_content = self.replace_request_msg(&msg.content); + let new_reasoning_content = self.replace_request_msg(&msg.reasoning_content); + if new_content != msg.content { + req_body = req_body.replace( + &Value::String(msg.content).to_string(), + &Value::String(new_content).to_string(), + ); + } + if new_reasoning_content != msg.reasoning_content { + req_body = req_body.replace( + &Value::String(msg.reasoning_content).to_string(), + &Value::String(new_reasoning_content).to_string(), + ); + } + } + self.replace_http_request_body(req_body.as_bytes()); + return DataAction::Continue; + } + } + if !config.deny_jsonpath.is_empty() { + if let Ok(json) = serde_json::from_str::(req_body.as_str()) { + for jsonpath in config.deny_jsonpath.clone() { + for v in jsonpath.find_slice(&json) { + if let JsonPathValue::Slice(d, _) = v { + if let Some(s) = d.as_str() { + if self.check_message(s) { + return self.deny(false); + } + let content = s.to_string(); + let new_content = self.replace_request_msg(&content); + if new_content != content { + req_body = req_body.replace( + &Value::String(content).to_string(), + &Value::String(new_content).to_string(), + ); + } + } + } + } + } + self.replace_http_request_body(req_body.as_bytes()); + return DataAction::Continue; + } + } + if config.deny_raw { + if self.check_message(&req_body) { + return self.deny(false); + } + let new_body = self.replace_request_msg(&req_body); + if new_body != req_body { + self.replace_http_request_body(new_body.as_bytes()) + } + return DataAction::Continue; + } + DataAction::Continue + } + fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction { + if self.config.is_none() { + return DataAction::Continue; + } + let config = self.config.as_ref().unwrap(); + let mut res_body = match serde_json::from_slice::(res_body) { + Ok(r) => r.to_string(), + Err(_) => { + if let Ok(r) = String::from_utf8(res_body.clone()) { + r + } else { + return DataAction::Continue; + } + } + }; + if config.deny_openai && self.is_openai { + if let Ok(res) = serde_json::from_str::(res_body.as_str()) { + for msg in res.choices { + if let Some(message) = msg.message { + if self.check_message(&message.content) + || self.check_message(&message.reasoning_content) + { + return self.deny(true); + } + + if self.mask_map.is_empty() { + continue; + } + let mut new_content = message.content.clone(); + let mut new_reasoning_content = message.reasoning_content.clone(); + for (from_word, to_word) in self.mask_map.iter() { + if let Some(to) = to_word { + new_content = new_content.replace(from_word, to); + new_reasoning_content = + new_reasoning_content.replace(from_word, to); + } + } + if new_content != message.content { + res_body = res_body.replace( + &Value::String(message.content).to_string(), + &Value::String(new_content).to_string(), + ); + } + if new_reasoning_content != message.reasoning_content { + res_body = res_body.replace( + &Value::String(message.reasoning_content).to_string(), + &Value::String(new_reasoning_content).to_string(), + ); + } + } + } + self.replace_http_response_body(res_body.as_bytes()); + + return DataAction::Continue; + } + } + if config.deny_raw { + if self.check_message(&res_body) { + return self.deny(true); + } + if !self.mask_map.is_empty() { + for (from_word, to_word) in self.mask_map.iter() { + if let Some(to) = to_word { + res_body = res_body.replace(from_word, to); + } + } + } + self.replace_http_response_body(res_body.as_bytes()); + return DataAction::Continue; + } + DataAction::Continue + } +} diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/deny_word.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/deny_word.rs index 99ecab4d5..3a1851490 100644 --- a/plugins/wasm-rust/extensions/ai-data-masking/src/deny_word.rs +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/deny_word.rs @@ -1,8 +1,25 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use std::collections::HashSet; use jieba_rs::Jieba; +use rust_embed::Embed; -use crate::Asset; +#[derive(Embed)] +#[folder = "res/"] +struct Asset; #[derive(Default, Debug, Clone)] pub(crate) struct DenyWord { diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs index 9938c50cd..059c40b33 100644 --- a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2023 Alibaba Group Holding Ltd. +// Copyright (c) 2025 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,705 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod ai_data_masking; mod deny_word; +mod msg_win_openai; mod msg_window; - -use crate::deny_word::DenyWord; -use crate::msg_window::MsgWindow; -use fancy_regex::Regex; -use grok::patterns; -use higress_wasm_rust::log::Log; -use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper}; -use higress_wasm_rust::request_wrapper::has_request_body; -use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher}; -use jsonpath_rust::{JsonPath, JsonPathValue}; -use lazy_static::lazy_static; -use proxy_wasm::traits::{Context, HttpContext, RootContext}; -use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel}; -use rust_embed::Embed; -use serde::de::Error; -use serde::Deserializer; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, VecDeque}; -use std::ops::DerefMut; -use std::rc::Rc; -use std::str::FromStr; -use std::vec; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_|Box::new(AiDataMaskingRoot::new())); -}} - -const PLUGIN_NAME: &str = "ai-data-masking"; -const GROK_PATTERN: &str = r"%\{(?(?[A-z0-9]+)(?::(?[A-z0-9_:;\/\s\.]+))?)\}"; - -#[derive(Embed)] -#[folder = "res/"] -struct Asset; - -struct System { - deny_word: DenyWord, - grok_regex: Regex, - grok_patterns: BTreeMap, -} -lazy_static! { - static ref SYSTEM: System = System::new(); -} - -struct AiDataMaskingRoot { - log: Log, - rule_matcher: SharedRuleMatcher, -} -struct AiDataMasking { - config: Option>, - mask_map: HashMap>, - is_openai: bool, - is_openai_stream: Option, - stream: bool, - log: Log, - msg_window: MsgWindow, - char_window_size: usize, - byte_window_size: usize, -} -fn deserialize_regexp<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value: Value = Deserialize::deserialize(deserializer)?; - if let Some(pattern) = value.as_str() { - let (p, _) = SYSTEM.grok_to_pattern(pattern); - if let Ok(reg) = Regex::new(&p) { - Ok(reg) - } else if let Ok(reg) = Regex::new(pattern) { - Ok(reg) - } else { - Err(Error::custom(format!("regexp error field {}", pattern))) - } - } else { - Err(Error::custom("regexp error not string".to_string())) - } -} - -fn deserialize_type<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value: Value = Deserialize::deserialize(deserializer)?; - if let Some(t) = value.as_str() { - if t == "replace" { - Ok(Type::Replace) - } else if t == "hash" { - Ok(Type::Hash) - } else { - Err(Error::custom(format!("regexp error value {}", t))) - } - } else { - Err(Error::custom("type error not string".to_string())) - } -} - -fn deserialize_denyword<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let value: Vec = Deserialize::deserialize(deserializer)?; - Ok(DenyWord::from_iter(value)) -} - -fn deserialize_jsonpath<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let value: Vec = Deserialize::deserialize(deserializer)?; - let mut ret = Vec::new(); - for v in value { - if v.is_empty() { - continue; - } - match JsonPath::from_str(&v) { - Ok(jp) => ret.push(jp), - Err(_) => return Err(Error::custom(format!("jsonpath error value {}", v))), - } - } - Ok(ret) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum Type { - Replace, - Hash, -} - -#[derive(Debug, Deserialize, Clone)] -struct Rule { - #[serde(deserialize_with = "deserialize_regexp")] - regex: Regex, - #[serde(deserialize_with = "deserialize_type", alias = "type")] - type_: Type, - #[serde(default)] - restore: bool, - #[serde(default)] - value: String, -} -fn default_deny_openai() -> bool { - true -} -fn default_deny_raw() -> bool { - false -} -fn default_system_deny() -> bool { - false -} -fn default_deny_code() -> u16 { - 200 -} -fn default_deny_content_type() -> String { - "application/json".to_string() -} -fn default_deny_raw_message() -> String { - "{\"errmsg\":\"提问或回答中包含敏感词,已被屏蔽\"}".to_string() -} -fn default_deny_message() -> String { - "提问或回答中包含敏感词,已被屏蔽".to_string() -} -#[derive(Default, Debug, Deserialize, Clone)] -pub struct AiDataMaskingConfig { - #[serde(default = "default_deny_openai")] - deny_openai: bool, - #[serde(default = "default_deny_raw")] - deny_raw: bool, - #[serde(default, deserialize_with = "deserialize_jsonpath")] - deny_jsonpath: Vec, - #[serde(default = "default_system_deny")] - system_deny: bool, - #[serde(default = "default_deny_code")] - deny_code: u16, - #[serde(default = "default_deny_message")] - deny_message: String, - #[serde(default = "default_deny_raw_message")] - deny_raw_message: String, - #[serde(default = "default_deny_content_type")] - deny_content_type: String, - #[serde(default)] - replace_roles: Vec, - #[serde(deserialize_with = "deserialize_denyword", default = "DenyWord::empty")] - deny_words: DenyWord, -} - -#[derive(Debug, Deserialize, Clone)] -struct Message { - #[serde(default)] - content: String, -} -#[derive(Debug, Deserialize, Clone)] -struct Req { - #[serde(default)] - stream: bool, - messages: Vec, -} - -#[derive(Default, Debug, Deserialize)] -struct ResMessage { - #[serde(default)] - message: Option, - #[serde(default)] - delta: Option, -} - -#[derive(Default, Debug, Deserialize, Serialize, Clone)] -struct Usage { - #[serde(default)] - completion_tokens: i32, - #[serde(default)] - prompt_tokens: i32, - #[serde(default)] - total_tokens: i32, -} - -impl Usage { - pub fn add(&mut self, usage: &Usage) { - self.completion_tokens += usage.completion_tokens; - self.prompt_tokens += usage.prompt_tokens; - self.total_tokens += usage.total_tokens; - } - pub fn reset(&mut self) { - self.completion_tokens = 0; - self.prompt_tokens = 0; - self.total_tokens = 0; - } -} - -#[derive(Default, Debug, Deserialize)] -struct Res { - #[serde(default)] - choices: Vec, - #[serde(default)] - usage: Usage, -} - -static SYSTEM_PATTERNS: &[(&str, &str)] = &[ - ("MOBILE", r#"\d{8,11}"#), - ("IDCARD", r#"\d{17}[0-9xX]|\d{15}"#), -]; - -impl System { - fn new() -> Self { - let grok_regex = Regex::new(GROK_PATTERN).unwrap(); - let grok_patterns = BTreeMap::new(); - let mut system = System { - deny_word: DenyWord::system(), - grok_regex, - grok_patterns, - }; - system.init(); - system - } - fn init(&mut self) { - let mut grok_temp_patterns = VecDeque::new(); - for patterns in [patterns(), SYSTEM_PATTERNS] { - for &(key, value) in patterns { - if self.grok_regex.is_match(value).is_ok_and(|r| r) { - grok_temp_patterns.push_back((String::from(key), String::from(value))); - } else { - self.grok_patterns - .insert(String::from(key), String::from(value)); - } - } - } - let mut last_ok: Option = None; - - while let Some((key, value)) = grok_temp_patterns.pop_front() { - if let Some(k) = &last_ok { - if k == &key { - break; - } - } - let (v, ok) = self.grok_to_pattern(&value); - if ok { - self.grok_patterns.insert(key, v); - last_ok = None; - } else { - if last_ok.is_none() { - last_ok = Some(key.clone()); - } - grok_temp_patterns.push_back((key, v)); - } - } - } - fn grok_to_pattern(&self, pattern: &str) -> (String, bool) { - let mut ok = true; - let mut ret = pattern.to_string(); - for capture in self.grok_regex.captures_iter(pattern) { - if capture.is_err() { - ok = false; - continue; - } - let c = capture.unwrap(); - if let (Some(full), Some(name)) = (c.get(0), c.name("pattern")) { - if let Some(p) = self.grok_patterns.get(name.as_str()) { - if let Some(alias) = c.name("alias") { - ret = ret.replace(full.as_str(), &format!("(?P<{}>{})", alias.as_str(), p)); - } else { - ret = ret.replace(full.as_str(), p); - } - } else { - ok = false; - } - } - } - (ret, ok) - } -} - -impl AiDataMaskingRoot { - fn new() -> Self { - AiDataMaskingRoot { - log: Log::new(PLUGIN_NAME.to_string()), - rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())), - } - } -} - -impl Context for AiDataMaskingRoot {} - -impl RootContext for AiDataMaskingRoot { - fn on_configure(&mut self, plugin_configuration_size: usize) -> bool { - on_configure( - self, - plugin_configuration_size, - self.rule_matcher.borrow_mut().deref_mut(), - &self.log, - ) - } - fn create_http_context(&self, context_id: u32) -> Option> { - self.create_http_context_use_wrapper(context_id) - } - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } -} - -impl RootContextWrapper for AiDataMaskingRoot { - fn rule_matcher(&self) -> &SharedRuleMatcher { - &self.rule_matcher - } - - fn create_http_context_wrapper( - &self, - _context_id: u32, - ) -> Option>> { - Some(Box::new(AiDataMasking { - mask_map: HashMap::new(), - config: None, - is_openai: false, - is_openai_stream: None, - stream: false, - msg_window: MsgWindow::new(), - log: Log::new(PLUGIN_NAME.to_string()), - char_window_size: 0, - byte_window_size: 0, - })) - } -} - -impl AiDataMasking { - fn check_message(&self, message: &str) -> bool { - if let Some(config) = &self.config { - if let Some(word) = config.deny_words.check(message) { - self.log().warn(&format!( - "custom deny word {} matched from {}", - word, message - )); - return true; - } else if config.system_deny { - if let Some(word) = SYSTEM.deny_word.check(message) { - self.log().warn(&format!( - "system deny word {} matched from {}", - word, message - )); - return true; - } - } - } - false - } - fn msg_to_response(&self, msg: &str, raw_msg: &str, content_type: &str) -> (String, String) { - if !self.is_openai { - (raw_msg.to_string(), content_type.to_string()) - } else if self.stream { - ( - format!( - "data:{}\n\n", - json!({"choices": [{"index": 0, "delta": {"role": "assistant", "content": msg}}], "usage": {}}) - ), - "text/event-stream;charset=UTF-8".to_string(), - ) - } else { - ( - json!({"choices": [{"index": 0, "message": {"role": "assistant", "content": msg}}], "usage": {}}).to_string(), - "application/json".to_string() - ) - } - } - fn deny(&mut self, in_response: bool) -> DataAction { - if in_response && self.stream { - self.replace_http_response_body(&[]); - return DataAction::Continue; - } - let (deny_code, (deny_message, content_type)) = if let Some(config) = &self.config { - ( - config.deny_code, - self.msg_to_response( - &config.deny_message, - &config.deny_raw_message, - &config.deny_content_type, - ), - ) - } else { - ( - default_deny_code(), - self.msg_to_response( - &default_deny_message(), - &default_deny_raw_message(), - &default_deny_content_type(), - ), - ) - }; - if in_response { - self.replace_http_response_body(deny_message.as_bytes()); - return DataAction::Continue; - } - self.send_http_response( - deny_code as u32, - vec![("Content-Type", &content_type)], - Some(deny_message.as_bytes()), - ); - DataAction::StopIterationAndBuffer - } - - fn replace_request_msg(&mut self, message: &str) -> String { - let config = self.config.as_ref().unwrap(); - let mut msg = message.to_string(); - for rule in &config.replace_roles { - let mut replace_pair = Vec::new(); - if rule.type_ == Type::Replace && !rule.restore { - msg = rule.regex.replace_all(&msg, &rule.value).to_string(); - } else { - for mc in rule.regex.find_iter(&msg) { - if mc.is_err() { - continue; - } - let m = mc.unwrap(); - let from_word = m.as_str(); - - let to_word = match rule.type_ { - Type::Hash => { - let digest = md5::compute(from_word.as_bytes()); - format!("{:x}", digest) - } - Type::Replace => rule.regex.replace(from_word, &rule.value).to_string(), - }; - if to_word.len() > self.byte_window_size { - self.byte_window_size = to_word.len(); - } - if to_word.chars().count() > self.char_window_size { - self.char_window_size = to_word.chars().count(); - } - - replace_pair.push((from_word.to_string(), to_word.clone())); - - if rule.restore && !to_word.is_empty() { - match self.mask_map.entry(to_word) { - std::collections::hash_map::Entry::Occupied(mut e) => { - e.insert(None); - } - std::collections::hash_map::Entry::Vacant(e) => { - e.insert(Some(from_word.to_string())); - } - } - } - } - for (from_word, to_word) in replace_pair { - msg = msg.replace(&from_word, &to_word); - } - } - } - if msg != message { - self.log() - .debug(&format!("replace_request_msg from {} to {}", message, msg)); - } - msg - } -} - -impl Context for AiDataMasking {} - -impl HttpContext for AiDataMasking { - fn on_http_request_headers( - &mut self, - _num_headers: usize, - _end_of_stream: bool, - ) -> HeaderAction { - if has_request_body() { - self.set_http_request_header("Content-Length", None); - HeaderAction::StopIteration - } else { - HeaderAction::Continue - } - } - fn on_http_response_headers( - &mut self, - _num_headers: usize, - _end_of_stream: bool, - ) -> HeaderAction { - self.set_http_response_header("Content-Length", None); - HeaderAction::Continue - } - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction { - if !self.stream { - return DataAction::Continue; - } - if body_size > 0 { - if let Some(body) = self.get_http_response_body(0, body_size) { - if self.is_openai && self.is_openai_stream.is_none() { - self.is_openai_stream = Some(body.starts_with(b"data:")); - } - self.msg_window.push(&body, self.is_openai_stream.unwrap()); - if let Ok(mut msg) = String::from_utf8(self.msg_window.message.clone()) { - if self.check_message(&msg) { - return self.deny(true); - } - if !self.mask_map.is_empty() { - for (from_word, to_word) in self.mask_map.iter() { - if let Some(to) = to_word { - msg = msg.replace(from_word, to); - } - } - } - self.msg_window.message = msg.as_bytes().to_vec(); - } - } - } - let new_body = if end_of_stream { - self.msg_window.finish(self.is_openai_stream.unwrap()) - } else { - self.msg_window.pop( - self.char_window_size * 2, - self.byte_window_size * 2, - self.is_openai_stream.unwrap(), - ) - }; - self.replace_http_response_body(&new_body); - DataAction::Continue - } -} - -impl HttpContextWrapper for AiDataMasking { - fn log(&self) -> &Log { - &self.log - } - fn on_config(&mut self, config: Rc) { - self.config = Some(config.clone()); - } - fn cache_request_body(&self) -> bool { - true - } - fn cache_response_body(&self) -> bool { - !self.stream - } - fn on_http_request_complete_body(&mut self, req_body: &Bytes) -> DataAction { - if self.config.is_none() { - return DataAction::Continue; - } - let config = self.config.as_ref().unwrap(); - let mut req_body = match String::from_utf8(req_body.clone()) { - Ok(r) => r, - Err(_) => return DataAction::Continue, - }; - if config.deny_openai { - if let Ok(r) = serde_json::from_str(req_body.as_str()) { - let req: Req = r; - self.is_openai = true; - self.stream = req.stream; - for msg in req.messages { - if self.check_message(&msg.content) { - return self.deny(false); - } - let new_content = self.replace_request_msg(&msg.content); - if new_content != msg.content { - if let (Ok(from), Ok(to)) = ( - serde_json::to_string(&msg.content), - serde_json::to_string(&new_content), - ) { - req_body = req_body.replace(&from, &to); - } - } - } - self.replace_http_request_body(req_body.as_bytes()); - return DataAction::Continue; - } - } - if !config.deny_jsonpath.is_empty() { - if let Ok(r) = serde_json::from_str(req_body.as_str()) { - let json: Value = r; - for jsonpath in config.deny_jsonpath.clone() { - for v in jsonpath.find_slice(&json) { - if let JsonPathValue::Slice(d, _) = v { - if let Some(s) = d.as_str() { - if self.check_message(s) { - return self.deny(false); - } - let content = s.to_string(); - let new_content = self.replace_request_msg(&content); - if new_content != content { - if let (Ok(from), Ok(to)) = ( - serde_json::to_string(&content), - serde_json::to_string(&new_content), - ) { - req_body = req_body.replace(&from, &to); - } - } - } - } - } - } - self.replace_http_request_body(req_body.as_bytes()); - return DataAction::Continue; - } - } - if config.deny_raw { - if self.check_message(&req_body) { - return self.deny(false); - } - let new_body = self.replace_request_msg(&req_body); - if new_body != req_body { - self.replace_http_request_body(new_body.as_bytes()) - } - return DataAction::Continue; - } - DataAction::Continue - } - fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction { - if self.config.is_none() { - return DataAction::Continue; - } - let config = self.config.as_ref().unwrap(); - let mut res_body = match String::from_utf8(res_body.clone()) { - Ok(r) => r, - Err(_) => { - return DataAction::Continue; - } - }; - if config.deny_openai && self.is_openai { - if let Ok(r) = serde_json::from_str(res_body.as_str()) { - let res: Res = r; - for msg in res.choices { - if let Some(meesage) = msg.message { - if self.check_message(&meesage.content) { - return self.deny(true); - } - - if self.mask_map.is_empty() { - continue; - } - let mut m = meesage.content.clone(); - for (from_word, to_word) in self.mask_map.iter() { - if let Some(to) = to_word { - m = m.replace(from_word, to); - } - } - if m != meesage.content { - if let (Ok(from), Ok(to)) = ( - serde_json::to_string(&meesage.content), - serde_json::to_string(&m), - ) { - res_body = res_body.replace(&from, &to); - } - } - } - } - self.replace_http_response_body(res_body.as_bytes()); - - return DataAction::Continue; - } - } - if config.deny_raw { - if self.check_message(&res_body) { - return self.deny(true); - } - if !self.mask_map.is_empty() { - for (from_word, to_word) in self.mask_map.iter() { - if let Some(to) = to_word { - res_body = res_body.replace(from_word, to); - } - } - } - self.replace_http_response_body(res_body.as_bytes()); - return DataAction::Continue; - } - DataAction::Continue - } -} +mod number_merge; diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/msg_win_openai.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/msg_win_openai.rs new file mode 100644 index 000000000..61f176c2e --- /dev/null +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/msg_win_openai.rs @@ -0,0 +1,356 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use higress_wasm_rust::event_stream::EventStream; +use serde::Deserialize; +use serde_json::Value; + +use crate::msg_window::MessageWindow; +use crate::number_merge::NumberMerge; + +#[derive(PartialEq, Eq, Clone, Copy)] +enum MsgFlag { + None, + Content, + ReasoningContent, +} +impl Default for MsgFlag { + fn default() -> Self { + Self::None + } +} +#[derive(Deserialize)] +struct Delta { + #[serde(default)] + content: Option, + #[serde(default)] + reasoning_content: Option, +} +#[derive(Deserialize)] +struct Choices { + #[serde(default)] + index: i64, + #[serde(default)] + delta: Option, + #[serde(default)] + finish_reason: Option, +} + +impl Delta { + fn get_flag_msg(&self, default_flag: &MsgFlag) -> (MsgFlag, &[u8]) { + if let Some(msg) = &self.content { + if !msg.is_empty() { + return (MsgFlag::Content, msg.as_bytes()); + } + } + if let Some(msg) = &self.reasoning_content { + if !msg.is_empty() { + return (MsgFlag::ReasoningContent, msg.as_bytes()); + } + } + (*default_flag, &[]) + } +} +const USAGE_PATH: &str = "usage"; +const CHOICES_PATH: &str = "choices"; + +type MessageLine = Vec<(MsgFlag, Vec)>; + +#[derive(Default)] +struct MessageWindowOpenAi { + message_window: MessageWindow, + ret_messages: MessageLine, + flag: MsgFlag, + last_value: Value, + finish_reason: Option, +} + +impl MessageWindowOpenAi { + fn update( + &mut self, + data: &[u8], + flag: MsgFlag, + value: &Value, + finish_reason: &Option, + ) { + self.last_value = value.clone(); + if data.is_empty() { + return; + } + if self.flag == MsgFlag::None { + self.flag = flag; + } + if self.flag != flag { + let last_flag = core::mem::replace(&mut self.flag, flag); + let msg = self.message_window.finish(); + self.ret_messages.push((last_flag, msg)); + } + self.message_window.update(data); + if let Some(fr) = finish_reason { + self.finish_reason = Some(fr.clone()); + } + } + + fn gen_value(&self, flag: &MsgFlag, msg: &[u8], finish: bool) -> Value { + let mut ret = self.last_value.clone(); + match flag { + MsgFlag::Content => { + ret["delta"]["content"] = Value::String(String::from_utf8_lossy(msg).to_string()); + if let Some(m) = ret["delta"].as_object_mut() { + m.remove("reasoning_content"); + } + } + MsgFlag::ReasoningContent => { + ret["delta"]["reasoning_content"] = + Value::String(String::from_utf8_lossy(msg).to_string()); + ret["delta"]["content"] = Value::String(String::new()); + } + _ => {} + } + if finish { + ret["finish_reason"] = self + .finish_reason + .as_ref() + .map_or(Value::Null, |v| Value::String(v.to_string())); + } else { + ret["finish_reason"] = Value::Null; + } + ret + } + + fn messages_to_value(&mut self) -> Vec { + let mut ret = Vec::new(); + for (flag, msg) in core::mem::take(&mut self.ret_messages) { + ret.push(self.gen_value(&flag, &msg, false)); + } + ret + } + + fn pop(&mut self, char_window_size: usize, byte_window_size: usize) -> Vec { + let mut ret = self.messages_to_value(); + + let msg = self.message_window.pop(char_window_size, byte_window_size); + if !msg.is_empty() { + ret.push(self.gen_value(&self.flag, &msg, false)); + } + + ret + } + fn finish(&mut self) -> Vec { + let mut ret = self.messages_to_value(); + let msg = self.message_window.finish(); + let flag = core::mem::replace(&mut self.flag, MsgFlag::None); + ret.push(self.gen_value(&flag, &msg, true)); + + ret + } + fn iter_mut(&mut self) -> impl Iterator> { + self.ret_messages + .iter_mut() + .map(|(_, msg)| msg) + .chain(self.message_window.iter_mut()) + } +} + +#[derive(Default)] +pub(crate) struct MsgWindow { + stream_parser: EventStream, + base_message_window: MessageWindow, + message_windows: HashMap, + last_value: Value, + usage: NumberMerge, +} + +impl MsgWindow { + fn update_event(&mut self, event: Vec) -> Option> { + if event.is_empty() || !event.starts_with(b"data:") { + Some(event) + } else if let Ok(res) = serde_json::from_slice::(&event[b"data:".len()..]) { + self.last_value = res; + if let Some(r) = self.last_value.as_object() { + if let Some(v) = r.get(USAGE_PATH) { + self.usage.add(v); + } + if let Some(v) = r.get(CHOICES_PATH) { + if let Some(a) = v.as_array() { + for item in a { + if let Ok(c) = serde_json::from_value::(item.clone()) { + if let Some(d) = &c.delta { + let mw = self.message_windows.entry(c.index).or_default(); + let (flag, msg) = d.get_flag_msg(&mw.flag); + mw.update(msg, flag, item, &c.finish_reason); + } + } + } + } + } + } + None + } else if event.starts_with(b"data: [DONE]") { + None + } else { + Some(event) + } + } + fn push_base(&mut self, data: &[u8]) { + self.base_message_window.update(data); + } + pub(crate) fn push(&mut self, data: &[u8], is_openai: bool) { + if is_openai { + self.stream_parser.update(data.to_vec()); + while let Some(event) = self.stream_parser.next() { + if let Some(msg) = self.update_event(event) { + self.push_base(&msg); + } + } + } else { + self.push_base(data); + } + } + + pub(crate) fn pop( + &mut self, + char_window_size: usize, + byte_window_size: usize, + is_openai: bool, + ) -> Vec { + if !is_openai { + return self + .base_message_window + .pop(char_window_size, byte_window_size); + } + let mut ret = Vec::new(); + for mw in self.message_windows.values_mut() { + for value in mw.pop(char_window_size, byte_window_size) { + let usage = self.usage.finish(); + let mut ret_value = self.last_value.clone(); + ret_value[CHOICES_PATH] = Value::Array(vec![value]); + ret_value[USAGE_PATH] = usage; + ret.extend(format!("data: {}\n\n", ret_value).as_bytes()) + } + } + ret + } + pub(crate) fn finish(&mut self, is_openai: bool) -> Vec { + if !is_openai { + return self.base_message_window.finish(); + } + if let Some(event) = self.stream_parser.flush() { + self.update_event(event); + } + let mut ret = Vec::new(); + for mw in &mut self.message_windows.values_mut() { + for value in mw.finish() { + let usage = self.usage.finish(); + let mut ret_value = self.last_value.clone(); + ret_value[CHOICES_PATH] = Value::Array(vec![value]); + ret_value[USAGE_PATH] = usage; + ret.extend(format!("data: {}\n\n", ret_value).as_bytes()) + } + } + ret + } + pub(crate) fn messages_iter_mut(&mut self) -> impl Iterator> { + self.base_message_window.iter_mut().chain( + self.message_windows + .values_mut() + .flat_map(|mw| mw.iter_mut()), + ) + } +} + +#[cfg(test)] +mod tests { + + use rust_embed::Embed; + + use super::*; + #[derive(Embed)] + #[folder = "test/"] + struct Asset; + #[derive(Deserialize)] + struct Res { + choices: Vec, + } + + impl Res { + fn get_text(&self) -> (String, String) { + let mut content = String::new(); + let mut reasoning_content = String::new(); + for choice in self.choices.iter() { + if let Some(delta) = &choice.delta { + if let Some(c) = &delta.content { + content += c; + } + if let Some(rc) = &delta.reasoning_content { + reasoning_content += rc; + } + } + } + (content, reasoning_content) + } + } + + #[test] + fn test_msg() { + let mut msg_win = MsgWindow::default(); + let data = raw_message("raw_message.txt"); + let mut buffer = Vec::new(); + for line in data.split("\n") { + msg_win.push(line.as_bytes(), true); + msg_win.push(b"\n\n", true); + for message in msg_win.messages_iter_mut() { + if let Ok(mut msg) = String::from_utf8(message.clone()) { + msg = msg.replace("Higress", "***higress***"); + message.clear(); + message.extend_from_slice(msg.as_bytes()); + } + } + + buffer.extend(msg_win.pop(7, 7, true)); + } + buffer.extend(msg_win.finish(true)); + let mut message = String::new(); + let mut reasoning_message = String::new(); + for line in buffer.split(|&x| x == b'\n') { + if line.is_empty() { + continue; + } + assert!(line.starts_with(b"data:")); + if line.starts_with(b"data: [DONE]") { + continue; + } + let des = serde_json::from_slice::(&line[b"data:".len()..]); + assert!(des.is_ok()); + let res = des.unwrap(); + let (c, rc) = res.get_text(); + message.push_str(&c); + reasoning_message.push_str(&rc); + } + let res = "***higress*** 是一个基于 Istio 的高性能服务网格数据平面项目,旨在提供高吞吐量、低延迟和可扩展的服务通信管理。它为企业级应用提供了丰富的流量治理功能,如负载均衡、熔断、限流等,并支持多协议代理(包括 HTTP/1.1, HTTP/2, gRPC)。***higress*** 的设计目标是优化 Istio 在大规模集群中的性能表现,满足高并发场景下的需求。"; + assert_eq!(message, res); + assert_eq!(reasoning_message, res); + } + + fn raw_message(file_name: &str) -> String { + if let Some(file) = Asset::get(file_name) { + if let Ok(data) = std::str::from_utf8(file.data.as_ref()) { + return data.to_string(); + } + } + String::new() + } +} diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/msg_window.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/msg_window.rs index 59d6f5e4b..84fe01019 100644 --- a/plugins/wasm-rust/extensions/ai-data-masking/src/msg_window.rs +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/msg_window.rs @@ -1,56 +1,28 @@ -use higress_wasm_rust::event_stream::EventStream; -use serde_json::json; - -use crate::{Res, Usage}; +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #[derive(Default)] -pub(crate) struct MsgWindow { - stream_parser: EventStream, - pub(crate) message: Vec, - usage: Usage, +pub(crate) struct MessageWindow { + message: Vec, } -impl MsgWindow { - pub fn new() -> Self { - MsgWindow::default() +impl MessageWindow { + pub(crate) fn update(&mut self, data: &[u8]) { + self.message.extend(data); } - fn update_event(&mut self, event: Vec) -> Option> { - if event.is_empty() || !event.starts_with(b"data:") { - Some(event) - } else if let Ok(res) = serde_json::from_slice::(&event[b"data:".len()..]) { - for choice in &res.choices { - if let Some(delta) = &choice.delta { - self.message.extend(delta.content.as_bytes()); - } - } - self.usage.add(&res.usage); - None - } else if event.starts_with(b"data: [DONE]") { - None - } else { - Some(event) - } - } - pub fn push(&mut self, data: &[u8], is_openai: bool) { - if is_openai { - self.stream_parser.update(data.to_vec()); - while let Some(event) = self.stream_parser.next() { - if let Some(msg) = self.update_event(event) { - self.message.extend(msg); - } - } - } else { - self.message.extend(data); - } - } - - pub fn pop( - &mut self, - char_window_size: usize, - byte_window_size: usize, - is_openai: bool, - ) -> Vec { + pub(crate) fn pop(&mut self, char_window_size: usize, byte_window_size: usize) -> Vec { if let Ok(message) = String::from_utf8(self.message.clone()) { let chars = message.chars().collect::>(); if chars.len() <= char_window_size { @@ -64,277 +36,31 @@ impl MsgWindow { .collect::() .as_bytes() .to_vec(); - - if is_openai { - let usage = self.usage.clone(); - self.usage.reset(); - format!( - "data: {}\n\n", - json!({"choices": [{"index": 0, "delta": {"role": "assistant", "content": ret}}], "usage": usage}) - ).as_bytes().to_vec() - } else { - ret.as_bytes().to_vec() - } + ret.as_bytes().to_vec() } else { let ret = self.message[..self.message.len() - byte_window_size].to_vec(); self.message = self.message[self.message.len() - byte_window_size..].to_vec(); ret } } - - pub fn finish(&mut self, is_openai: bool) -> Vec { - if let Some(event) = self.stream_parser.flush() { - self.update_event(event); - } - if self.message.is_empty() { - Vec::new() - } else if is_openai { - format!( - "data: {}\n\ndata: [DONE]\n\n", - json!({"choices": [{"index": 0, "delta": {"role": "assistant", "content": String::from_utf8_lossy(&self.message)}}], "usage": self.usage}) - ).as_bytes().to_vec() - } else { - self.message.clone() - } + pub(crate) fn finish(&mut self) -> Vec { + core::mem::take(&mut self.message) + } + pub(crate) fn iter_mut(&mut self) -> impl Iterator> { + std::iter::once(&mut self.message) } } #[cfg(test)] mod tests { - - use super::*; - #[test] - fn test_msg() { - let mut msg_win = MsgWindow::default(); - let data = r#"data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"H"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ig"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ress"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 是"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"一个"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"基于"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" Ist"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"io"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 的"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高性能"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"服务"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"网格"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"数据"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"平面"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"项目"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"旨在"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"提供"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"吞"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"吐"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"量"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"低"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"延迟"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"和"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"可"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"扩展"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"的服务"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"通信"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"管理"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"。"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"它"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"为企业"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"级"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"应用"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"提供了"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"丰富的"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"流量"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"治理"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"功能"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"如"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"负载"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"均衡"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"熔"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"断"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"限"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"流"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"等"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":",并"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"支持"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"多"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"协议"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"代理"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"("},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"包括"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" HTTP"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"/"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"1"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"."},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"1"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" HTTP"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"/"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"2"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" g"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"RPC"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":")。"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"H"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ig"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ress"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 的"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"设计"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"目标"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"是"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"优化"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" Ist"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"io"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 在"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"大规模"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"集群"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"中的"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"性能"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"表现"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"满足"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"并发"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"场景"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"下的"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"需求"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"。"},"finish_reason":null}]} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{}} - -data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{}}],"usage":{"prompt_tokens":372,"completion_tokens":9,"total_tokens":381}} - -data: [DONE] - -"#; - let mut buffer = Vec::new(); - for line in data.split("\n\n") { - msg_win.push(line.as_bytes(), true); - msg_win.push(b"\n\n", true); - if let Ok(mut msg) = String::from_utf8(msg_win.message.clone()) { - msg = msg.replace("Higress", "***higress***"); - msg_win.message = msg.as_bytes().to_vec(); - } - buffer.extend(msg_win.pop(7, 7, true)); - } - buffer.extend(msg_win.finish(true)); - let mut message = String::new(); - for line in buffer.split(|&x| x == b'\n') { - if line.is_empty() { - continue; - } - assert!(line.starts_with(b"data:")); - if line.starts_with(b"data: [DONE]") { - continue; - } - let des = serde_json::from_slice(&line[b"data:".len()..]); - assert!(des.is_ok()); - let res: Res = des.unwrap(); - for choice in &res.choices { - if let Some(delta) = &choice.delta { - message.push_str(&delta.content); - } - } - } - assert_eq!(message, "***higress*** 是一个基于 Istio 的高性能服务网格数据平面项目,旨在提供高吞吐量、低延迟和可扩展的服务通信管理。它为企业级应用提供了丰富的流量治理功能,如负载均衡、熔断、限流等,并支持多协议代理(包括 HTTP/1.1, HTTP/2, gRPC)。***higress*** 的设计目标是优化 Istio 在大规模集群中的性能表现,满足高并发场景下的需求。"); + fn test_msg_window() { + let mut msg_window = super::MessageWindow::default(); + msg_window.update(b"hello world"); + assert_eq!(msg_window.pop(5, 5), b"hello "); + assert_eq!(msg_window.pop(5, 5), b""); + msg_window.update(b"hello world"); + assert_eq!(msg_window.pop(5, 5), b"worldhello "); + assert_eq!(msg_window.finish(), b"world"); } } diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/number_merge.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/number_merge.rs new file mode 100644 index 000000000..4eaa208b7 --- /dev/null +++ b/plugins/wasm-rust/extensions/ai-data-masking/src/number_merge.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use serde_json::{json, Number, Value}; + +fn merge_number(target: &mut Value, add: &Value) { + if target.is_null() { + if add.is_object() { + *target = json!({}); + } else if add.is_number() { + *target = Value::from(0i64); + } else { + return; + } + } + match (target, add) { + (Value::Object(t), Value::Object(a)) => { + for (key, value) in a.iter() { + if let Some(v) = t.get_mut(key) { + merge_number(v, value); + } else { + t.insert(key.clone(), value.clone()); + } + } + } + (Value::Number(t), Value::Number(a)) => { + *t = Number::from( + t.as_i64() + .unwrap_or_default() + .saturating_add(a.as_i64().unwrap_or_default()), + ); + } + _ => {} + } +} +#[derive(Default, Clone)] +pub(crate) struct NumberMerge { + value: Value, +} + +impl NumberMerge { + pub(crate) fn add(&mut self, number: &Value) { + merge_number(&mut self.value, number); + } + pub(crate) fn finish(&mut self) -> Value { + core::mem::replace(&mut self.value, Value::Null) + } +} diff --git a/plugins/wasm-rust/extensions/ai-data-masking/test/raw_message.txt b/plugins/wasm-rust/extensions/ai-data-masking/test/raw_message.txt new file mode 100644 index 000000000..2f6668a4f --- /dev/null +++ b/plugins/wasm-rust/extensions/ai-data-masking/test/raw_message.txt @@ -0,0 +1,196 @@ +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"H"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"ig"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"ress"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" 是"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"一个"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"基于"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Ist"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"io"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" 的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"高性能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"服务"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"网格"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"数据"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"平面"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"项目"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"旨在"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"提供"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"高"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"吞"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"吐"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"量"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"低"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"延迟"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"和"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"可"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"扩展"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"的服务"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"通信"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"管理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"它"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"为企业"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"级"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"应用"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"提供了"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"丰富的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"流量"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"治理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"功能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"如"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"负载"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"均衡"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"熔"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"断"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"限"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"流"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"等"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":",并"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"支持"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"多"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"协议"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"代理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"("},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"包括"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" HTTP"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"/"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"1"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"."},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"1"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" HTTP"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"/"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"2"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" g"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"RPC"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":")。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"H"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"ig"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"ress"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" 的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"设计"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"目标"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"是"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"优化"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Ist"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"io"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" 在"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"大规模"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"集群"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"中的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"性能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"表现"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"满足"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"高"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"并发"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"场景"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"下的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"需求"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"H"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ig"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ress"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 是"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"一个"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"基于"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" Ist"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"io"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高性能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"服务"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"网格"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"数据"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"平面"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"项目"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"旨在"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"提供"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872009,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"吞"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"吐"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"量"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"低"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"延迟"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"和"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"可"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"扩展"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"的服务"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"通信"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"管理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"它"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"为企业"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"级"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"应用"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"提供了"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"丰富的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"流量"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"治理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"功能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"如"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"负载"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"均衡"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"熔"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"断"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"、"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"限"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"流"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872010,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"等"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":",并"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"支持"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"多"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"协议"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"代理"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"("},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"包括"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" HTTP"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"/"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"1"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"."},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"1"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" HTTP"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"/"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"2"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" g"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"RPC"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":")。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"H"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ig"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"ress"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"设计"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"目标"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"是"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"优化"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" Ist"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"io"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" 在"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"大规模"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"集群"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872011,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"中的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"性能"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"表现"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":","},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"满足"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"高"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"并发"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"场景"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"下的"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"需求"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"。"},"finish_reason":null}]} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":null,"finish_reason":"stop"}],"usage":null} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":1,"delta":{},"finish_reason":"stop"}],"usage":{}} +data: {"id":"chatcmpl-936","object":"chat.completion.chunk","created":1739872012,"model":"qwen2.5-coder:32b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{}}],"usage":{"prompt_tokens":372,"completion_tokens":9,"total_tokens":381}} +data: [DONE] \ No newline at end of file diff --git a/test/e2e/conformance/tests/rust-wasm-ai-data-masking.go b/test/e2e/conformance/tests/rust-wasm-ai-data-masking.go index 3b49c2c6a..8934a4b27 100644 --- a/test/e2e/conformance/tests/rust-wasm-ai-data-masking.go +++ b/test/e2e/conformance/tests/rust-wasm-ai-data-masking.go @@ -183,7 +183,7 @@ var RustWasmPluginsAiDataMasking = suite.ConformanceTest{ "replace.raw.com", true, []byte("{\"test\":[{\"test\":\"test\", \"test1\":\"127.0.0.1 admin@gmail.com sk-12345\"}]}"), - []byte("{\"res\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}"), + []byte("{\"res\":\"***.***.***.*** c11e7177eb60c80cf983ddf8ca98f2dc1272d4c612204ce9bedd2460b18939cc ****@gmail.com\"}"), )) t.Run("WasmPlugins ai-data-masking", func(t *testing.T) { diff --git a/test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml b/test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml index 71d7d620f..995cc4eb2 100644 --- a/test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml +++ b/test/e2e/conformance/tests/rust-wasm-ai-data-masking.yaml @@ -62,7 +62,7 @@ spec: config: headers: - Content-Type=application/json - "body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}}],\"usage\":{}}" + "body": "{\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"***.***.***.*** c11e7177eb60c80cf983ddf8ca98f2dc1272d4c612204ce9bedd2460b18939cc ****@gmail.com\"}}],\"usage\":{}}" - domain: - system_deny.openai.com @@ -93,7 +93,7 @@ spec: config: headers: - Content-Type=application/json - "body": "{\"res\":\"***.***.***.*** 48a7e98a91d93896d8dac522c5853948 ****@gmail.com\"}" + "body": "{\"res\":\"***.***.***.*** c11e7177eb60c80cf983ddf8ca98f2dc1272d4c612204ce9bedd2460b18939cc ****@gmail.com\"}" - domain: - system_deny.raw.com config: