Compare commits
9 Commits
main
...
brucemacd/
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b438a483ba | ||
![]() |
9bb5e3ee69 | ||
![]() |
2de832552a | ||
![]() |
32dd67957d | ||
![]() |
a5f2db3744 | ||
![]() |
68525466f2 | ||
![]() |
99ab9210ba | ||
![]() |
4d9568172d | ||
![]() |
00ba065e90 |
@ -18,7 +18,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -30,6 +29,28 @@ import (
|
|||||||
"github.com/ollama/ollama/version"
|
"github.com/ollama/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StatusError is an error with an HTTP status code and message,
|
||||||
|
// it is parsed on the client-side and not returned from the API
|
||||||
|
type StatusError struct {
|
||||||
|
StatusCode int // e.g. 200
|
||||||
|
Status string // e.g. "200 OK"
|
||||||
|
ErrorResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusError) Error() string {
|
||||||
|
switch {
|
||||||
|
case e.Status != "" && e.Err != "":
|
||||||
|
return fmt.Sprintf("%s: %s", e.Status, e.Err)
|
||||||
|
case e.Status != "":
|
||||||
|
return e.Status
|
||||||
|
case e.Err != "":
|
||||||
|
return e.Err
|
||||||
|
default:
|
||||||
|
// this should not happen
|
||||||
|
return "something went wrong, please see the ollama server logs for details"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Client encapsulates client state for interacting with the ollama
|
// Client encapsulates client state for interacting with the ollama
|
||||||
// service. Use [ClientFromEnvironment] to create new Clients.
|
// service. Use [ClientFromEnvironment] to create new Clients.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -47,7 +68,7 @@ func checkError(resp *http.Response, body []byte) error {
|
|||||||
err := json.Unmarshal(body, &apiError)
|
err := json.Unmarshal(body, &apiError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Use the full body as the message if we fail to decode a response.
|
// Use the full body as the message if we fail to decode a response.
|
||||||
apiError.ErrorMessage = string(body)
|
apiError.Err = string(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiError
|
return apiError
|
||||||
@ -132,7 +153,7 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData
|
|||||||
const maxBufferSize = 512 * format.KiloByte
|
const maxBufferSize = 512 * format.KiloByte
|
||||||
|
|
||||||
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
|
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
|
||||||
var buf *bytes.Buffer
|
var buf io.Reader
|
||||||
if data != nil {
|
if data != nil {
|
||||||
bts, err := json.Marshal(data)
|
bts, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -163,24 +184,22 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
|
|||||||
scanBuf := make([]byte, 0, maxBufferSize)
|
scanBuf := make([]byte, 0, maxBufferSize)
|
||||||
scanner.Buffer(scanBuf, maxBufferSize)
|
scanner.Buffer(scanBuf, maxBufferSize)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
var errorResponse struct {
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
bts := scanner.Bytes()
|
bts := scanner.Bytes()
|
||||||
|
|
||||||
|
var errorResponse ErrorResponse
|
||||||
if err := json.Unmarshal(bts, &errorResponse); err != nil {
|
if err := json.Unmarshal(bts, &errorResponse); err != nil {
|
||||||
return fmt.Errorf("unmarshal: %w", err)
|
return fmt.Errorf("unmarshal: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorResponse.Error != "" {
|
if errorResponse.Err != "" {
|
||||||
return errors.New(errorResponse.Error)
|
return errorResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode >= http.StatusBadRequest {
|
if response.StatusCode >= http.StatusBadRequest {
|
||||||
return StatusError{
|
return StatusError{
|
||||||
StatusCode: response.StatusCode,
|
StatusCode: response.StatusCode,
|
||||||
Status: response.Status,
|
Status: response.Status,
|
||||||
ErrorMessage: errorResponse.Error,
|
ErrorResponse: errorResponse,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,3 +49,270 @@ func TestClientFromEnvironment(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testError represents an internal error type for testing different error formats
|
||||||
|
type testError struct {
|
||||||
|
message string // basic error message
|
||||||
|
structured *ErrorResponse // structured error response, nil for basic format
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e testError) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientStream(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
responses []any
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic error format",
|
||||||
|
responses: []any{
|
||||||
|
testError{
|
||||||
|
message: "test error message",
|
||||||
|
statusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "test error message",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "structured error format",
|
||||||
|
responses: []any{
|
||||||
|
testError{
|
||||||
|
message: "test structured error",
|
||||||
|
structured: &ErrorResponse{
|
||||||
|
Err: "test structured error",
|
||||||
|
Hint: "test hint",
|
||||||
|
},
|
||||||
|
statusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "test structured error\ntest hint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error after chunks - basic format",
|
||||||
|
responses: []any{
|
||||||
|
ChatResponse{Message: Message{Content: "partial 1"}},
|
||||||
|
ChatResponse{Message: Message{Content: "partial 2"}},
|
||||||
|
testError{
|
||||||
|
message: "mid-stream basic error",
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "mid-stream basic error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error after chunks - structured format",
|
||||||
|
responses: []any{
|
||||||
|
ChatResponse{Message: Message{Content: "partial 1"}},
|
||||||
|
ChatResponse{Message: Message{Content: "partial 2"}},
|
||||||
|
testError{
|
||||||
|
message: "mid-stream structured error",
|
||||||
|
structured: &ErrorResponse{
|
||||||
|
Err: "mid-stream structured error",
|
||||||
|
Hint: "additional context",
|
||||||
|
},
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "mid-stream structured error\nadditional context",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "successful stream completion",
|
||||||
|
responses: []any{
|
||||||
|
ChatResponse{Message: Message{Content: "chunk 1"}},
|
||||||
|
ChatResponse{Message: Message{Content: "chunk 2"}},
|
||||||
|
ChatResponse{
|
||||||
|
Message: Message{Content: "final chunk"},
|
||||||
|
Done: true,
|
||||||
|
DoneReason: "stop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected http.Flusher")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/x-ndjson")
|
||||||
|
|
||||||
|
for _, resp := range tc.responses {
|
||||||
|
if errResp, ok := resp.(testError); ok {
|
||||||
|
w.WriteHeader(errResp.statusCode)
|
||||||
|
var err error
|
||||||
|
if errResp.structured != nil {
|
||||||
|
err = json.NewEncoder(w).Encode(errResp.structured)
|
||||||
|
} else {
|
||||||
|
err = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": errResp.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to encode error response:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %v", err)
|
||||||
|
}
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client := NewClient(&url.URL{Scheme: "http", Host: ts.Listener.Addr().String()}, http.DefaultClient)
|
||||||
|
|
||||||
|
var receivedChunks []ChatResponse
|
||||||
|
err := client.stream(context.Background(), http.MethodPost, "/v1/chat", nil, func(chunk []byte) error {
|
||||||
|
var resp ChatResponse
|
||||||
|
if err := json.Unmarshal(chunk, &resp); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal chunk: %w", err)
|
||||||
|
}
|
||||||
|
receivedChunks = append(receivedChunks, resp)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil, want error %q", tc.wantErr)
|
||||||
|
}
|
||||||
|
if err.Error() != tc.wantErr {
|
||||||
|
t.Errorf("error message mismatch: got %q, want %q", err.Error(), tc.wantErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got error %q, want nil", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientDo(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
response any
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic error format",
|
||||||
|
response: testError{
|
||||||
|
message: "test error message",
|
||||||
|
statusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
wantErr: "test error message",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "structured error format",
|
||||||
|
response: testError{
|
||||||
|
message: "test structured error",
|
||||||
|
structured: &ErrorResponse{
|
||||||
|
Err: "test structured error",
|
||||||
|
Hint: "test hint",
|
||||||
|
},
|
||||||
|
statusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
wantErr: "test structured error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error - basic format",
|
||||||
|
response: testError{
|
||||||
|
message: "internal error",
|
||||||
|
statusCode: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
wantErr: "internal error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server error - structured format",
|
||||||
|
response: testError{
|
||||||
|
message: "internal server error",
|
||||||
|
structured: &ErrorResponse{
|
||||||
|
Err: "internal server error",
|
||||||
|
Hint: "please try again later",
|
||||||
|
},
|
||||||
|
statusCode: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
wantErr: "internal server error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "successful response",
|
||||||
|
response: struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}{
|
||||||
|
ID: "msg_123",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if errResp, ok := tc.response.(testError); ok {
|
||||||
|
w.WriteHeader(errResp.statusCode)
|
||||||
|
var err error
|
||||||
|
if errResp.structured != nil {
|
||||||
|
err = json.NewEncoder(w).Encode(errResp.structured)
|
||||||
|
} else {
|
||||||
|
err = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": errResp.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to encode error response:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(tc.response); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %v", err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
client := NewClient(&url.URL{Scheme: "http", Host: ts.Listener.Addr().String()}, http.DefaultClient)
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
err := client.do(context.Background(), http.MethodPost, "/v1/messages", nil, &resp)
|
||||||
|
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got nil, want error %q", tc.wantErr)
|
||||||
|
}
|
||||||
|
if err.Error() != tc.wantErr {
|
||||||
|
t.Errorf("error message mismatch: got %q, want %q", err.Error(), tc.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("got error %q, want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedResp, ok := tc.response.(struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}); ok {
|
||||||
|
if resp.ID != expectedResp.ID {
|
||||||
|
t.Errorf("response ID mismatch: got %q, want %q", resp.ID, expectedResp.ID)
|
||||||
|
}
|
||||||
|
if resp.Success != expectedResp.Success {
|
||||||
|
t.Errorf("response Success mismatch: got %v, want %v", resp.Success, expectedResp.Success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
37
api/types.go
37
api/types.go
@ -12,27 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusError is an error with an HTTP status code and message.
|
|
||||||
type StatusError struct {
|
|
||||||
StatusCode int
|
|
||||||
Status string
|
|
||||||
ErrorMessage string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e StatusError) Error() string {
|
|
||||||
switch {
|
|
||||||
case e.Status != "" && e.ErrorMessage != "":
|
|
||||||
return fmt.Sprintf("%s: %s", e.Status, e.ErrorMessage)
|
|
||||||
case e.Status != "":
|
|
||||||
return e.Status
|
|
||||||
case e.ErrorMessage != "":
|
|
||||||
return e.ErrorMessage
|
|
||||||
default:
|
|
||||||
// this should not happen
|
|
||||||
return "something went wrong, please see the ollama server logs for details"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageData represents the raw binary data of an image file.
|
// ImageData represents the raw binary data of an image file.
|
||||||
type ImageData []byte
|
type ImageData []byte
|
||||||
|
|
||||||
@ -661,6 +640,22 @@ func (d *Duration) UnmarshalJSON(b []byte) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorResponse implements a structured error interface that is returned from the Ollama server
|
||||||
|
type ErrorResponse struct {
|
||||||
|
// Err is the error from the server. It helps with debugging the code-path
|
||||||
|
Err string `json:"error"`
|
||||||
|
|
||||||
|
// Hint is a user-friendly message about what went wrong, with suggested troubleshooting
|
||||||
|
Hint string `json:"hint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorResponse) Error() string {
|
||||||
|
if e.Hint == "" {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s\n%s", e.Err, e.Hint)
|
||||||
|
}
|
||||||
|
|
||||||
// FormatParams converts specified parameter options to their correct types
|
// FormatParams converts specified parameter options to their correct types
|
||||||
func FormatParams(params map[string][]string) (map[string]interface{}, error) {
|
func FormatParams(params map[string][]string) (map[string]interface{}, error) {
|
||||||
opts := Options{}
|
opts := Options{}
|
||||||
|
@ -610,14 +610,14 @@ type EmbedWriter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *BaseWriter) writeError(data []byte) (int, error) {
|
func (w *BaseWriter) writeError(data []byte) (int, error) {
|
||||||
var serr api.StatusError
|
var er api.ErrorResponse // error response is used here to parse the error message
|
||||||
err := json.Unmarshal(data, &serr)
|
err := json.Unmarshal(data, &er)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.ResponseWriter.Header().Set("Content-Type", "application/json")
|
w.ResponseWriter.Header().Set("Content-Type", "application/json")
|
||||||
err = json.NewEncoder(w.ResponseWriter).Encode(NewError(http.StatusInternalServerError, serr.Error()))
|
err = json.NewEncoder(w.ResponseWriter).Encode(NewError(http.StatusInternalServerError, er.Err))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -550,7 +550,7 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
|
|||||||
|
|
||||||
manifest, err = pullModelManifest(ctx, mp, regOpts)
|
manifest, err = pullModelManifest(ctx, mp, regOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("pull model manifest: %s", err)
|
return fmt.Errorf("pull model manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var layers []Layer
|
var layers []Layer
|
||||||
@ -629,13 +629,18 @@ func PullModel(ctx context.Context, name string, regOpts *registryOptions, fn fu
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrRemoteModelNotFound = errors.New("model not found")
|
||||||
|
|
||||||
func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*Manifest, error) {
|
func pullModelManifest(ctx context.Context, mp ModelPath, regOpts *registryOptions) (*Manifest, error) {
|
||||||
requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag)
|
requestURL := mp.BaseURL().JoinPath("v2", mp.GetNamespaceRepository(), "manifests", mp.Tag)
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
headers.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
headers.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||||
resp, err := makeRequestWithRetry(ctx, http.MethodGet, requestURL, headers, nil, regOpts)
|
resp, err := makeRequestWithRetry(ctx, http.MethodGet, requestURL, headers, nil, regOpts)
|
||||||
if err != nil {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// The model was not found on the remote registry
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrRemoteModelNotFound, err)
|
||||||
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
@ -564,7 +564,8 @@ func (s *Server) PullHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := model.ParseName(cmp.Or(req.Model, req.Name))
|
reqName := cmp.Or(req.Model, req.Name)
|
||||||
|
name := model.ParseName(reqName)
|
||||||
if !name.IsValid() {
|
if !name.IsValid() {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errtypes.InvalidModelNameErrMsg})
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": errtypes.InvalidModelNameErrMsg})
|
||||||
return
|
return
|
||||||
@ -591,7 +592,18 @@ func (s *Server) PullHandler(c *gin.Context) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := PullModel(ctx, name.DisplayShortest(), regOpts, fn); err != nil {
|
if err := PullModel(ctx, name.DisplayShortest(), regOpts, fn); err != nil {
|
||||||
ch <- gin.H{"error": err.Error()}
|
if errors.Is(err, ErrRemoteModelNotFound) {
|
||||||
|
hint := fmt.Sprintf("Model %q not found - please check the model name is correct and try again", reqName)
|
||||||
|
if name.Host == DefaultRegistry {
|
||||||
|
hint = fmt.Sprintf("Model %q not found - search available models at: https://ollama.com/search?q=%s", reqName, reqName)
|
||||||
|
}
|
||||||
|
ch <- api.ErrorResponse{
|
||||||
|
Err: err.Error(),
|
||||||
|
Hint: hint,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ch <- gin.H{"error": err.Error()}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user