diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f040b9fd..c7028e00 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ see if the change were accepted. The title should look like: - : + : The package is the most affected Go package. If the change does not affect Go code, then use the directory name instead. Changes to a single well-known diff --git a/README.md b/README.md index d7b93082..8cb71fcd 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ See the [API documentation](./docs/api.md) for all endpoints. - [PartCAD](https://github.com/openvmp/partcad/) (CAD model generation with OpenSCAD and CadQuery) - [Ollama4j Web UI](https://github.com/ollama4j/ollama4j-web-ui) - Java-based Web UI for Ollama built with Vaadin, Spring Boot and Ollama4j - [PyOllaMx](https://github.com/kspviswa/pyOllaMx) - macOS application capable of chatting with both Ollama and Apple MLX models. -- [Claude Dev](https://github.com/saoudrizwan/claude-dev) - VSCode extension for multi-file/whole-repo coding +- [Cline](https://github.com/cline/cline) - Formerly known as Claude Dev is a VSCode extension for multi-file/whole-repo coding - [Cherry Studio](https://github.com/kangfenmao/cherry-studio) (Desktop client with Ollama support) - [ConfiChat](https://github.com/1runeberg/confichat) (Lightweight, standalone, multi-platform, and privacy focused LLM chat interface with optional encryption) - [Archyve](https://github.com/nickthecook/archyve) (RAG-enabling document library) @@ -462,6 +462,7 @@ See the [API documentation](./docs/api.md) for all endpoints. - [DeepShell](https://github.com/Abyss-c0re/deepshell) Your self-hosted AI assistant. Interactive Shell, Files and Folders analysis. - [orbiton](https://github.com/xyproto/orbiton) Configuration-free text editor and IDE with support for tab completion with Ollama. - [orca-cli](https://github.com/molbal/orca-cli) Ollama Registry CLI Application - Browse, pull and download models from Ollama Registry in your terminal. +- [GGUF-to-Ollama](https://github.com/jonathanhecl/gguf-to-ollama) - Importing GGUF to Ollama made easy (multiplatform) ### Apple Vision Pro diff --git a/api/types.go b/api/types.go index a70fb120..0e63ef8b 100644 --- a/api/types.go +++ b/api/types.go @@ -166,6 +166,48 @@ type Tool struct { Function ToolFunction `json:"function"` } +// PropertyType can be either a string or an array of strings +type PropertyType []string + +// UnmarshalJSON implements the json.Unmarshaler interface +func (pt *PropertyType) UnmarshalJSON(data []byte) error { + // Try to unmarshal as a string first + var s string + if err := json.Unmarshal(data, &s); err == nil { + *pt = []string{s} + return nil + } + + // If that fails, try to unmarshal as an array of strings + var a []string + if err := json.Unmarshal(data, &a); err != nil { + return err + } + *pt = a + return nil +} + +// MarshalJSON implements the json.Marshaler interface +func (pt PropertyType) MarshalJSON() ([]byte, error) { + if len(pt) == 1 { + // If there's only one type, marshal as a string + return json.Marshal(pt[0]) + } + // Otherwise marshal as an array + return json.Marshal([]string(pt)) +} + +// String returns a string representation of the PropertyType +func (pt PropertyType) String() string { + if len(pt) == 0 { + return "" + } + if len(pt) == 1 { + return pt[0] + } + return fmt.Sprintf("%v", []string(pt)) +} + type ToolFunction struct { Name string `json:"name"` Description string `json:"description"` @@ -173,9 +215,9 @@ type ToolFunction struct { Type string `json:"type"` Required []string `json:"required"` Properties map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` } `json:"properties"` } `json:"parameters"` } diff --git a/api/types_test.go b/api/types_test.go index b28d4249..e22c047f 100644 --- a/api/types_test.go +++ b/api/types_test.go @@ -231,3 +231,83 @@ func TestMessage_UnmarshalJSON(t *testing.T) { } } } + +func TestPropertyType_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected PropertyType + }{ + { + name: "string type", + input: `"string"`, + expected: PropertyType{"string"}, + }, + { + name: "array of types", + input: `["string", "number"]`, + expected: PropertyType{"string", "number"}, + }, + { + name: "array with single type", + input: `["string"]`, + expected: PropertyType{"string"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var pt PropertyType + if err := json.Unmarshal([]byte(test.input), &pt); err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(pt) != len(test.expected) { + t.Errorf("Length mismatch: got %v, expected %v", len(pt), len(test.expected)) + } + + for i, v := range pt { + if v != test.expected[i] { + t.Errorf("Value mismatch at index %d: got %v, expected %v", i, v, test.expected[i]) + } + } + }) + } +} + +func TestPropertyType_MarshalJSON(t *testing.T) { + tests := []struct { + name string + input PropertyType + expected string + }{ + { + name: "single type", + input: PropertyType{"string"}, + expected: `"string"`, + }, + { + name: "multiple types", + input: PropertyType{"string", "number"}, + expected: `["string","number"]`, + }, + { + name: "empty type", + input: PropertyType{}, + expected: `[]`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data, err := json.Marshal(test.input) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if string(data) != test.expected { + t.Errorf("Marshaled data mismatch: got %v, expected %v", string(data), test.expected) + } + }) + } +} diff --git a/ml/backend/ggml/ggml.go b/ml/backend/ggml/ggml.go index a106fed5..7e81e995 100644 --- a/ml/backend/ggml/ggml.go +++ b/ml/backend/ggml/ggml.go @@ -319,7 +319,14 @@ func New(ctx context.Context, r *os.File, params ml.BackendParams) (ml.Backend, tts[i] = tt } - sr := io.NewSectionReader(r, int64(meta.Tensors().Offset+t.Offset), int64(t.Size())) + // Create a new FD for each goroutine so that each FD is read sequentially, rather than + // seeking around within an FD shared between all goroutines. + file, err := os.Open(r.Name()) + if err != nil { + return err + } + defer file.Close() + sr := io.NewSectionReader(file, int64(meta.Tensors().Offset+t.Offset), int64(t.Size())) bts := make([]byte, 128*format.KibiByte) var s uint64 diff --git a/openai/openai_test.go b/openai/openai_test.go index a6acfcac..6039cb65 100644 --- a/openai/openai_test.go +++ b/openai/openai_test.go @@ -283,24 +283,24 @@ func TestChatMiddleware(t *testing.T) { Type string `json:"type"` Required []string `json:"required"` Properties map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` } `json:"properties"` }{ Type: "object", Required: []string{"location"}, Properties: map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` }{ "location": { - Type: "string", + Type: api.PropertyType{"string"}, Description: "The city and state", }, "unit": { - Type: "string", + Type: api.PropertyType{"string"}, Enum: []string{"celsius", "fahrenheit"}, }, }, diff --git a/parser/parser.go b/parser/parser.go index 9a98c8ea..a14ac5ff 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -11,10 +11,13 @@ import ( "os" "os/user" "path/filepath" + "runtime" "slices" "strconv" "strings" + "sync" + "golang.org/x/sync/errgroup" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -144,12 +147,25 @@ func fileDigestMap(path string) (map[string]string, error) { files = []string{path} } + var mu sync.Mutex + var g errgroup.Group + g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1)) for _, f := range files { - digest, err := digestForFile(f) - if err != nil { - return nil, err - } - fl[f] = digest + g.Go(func() error { + digest, err := digestForFile(f) + if err != nil { + return err + } + + mu.Lock() + defer mu.Unlock() + fl[f] = digest + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err } return fl, nil diff --git a/server/routes_generate_test.go b/server/routes_generate_test.go index f219387c..2613978a 100644 --- a/server/routes_generate_test.go +++ b/server/routes_generate_test.go @@ -372,24 +372,24 @@ func TestGenerateChat(t *testing.T) { Type string `json:"type"` Required []string `json:"required"` Properties map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` } `json:"properties"` }{ Type: "object", Required: []string{"location"}, Properties: map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` }{ "location": { - Type: "string", + Type: api.PropertyType{"string"}, Description: "The city and state", }, "unit": { - Type: "string", + Type: api.PropertyType{"string"}, Enum: []string{"celsius", "fahrenheit"}, }, }, @@ -469,24 +469,24 @@ func TestGenerateChat(t *testing.T) { Type string `json:"type"` Required []string `json:"required"` Properties map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` } `json:"properties"` }{ Type: "object", Required: []string{"location"}, Properties: map[string]struct { - Type string `json:"type"` - Description string `json:"description"` - Enum []string `json:"enum,omitempty"` + Type api.PropertyType `json:"type"` + Description string `json:"description"` + Enum []string `json:"enum,omitempty"` }{ "location": { - Type: "string", + Type: api.PropertyType{"string"}, Description: "The city and state", }, "unit": { - Type: "string", + Type: api.PropertyType{"string"}, Enum: []string{"celsius", "fahrenheit"}, }, },