fix(providers): correct GLM API base URL to /api/paas/v4
* fix: add OpenAI-style tool_calls support for MiniMax and other providers
MiniMax and some other providers return tool calls in OpenAI's native
JSON format instead of ZeroClaw's XML-style <invoke> tag format.
This fix adds support for parsing OpenAI-style tool_calls:
- {"tool_calls": [{"type": "function", "function": {"name": "...", "arguments": "{...}"}}]}
The parser now:
1. First tries to parse as OpenAI-style JSON with tool_calls array
2. Falls back to ZeroClaw's original <invoke> tag format
3. Correctly handles the nested JSON string in the arguments field
Added 3 new tests covering:
- Single tool call in OpenAI format
- Multiple tool calls in OpenAI format
- Tool calls without content field
Fixes #226
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(providers): correct GLM API base URL to /api/paas/v4
The GLM (Zhipu) provider was using the incorrect base URL
`https://open.bigmodel.cn/api/paas` which resulted in 404 errors
when making API calls. The correct endpoint is
`https://open.bigmodel.cn/api/paas/v4`.
This fixes issue #238 where the agent would appear unresponsive
when using GLM-5 as the default model.
The fix aligns with the existing test `chat_completions_url_glm`
which already expected the correct v4 endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8eb57836d8
commit
3014926687
2 changed files with 76 additions and 1 deletions
|
|
@ -77,6 +77,45 @@ fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
|
||||||
let mut calls = Vec::new();
|
let mut calls = Vec::new();
|
||||||
let mut remaining = response;
|
let mut remaining = response;
|
||||||
|
|
||||||
|
// First, try to parse as OpenAI-style JSON response with tool_calls array
|
||||||
|
// This handles providers like Minimax that return tool_calls in native JSON format
|
||||||
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(response.trim()) {
|
||||||
|
if let Some(tool_calls) = json_value.get("tool_calls").and_then(|v| v.as_array()) {
|
||||||
|
for tc in tool_calls {
|
||||||
|
if let Some(function) = tc.get("function") {
|
||||||
|
let name = function
|
||||||
|
.get("name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Arguments in OpenAI format are a JSON string that needs parsing
|
||||||
|
let arguments = if let Some(args_str) = function.get("arguments").and_then(|v| v.as_str()) {
|
||||||
|
serde_json::from_str::<serde_json::Value>(args_str)
|
||||||
|
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()))
|
||||||
|
} else {
|
||||||
|
serde_json::Value::Object(serde_json::Map::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
if !name.is_empty() {
|
||||||
|
calls.push(ParsedToolCall { name, arguments });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found tool_calls, extract any content field as text
|
||||||
|
if !calls.is_empty() {
|
||||||
|
if let Some(content) = json_value.get("content").and_then(|v| v.as_str()) {
|
||||||
|
if !content.trim().is_empty() {
|
||||||
|
text_parts.push(content.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (text_parts.join("\n"), calls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to XML-style <invoke> tag parsing (ZeroClaw's original format)
|
||||||
while let Some(start) = remaining.find("<tool_call>") {
|
while let Some(start) = remaining.find("<tool_call>") {
|
||||||
// Everything before the tag is text
|
// Everything before the tag is text
|
||||||
let before = &remaining[..start];
|
let before = &remaining[..start];
|
||||||
|
|
@ -538,6 +577,42 @@ After text."#;
|
||||||
assert_eq!(calls.len(), 1);
|
assert_eq!(calls.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tool_calls_handles_openai_format() {
|
||||||
|
// OpenAI-style response with tool_calls array
|
||||||
|
let response = r#"{"content": "Let me check that for you.", "tool_calls": [{"type": "function", "function": {"name": "shell", "arguments": "{\"command\": \"ls -la\"}"}}]}"#;
|
||||||
|
|
||||||
|
let (text, calls) = parse_tool_calls(response);
|
||||||
|
assert_eq!(text, "Let me check that for you.");
|
||||||
|
assert_eq!(calls.len(), 1);
|
||||||
|
assert_eq!(calls[0].name, "shell");
|
||||||
|
assert_eq!(
|
||||||
|
calls[0].arguments.get("command").unwrap().as_str().unwrap(),
|
||||||
|
"ls -la"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tool_calls_handles_openai_format_multiple_calls() {
|
||||||
|
let response = r#"{"tool_calls": [{"type": "function", "function": {"name": "file_read", "arguments": "{\"path\": \"a.txt\"}"}}, {"type": "function", "function": {"name": "file_read", "arguments": "{\"path\": \"b.txt\"}"}}]}"#;
|
||||||
|
|
||||||
|
let (text, calls) = parse_tool_calls(response);
|
||||||
|
assert_eq!(calls.len(), 2);
|
||||||
|
assert_eq!(calls[0].name, "file_read");
|
||||||
|
assert_eq!(calls[1].name, "file_read");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tool_calls_openai_format_without_content() {
|
||||||
|
// Some providers don't include content field with tool_calls
|
||||||
|
let response = r#"{"tool_calls": [{"type": "function", "function": {"name": "memory_recall", "arguments": "{}"}}]}"#;
|
||||||
|
|
||||||
|
let (text, calls) = parse_tool_calls(response);
|
||||||
|
assert!(text.is_empty()); // No content field
|
||||||
|
assert_eq!(calls.len(), 1);
|
||||||
|
assert_eq!(calls[0].name, "memory_recall");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_tool_instructions_includes_all_tools() {
|
fn build_tool_instructions_includes_all_tools() {
|
||||||
use crate::security::SecurityPolicy;
|
use crate::security::SecurityPolicy;
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result<Box<
|
||||||
"Z.AI", "https://api.z.ai/api/coding/paas/v4", key, AuthStyle::Bearer,
|
"Z.AI", "https://api.z.ai/api/coding/paas/v4", key, AuthStyle::Bearer,
|
||||||
))),
|
))),
|
||||||
"glm" | "zhipu" => Ok(Box::new(OpenAiCompatibleProvider::new(
|
"glm" | "zhipu" => Ok(Box::new(OpenAiCompatibleProvider::new(
|
||||||
"GLM", "https://open.bigmodel.cn/api/paas", key, AuthStyle::Bearer,
|
"GLM", "https://open.bigmodel.cn/api/paas/v4", key, AuthStyle::Bearer,
|
||||||
))),
|
))),
|
||||||
"minimax" => Ok(Box::new(OpenAiCompatibleProvider::new(
|
"minimax" => Ok(Box::new(OpenAiCompatibleProvider::new(
|
||||||
"MiniMax", "https://api.minimax.chat", key, AuthStyle::Bearer,
|
"MiniMax", "https://api.minimax.chat", key, AuthStyle::Bearer,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue