2ada81e068
Also, document OLLAMA_HOST client semantics per command that honors it. This looks nicer than having a general puprose environment variable section in the root usage which was showing up after the "addition help topics" section outputed by Cobra's default template. It was decided this was easier to work with than using a custom template for Cobra right now.
1047 lines
22 KiB
Go
1047 lines
22 KiB
Go
package cmd
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/containerd/console"
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/term"
|
|
|
|
"github.com/jmorganca/ollama/api"
|
|
"github.com/jmorganca/ollama/format"
|
|
"github.com/jmorganca/ollama/parser"
|
|
"github.com/jmorganca/ollama/progress"
|
|
"github.com/jmorganca/ollama/server"
|
|
"github.com/jmorganca/ollama/version"
|
|
)
|
|
|
|
func CreateHandler(cmd *cobra.Command, args []string) error {
|
|
filename, _ := cmd.Flags().GetString("file")
|
|
filename, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.Stop()
|
|
|
|
bars := make(map[string]*progress.Bar)
|
|
|
|
modelfile, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
commands, err := parser.Parse(bytes.NewReader(modelfile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
status := "transferring model data"
|
|
spinner := progress.NewSpinner(status)
|
|
p.Add(status, spinner)
|
|
|
|
for _, c := range commands {
|
|
switch c.Name {
|
|
case "model", "adapter":
|
|
path := c.Args
|
|
if path == "~" {
|
|
path = home
|
|
} else if strings.HasPrefix(path, "~/") {
|
|
path = filepath.Join(home, path[2:])
|
|
}
|
|
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Join(filepath.Dir(filename), path)
|
|
}
|
|
|
|
fi, err := os.Stat(path)
|
|
if errors.Is(err, os.ErrNotExist) && c.Name == "model" {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO make this work w/ adapters
|
|
if fi.IsDir() {
|
|
tf, err := os.CreateTemp("", "ollama-tf")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tf.Name())
|
|
|
|
zf := zip.NewWriter(tf)
|
|
|
|
files, err := filepath.Glob(filepath.Join(path, "model-*.safetensors"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return fmt.Errorf("no safetensors files were found in '%s'", path)
|
|
}
|
|
|
|
// add the safetensor config file + tokenizer
|
|
files = append(files, filepath.Join(path, "config.json"))
|
|
files = append(files, filepath.Join(path, "added_tokens.json"))
|
|
files = append(files, filepath.Join(path, "tokenizer.model"))
|
|
|
|
for _, fn := range files {
|
|
f, err := os.Open(fn)
|
|
if os.IsNotExist(err) && strings.HasSuffix(fn, "added_tokens.json") {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h, err := zip.FileInfoHeader(fi)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.Name = filepath.Base(fn)
|
|
h.Method = zip.Store
|
|
|
|
w, err := zf.CreateHeader(h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(w, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
if err := zf.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tf.Close(); err != nil {
|
|
return err
|
|
}
|
|
path = tf.Name()
|
|
}
|
|
|
|
digest, err := createBlob(cmd, client, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
modelfile = bytes.ReplaceAll(modelfile, []byte(c.Args), []byte("@"+digest))
|
|
}
|
|
}
|
|
|
|
fn := func(resp api.ProgressResponse) error {
|
|
if resp.Digest != "" {
|
|
spinner.Stop()
|
|
|
|
bar, ok := bars[resp.Digest]
|
|
if !ok {
|
|
bar = progress.NewBar(fmt.Sprintf("pulling %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
|
|
bars[resp.Digest] = bar
|
|
p.Add(resp.Digest, bar)
|
|
}
|
|
|
|
bar.Set(resp.Completed)
|
|
} else if status != resp.Status {
|
|
spinner.Stop()
|
|
|
|
status = resp.Status
|
|
spinner = progress.NewSpinner(status)
|
|
p.Add(status, spinner)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
request := api.CreateRequest{Name: args[0], Modelfile: string(modelfile)}
|
|
if err := client.Create(cmd.Context(), &request, fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createBlob(cmd *cobra.Command, client *api.Client, path string) (string, error) {
|
|
bin, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer bin.Close()
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, bin); err != nil {
|
|
return "", err
|
|
}
|
|
bin.Seek(0, io.SeekStart)
|
|
|
|
digest := fmt.Sprintf("sha256:%x", hash.Sum(nil))
|
|
if err = client.CreateBlob(cmd.Context(), digest, bin); err != nil {
|
|
return "", err
|
|
}
|
|
return digest, nil
|
|
}
|
|
|
|
func RunHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
name := args[0]
|
|
|
|
// check if the model exists on the server
|
|
show, err := client.Show(cmd.Context(), &api.ShowRequest{Name: name})
|
|
var statusError api.StatusError
|
|
switch {
|
|
case errors.As(err, &statusError) && statusError.StatusCode == http.StatusNotFound:
|
|
if err := PullHandler(cmd, []string{name}); err != nil {
|
|
return err
|
|
}
|
|
|
|
show, err = client.Show(cmd.Context(), &api.ShowRequest{Name: name})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case err != nil:
|
|
return err
|
|
}
|
|
|
|
interactive := true
|
|
|
|
opts := runOptions{
|
|
Model: args[0],
|
|
WordWrap: os.Getenv("TERM") == "xterm-256color",
|
|
Options: map[string]interface{}{},
|
|
MultiModal: slices.Contains(show.Details.Families, "clip"),
|
|
ParentModel: show.Details.ParentModel,
|
|
}
|
|
|
|
format, err := cmd.Flags().GetString("format")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.Format = format
|
|
|
|
prompts := args[1:]
|
|
// prepend stdin to the prompt if provided
|
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
in, err := io.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prompts = append([]string{string(in)}, prompts...)
|
|
opts.WordWrap = false
|
|
interactive = false
|
|
}
|
|
opts.Prompt = strings.Join(prompts, " ")
|
|
if len(prompts) > 0 {
|
|
interactive = false
|
|
}
|
|
|
|
nowrap, err := cmd.Flags().GetBool("nowordwrap")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opts.WordWrap = !nowrap
|
|
|
|
if !interactive {
|
|
return generate(cmd, opts)
|
|
}
|
|
|
|
return generateInteractive(cmd, opts)
|
|
}
|
|
|
|
func PushHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
insecure, err := cmd.Flags().GetBool("insecure")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.Stop()
|
|
|
|
bars := make(map[string]*progress.Bar)
|
|
var status string
|
|
var spinner *progress.Spinner
|
|
|
|
fn := func(resp api.ProgressResponse) error {
|
|
if resp.Digest != "" {
|
|
if spinner != nil {
|
|
spinner.Stop()
|
|
}
|
|
|
|
bar, ok := bars[resp.Digest]
|
|
if !ok {
|
|
bar = progress.NewBar(fmt.Sprintf("pushing %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
|
|
bars[resp.Digest] = bar
|
|
p.Add(resp.Digest, bar)
|
|
}
|
|
|
|
bar.Set(resp.Completed)
|
|
} else if status != resp.Status {
|
|
if spinner != nil {
|
|
spinner.Stop()
|
|
}
|
|
|
|
status = resp.Status
|
|
spinner = progress.NewSpinner(status)
|
|
p.Add(status, spinner)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
request := api.PushRequest{Name: args[0], Insecure: insecure}
|
|
if err := client.Push(cmd.Context(), &request, fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
spinner.Stop()
|
|
return nil
|
|
}
|
|
|
|
func ListHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
models, err := client.List(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var data [][]string
|
|
|
|
for _, m := range models.Models {
|
|
if len(args) == 0 || strings.HasPrefix(m.Name, args[0]) {
|
|
data = append(data, []string{m.Name, m.Digest[:12], format.HumanBytes(m.Size), format.HumanTime(m.ModifiedAt, "Never")})
|
|
}
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetHeader([]string{"NAME", "ID", "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 DeleteHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range args {
|
|
req := api.DeleteRequest{Name: name}
|
|
if err := client.Delete(cmd.Context(), &req); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("deleted '%s'\n", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ShowHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(args) != 1 {
|
|
return errors.New("missing model name")
|
|
}
|
|
|
|
license, errLicense := cmd.Flags().GetBool("license")
|
|
modelfile, errModelfile := cmd.Flags().GetBool("modelfile")
|
|
parameters, errParams := cmd.Flags().GetBool("parameters")
|
|
system, errSystem := cmd.Flags().GetBool("system")
|
|
template, errTemplate := cmd.Flags().GetBool("template")
|
|
|
|
for _, boolErr := range []error{errLicense, errModelfile, errParams, errSystem, errTemplate} {
|
|
if boolErr != nil {
|
|
return errors.New("error retrieving flags")
|
|
}
|
|
}
|
|
|
|
flagsSet := 0
|
|
showType := ""
|
|
|
|
if license {
|
|
flagsSet++
|
|
showType = "license"
|
|
}
|
|
|
|
if modelfile {
|
|
flagsSet++
|
|
showType = "modelfile"
|
|
}
|
|
|
|
if parameters {
|
|
flagsSet++
|
|
showType = "parameters"
|
|
}
|
|
|
|
if system {
|
|
flagsSet++
|
|
showType = "system"
|
|
}
|
|
|
|
if template {
|
|
flagsSet++
|
|
showType = "template"
|
|
}
|
|
|
|
if flagsSet > 1 {
|
|
return errors.New("only one of '--license', '--modelfile', '--parameters', '--system', or '--template' can be specified")
|
|
} else if flagsSet == 0 {
|
|
return errors.New("one of '--license', '--modelfile', '--parameters', '--system', or '--template' must be specified")
|
|
}
|
|
|
|
req := api.ShowRequest{Name: args[0]}
|
|
resp, err := client.Show(cmd.Context(), &req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch showType {
|
|
case "license":
|
|
fmt.Println(resp.License)
|
|
case "modelfile":
|
|
fmt.Println(resp.Modelfile)
|
|
case "parameters":
|
|
fmt.Println(resp.Parameters)
|
|
case "system":
|
|
fmt.Println(resp.System)
|
|
case "template":
|
|
fmt.Println(resp.Template)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CopyHandler(cmd *cobra.Command, args []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := api.CopyRequest{Source: args[0], Destination: args[1]}
|
|
if err := client.Copy(cmd.Context(), &req); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("copied '%s' to '%s'\n", args[0], args[1])
|
|
return nil
|
|
}
|
|
|
|
func PullHandler(cmd *cobra.Command, args []string) error {
|
|
insecure, err := cmd.Flags().GetBool("insecure")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.Stop()
|
|
|
|
bars := make(map[string]*progress.Bar)
|
|
|
|
var status string
|
|
var spinner *progress.Spinner
|
|
|
|
fn := func(resp api.ProgressResponse) error {
|
|
if resp.Digest != "" {
|
|
if spinner != nil {
|
|
spinner.Stop()
|
|
}
|
|
|
|
bar, ok := bars[resp.Digest]
|
|
if !ok {
|
|
bar = progress.NewBar(fmt.Sprintf("pulling %s...", resp.Digest[7:19]), resp.Total, resp.Completed)
|
|
bars[resp.Digest] = bar
|
|
p.Add(resp.Digest, bar)
|
|
}
|
|
|
|
bar.Set(resp.Completed)
|
|
} else if status != resp.Status {
|
|
if spinner != nil {
|
|
spinner.Stop()
|
|
}
|
|
|
|
status = resp.Status
|
|
spinner = progress.NewSpinner(status)
|
|
p.Add(status, spinner)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
request := api.PullRequest{Name: args[0], Insecure: insecure}
|
|
if err := client.Pull(cmd.Context(), &request, fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type generateContextKey string
|
|
|
|
type runOptions struct {
|
|
Model string
|
|
ParentModel string
|
|
Prompt string
|
|
Messages []api.Message
|
|
WordWrap bool
|
|
Format string
|
|
System string
|
|
Template string
|
|
Images []api.ImageData
|
|
Options map[string]interface{}
|
|
MultiModal bool
|
|
}
|
|
|
|
type displayResponseState struct {
|
|
lineLength int
|
|
wordBuffer string
|
|
}
|
|
|
|
func displayResponse(content string, wordWrap bool, state *displayResponseState) {
|
|
termWidth, _, _ := term.GetSize(int(os.Stdout.Fd()))
|
|
if wordWrap && termWidth >= 10 {
|
|
for _, ch := range content {
|
|
if state.lineLength+1 > termWidth-5 {
|
|
if len(state.wordBuffer) > termWidth-10 {
|
|
fmt.Printf("%s%c", state.wordBuffer, ch)
|
|
state.wordBuffer = ""
|
|
state.lineLength = 0
|
|
continue
|
|
}
|
|
|
|
// backtrack the length of the last word and clear to the end of the line
|
|
fmt.Printf("\x1b[%dD\x1b[K\n", len(state.wordBuffer))
|
|
fmt.Printf("%s%c", state.wordBuffer, ch)
|
|
state.lineLength = len(state.wordBuffer) + 1
|
|
} else {
|
|
fmt.Print(string(ch))
|
|
state.lineLength += 1
|
|
|
|
switch ch {
|
|
case ' ':
|
|
state.wordBuffer = ""
|
|
case '\n':
|
|
state.lineLength = 0
|
|
default:
|
|
state.wordBuffer += string(ch)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Printf("%s%s", state.wordBuffer, content)
|
|
if len(state.wordBuffer) > 0 {
|
|
state.wordBuffer = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func chat(cmd *cobra.Command, opts runOptions) (*api.Message, error) {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.StopAndClear()
|
|
|
|
spinner := progress.NewSpinner("")
|
|
p.Add("", spinner)
|
|
|
|
cancelCtx, cancel := context.WithCancel(cmd.Context())
|
|
defer cancel()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT)
|
|
|
|
go func() {
|
|
<-sigChan
|
|
cancel()
|
|
}()
|
|
|
|
var state *displayResponseState = &displayResponseState{}
|
|
var latest api.ChatResponse
|
|
var fullResponse strings.Builder
|
|
var role string
|
|
|
|
fn := func(response api.ChatResponse) error {
|
|
p.StopAndClear()
|
|
|
|
latest = response
|
|
|
|
role = response.Message.Role
|
|
content := response.Message.Content
|
|
fullResponse.WriteString(content)
|
|
|
|
displayResponse(content, opts.WordWrap, state)
|
|
|
|
return nil
|
|
}
|
|
|
|
req := &api.ChatRequest{
|
|
Model: opts.Model,
|
|
Messages: opts.Messages,
|
|
Format: opts.Format,
|
|
Options: opts.Options,
|
|
}
|
|
|
|
if err := client.Chat(cancelCtx, req, fn); err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if len(opts.Messages) > 0 {
|
|
fmt.Println()
|
|
fmt.Println()
|
|
}
|
|
|
|
verbose, err := cmd.Flags().GetBool("verbose")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if verbose {
|
|
latest.Summary()
|
|
}
|
|
|
|
return &api.Message{Role: role, Content: fullResponse.String()}, nil
|
|
}
|
|
|
|
func generate(cmd *cobra.Command, opts runOptions) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p := progress.NewProgress(os.Stderr)
|
|
defer p.StopAndClear()
|
|
|
|
spinner := progress.NewSpinner("")
|
|
p.Add("", spinner)
|
|
|
|
var latest api.GenerateResponse
|
|
|
|
generateContext, ok := cmd.Context().Value(generateContextKey("context")).([]int)
|
|
if !ok {
|
|
generateContext = []int{}
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
defer cancel()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT)
|
|
|
|
go func() {
|
|
<-sigChan
|
|
cancel()
|
|
}()
|
|
|
|
var state *displayResponseState = &displayResponseState{}
|
|
|
|
fn := func(response api.GenerateResponse) error {
|
|
p.StopAndClear()
|
|
|
|
latest = response
|
|
content := response.Response
|
|
|
|
displayResponse(content, opts.WordWrap, state)
|
|
|
|
return nil
|
|
}
|
|
|
|
if opts.MultiModal {
|
|
opts.Prompt, opts.Images, err = extractFileData(opts.Prompt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
request := api.GenerateRequest{
|
|
Model: opts.Model,
|
|
Prompt: opts.Prompt,
|
|
Context: generateContext,
|
|
Images: opts.Images,
|
|
Format: opts.Format,
|
|
System: opts.System,
|
|
Template: opts.Template,
|
|
Options: opts.Options,
|
|
}
|
|
|
|
if err := client.Generate(ctx, &request, fn); err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
if opts.Prompt != "" {
|
|
fmt.Println()
|
|
fmt.Println()
|
|
}
|
|
|
|
if !latest.Done {
|
|
return nil
|
|
}
|
|
|
|
verbose, err := cmd.Flags().GetBool("verbose")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
latest.Summary()
|
|
}
|
|
|
|
ctx = context.WithValue(cmd.Context(), generateContextKey("context"), latest.Context)
|
|
cmd.SetContext(ctx)
|
|
|
|
return nil
|
|
}
|
|
|
|
func RunServer(cmd *cobra.Command, _ []string) error {
|
|
host, port, err := net.SplitHostPort(strings.Trim(os.Getenv("OLLAMA_HOST"), "\"'"))
|
|
if err != nil {
|
|
host, port = "127.0.0.1", "11434"
|
|
if ip := net.ParseIP(strings.Trim(os.Getenv("OLLAMA_HOST"), "[]")); ip != nil {
|
|
host = ip.String()
|
|
}
|
|
}
|
|
|
|
if err := initializeKeypair(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ln, err := net.Listen("tcp", net.JoinHostPort(host, port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return server.Serve(ln)
|
|
}
|
|
|
|
func initializeKeypair() error {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
privKeyPath := filepath.Join(home, ".ollama", "id_ed25519")
|
|
pubKeyPath := filepath.Join(home, ".ollama", "id_ed25519.pub")
|
|
|
|
_, err = os.Stat(privKeyPath)
|
|
if os.IsNotExist(err) {
|
|
fmt.Printf("Couldn't find '%s'. Generating new private key.\n", privKeyPath)
|
|
cryptoPublicKey, cryptoPrivateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
privateKeyBytes, err := ssh.MarshalPrivateKey(cryptoPrivateKey, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(privKeyPath), 0o755); err != nil {
|
|
return fmt.Errorf("could not create directory %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(privKeyPath, pem.EncodeToMemory(privateKeyBytes), 0o600); err != nil {
|
|
return err
|
|
}
|
|
|
|
sshPublicKey, err := ssh.NewPublicKey(cryptoPublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
publicKeyBytes := ssh.MarshalAuthorizedKey(sshPublicKey)
|
|
|
|
if err := os.WriteFile(pubKeyPath, publicKeyBytes, 0o644); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Your new public key is: \n\n%s\n", publicKeyBytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//nolint:unused
|
|
func waitForServer(ctx context.Context, client *api.Client) error {
|
|
// wait for the server to start
|
|
timeout := time.After(5 * time.Second)
|
|
tick := time.Tick(500 * time.Millisecond)
|
|
for {
|
|
select {
|
|
case <-timeout:
|
|
return errors.New("timed out waiting for server to start")
|
|
case <-tick:
|
|
if err := client.Heartbeat(ctx); err == nil {
|
|
return nil // server has started
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := client.Heartbeat(cmd.Context()); err != nil {
|
|
if !strings.Contains(err.Error(), " refused") {
|
|
return err
|
|
}
|
|
if err := startApp(cmd.Context(), client); err != nil {
|
|
return fmt.Errorf("could not connect to ollama app, is it running?")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func versionHandler(cmd *cobra.Command, _ []string) {
|
|
client, err := api.ClientFromEnvironment()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
serverVersion, err := client.Version(cmd.Context())
|
|
if err != nil {
|
|
fmt.Println("Warning: could not connect to a running Ollama instance")
|
|
}
|
|
|
|
if serverVersion != "" {
|
|
fmt.Printf("ollama version is %s\n", serverVersion)
|
|
}
|
|
|
|
if serverVersion != version.Version {
|
|
fmt.Printf("Warning: client version is %s\n", version.Version)
|
|
}
|
|
}
|
|
|
|
func appendHostEnvDocs(cmd *cobra.Command) {
|
|
const hostEnvDocs = `
|
|
Environment Variables:
|
|
OLLAMA_HOST The host:port or base URL of the Ollama server (e.g. http://localhost:11434)
|
|
`
|
|
cmd.SetUsageTemplate(cmd.UsageTemplate() + hostEnvDocs)
|
|
}
|
|
|
|
func NewCLI() *cobra.Command {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
cobra.EnableCommandSorting = false
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// Enable colorful ANSI escape code in Windows terminal (disabled by default)
|
|
console.ConsoleFromFile(os.Stdout) //nolint:errcheck
|
|
}
|
|
|
|
rootCmd := &cobra.Command{
|
|
Use: "ollama",
|
|
Short: "Large language model runner",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
CompletionOptions: cobra.CompletionOptions{
|
|
DisableDefaultCmd: true,
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if version, _ := cmd.Flags().GetBool("version"); version {
|
|
versionHandler(cmd, args)
|
|
return
|
|
}
|
|
|
|
cmd.Print(cmd.UsageString())
|
|
},
|
|
}
|
|
|
|
rootCmd.Flags().BoolP("version", "v", false, "Show version information")
|
|
|
|
createCmd := &cobra.Command{
|
|
Use: "create MODEL",
|
|
Short: "Create a model from a Modelfile",
|
|
Args: cobra.ExactArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: CreateHandler,
|
|
}
|
|
|
|
createCmd.Flags().StringP("file", "f", "Modelfile", "Name of the Modelfile (default \"Modelfile\")")
|
|
|
|
showCmd := &cobra.Command{
|
|
Use: "show MODEL",
|
|
Short: "Show information for a model",
|
|
Args: cobra.ExactArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: ShowHandler,
|
|
}
|
|
|
|
showCmd.Flags().Bool("license", false, "Show license of a model")
|
|
showCmd.Flags().Bool("modelfile", false, "Show Modelfile of a model")
|
|
showCmd.Flags().Bool("parameters", false, "Show parameters of a model")
|
|
showCmd.Flags().Bool("template", false, "Show template of a model")
|
|
showCmd.Flags().Bool("system", false, "Show system message of a model")
|
|
|
|
runCmd := &cobra.Command{
|
|
Use: "run MODEL [PROMPT]",
|
|
Short: "Run a model",
|
|
Args: cobra.MinimumNArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: RunHandler,
|
|
}
|
|
|
|
runCmd.Flags().Bool("verbose", false, "Show timings for response")
|
|
runCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
|
runCmd.Flags().Bool("nowordwrap", false, "Don't wrap words to the next line automatically")
|
|
runCmd.Flags().String("format", "", "Response format (e.g. json)")
|
|
serveCmd := &cobra.Command{
|
|
Use: "serve",
|
|
Aliases: []string{"start"},
|
|
Short: "Start ollama",
|
|
Args: cobra.ExactArgs(0),
|
|
RunE: RunServer,
|
|
}
|
|
serveCmd.SetUsageTemplate(serveCmd.UsageTemplate() + `
|
|
Environment Variables:
|
|
|
|
OLLAMA_HOST The host:port to bind to (default "127.0.0.1:11434")
|
|
OLLAMA_ORIGINS A comma separated list of allowed origins.
|
|
OLLAMA_MODELS The path to the models directory (default is "~/.ollama/models")
|
|
`)
|
|
|
|
pullCmd := &cobra.Command{
|
|
Use: "pull MODEL",
|
|
Short: "Pull a model from a registry",
|
|
Args: cobra.ExactArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: PullHandler,
|
|
}
|
|
|
|
pullCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
|
|
|
pushCmd := &cobra.Command{
|
|
Use: "push MODEL",
|
|
Short: "Push a model to a registry",
|
|
Args: cobra.ExactArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: PushHandler,
|
|
}
|
|
|
|
pushCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
|
|
|
listCmd := &cobra.Command{
|
|
Use: "list",
|
|
Aliases: []string{"ls"},
|
|
Short: "List models",
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: ListHandler,
|
|
}
|
|
copyCmd := &cobra.Command{
|
|
Use: "cp SOURCE TARGET",
|
|
Short: "Copy a model",
|
|
Args: cobra.ExactArgs(2),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: CopyHandler,
|
|
}
|
|
|
|
deleteCmd := &cobra.Command{
|
|
Use: "rm MODEL [MODEL...]",
|
|
Short: "Remove a model",
|
|
Args: cobra.MinimumNArgs(1),
|
|
PreRunE: checkServerHeartbeat,
|
|
RunE: DeleteHandler,
|
|
}
|
|
|
|
for _, cmd := range []*cobra.Command{
|
|
createCmd,
|
|
showCmd,
|
|
runCmd,
|
|
pullCmd,
|
|
pushCmd,
|
|
listCmd,
|
|
copyCmd,
|
|
deleteCmd,
|
|
} {
|
|
appendHostEnvDocs(cmd)
|
|
}
|
|
|
|
rootCmd.AddCommand(
|
|
serveCmd,
|
|
createCmd,
|
|
showCmd,
|
|
runCmd,
|
|
pullCmd,
|
|
pushCmd,
|
|
listCmd,
|
|
copyCmd,
|
|
deleteCmd,
|
|
)
|
|
|
|
return rootCmd
|
|
}
|