Skip to content

Commit a007f7a

Browse files
fix: moonshot access (#3292)
Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
1 parent 9a64b19 commit a007f7a

File tree

1 file changed

+102
-5
lines changed

1 file changed

+102
-5
lines changed

camel/models/moonshot_model.py

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
# limitations under the License.
1313
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
1414

15+
import copy
1516
import os
1617
from typing import Any, Dict, List, Optional, Type, Union
1718

1819
from openai import AsyncStream
1920
from pydantic import BaseModel
2021

2122
from camel.configs import MoonshotConfig
23+
from camel.logger import get_logger
2224
from camel.messages import OpenAIMessage
2325
from camel.models._utils import try_modify_message_with_format
2426
from camel.models.openai_compatible_model import OpenAICompatibleModel
@@ -34,6 +36,8 @@
3436
update_langfuse_trace,
3537
)
3638

39+
logger = get_logger(__name__)
40+
3741
if 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

Comments
 (0)