Skip to content

Commit

Permalink
add new list command (ollama#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
pdevine authored Jul 18, 2023
1 parent da7ddbb commit 5bea29f
Show file tree
Hide file tree
Showing 10 changed files with 450 additions and 11 deletions.
86 changes: 75 additions & 11 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,31 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)

type StatusError struct {
StatusCode int
Status string
Message string
type Client struct {
base url.URL
HTTP http.Client
Headers http.Header
}

func (e StatusError) Error() string {
if e.Message != "" {
return fmt.Sprintf("%s: %s", e.Status, e.Message)
func checkError(resp *http.Response, body []byte) error {
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
return nil
}

return e.Status
}
apiError := StatusError{StatusCode: resp.StatusCode}

type Client struct {
base url.URL
err := json.Unmarshal(body, &apiError)
if err != nil {
// Use the full body as the message if we fail to decode a response.
apiError.Message = string(body)
}

return apiError
}

func NewClient(hosts ...string) *Client {
Expand All @@ -36,7 +41,58 @@ func NewClient(hosts ...string) *Client {

return &Client{
base: url.URL{Scheme: "http", Host: host},
HTTP: http.Client{},
}
}

func (c *Client) do(ctx context.Context, method, path string, reqData, respData any) error {
var reqBody io.Reader
var data []byte
var err error
if reqData != nil {
data, err = json.Marshal(reqData)
if err != nil {
return err
}
reqBody = bytes.NewReader(data)
}

url := c.base.JoinPath(path).String()

req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

for k, v := range c.Headers {
req.Header[k] = v
}

respObj, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer respObj.Body.Close()

respBody, err := io.ReadAll(respObj.Body)
if err != nil {
return err
}

if err := checkError(respObj, respBody); err != nil {
return err
}

if len(respBody) > 0 && respData != nil {
if err := json.Unmarshal(respBody, respData); err != nil {
return err
}
}
return nil

}

func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
Expand Down Expand Up @@ -142,3 +198,11 @@ func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgre
return fn(resp)
})
}

func (c *Client) List(ctx context.Context) (*ListResponse, error) {
var lr ListResponse
if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil {
return nil, err
}
return &lr, nil
}
23 changes: 23 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import (
"time"
)

type StatusError struct {
StatusCode int
Status string
Message string
}

func (e StatusError) Error() string {
if e.Message != "" {
return fmt.Sprintf("%s: %s", e.Status, e.Message)
}
return e.Status
}

type GenerateRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Expand Down Expand Up @@ -52,6 +65,16 @@ type PushProgress struct {
Percent float64 `json:"percent,omitempty"`
}

type ListResponse struct {
Models []ListResponseModel `json:"models"`
}

type ListResponseModel struct {
Name string `json:"name"`
ModifiedAt time.Time `json:"modified_at"`
Size int `json:"size"`
}

type GenerateResponse struct {
Model string `json:"model"`
CreatedAt time.Time `json:"created_at"`
Expand Down
38 changes: 38 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import (
"strings"
"time"

"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/jmorganca/ollama/api"
"github.com/jmorganca/ollama/format"
"github.com/jmorganca/ollama/server"
)

Expand Down Expand Up @@ -89,6 +92,34 @@ func push(cmd *cobra.Command, args []string) error {
return nil
}

func list(cmd *cobra.Command, args []string) error {
client := api.NewClient()

models, err := client.List(context.Background())
if err != nil {
return err
}

var data [][]string

for _, m := range models.Models {
data = append(data, []string{m.Name, humanize.Bytes(uint64(m.Size)), format.HumanTime(m.ModifiedAt, "Never")})
}

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"NAME", "SIZE", "MODIFIED"})
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetNoWhiteSpace(true)
table.SetTablePadding("\t")
table.AppendBulk(data)
table.Render()

return nil
}

func RunPull(cmd *cobra.Command, args []string) error {
return pull(args[0])
}
Expand Down Expand Up @@ -308,12 +339,19 @@ func NewCLI() *cobra.Command {
RunE: push,
}

listCmd := &cobra.Command{
Use: "list",
Short: "List models",
RunE: list,
}

rootCmd.AddCommand(
serveCmd,
createCmd,
runCmd,
pullCmd,
pushCmd,
listCmd,
)

return rootCmd
Expand Down
141 changes: 141 additions & 0 deletions format/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package format

import (
"fmt"
"math"
"strings"
"time"
)

// HumanDuration returns a human-readable approximation of a duration
// (eg. "About a minute", "4 hours ago", etc.).
// Modified version of github.com/docker/go-units.HumanDuration
func HumanDuration(d time.Duration) string {
return HumanDurationWithCase(d, true)
}

// HumanDurationWithCase returns a human-readable approximation of a
// duration (eg. "About a minute", "4 hours ago", etc.). but allows
// you to specify whether the first word should be capitalized
// (eg. "About" vs. "about")
func HumanDurationWithCase(d time.Duration, useCaps bool) string {
seconds := int(d.Seconds())

switch {
case seconds < 1:
if useCaps {
return "Less than a second"
}
return "less than a second"
case seconds == 1:
return "1 second"
case seconds < 60:
return fmt.Sprintf("%d seconds", seconds)
}

minutes := int(d.Minutes())
switch {
case minutes == 1:
if useCaps {
return "About a minute"
}
return "about a minute"
case minutes < 60:
return fmt.Sprintf("%d minutes", minutes)
}

hours := int(math.Round(d.Hours()))
switch {
case hours == 1:
if useCaps {
return "About an hour"
}
return "about an hour"
case hours < 48:
return fmt.Sprintf("%d hours", hours)
case hours < 24*7*2:
return fmt.Sprintf("%d days", hours/24)
case hours < 24*30*2:
return fmt.Sprintf("%d weeks", hours/24/7)
case hours < 24*365*2:
return fmt.Sprintf("%d months", hours/24/30)
}

return fmt.Sprintf("%d years", int(d.Hours())/24/365)
}

func HumanTime(t time.Time, zeroValue string) string {
return humanTimeWithCase(t, zeroValue, true)
}

func HumanTimeLower(t time.Time, zeroValue string) string {
return humanTimeWithCase(t, zeroValue, false)
}

func humanTimeWithCase(t time.Time, zeroValue string, useCaps bool) string {
if t.IsZero() {
return zeroValue
}

delta := time.Since(t)
if delta < 0 {
return HumanDurationWithCase(-delta, useCaps) + " from now"
}
return HumanDurationWithCase(delta, useCaps) + " ago"
}

// ExcatDuration returns a human readable hours/minutes/seconds or milliseconds format of a duration
// the most precise level of duration is milliseconds
func ExactDuration(d time.Duration) string {
if d.Seconds() < 1 {
if d.Milliseconds() == 1 {
return fmt.Sprintf("%d millisecond", d.Milliseconds())
}
return fmt.Sprintf("%d milliseconds", d.Milliseconds())
}

var readableDur strings.Builder

dur := d.String()

// split the default duration string format of 0h0m0s into something nicer to read
h := strings.Split(dur, "h")
if len(h) > 1 {
hours := h[0]
if hours == "1" {
readableDur.WriteString(fmt.Sprintf("%s hour ", hours))
} else {
readableDur.WriteString(fmt.Sprintf("%s hours ", hours))
}
dur = h[1]
}

m := strings.Split(dur, "m")
if len(m) > 1 {
mins := m[0]
switch mins {
case "0":
// skip
case "1":
readableDur.WriteString(fmt.Sprintf("%s minute ", mins))
default:
readableDur.WriteString(fmt.Sprintf("%s minutes ", mins))
}
dur = m[1]
}

s := strings.Split(dur, "s")
if len(s) > 0 {
sec := s[0]
switch sec {
case "0":
// skip
case "1":
readableDur.WriteString(fmt.Sprintf("%s second ", sec))
default:
readableDur.WriteString(fmt.Sprintf("%s seconds ", sec))
}
}

return strings.TrimSpace(readableDur.String())
}
Loading

0 comments on commit 5bea29f

Please sign in to comment.