Repository: ollama/ollama

Issue:
cogito:3b with tools not working with Ollama 0.8.0
### What is the issue?

I have just updated to Ollama 0.8.0 and my chat requests with tools are failing. It only works if I remove the tools from my requests! 
Note that before the upgrade the same requests were working fine

I removed and downloaded cogito:3b after the upgrade but it didn't changed anything. Trying the same requests with qwen3:4b are working fine


### Relevant log output

```shell
Request:
curl http://localhost:11434/api/chat -d '{"Messages":[{"Role":"User","Content":"What weather will be having tomorrow night in 
New York?"}],"Tools":[{"Type":"function","Function":{"Name":"GetCurrentWeather","Description":"Gets the weather for the given 
location.\\r\\nIf measuringUnit is not provided, use Celsius.\\r\\nIf whichDay is not not provided, use Now.","Parameters":
{"Type":"object","Properties":{"location":{"Type":"string","Description":"The location or city to get the weather 
for","Enum":null},"measuringUnit":{"Type":"string","Description":"The unit to measure the temperature in","Enum":
["Celsius","Fahrenheit"]},"whichDay":{"Type":"string","Description":"Which day or week the report is requested","Enum":
["Now","Today","Tomorrow","DayAfterTomorrow","Week","Forecast"]}},"required":["location","measuringUnit","whichDay"]}}},
{"Type":"function","Function":{"Name":"GetUserLocation","Description":"Use this to get the user current location.","Parameters":
{"Type":"object","Properties":{},"required":[]}}}],"Model":"cogito:3b","Stream":true}'

Response:
{"error":"invalid character '\u003c' looking for beginning of value"}
```

### OS

Windows

### GPU

_No response_

### CPU

Intel

### Ollama version

0.8.0

Discussion:
@rensftw: Same thing happens for me with `cogito:latest` on macOS M1 Pro

Request:
<details>
<pre>
<code>
curl http://localhost:11434/api/chat -d '{
  "model": "cogito:latest",
  "messages": [
    {
      "role": "user",
      "content": "What is the weather today in Toronto?"
    }
  ],
  "stream": true,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_current_weather",
        "description": "Get the current weather for a location",
        "parameters": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "The location to get the weather for, e.g. San Francisco, CA"
            },
            "format": {
              "type": "string",
              "description": "The format to return the weather in, e.g. 'celsius' or 'fahrenheit'",
              "enum": ["celsius", "fahrenheit"]
            }
          },
          "required": ["location", "format"]
        }
      }
    }
  ]
}'
</code>
</pre>
</details>

Response:
```json
{
  "error": "invalid character '<' looking for beginning of value"
}
```@rick-github: The template for cogito wraps each `ToolCalls` item in `<tool_call></tool_call>` tags, unlike most models that wrap the entire list.  Same is true for nemotron-mini:4b-instruct-q4_K_M and some user models like MFDoom/deepseek-r1-tool-calling and maryasov/qwen2.5-coder-cline.@pihapi: ollama --version
ollama version is 0.9.0

ebdm/gemma3-enhanced:12b

level=ERROR source=routes.go:1530 msg="failed to create tool parser" error="invalid character '<' looking for beginning of value"

Everything worked, but then updates arrived and everything stopped working! 


In what version did this work, please? 
And is it possible to disable updates? 
Or is it better to switch to LMStudio ... ?@rick-github: You can revert to a version of ollama that doesn't include the tool changes by running [OllamaSetup.exe](https://github.com/ollama/ollama/releases/download/v0.7.1/OllamaSetup.exe) from the Assets section of a previous release.@ParthSareen: Sorry about that! Will check it out today!

Solution patch:
diff --git a/tools/tools_utils.go b/tools/tools_utils.go
index 48531b78..b6f80729 100644
--- a/tools/tools_utils.go
+++ b/tools/tools_utils.go
@@ -166,31 +166,26 @@ func extractToolArgs(tmpl *gotmpl.Template) (name, arguments string, err error)
 		return "", "", err
 	}
 
-	var obj any
-	err = json.Unmarshal(b.Bytes(), &obj)
-	if err != nil {
-		return "", "", err
+	// Extract JSON object between curly braces
+	// JSON arrays are also valid as they will not be repeated in the template
+	output := b.String()
+	start := strings.Index(output, "{")
+	end := strings.LastIndex(output, "}")
+	if start == -1 || end == -1 || start > end {
+		return "", "", errors.New("no valid JSON object found in template output")
 	}
+	jsonStr := output[start : end+1]
 
-	var objs []map[string]any
-	switch v := obj.(type) {
-	case map[string]any:
-		objs = []map[string]any{v}
-	case []map[string]any:
-		objs = v
-	case []any:
-		objs = collect(v)
-	}
-	if len(objs) == 0 {
-		return "", "", errors.New("no template objects found")
+	var obj map[string]any
+	if err := json.Unmarshal([]byte(jsonStr), &obj); err != nil {
+		return "", "", err
 	}
 
-	// find the keys that correspond to the name and arguments fields
-	for k, v := range objs[0] {
-		switch v.(type) {
-		case string:
+	// Find name and arguments fields
+	for k, v := range obj {
+		if str, ok := v.(string); ok && str == "@@name@@" {
 			name = k
-		case map[string]any:
+		} else if _, ok := v.(map[string]any); ok {
 			arguments = k
 		}
 	}



Test patch:
diff --git a/tools/tools_utils_test.go b/tools/tools_utils_test.go
index 769183b7..e346117a 100644
--- a/tools/tools_utils_test.go
+++ b/tools/tools_utils_test.go
@@ -271,74 +271,99 @@ func TestExtractToolArgs(t *testing.T) {
 	cases := []struct {
 		name     string
 		template string
-		want     string
-		ok       bool
+		wantName string
+		wantArgs string
+		wantErr  bool
 	}{
 		{
-			name:     "basic tool call with text after",
-			template: `{{if .ToolCalls}}tool response{{end}}`,
-			want:     "tool response",
-			ok:       true,
+			name: "basic tool call",
+			template: `{{ range .ToolCalls }}
+{"name": "{{ .Function.Name }}", "parameters": {{ .Function.Arguments }}}{{ end }}`,
+			wantName: "name",
+			wantArgs: "parameters",
+			wantErr:  false,
 		},
 		{
-			name:     "tool call with mixed content after",
-			template: `{{if .ToolCalls}}<tool_call>{{.Something}}{{end}}`,
-			want:     "<tool_call>",
-			ok:       true,
-		},
-		{
-			name:     "tool call with no text after",
-			template: `{{if .ToolCalls}}{{.Something}}{{end}}`,
-			want:     "",
-			ok:       true,
+			name: "tool call with whitespace",
+			template: `{{range .ToolCalls}}
+  {"name": "{{.Function.Name}}", "parameters": {{.Function.Arguments}}}
+{{end}}`,
+			wantName: "name",
+			wantArgs: "parameters",
+			wantErr:  false,
 		},
 		{
-			name:     "nested tool call",
-			template: `{{if .Something}}{{if .ToolCalls}}[TOOL_CALL]{{end}}{{end}}`,
-			want:     "[TOOL_CALL]",
-			ok:       true,
+			name: "tool call with extra content",
+			template: `Before {{range .ToolCalls}}
+{"name": "{{.Function.Name}}", "arguments": {{.Function.Arguments}}}{{end}} After`,
+			wantName: "name",
+			wantArgs: "arguments",
+			wantErr:  false,
 		},
 		{
 			name:     "no tool calls",
 			template: `{{if .Something}}no tools here{{end}}`,
-			want:     "",
-			ok:       false,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
 		},
 		{
 			name:     "empty template",
 			template: ``,
-			want:     "",
-			ok:       false,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
 		},
 		{
-			name:     "multiple tool calls sections",
-			template: `{{if .ToolCalls}}first{{end}}{{if .ToolCalls}}second{{end}}`,
-			want:     "first",
-			ok:       true,
+			name: "prefix within tool call",
+			template: `{{- if .ToolCalls }}
+{{ range .ToolCalls }}
+<tool_call>
+{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}
+</tool_call>{{ end }}{{- end }}`,
+			wantName: "name",
+			wantArgs: "arguments",
+			wantErr:  false,
 		},
 		{
-			name:     "range over tool calls",
-			template: `{{if .ToolCalls}}{{range .ToolCalls}}tool{{end}}{{end}}`,
-			want:     "",
-			ok:       true,
+			name: "JSON array",
+			template: `{{ range .ToolCalls }}
+[{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}}]{{ end }}`,
+			wantName: "name",
+			wantArgs: "arguments",
+			wantErr:  false,
 		},
 		{
-			name:     "tool calls with pipe delimiters",
-			template: `{{if .ToolCalls}}
{{end}}`,
-			want:     "
",
-			ok:       true,
+			name: "invalid JSON",
+			template: `{{ range .ToolCalls }}
+{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}, invalid}{{ end }}`,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
 		},
 		{
-			name:     "tool calls with nested template",
-			template: `{{if .ToolCalls}}{{template "tool" .}}{{end}}`,
-			want:     "",
-			ok:       true,
+			name: "missing name field",
+			template: `{{ range .ToolCalls }}
+{"parameters": {{ .Function.Arguments }}}{{ end }}`,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
 		},
 		{
-			name:     "tool calls with whitespace variations",
-			template: `{{if .ToolCalls}}  tool  {{end}}`,
-			want:     "  tool  ",
-			ok:       true,
+			name: "missing arguments field",
+			template: `{{ range .ToolCalls }}
+{"name": "{{ .Function.Name }}"}{{ end }}`,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
+		},
+		{
+			name: "malformed JSON",
+			template: `{{ range .ToolCalls }}
+{"name": {{ .Function.Name }}, "arguments": {{ .Function.Arguments }}{{ end }}`,
+			wantName: "",
+			wantArgs: "",
+			wantErr:  true,
 		},
 	}
 
@@ -349,12 +374,20 @@ func TestExtractToolArgs(t *testing.T) {
 				t.Fatalf("failed to parse template: %v", err)
 			}
 
-			got, ok := extractToolCallsFormat(tmpl)
-			if got != tt.want {
-				t.Errorf("TextAfterToolCalls() got = %q, want %q", got, tt.want)
+			gotName, gotArgs, err := extractToolArgs(tmpl)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("extractToolArgs() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err != nil {
+				return
+			}
+
+			if gotName != tt.wantName {
+				t.Errorf("extractToolArgs() gotName = %q, want %q", gotName, tt.wantName)
 			}
-			if ok != tt.ok {
-				t.Errorf("TextAfterToolCalls() ok = %v, want %v", ok, tt.ok)
+			if gotArgs != tt.wantArgs {
+				t.Errorf("extractToolArgs() gotArgs = %q, want %q", gotArgs, tt.wantArgs)
 			}
 		})
 	}