1212# limitations under the License.
1313# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
1414
15+ import copy
1516import os
1617from typing import Any , Dict , List , Optional , Type , Union
1718
1819from openai import AsyncStream
1920from pydantic import BaseModel
2021
2122from camel .configs import MoonshotConfig
23+ from camel .logger import get_logger
2224from camel .messages import OpenAIMessage
2325from camel .models ._utils import try_modify_message_with_format
2426from camel .models .openai_compatible_model import OpenAICompatibleModel
3436 update_langfuse_trace ,
3537)
3638
39+ logger = get_logger (__name__ )
40+
3741if os .environ .get ("LANGFUSE_ENABLED" , "False" ).lower () == "true" :
3842 try :
3943 from langfuse .decorators import observe
@@ -84,7 +88,7 @@ def __init__(
8488 model_type : Union [ModelType , str ],
8589 model_config_dict : Optional [Dict [str , Any ]] = None ,
8690 api_key : Optional [str ] = None ,
87- url : Optional [str ] = "https://api.moonshot.ai/v1" ,
91+ url : Optional [str ] = None ,
8892 token_counter : Optional [BaseTokenCounter ] = None ,
8993 timeout : Optional [float ] = None ,
9094 max_retries : int = 3 ,
@@ -93,7 +97,12 @@ def __init__(
9397 if model_config_dict is None :
9498 model_config_dict = MoonshotConfig ().as_dict ()
9599 api_key = api_key or os .environ .get ("MOONSHOT_API_KEY" )
96- url = url or os .environ .get ("MOONSHOT_API_BASE_URL" )
100+ # Preserve default URL if not provided
101+ if url is None :
102+ url = (
103+ os .environ .get ("MOONSHOT_API_BASE_URL" )
104+ or "https://api.moonshot.ai/v1"
105+ )
97106 timeout = timeout or float (os .environ .get ("MODEL_TIMEOUT" , 180 ))
98107 super ().__init__ (
99108 model_type = model_type ,
@@ -125,19 +134,107 @@ def _prepare_request(
125134 Returns:
126135 Dict[str, Any]: The prepared request configuration.
127136 """
128- import copy
129-
130137 request_config = copy .deepcopy (self .model_config_dict )
131138
132139 if tools :
133- request_config ["tools" ] = tools
140+ # Clean tools to remove null types (Moonshot API incompatibility)
141+ cleaned_tools = self ._clean_tool_schemas (tools )
142+ request_config ["tools" ] = cleaned_tools
134143 elif response_format :
135144 # Use the same approach as DeepSeek for structured output
136145 try_modify_message_with_format (messages [- 1 ], response_format )
137146 request_config ["response_format" ] = {"type" : "json_object" }
138147
139148 return request_config
140149
150+ def _clean_tool_schemas (
151+ self , tools : List [Dict [str , Any ]]
152+ ) -> List [Dict [str , Any ]]:
153+ r"""Clean tool schemas to remove null types for Moonshot compatibility.
154+
155+ Moonshot API doesn't accept {"type": "null"} in anyOf schemas.
156+ This method removes null type definitions from parameters.
157+
158+ Args:
159+ tools (List[Dict[str, Any]]): Original tool schemas.
160+
161+ Returns:
162+ List[Dict[str, Any]]: Cleaned tool schemas.
163+ """
164+
165+ def remove_null_from_schema (schema : Any ) -> Any :
166+ """Recursively remove null types from schema."""
167+ if isinstance (schema , dict ):
168+ # Create a copy to avoid modifying the original
169+ result = {}
170+
171+ for key , value in schema .items ():
172+ if key == 'type' and isinstance (value , list ):
173+ # Handle type arrays like ["string", "null"]
174+ filtered_types = [t for t in value if t != 'null' ]
175+ if len (filtered_types ) == 1 :
176+ # Single type remains, convert to string
177+ result [key ] = filtered_types [0 ]
178+ elif len (filtered_types ) > 1 :
179+ # Multiple types remain, keep as array
180+ result [key ] = filtered_types
181+ else :
182+ # All were null, use string as fallback
183+ logger .warning (
184+ "All types in tool schema type array "
185+ "were null, falling back to 'string' "
186+ "type for Moonshot API compatibility. "
187+ "Original tool schema may need review."
188+ )
189+ result [key ] = 'string'
190+ elif key == 'anyOf' :
191+ # Handle anyOf with null types
192+ filtered = [
193+ item
194+ for item in value
195+ if not (
196+ isinstance (item , dict )
197+ and item .get ('type' ) == 'null'
198+ )
199+ ]
200+ if len (filtered ) == 1 :
201+ # If only one type remains, flatten it
202+ return remove_null_from_schema (filtered [0 ])
203+ elif len (filtered ) > 1 :
204+ result [key ] = [
205+ remove_null_from_schema (item )
206+ for item in filtered
207+ ]
208+ else :
209+ # All were null, return string type as fallback
210+ logger .warning (
211+ "All types in tool schema anyOf were null, "
212+ "falling back to 'string' type for "
213+ "Moonshot API compatibility. Original "
214+ "tool schema may need review."
215+ )
216+ return {"type" : "string" }
217+ else :
218+ # Recursively process other values
219+ result [key ] = remove_null_from_schema (value )
220+
221+ return result
222+ elif isinstance (schema , list ):
223+ return [remove_null_from_schema (item ) for item in schema ]
224+ else :
225+ return schema
226+
227+ cleaned_tools = copy .deepcopy (tools )
228+ for tool in cleaned_tools :
229+ if 'function' in tool and 'parameters' in tool ['function' ]:
230+ params = tool ['function' ]['parameters' ]
231+ if 'properties' in params :
232+ params ['properties' ] = remove_null_from_schema (
233+ params ['properties' ]
234+ )
235+
236+ return cleaned_tools
237+
141238 @observe ()
142239 async def _arun (
143240 self ,
0 commit comments