diff --git a/cmd/cmd.go b/cmd/cmd.go
index 7955012c8..1d1d116ba 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -1137,6 +1137,14 @@ func chat(cmd *cobra.Command, opts runOptions) (*api.Message, error) {
if errors.Is(err, context.Canceled) {
return nil, nil
}
+
+ // this error should ideally be wrapped properly by the client
+ if strings.Contains(err.Error(), "upstream error") {
+ p.StopAndClear()
+ fmt.Println("An error occurred while processing your message. Please try again.")
+ fmt.Println()
+ return nil, nil
+ }
return nil, err
}
diff --git a/cmd/interactive.go b/cmd/interactive.go
index a285b365c..08ab4947b 100644
--- a/cmd/interactive.go
+++ b/cmd/interactive.go
@@ -385,18 +385,21 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
case "modelfile":
fmt.Println(resp.Modelfile)
case "parameters":
+ fmt.Println("Model defined parameters:")
if resp.Parameters == "" {
- fmt.Println("No parameters were specified for this model.")
+ fmt.Println(" No additional parameters were specified for this model.")
} else {
- if len(opts.Options) > 0 {
- fmt.Println("User defined parameters:")
- for k, v := range opts.Options {
- fmt.Printf("%-*s %v\n", 30, k, v)
- }
- fmt.Println()
+ for _, l := range strings.Split(resp.Parameters, "\n") {
+ fmt.Printf(" %s\n", l)
}
- fmt.Println("Model defined parameters:")
- fmt.Println(resp.Parameters)
+ }
+ fmt.Println()
+ if len(opts.Options) > 0 {
+ fmt.Println("User defined parameters:")
+ for k, v := range opts.Options {
+ fmt.Printf(" %-*s %v\n", 30, k, v)
+ }
+ fmt.Println()
}
case "system":
switch {
diff --git a/convert/convert_test.go b/convert/convert_test.go
index 105fbb3d3..95cccd56e 100644
--- a/convert/convert_test.go
+++ b/convert/convert_test.go
@@ -11,14 +11,13 @@ import (
"io"
"io/fs"
"log/slog"
+ "maps"
"os"
"path/filepath"
"slices"
"strings"
"testing"
- "golang.org/x/exp/maps"
-
"github.com/ollama/ollama/fs/ggml"
)
@@ -137,9 +136,7 @@ func TestConvertModel(t *testing.T) {
t.Fatal(err)
}
- keys := maps.Keys(expect)
- slices.Sort(keys)
- for _, k := range keys {
+ for _, k := range slices.Sorted(maps.Keys(expect)) {
if v, ok := actual[k]; !ok {
t.Errorf("missing %s", k)
} else if v != expect[k] {
@@ -343,9 +340,7 @@ func TestConvertAdapter(t *testing.T) {
actual := generateResultsJSON(t, r, m.KV(), m.Tensors())
- keys := maps.Keys(c.Expected)
- slices.Sort(keys)
- for _, k := range keys {
+ for _, k := range slices.Sorted(maps.Keys(c.Expected)) {
if v, ok := actual[k]; !ok {
t.Errorf("missing %s", k)
} else if v != c.Expected[k] {
diff --git a/convert/reader_safetensors.go b/convert/reader_safetensors.go
index f58585321..f182a656c 100644
--- a/convert/reader_safetensors.go
+++ b/convert/reader_safetensors.go
@@ -8,12 +8,12 @@ import (
"fmt"
"io"
"io/fs"
+ "maps"
"slices"
"strings"
"github.com/d4l3k/go-bfloat16"
"github.com/x448/float16"
- "golang.org/x/exp/maps"
)
type safetensorMetadata struct {
@@ -46,8 +46,7 @@ func parseSafetensors(fsys fs.FS, replacer *strings.Replacer, ps ...string) ([]T
return nil, err
}
- keys := maps.Keys(headers)
- slices.Sort(keys)
+ keys := slices.Sorted(maps.Keys(headers))
names := make(map[string]struct{}, len(keys))
diff --git a/convert/tokenizer.go b/convert/tokenizer.go
index bedcd4f80..41d0310a0 100644
--- a/convert/tokenizer.go
+++ b/convert/tokenizer.go
@@ -8,11 +8,10 @@ import (
"fmt"
"io/fs"
"log/slog"
+ "maps"
"os"
"slices"
"strings"
-
- "golang.org/x/exp/maps"
)
const (
@@ -260,11 +259,8 @@ func parseVocabularyFromTokenizer(fsys fs.FS) (*Vocabulary, error) {
tokens[token.ID] = token
}
- keys := maps.Keys(tokens)
- slices.Sort(keys)
-
v := Vocabulary{Model: "gpt2"}
- for _, k := range keys {
+ for _, k := range slices.Sorted(maps.Keys(tokens)) {
token := tokens[k]
v.Tokens = append(v.Tokens, token.Content)
v.Scores = append(v.Scores, float32(token.ID))
diff --git a/docs/linux.md b/docs/linux.md
index 72a5ff019..0c19ef0b4 100644
--- a/docs/linux.md
+++ b/docs/linux.md
@@ -16,7 +16,7 @@ curl -fsSL https://ollama.com/install.sh | sh
Download and extract the package:
```shell
-curl -L https://ollama.com/download/ollama-linux-amd64.tgz -o ollama-linux-amd64.tgz
+curl -LO https://ollama.com/download/ollama-linux-amd64.tgz
sudo tar -C /usr -xzf ollama-linux-amd64.tgz
```
diff --git a/go.mod b/go.mod
index ec3f61bba..46e7f433f 100644
--- a/go.mod
+++ b/go.mod
@@ -71,7 +71,7 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.36.0
- golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
+ golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0
golang.org/x/term v0.30.0
diff --git a/server/routes.go b/server/routes.go
index 603cd42a2..40348e737 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -842,8 +842,11 @@ func GetModelInfo(req api.ShowRequest) (*api.ShowResponse, error) {
}
resp.Parameters = strings.Join(params, "\n")
- for k, v := range req.Options {
- if _, ok := req.Options[k]; ok {
+ if len(req.Options) > 0 {
+ if m.Options == nil {
+ m.Options = make(map[string]any)
+ }
+ for k, v := range req.Options {
m.Options[k] = v
}
}
diff --git a/server/routes_test.go b/server/routes_test.go
index 7c44bc957..87b526633 100644
--- a/server/routes_test.go
+++ b/server/routes_test.go
@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "slices"
"sort"
"strings"
"testing"
@@ -82,19 +83,6 @@ func createTestFile(t *testing.T, name string) (string, string) {
return f.Name(), digest
}
-// equalStringSlices checks if two slices of strings are equal.
-func equalStringSlices(a, b []string) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
-}
-
type panicTransport struct{}
func (t *panicTransport) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -447,7 +435,7 @@ func TestRoutes(t *testing.T) {
"stop \"foo\"",
"top_p 0.9",
}
- if !equalStringSlices(params, expectedParams) {
+ if !slices.Equal(params, expectedParams) {
t.Errorf("expected parameters %v, got %v", expectedParams, params)
}
paramCount, ok := showResp.ModelInfo["general.parameter_count"].(float64)
diff --git a/template/template.go b/template/template.go
index 242708f16..d28ace413 100644
--- a/template/template.go
+++ b/template/template.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"io"
+ "maps"
"math"
"slices"
"strings"
@@ -14,7 +15,6 @@ import (
"text/template/parse"
"github.com/agnivade/levenshtein"
- "golang.org/x/exp/maps"
"github.com/ollama/ollama/api"
)
@@ -157,9 +157,7 @@ func (t *Template) Vars() []string {
set[strings.ToLower(n)] = struct{}{}
}
- vars = maps.Keys(set)
- slices.Sort(vars)
- return vars
+ return slices.Sorted(maps.Keys(set))
}
type Values struct {
diff --git a/tools/tools.go b/tools/tools.go
index c149885f6..f473ab6a6 100644
--- a/tools/tools.go
+++ b/tools/tools.go
@@ -120,16 +120,14 @@ func (p *Parser) parseToolCall() *api.ToolCall {
return nil
}
- // only look for arguments after the tool name if the tool has parameters
- // TODO (jmorganca): while probably uncommon, this doesn't support
- // parsing arguments before the tool name, which may be needed in the future
- args := map[string]any{}
- if len(tool.Function.Parameters.Properties) > 0 {
- var i int
- if args, i = findArguments(*tool, p.buffer[end:]); args == nil {
- return nil
+ var args map[string]any
+ if found, i := findArguments(p.buffer); found == nil {
+ return nil
+ } else {
+ args = found
+ if i > end {
+ end = i
}
- end += i
}
tc := &api.ToolCall{
@@ -217,93 +215,70 @@ func findTool(tools []api.Tool, buf []byte) (*api.Tool, int) {
// objects for functions that have all-optional parameters
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
// `{"name": "get_conditions"}` will not currently work
-func findArguments(tool api.Tool, buffer []byte) (map[string]any, int) {
+func findArguments(buffer []byte) (map[string]any, int) {
if len(buffer) == 0 {
return nil, 0
}
var braces int
var start int = -1
- var end int
- var object []byte
- // find any outer json object
for i, c := range buffer {
if c == '{' {
- braces++
- if start == -1 {
+ if braces == 0 {
start = i
}
- }
+ braces++
+ } else if c == '}' && braces > 0 {
+ braces--
+ if braces == 0 && start != -1 {
+ object := buffer[start : i+1]
- if c == '}' {
- if start != -1 {
- braces--
- if braces == 0 {
- end = i + 1
- object = buffer[start:end]
- break
+ var data map[string]any
+ if err := json.Unmarshal(object, &data); err != nil {
+ start = -1
+ continue
}
- }
- }
- }
- if braces > 0 {
- return nil, 0
- }
-
- var data map[string]any
- if err := json.Unmarshal(object, &data); err != nil {
- return nil, 0
- }
-
- var find func(obj any) map[string]any
- find = func(obj any) map[string]any {
- switch obj := obj.(type) {
- case map[string]any:
- valid := true
- // check if all keys in the object exist in the tool's parameters
- for key := range obj {
- if _, exists := tool.Function.Parameters.Properties[key]; !exists {
- valid = false
- break
- }
- }
-
- // check for required parameters
- // TODO (jmorganca): this should error instead of silently failing
- if valid {
- for _, required := range tool.Function.Parameters.Required {
- if _, exists := obj[required]; !exists {
- valid = false
- break
+ var findObject func(obj map[string]any) (map[string]any, bool)
+ findObject = func(obj map[string]any) (map[string]any, bool) {
+ if _, hasName := obj["name"]; hasName {
+ if args, ok := obj["arguments"].(map[string]any); ok {
+ return args, true
+ }
+ if args, ok := obj["parameters"].(map[string]any); ok {
+ return args, true
+ }
+ return nil, true
}
- }
- }
- if valid {
- return obj
- }
+ for _, v := range obj {
+ switch child := v.(type) {
+ case map[string]any:
+ if result, found := findObject(child); found {
+ return result, true
+ }
+ case []any:
+ for _, item := range child {
+ if childObj, ok := item.(map[string]any); ok {
+ if result, found := findObject(childObj); found {
+ return result, true
+ }
+ }
+ }
+ }
+ }
- for _, value := range obj {
- if result := find(value); result != nil {
- return result
+ return nil, false
}
- }
- case []any:
- for _, item := range obj {
- if result := find(item); result != nil {
- return result
+
+ if args, found := findObject(data); found {
+ return args, i
}
+
+ return data, i
}
}
-
- return nil
- }
-
- result := find(data)
- if result != nil {
- return result, end
}
return nil, 0
diff --git a/tools/tools_test.go b/tools/tools_test.go
index 092ae3233..a0f7b6b00 100644
--- a/tools/tools_test.go
+++ b/tools/tools_test.go
@@ -227,13 +227,6 @@ func TestParser(t *testing.T) {
},
},
},
- {
- name: "invalid arguments",
- inputs: []string{`{"name": "get_conditions", "arguments": {"city": "San Francisco"}}`},
- content: "",
- tmpl: qwen,
- calls: nil,
- },
{
name: "empty args",
inputs: []string{`{"name": "get_conditions", "arguments": {}}`},
@@ -249,13 +242,6 @@ func TestParser(t *testing.T) {
},
},
},
- {
- name: "missing required args",
- inputs: []string{`{"name": "get_temperature", "arguments": {}}`},
- content: "",
- tmpl: qwen,
- calls: nil,
- },
{
name: "text before tool call",
inputs: []string{`Let me check the weather. {"name": "get_temperature", "arguments": {"city": "New York"}}`},
@@ -273,21 +259,6 @@ func TestParser(t *testing.T) {
},
},
},
- {
- name: "qwen no args tool call",
- inputs: []string{`Let me say hello to the user. I'll use the say_hello tool {"name": "say_hello"}`},
- content: "Let me say hello to the user. I'll use the say_hello tool ",
- tmpl: qwen,
- calls: []api.ToolCall{
- {
- Function: api.ToolCallFunction{
- Index: 0,
- Name: "say_hello",
- Arguments: api.ToolCallFunctionArguments{},
- },
- },
- },
- },
{
name: "qwen no args with text",
inputs: []string{"Let me say hello to the user. I'll use the say_hello tool. "},
@@ -521,52 +492,6 @@ func TestParser(t *testing.T) {
content: "for { fmt.Println(\"hello\") }",
tmpl: json,
},
- {
- name: "json no args tool call",
- inputs: []string{
- "{\"name\": \"say_hello\"}",
- },
- content: "",
- tmpl: json,
- calls: []api.ToolCall{
- {
- Function: api.ToolCallFunction{
- Index: 0,
- Name: "say_hello",
- Arguments: api.ToolCallFunctionArguments{},
- },
- },
- },
- },
- {
- name: "json no args no tool call",
- inputs: []string{
- "I'll use the say_hello tool to say hello to the user.",
- },
- content: "I'll use the say_hello tool to say hello to the user.",
- tmpl: json,
- calls: nil,
- },
-
- // TODO (jmorganca): this is a false positive, we should
- // not be parsing this as a tool call
- {
- name: "json no args false positive",
- inputs: []string{
- `{say_hello!!!}`,
- },
- content: "",
- tmpl: json,
- calls: []api.ToolCall{
- {
- Function: api.ToolCallFunction{
- Index: 0,
- Name: "say_hello",
- Arguments: api.ToolCallFunctionArguments{},
- },
- },
- },
- },
{
name: "list multiple",
inputs: []string{
@@ -684,26 +609,6 @@ func TestParser(t *testing.T) {
tmpl: list,
calls: nil,
},
- {
- name: "list with no arguments",
- inputs: []string{
- "[",
- "{",
- "\"name\": \"say_hello\"",
- "}",
- },
- content: "",
- tmpl: list,
- calls: []api.ToolCall{
- {
- Function: api.ToolCallFunction{
- Index: 0,
- Name: "say_hello",
- Arguments: api.ToolCallFunctionArguments{},
- },
- },
- },
- },
{
name: "tool name with collision",
inputs: []string{
@@ -711,7 +616,7 @@ func TestParser(t *testing.T) {
"{",
"\"name\": \"say_hello",
"_world\",",
- "}",
+ "\"arguments\": {}}",
"}",
},
content: "",
@@ -733,13 +638,13 @@ func TestParser(t *testing.T) {
"{",
"\"name\": \"say_hello",
"_world\",",
- "}",
+ "\"arguments\": {}}",
"",
"",
"{",
"\"name\": \"say_hello",
"\",",
- "}",
+ "\"arguments\": {}}",
"",
},
content: "",
@@ -773,7 +678,7 @@ func TestParser(t *testing.T) {
{
name: "tool name with collision non streaming multiple",
inputs: []string{
- `{"name": "say_hello"}{"name": "say_hello_world"}`,
+ `{"name": "say_hello", "arguments": {}}{"name": "say_hello_world", "arguments": {}}`,
},
content: "",
tmpl: qwen,
@@ -797,7 +702,7 @@ func TestParser(t *testing.T) {
{
name: "tool name with collision non streaming shorter",
inputs: []string{
- `{"name": "say_hello"}`,
+ `{"name": "say_hello", "arguments": {}}`,
},
content: "",
tmpl: qwen,
@@ -814,7 +719,7 @@ func TestParser(t *testing.T) {
{
name: "tool name with collision non streaming longer",
inputs: []string{
- `{"name": "say_hello_world"}`,
+ `{"name": "say_hello_world", "arguments": {}}`,
},
content: "",
tmpl: qwen,
@@ -871,6 +776,26 @@ func TestParser(t *testing.T) {
},
},
},
+ {
+ name: "args before name",
+ inputs: []string{
+ `{"arguments": {"a": "5", "b": "10"}, "name": "add"}`,
+ },
+ content: "",
+ tmpl: qwen,
+ calls: []api.ToolCall{
+ {
+ Function: api.ToolCallFunction{
+ Index: 0,
+ Name: "add",
+ Arguments: api.ToolCallFunctionArguments{
+ "a": "5",
+ "b": "10",
+ },
+ },
+ },
+ },
+ },
}
for _, tt := range tests {
@@ -1167,75 +1092,25 @@ func TestFindTag(t *testing.T) {
}
func TestFindArguments(t *testing.T) {
- tool := api.Tool{
- Type: "function",
- Function: api.ToolFunction{
- Name: "get_temperature",
- Description: "Retrieve the temperature for a given location",
- 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"`
- }{
- Type: "object",
- Properties: map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- }{
- "format": {
- Type: api.PropertyType{"string"},
- Description: "The format to return the temperature in",
- Enum: []any{"fahrenheit", "celsius"},
- },
- "location": {
- Type: api.PropertyType{"string"},
- Description: "The location to get the temperature for",
- },
- },
- },
- },
- }
-
- tool2 := api.Tool{
- Type: "function",
- Function: api.ToolFunction{
- Name: "say_hello",
- Description: "Say hello to the user",
- },
- }
-
tests := []struct {
name string
buffer []byte
want map[string]any
- tool api.Tool
}{
{
name: "empty string",
buffer: []byte{},
want: nil,
- tool: tool,
},
{
name: "whitespace only",
buffer: []byte(" \n\t "),
want: nil,
- tool: tool,
},
{
name: "unbalanced braces - missing closing",
buffer: []byte(`{"format": "fahrenheit", "location": "San Francisco"`),
want: nil,
- tool: tool,
},
{
name: "unbalanced braces - extra closing",
@@ -1243,13 +1118,11 @@ func TestFindArguments(t *testing.T) {
want: map[string]any{
"format": "fahrenheit",
},
- tool: tool,
},
{
name: "invalid JSON",
buffer: []byte(`{format: fahrenheit, location: "San Francisco"}`),
want: nil,
- tool: tool,
},
{
name: "valid json",
@@ -1258,7 +1131,6 @@ func TestFindArguments(t *testing.T) {
"format": "fahrenheit",
"location": "San Francisco, CA",
},
- tool: tool,
},
{
name: "valid arguments with special tokens",
@@ -1267,16 +1139,14 @@ func TestFindArguments(t *testing.T) {
"format": "fahrenheit",
"location": "San Francisco, CA",
},
- tool: tool,
},
{
name: "valid arguments in array",
- buffer: []byte(`[{"arguments": {"format": "fahrenheit", "location": "San Francisco, CA"}}`),
+ buffer: []byte(`[{"name": "get_temperature", "arguments": {"format": "fahrenheit", "location": "San Francisco, CA"}}`),
want: map[string]any{
"format": "fahrenheit",
"location": "San Francisco, CA",
},
- tool: tool,
},
{
name: "nested deep",
@@ -1285,7 +1155,6 @@ func TestFindArguments(t *testing.T) {
"format": "fahrenheit",
"location": "San Francisco, CA",
},
- tool: tool,
},
{
name: "one arg",
@@ -1293,7 +1162,6 @@ func TestFindArguments(t *testing.T) {
want: map[string]any{
"location": "San Francisco, CA",
},
- tool: tool,
},
{
name: "two args",
@@ -1302,13 +1170,6 @@ func TestFindArguments(t *testing.T) {
"location": "San Francisco, CA",
"format": "fahrenheit",
},
- tool: tool,
- },
- {
- name: "no args",
- buffer: []byte(`{"name": "say_hello"}`),
- want: nil,
- tool: tool2,
},
{
name: "deepseek",
@@ -1316,7 +1177,6 @@ func TestFindArguments(t *testing.T) {
want: map[string]any{
"location": "Tokyo",
},
- tool: tool,
},
{
name: "deepseek",
@@ -1324,13 +1184,12 @@ func TestFindArguments(t *testing.T) {
want: map[string]any{
"location": "Tokyo",
},
- tool: tool,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got, _ := findArguments(tt.tool, tt.buffer)
+ got, _ := findArguments(tt.buffer)
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("scanArguments() args mismatch (-got +want):\n%s", diff)