Skip to content

Commit 185975b

Browse files
authored
Merge pull request #804 from rumpl/fix-mistral
Fix mistral tool calling
2 parents d5487a5 + 9e44e4a commit 185975b

3 files changed

Lines changed: 59 additions & 41 deletions

File tree

e2e/cagent_exec_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,9 @@ func TestExec_Mistral(t *testing.T) {
6767
}
6868

6969
func TestExec_Mistral_ToolCall(t *testing.T) {
70-
t.Skip("This is awkard")
71-
7270
out := cagentExec(t, "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.")
7371

74-
require.Equal(t, "\n--- Agent: root ---\n", out)
72+
require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n\n1", out)
7573
}
7674

7775
func TestExec_ToolCallsNeedAcceptance(t *testing.T) {

e2e/testdata/cassettes/TestExec_Mistral_ToolCall.yaml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,28 @@ interactions:
1616
proto_major: 2
1717
proto_minor: 0
1818
content_length: -1
19-
body: "data: {\"id\":\"48a9784f10cd40f8894f7e6b19d3835a\",\"object\":\"chat.completion.chunk\",\"created\":1763052705,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"48a9784f10cd40f8894f7e6b19d3835a\",\"object\":\"chat.completion.chunk\",\"created\":1763052705,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"fE4gFHSUO\",\"function\":{\"name\":\"list_directory\",\"arguments\":\"{\\\"path\\\": \\\"testdata/working_dir\\\"}\"},\"index\":0}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1684,\"total_tokens\":1699,\"completion_tokens\":15},\"p\":\"abcdefghijklmnopqrst\"}\n\ndata: [DONE]\n\n"
19+
body: "data: {\"id\":\"97ac5a104c1a45898248c01e65c985c0\",\"object\":\"chat.completion.chunk\",\"created\":1763163876,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"97ac5a104c1a45898248c01e65c985c0\",\"object\":\"chat.completion.chunk\",\"created\":1763163876,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"1IkZdXSvy\",\"function\":{\"name\":\"list_directory\",\"arguments\":\"{\\\"path\\\": \\\"testdata/working_dir\\\"}\"},\"index\":0}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1684,\"total_tokens\":1699,\"completion_tokens\":15},\"p\":\"abcdefghijkl\"}\n\ndata: [DONE]\n\n"
2020
headers: {}
2121
status: 200 OK
2222
code: 200
23-
duration: 360.572958ms
23+
duration: 1.102535125s
24+
- id: 1
25+
request:
26+
proto: HTTP/1.1
27+
proto_major: 1
28+
proto_minor: 1
29+
content_length: 0
30+
host: api.mistral.ai
31+
body: "{\"messages\":[{\"content\":\"You are a knowledgeable assistant that helps users with various tasks.\\nBe helpful, accurate, and concise in your responses.\\n\",\"role\":\"system\"},{\"content\":\"## Filesystem Tool Instructions\\n\\nThis toolset provides comprehensive filesystem operations with built-in security restrictions.\\n\\n### Security Model\\n- All operations are restricted to allowed directories only\\n- Use list_allowed_directories to see available paths\\n- Subdirectories within allowed directories are accessible\\n- Use add_allowed_directory to request access to new directories (requires user consent)\\n\\n### Directory Access Management\\n- If you need access to a directory outside the allowed list, use add_allowed_directory\\n- This will request user consent before expanding filesystem access\\n- Always provide a clear reason when requesting new directory access\\n\\n### Common Patterns\\n- Always check if directories exist before creating files\\n- Prefer read_multiple_files for batch operations\\n- Use search_files_content for finding specific code or text\\n\\n### Performance Tips\\n- Use read_multiple_files instead of multiple read_file calls\\n- Use directory_tree with max_depth to limit large traversals\\n- Use appropriate exclude patterns in search operations\",\"role\":\"system\"},{\"content\":\"How many files in testdata/working_dir? Only output the number.\",\"role\":\"user\"},{\"tool_calls\":[{\"id\":\"1IkZdXSvy\",\"function\":{\"arguments\":\"{\\\"path\\\": \\\"testdata/working_dir\\\"}\",\"name\":\"list_directory\"},\"type\":\"function\"}],\"role\":\"assistant\"},{\"content\":\"FILE README.me\\n\",\"tool_call_id\":\"1IkZdXSvy\",\"role\":\"tool\"}],\"model\":\"mistral-small\",\"stream_options\":{\"include_usage\":true},\"tools\":[{\"function\":{\"name\":\"create_directory\",\"description\":\"Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to create\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"directory_tree\",\"description\":\"Get a recursive tree view of files and directories as a JSON structure.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"max_depth\":{\"description\":\"Maximum depth to traverse (optional)\",\"type\":\"integer\"},\"path\":{\"description\":\"The directory path to traverse\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"edit_file\",\"description\":\"Make line-based edits to a text file. Each edit replaces exact line sequences with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"Array of edit operations\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"The replacement text\",\"type\":\"string\"},\"oldText\":{\"description\":\"The exact text to replace\",\"type\":\"string\"}},\"required\":[\"oldText\",\"newText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"The file path to edit\",\"type\":\"string\"}},\"required\":[\"path\",\"edits\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"get_file_info\",\"description\":\"Retrieve detailed metadata about a file or directory.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The file or directory path to inspect\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_allowed_directories\",\"description\":\"Returns a list of directories that the server has permission to access. Don't call if you access only the current working directory. It's always allowed.\",\"parameters\":{\"properties\":{},\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"add_allowed_directory\",\"description\":\"Request to add a new directory to the allowed directories list. This requires explicit user consent for security reasons.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to add to allowed directories\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_directory\",\"description\":\"Get a detailed listing of all files and directories in a specified path.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to list\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"list_directory_with_sizes\",\"description\":\"Get a detailed listing of all files and directories in a specified path, including sizes.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The directory path to list\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"move_file\",\"description\":\"Move or rename files and directories.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"destination\":{\"description\":\"The destination path\",\"type\":\"string\"},\"source\":{\"description\":\"The source path\",\"type\":\"string\"}},\"required\":[\"source\",\"destination\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_file\",\"description\":\"Read the complete contents of a file from the file system.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"path\":{\"description\":\"The file path to read\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"read_multiple_files\",\"description\":\"Read the contents of multiple files simultaneously.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"json\":{\"description\":\"Whether to return the result as JSON\",\"type\":\"boolean\"},\"paths\":{\"description\":\"Array of file paths to read\",\"items\":{\"type\":\"string\"},\"type\":\"array\"}},\"required\":[\"paths\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files\",\"description\":\"Recursively search for files and directories matching a pattern. Prints the full paths of matching files and the total number of files found.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"pattern\":{\"description\":\"The search pattern\",\"type\":\"string\"}},\"required\":[\"path\",\"pattern\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"search_files_content\",\"description\":\"Searches for text or regex patterns in the content of files matching a GLOB pattern.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"excludePatterns\":{\"description\":\"Patterns to exclude from search\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"is_regex\":{\"description\":\"If true, treat query as regex; otherwise literal text\",\"type\":\"boolean\"},\"path\":{\"description\":\"The starting directory path\",\"type\":\"string\"},\"query\":{\"description\":\"The text or regex pattern to search for\",\"type\":\"string\"}},\"required\":[\"path\",\"query\"],\"type\":\"object\"}},\"type\":\"function\"},{\"function\":{\"name\":\"write_file\",\"description\":\"Create a new file or completely overwrite an existing file with new content.\",\"parameters\":{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"The content to write to the file\",\"type\":\"string\"},\"path\":{\"description\":\"The file path to write\",\"type\":\"string\"}},\"required\":[\"path\",\"content\"],\"type\":\"object\"}},\"type\":\"function\"}],\"stream\":true}"
32+
url: https://api.mistral.ai/v1/chat/completions
33+
method: POST
34+
response:
35+
proto: HTTP/2.0
36+
proto_major: 2
37+
proto_minor: 0
38+
content_length: -1
39+
body: "data: {\"id\":\"1350054467dc4790a3769cb7b6fcae36\",\"object\":\"chat.completion.chunk\",\"created\":1763163877,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"1350054467dc4790a3769cb7b6fcae36\",\"object\":\"chat.completion.chunk\",\"created\":1763163877,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"1\"},\"finish_reason\":null}],\"p\":\"abcdefghijkl\"}\n\ndata: {\"id\":\"1350054467dc4790a3769cb7b6fcae36\",\"object\":\"chat.completion.chunk\",\"created\":1763163877,\"model\":\"mistral-small\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1721,\"total_tokens\":1723,\"completion_tokens\":2},\"p\":\"abcdefghijklmnopqrstu\"}\n\ndata: [DONE]\n\n"
40+
headers: {}
41+
status: 200 OK
42+
code: 200
43+
duration: 181.057541ms

pkg/model/provider/oaistream/adapter.go

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -51,42 +51,6 @@ func (a *StreamAdapter) Recv() (chat.MessageStreamResponse, error) {
5151
Choices: make([]chat.MessageStreamChoice, len(openaiResponse.Choices)),
5252
}
5353

54-
// Check if Usage field is present using the JSON metadata
55-
if openaiResponse.JSON.Usage.Valid() {
56-
usage := openaiResponse.Usage
57-
response.Usage = &chat.Usage{
58-
InputTokens: int(usage.PromptTokens),
59-
OutputTokens: int(usage.CompletionTokens),
60-
CachedInputTokens: 0,
61-
CachedOutputTokens: 0,
62-
ReasoningTokens: 0,
63-
}
64-
if usage.JSON.PromptTokensDetails.Valid() {
65-
response.Usage.CachedInputTokens = int(usage.PromptTokensDetails.CachedTokens)
66-
}
67-
if usage.JSON.CompletionTokensDetails.Valid() {
68-
response.Usage.ReasoningTokens = int(usage.CompletionTokensDetails.ReasoningTokens)
69-
}
70-
// Use the tracked finish reason instead of hardcoding stop
71-
finishReason := a.lastFinishReason
72-
if finishReason == chat.FinishReasonNull || finishReason == "" {
73-
finishReason = chat.FinishReasonStop
74-
}
75-
// OPENAI returns the usage without a finish reason or a choice, so we fake it here
76-
// and create a new choice for the last event in the stream
77-
if len(openaiResponse.Choices) == 0 {
78-
response.Choices = append(response.Choices, chat.MessageStreamChoice{
79-
FinishReason: finishReason,
80-
})
81-
} else {
82-
// Other openai-compatible providers DO return a choice with finish reason...
83-
response.Choices[0].FinishReason = finishReason
84-
}
85-
if finishReason == chat.FinishReasonStop {
86-
return response, nil
87-
}
88-
}
89-
9054
// Convert the choices
9155
for i := range openaiResponse.Choices {
9256
choice := &openaiResponse.Choices[i]
@@ -145,6 +109,42 @@ func (a *StreamAdapter) Recv() (chat.MessageStreamResponse, error) {
145109
}
146110
}
147111

112+
// Check if Usage field is present using the JSON metadata
113+
if openaiResponse.JSON.Usage.Valid() {
114+
usage := openaiResponse.Usage
115+
response.Usage = &chat.Usage{
116+
InputTokens: int(usage.PromptTokens),
117+
OutputTokens: int(usage.CompletionTokens),
118+
CachedInputTokens: 0,
119+
CachedOutputTokens: 0,
120+
ReasoningTokens: 0,
121+
}
122+
if usage.JSON.PromptTokensDetails.Valid() {
123+
response.Usage.CachedInputTokens = int(usage.PromptTokensDetails.CachedTokens)
124+
}
125+
if usage.JSON.CompletionTokensDetails.Valid() {
126+
response.Usage.ReasoningTokens = int(usage.CompletionTokensDetails.ReasoningTokens)
127+
}
128+
// Use the tracked finish reason instead of hardcoding stop
129+
finishReason := a.lastFinishReason
130+
if finishReason == chat.FinishReasonNull || finishReason == "" {
131+
finishReason = chat.FinishReasonStop
132+
}
133+
// OPENAI returns the usage without a finish reason or a choice, so we fake it here
134+
// and create a new choice for the last event in the stream
135+
if len(openaiResponse.Choices) == 0 {
136+
response.Choices = append(response.Choices, chat.MessageStreamChoice{
137+
FinishReason: finishReason,
138+
})
139+
} else {
140+
// Other openai-compatible providers DO return a choice with finish reason...
141+
response.Choices[0].FinishReason = finishReason
142+
}
143+
if finishReason == chat.FinishReasonStop {
144+
return response, nil
145+
}
146+
}
147+
148148
return response, nil
149149
}
150150

0 commit comments

Comments
 (0)