22import logging
33import re
44import sys
5+ import typing as t
56from datetime import datetime
6- from typing import List , Optional , Tuple , Union
77
88import discord
99from openai .types .chat .chat_completion_message import ChatCompletionMessage
@@ -103,7 +103,7 @@ def clean_name(name: str):
103103 return cleaned_name
104104
105105
106- def get_attachments (message : discord .Message ) -> List [discord .Attachment ]:
106+ def get_attachments (message : discord .Message ) -> list [discord .Attachment ]:
107107 """Get all attachments from context"""
108108 attachments = []
109109 if message .attachments :
@@ -118,7 +118,7 @@ def get_attachments(message: discord.Message) -> List[discord.Attachment]:
118118 return attachments
119119
120120
121- async def wait_message (ctx : commands .Context ) -> Optional [discord .Message ]:
121+ async def wait_message (ctx : commands .Context ) -> t . Optional [discord .Message ]:
122122 def check (message : discord .Message ):
123123 return message .author == ctx .author and message .channel == ctx .channel
124124
@@ -169,14 +169,14 @@ def embed_to_content(message: discord.Message) -> None:
169169 message .content = extracted
170170
171171
172- def extract_code_blocks (content : str ) -> List [str ]:
172+ def extract_code_blocks (content : str ) -> list [str ]:
173173 code_blocks = re .findall (r"```(?:\w+)(.*?)```" , content , re .DOTALL )
174174 if not code_blocks :
175175 code_blocks = re .findall (r"```(.*?)```" , content , re .DOTALL )
176176 return [block .strip () for block in code_blocks ]
177177
178178
179- def extract_code_blocks_with_lang (content : str ) -> List [ Tuple [str , str ]]:
179+ def extract_code_blocks_with_lang (content : str ) -> list [ tuple [str , str ]]:
180180 code_blocks = re .findall (r"```(\w+)(.*?)```" , content , re .DOTALL )
181181 if not code_blocks :
182182 code_blocks = re .findall (r"```(.*?)```" , content , re .DOTALL )
@@ -223,8 +223,8 @@ def get_params(
223223 bot : Red ,
224224 guild : discord .Guild ,
225225 now : datetime ,
226- author : Optional [discord .Member ],
227- channel : Optional [Union [ discord .TextChannel , discord .Thread , discord .ForumChannel ] ],
226+ author : t . Optional [discord .Member ],
227+ channel : t . Optional [discord .TextChannel | discord .Thread | discord .ForumChannel ],
228228 extras : dict ,
229229) -> dict :
230230 roles = [role for role in author .roles if "everyone" not in role .name ] if author else []
@@ -262,9 +262,9 @@ def get_params(
262262
263263
264264async def ensure_message_compatibility (
265- messages : List [dict ],
265+ messages : list [dict ],
266266 conf : GuildSettings ,
267- user : Optional [discord .Member ],
267+ user : t . Optional [discord .Member ],
268268) -> bool :
269269 cleaned = False
270270
@@ -281,7 +281,7 @@ async def ensure_message_compatibility(
281281 return cleaned
282282
283283
284- async def ensure_supports_vision (messages : List [dict ], conf : GuildSettings , user : Optional [discord .Member ]) -> bool :
284+ async def ensure_supports_vision (messages : list [dict ], conf : GuildSettings , user : t . Optional [discord .Member ]) -> bool :
285285 """Make sure that if a conversation payload contains images that the model supports vision"""
286286 cleaned = False
287287
@@ -301,7 +301,7 @@ async def ensure_supports_vision(messages: List[dict], conf: GuildSettings, user
301301 return cleaned
302302
303303
304- async def purge_images (messages : List [dict ]) -> bool :
304+ async def purge_images (messages : list [dict ]) -> bool :
305305 """Remove all images sourced from URLs from the message payload"""
306306 cleaned = False
307307 for idx , message in enumerate (list (messages )):
@@ -319,6 +319,36 @@ async def purge_images(messages: List[dict]) -> bool:
319319 return cleaned
320320
321321
322+ def clean_text_content (text : str ) -> tuple [str , bool ]:
323+ """Remove invisible Unicode characters that AI detectors might flag.
324+
325+ Returns: (cleaned_text, was_modified)
326+ """
327+ if not text :
328+ return text , False
329+
330+ original_length = len (text )
331+ to_clean = [
332+ "\u200b " , # Zero-width space
333+ "\u200c " , # Zero-width non-joiner
334+ "\u200d " , # Zero-width joiner
335+ "\u2060 " , # Invisible separator
336+ "\u2061 " , # Invisible times
337+ "\u00ad " , # Soft hyphen
338+ "\u180e " , # Mongolian vowel separator
339+ "\u200b -" , # Zero-width space (non-breaking)
340+ "\u200f " , # Right-to-left mark
341+ "\u202a -" , # Left-to-right embedding
342+ "\u202e " , # Right-to-left embedding
343+ "\u2066 -" , # Left-to-right override
344+ "\u2069 " , # Pop directional formatting
345+ "\ufeff " , # Zero-width no-break space (BOM)
346+ ]
347+ for char in to_clean :
348+ text = text .replace (char , "" )
349+ return text , len (text ) != original_length
350+
351+
322352async def clean_response (response : ChatCompletionMessage ) -> bool :
323353 """Clean the model response since its stupid and breaks itself
324354
@@ -335,9 +365,9 @@ async def clean_response(response: ChatCompletionMessage) -> bool:
335365 ```
336366 Will return: Bad Request Error(400): 'multi_tool_use.create_ticket_for_user' does not match '^[a-zA-Z0-9_-]{1,64}$' - 'messages.16.tool_calls.0.function.name'
337367 """
338- if not response .tool_calls and not response .function_call :
339- return False
340368 modified = False
369+
370+ # Clean function/tool names
341371 if response .tool_calls :
342372 for tool_call in response .tool_calls :
343373 original = tool_call .function .name
@@ -351,10 +381,27 @@ async def clean_response(response: ChatCompletionMessage) -> bool:
351381 if cleaned != original :
352382 response .function_call .name = cleaned
353383 modified = True
384+
385+ # Clean content from invisible Unicode characters
386+ if response .content :
387+ if isinstance (response .content , str ):
388+ cleaned_content , was_cleaned = clean_text_content (response .content )
389+ if was_cleaned :
390+ response .content = cleaned_content
391+ modified = True
392+ elif isinstance (response .content , list ):
393+ # Handle multi-modal content (list of content items)
394+ for item in response .content :
395+ if item .get ("type" ) == "text" and "text" in item :
396+ cleaned_text , was_cleaned = clean_text_content (item ["text" ])
397+ if was_cleaned :
398+ item ["text" ] = cleaned_text
399+ modified = True
400+
354401 return modified
355402
356403
357- async def clean_responses (messages : List [dict ]) -> bool :
404+ async def clean_responses (messages : list [dict ]) -> bool :
358405 """Same as clean_response but cleans the whole message payload"""
359406 modified = False
360407 for message in messages :
@@ -369,7 +416,7 @@ async def clean_responses(messages: List[dict]) -> bool:
369416 return modified
370417
371418
372- async def ensure_tool_consistency (messages : List [dict ]) -> bool :
419+ async def ensure_tool_consistency (messages : list [dict ]) -> bool :
373420 """
374421 Ensure all tool calls satisfy schema requirements, modifying the messages payload in-place.
375422
0 commit comments