diff --git a/client/ollama/ollama.go b/client/ollama/ollama.go index 2c89c4cd..45b82126 100644 --- a/client/ollama/ollama.go +++ b/client/ollama/ollama.go @@ -1,14 +1,18 @@ package ollama import ( + "bytes" "cmp" "context" + "encoding/json" + "io" "io/fs" "iter" + "net/http" "os" + "strings" "bllamo.com/client/ollama/apitype" - "bllamo.com/oweb" "bllamo.com/types/empty" ) @@ -41,7 +45,7 @@ func (c *Client) Build(ctx context.Context, ref string, modelfile []byte, source // Push requests the remote Ollama service to push a model to the server. func (c *Client) Push(ctx context.Context, ref string) error { - _, err := oweb.Do[empty.Message](ctx, "POST", c.BaseURL+"/v1/push", apitype.PushRequest{Name: ref}) + _, err := Do[empty.Message](ctx, "POST", c.BaseURL+"/v1/push", apitype.PushRequest{Name: ref}) return err } @@ -68,3 +72,73 @@ func (c *Client) Copy(ctx context.Context, dstRef, srcRef string) error { func (c *Client) Run(ctx context.Context, ref string, messages []apitype.Message) error { panic("not implemented") } + +type Error struct { + Status int `json:"-"` + Code string `json:"code"` + Message string `json:"message"` + Field string `json:"field,omitempty"` + RawBody []byte `json:"-"` +} + +func (e *Error) Error() string { + var b strings.Builder + b.WriteString("ollama: ") + b.WriteString(e.Code) + if e.Message != "" { + b.WriteString(": ") + b.WriteString(e.Message) + } + return b.String() +} + +func Do[Res any](ctx context.Context, method, urlStr string, in any) (*Res, error) { + var body bytes.Buffer + // TODO(bmizerany): pool and reuse this buffer AND the encoder + if err := encodeJSON(&body, in); err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, urlStr, &body) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode/100 != 2 { + var b bytes.Buffer + body := io.TeeReader(res.Body, &b) + e, err := decodeJSON[Error](body) + if err != nil { + return nil, err + } + e.RawBody = b.Bytes() + return nil, e + } + + return decodeJSON[Res](res.Body) +} + +// decodeJSON decodes JSON from r into a new value of type T. +// +// NOTE: This is (and encodeJSON) are copies and paste from oweb.go, please +// do not try and consolidate so we can keep ollama/client free from +// dependencies which are moving targets and not pulling enough weight to +// justify their inclusion. +func decodeJSON[T any](r io.Reader) (*T, error) { + var v T + if err := json.NewDecoder(r).Decode(&v); err != nil { + return nil, err + } + return &v, nil +} + +// NOTE: see NOT above decodeJSON +func encodeJSON(w io.Writer, v any) error { + // TODO(bmizerany): pool and reuse encoder + return json.NewEncoder(w).Encode(v) +} diff --git a/oweb/oweb.go b/oweb/oweb.go index a5cb499c..f48e0496 100644 --- a/oweb/oweb.go +++ b/oweb/oweb.go @@ -1,28 +1,19 @@ package oweb import ( - "bytes" "cmp" - "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" - "strings" + + "bllamo.com/client/ollama" ) -type Error struct { - Status int `json:"-"` - Code string `json:"code"` - Message string `json:"message"` - Field string `json:"field,omitempty"` - RawBody []byte `json:"-"` -} - func Missing(field string) error { - return &Error{ + return &ollama.Error{ Status: 400, Code: "missing", Field: field, @@ -31,7 +22,7 @@ func Missing(field string) error { } func Mistake(code, field, message string) error { - return &Error{ + return &ollama.Error{ Status: 400, Code: code, Field: field, @@ -40,29 +31,18 @@ func Mistake(code, field, message string) error { } func Fault(code, message string) error { - return &Error{ + return &ollama.Error{ Status: 500, Code: "fault", Message: message, } } -func (e *Error) Error() string { - var b strings.Builder - b.WriteString("ollama: ") - b.WriteString(e.Code) - if e.Message != "" { - b.WriteString(": ") - b.WriteString(e.Message) - } - return b.String() -} - // Convinience errors var ( - ErrNotFound = &Error{Status: 404, Code: "not_found"} - ErrInternal = &Error{Status: 500, Code: "internal_error"} - ErrMethodNotAllowed = &Error{Status: 405, Code: "method_not_allowed"} + ErrNotFound = &ollama.Error{Status: 404, Code: "not_found"} + ErrInternal = &ollama.Error{Status: 500, Code: "internal_error"} + ErrMethodNotAllowed = &ollama.Error{Status: 405, Code: "method_not_allowed"} ) type HandlerFunc func(w http.ResponseWriter, r *http.Request) error @@ -71,12 +51,12 @@ func Serve(h HandlerFunc, w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { // TODO: take a slog.Logger log.Printf("error: %v", err) - var e *Error - if !errors.As(err, &e) { - e = ErrInternal + var oe *ollama.Error + if !errors.As(err, &oe) { + oe = ErrInternal } - w.WriteHeader(cmp.Or(e.Status, 400)) - if err := EncodeJSON(w, e); err != nil { + w.WriteHeader(cmp.Or(oe.Status, 400)) + if err := EncodeJSON(w, oe); err != nil { log.Printf("error encoding error: %v", err) } } @@ -86,11 +66,11 @@ func DecodeUserJSON[T any](r io.Reader) (*T, error) { v, err := DecodeJSON[T](r) var e *json.SyntaxError if errors.As(err, &e) { - return nil, &Error{Code: "invalid_json", Message: e.Error()} + return nil, &ollama.Error{Code: "invalid_json", Message: e.Error()} } var se *json.UnmarshalTypeError if errors.As(err, &se) { - return nil, &Error{ + return nil, &ollama.Error{ Code: "invalid_json", Message: fmt.Sprintf("%s (%q) is not a %s", se.Field, se.Value, se.Type), } @@ -110,34 +90,3 @@ func DecodeJSON[T any](r io.Reader) (*T, error) { func EncodeJSON(w io.Writer, v any) error { return json.NewEncoder(w).Encode(v) } - -func Do[Res any](ctx context.Context, method, urlStr string, in any) (*Res, error) { - var body bytes.Buffer - if err := EncodeJSON(&body, in); err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, method, urlStr, &body) - if err != nil { - return nil, err - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode/100 != 2 { - var b bytes.Buffer - body := io.TeeReader(res.Body, &b) - e, err := DecodeJSON[Error](body) - if err != nil { - return nil, err - } - e.RawBody = b.Bytes() - return nil, e - } - - return DecodeJSON[Res](res.Body) -} diff --git a/registry/client.go b/registry/client.go index 2336ab19..b26be554 100644 --- a/registry/client.go +++ b/registry/client.go @@ -6,7 +6,7 @@ import ( "io" "net/http" - "bllamo.com/oweb" + "bllamo.com/client/ollama" ) type Client struct { @@ -16,7 +16,7 @@ type Client struct { // Push pushes a manifest to the server. func (c *Client) Push(ctx context.Context, ref string, manifest []byte) ([]Requirement, error) { // TODO(bmizerany): backoff - v, err := oweb.Do[PushResponse](ctx, "POST", c.BaseURL+"/v1/push/"+ref, struct { + v, err := ollama.Do[PushResponse](ctx, "POST", c.BaseURL+"/v1/push/"+ref, struct { Manifest json.RawMessage `json:"manifest"` }{manifest}) if err != nil { @@ -38,7 +38,7 @@ func PushLayer(ctx context.Context, dstURL string, size int64, file io.Reader) e } defer res.Body.Close() if res.StatusCode != 200 { - e := &oweb.Error{Status: res.StatusCode} + e := &ollama.Error{Status: res.StatusCode} msg, err := io.ReadAll(res.Body) if err != nil { return err diff --git a/registry/server.go b/registry/server.go index 2da28c35..991a1240 100644 --- a/registry/server.go +++ b/registry/server.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "bllamo.com/client/ollama" "bllamo.com/oweb" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -19,8 +20,8 @@ type Server struct{} func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := s.serveHTTP(w, r); err != nil { - log.Printf("error: %v", err) - var e *oweb.Error + log.Printf("error: %v", err) // TODO(bmizerany): take a slog.Logger + var e *ollama.Error if !errors.As(err, &e) { e = oweb.ErrInternal }