Repository: ollama/ollama
Pull request: types: include the 'items' and '$defs' fields to properly handle "array" types

This PR adds the capability for Ollama to handle tool argument types that are homegeneous arrays more correctly.

## BACKGROUND

JSON-Schema allows the description of object with named properties.  In addition, the type of each property  can be indicated with a "type" annotation with value "string", "integer", "number", "array", etc.

``` json
{
  "field1": {
    "type": "string"
   },
   "field2": {
     "type": "number"
   }
}
```


An "array" can hold objects of arbitrary type.  Thus it is acceptable to have an array
like

``` json
[1, 2.0, "tom", false, 5]
```

If an array should be constrained to hold values that all obey the same schema, then
an additional keyword, "items", can be included to indicate that.

``` json
{
  "field3": {
    "type": "array",
    "items": "number"
   }
}
```

The example above shows how a field would be constrained to only hold lists of numbers.

Ollama does not recognize the "items" keyword in a JSON-Schema.


## EXPLANATION

Ollama does not demarshall or store the contents of an "items" field, even when it is presented in the /api/chat request given by a client.  Without including it, an LLM cannot know the structure of an array when it is trying to fullfill a request.

(In my tests with Llama3.2 and an MCP tool that accepted an array of objects with fields, Llama3.2 would consistently provide an array of strings since it could not know any better.  The tool call would simply fail.)

With the modification to include the "items" field, Llama3.2 can fullfill a request that satisfies the type signature of a function that takes an array of objects.

Interestingly, Ollama already includes all of the fields present in a Tool definition when it serializes it to a string, so by simply including the missing field it is passed to the LLM.


EXAMPLE and SUCCESS

The following is an example request generated by OTERM (https://github.com/ggozad/oterm) with an attached MCP Tool (Model Context Protocol Tool) called "preparation_tool".  The preparation_tool operates on an array of structures, all of the same type.  With the code modifications shown here, the "items" field is included with the text presented to Llama3.2 which can form a tool call that matches the type signature.

The prompt loads a list of (sample, mass) pairs as a markdown table and asks the LLM to call the MCP Tool with the data.

``` 
{'format': '',
 'keep_alive': '5m',
 'messages': [{'content': 'Process the following table of data with the '
                          'preparation_tool.\n'
                          '<table>\n'
                          '<tr>\n'
                          '  <th>Sample Name</th> <th>Input Mass</th>\n'
                          '<tr>\n'
                          '<tr>\n'
                          '  <td> NA12878 </td> <td> 45 </td>\n'
                          '</tr\n'
                          '<tr>\n'
                          '  <td> NCC1701 </td> <td> 77 </td>\n'
                          '</tr\n'
                          '<tr>\n'
                          '  <td> NACL </td> <td> 12 </td>\n'
                          '</tr\n'
                          '<tr>\n'
                          '  <td> NA12879 </td> <td> 32 </td>\n'
                          '</tr\n'
                          '</table>\n'
                          '\n',
               'role': 'user'}],
 'model': 'llama3.2:latest',
 'options': {'stop': ['
',
                      '
',
                      '
']},
 'stream': False,
 'tools': [{'function': {'description': 'Prepare the samples for analyze.  '
                                        "Mark samples as 'passed' or "
                                        "'failed'. ",
                         'name': 'preparation_tool',
                         'parameters': {
                             'properties': {
                                 'sample_list': {
                                     'items': {
                                         'properties': {
                                             'mass': {
                                                 'description': 'sample mass in ng',
                                                 'title': 'Mass',
                                                 'type': 'integer'
                                             },
                                             'sample_name': {
                                                 'description': 'sample name identifier',
                                                 'title': 'Sample Name',
                                                 'type': 'string'
                                             }
                                         },
                                         'required': ['sample_name', 'mass'],
                                         'title': 'InputSample', 'type': 'object'
                                     },
                                     'type': 'array'
                                 }
                             },
                             'required': ['sample_list'],
                             'type': 'object'}
                        },
            'type': 'function'}]}
```

## FUTURE WORK

This PR simply annotates the type of the "items" field as "any" which allows the receipt of correctly-formated JSON, that is flattened and passed to the Model.  A better implementation might include a more complete JSON Schema type structure.

It would be nice to be able to add a test that exercises this part of the code.


## REFERENCES

https://modelcontextprotocol.io/
https://github.com/modelcontextprotocol/python-sdk/

## OLLAMA DEBUG OUTPUT WITH THE FIX PRESENT

``` 
time=2025-04-02T06:48:09.741-07:00 level=DEBUG source=routes.go:1521 msg="chat request" images=0 prompt="
system
\n\nCutting Knowledge Date: December 2023\n\nWhen you receive a tool call response, use the output to format an answer to the orginal user question.\n\nYou are a helpful assistant with tool calling capabilities.

user
\n\nGiven the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.\n\nRespond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}. Do not use variables.\n\n{\"type\":\"function\",\"function\":{\"name\":\"create_table\",\"description\":\"Create a new table in the SQLite database\",\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"CREATE TABLE SQL statement\"}}}}}\n{\"type\":\"function\",\"function\":{\"name\":\"list_tables\",\"description\":\"List all tables in the SQLite database\",\"parameters\":{\"type\":\"object\",\"required\":null,\"properties\":{}}}}\n{\"type\":\"function\",\"function\":{\"name\":\"preparation_tool\",\"description\":\"Prepare the samples for analyze.  Mark samples as 'passed' or 'failed'. \",\"parameters\":{\"type\":\"object\",\"required\":[\"sample_list\"],\"properties\":{\"sample_list\":{\"type\":\"array\",\"items\":{\"properties\":{\"mass\":{\"description\":\"sample mass in ng\",\"title\":\"Mass\",\"type\":\"integer\"},\"sample_name\":{\"description\":\"sample name identifier\",\"title\":\"Sample Name\",\"type\":\"string\"}},\"required\":[\"sample_name\",\"mass\"],\"title\":\"InputSample\",\"type\":\"object\"},\"description\":\"\"}}}}}\n\nProcess the following table of data with the preparation_tool.\n<table>\n<tr>\n  <th>Sample Name</th> <th>Input Mass</th>\n<tr>\n<tr>\n  <td> NA12878 </td> <td> 45 </td>\n</tr\n<tr>\n  <td> NCC1701 </td> <td> 77 </td>\n</tr\n<tr>\n  <td> NACL </td> <td> 12 </td>\n</tr\n<tr>\n  <td> NA12879 </td> <td> 32 </td>\n</tr\n</table>\n

assistant
\n\n"
```


## OLLAMA DEBUG OUTPUT WITHOUT THE FIX

```
time=2025-04-02T06:51:06.962-07:00 level=DEBUG source=routes.go:1521 msg="chat request" images=0 prompt="
system
\n\nCutting Knowledge Date: December 2023\n\nWhen you receive a tool call response, use the output to format an answer to the orginal user question.\n\nYou are a helpful assistant with tool calling capabilities.

user
\n\nGiven the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.\n\nRespond in the format {\"name\": function name, \"parameters\": dictionary of argument name and its value}. Do not use variables.\n\n{\"type\":\"function\",\"function\":{\"name\":\"create_table\",\"description\":\"Create a new table in the SQLite database\",\"parameters\":{\"type\":\"object\",\"required\":[\"query\"],\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"CREATE TABLE SQL statement\"}}}}}\n{\"type\":\"function\",\"function\":{\"name\":\"list_tables\",\"description\":\"List all tables in the SQLite database\",\"parameters\":{\"type\":\"object\",\"required\":null,\"properties\":{}}}}\n{\"type\":\"function\",\"function\":{\"name\":\"preparation_tool\",\"description\":\"Prepare the samples for analyze.  Mark samples as 'passed' or 'failed'. \",\"parameters\":{\"type\":\"object\",\"required\":[\"sample_list\"],\"properties\":{\"sample_list\":{\"type\":\"array\",\"description\":\"\"}}}}}\n\nProcess the following table of data with the preparation_tool.\n<table>\n<tr>\n  <th>Sample Name</th> <th>Input Mass</th>\n<tr>\n<tr>\n  <td> NA12878 </td> <td> 45 </td>\n</tr\n<tr>\n  <td> NCC1701 </td> <td> 77 </td>\n</tr\n<tr>\n  <td> NACL </td> <td> 12 </td>\n</tr\n<tr>\n  <td> NA12879 </td> <td> 32 </td>\n</tr\n</table>\n

assistant
\n\n"
```



Changed files:
filename: api/types.go
patch: @@ -163,6 +163,7 @@ func (t *ToolCallFunctionArguments) String() string {
 
 type Tool struct {
 	Type     string       `json:"type"`
+	Items    any          `json:"items,omitempty"`
 	Function ToolFunction `json:"function"`
 }
 
@@ -213,9 +214,12 @@ type ToolFunction struct {
 	Description string `json:"description"`
 	Parameters  struct {
 		Type       string   `json:"type"`
+		Defs       any      `json:"$defs,omitempty"`
+		Items      any      `json:"items,omitempty"`
 		Required   []string `json:"required"`
 		Properties map[string]struct {
 			Type        PropertyType `json:"type"`
+			Items       any          `json:"items,omitempty"`
 			Description string       `json:"description"`
 			Enum        []any        `json:"enum,omitempty"`
 		} `json:"properties"`

filename: openai/openai_test.go
patch: @@ -281,9 +281,12 @@ func TestChatMiddleware(t *testing.T) {
 							Description: "Get the current weather",
 							Parameters: struct {
 								Type       string   `json:"type"`
+								Defs       any      `json:"$defs,omitempty"`
+								Items      any      `json:"items,omitempty"`
 								Required   []string `json:"required"`
 								Properties map[string]struct {
 									Type        api.PropertyType `json:"type"`
+									Items       any              `json:"items,omitempty"`
 									Description string           `json:"description"`
 									Enum        []any            `json:"enum,omitempty"`
 								} `json:"properties"`
@@ -292,6 +295,7 @@ func TestChatMiddleware(t *testing.T) {
 								Required: []string{"location"},
 								Properties: map[string]struct {
 									Type        api.PropertyType `json:"type"`
+									Items       any              `json:"items,omitempty"`
 									Description string           `json:"description"`
 									Enum        []any            `json:"enum,omitempty"`
 								}{

filename: server/routes_generate_test.go
patch: @@ -370,9 +370,12 @@ func TestGenerateChat(t *testing.T) {
 					Description: "Get the current weather",
 					Parameters: struct {
 						Type       string   `json:"type"`
+						Defs       any      `json:"$defs,omitempty"`
+						Items      any      `json:"items,omitempty"`
 						Required   []string `json:"required"`
 						Properties map[string]struct {
 							Type        api.PropertyType `json:"type"`
+							Items       any              `json:"items,omitempty"`
 							Description string           `json:"description"`
 							Enum        []any            `json:"enum,omitempty"`
 						} `json:"properties"`
@@ -381,6 +384,7 @@ func TestGenerateChat(t *testing.T) {
 						Required: []string{"location"},
 						Properties: map[string]struct {
 							Type        api.PropertyType `json:"type"`
+							Items       any              `json:"items,omitempty"`
 							Description string           `json:"description"`
 							Enum        []any            `json:"enum,omitempty"`
 						}{
@@ -467,9 +471,12 @@ func TestGenerateChat(t *testing.T) {
 					Description: "Get the current weather",
 					Parameters: struct {
 						Type       string   `json:"type"`
+						Defs       any      `json:"$defs,omitempty"`
+						Items      any      `json:"items,omitempty"`
 						Required   []string `json:"required"`
 						Properties map[string]struct {
 							Type        api.PropertyType `json:"type"`
+							Items       any              `json:"items,omitempty"`
 							Description string           `json:"description"`
 							Enum        []any            `json:"enum,omitempty"`
 						} `json:"properties"`
@@ -478,6 +485,7 @@ func TestGenerateChat(t *testing.T) {
 						Required: []string{"location"},
 						Properties: map[string]struct {
 							Type        api.PropertyType `json:"type"`
+							Items       any              `json:"items,omitempty"`
 							Description string           `json:"description"`
 							Enum        []any            `json:"enum,omitempty"`
 						}{