Compare commits
41 Commits
upload-pro
...
v0.0.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e934b497 | ||
|
|
e751e47d70 | ||
|
|
19d0f2b4cc | ||
|
|
c48f07f821 | ||
|
|
dc642aa07d | ||
|
|
f1ff892fdd | ||
|
|
3f2a100465 | ||
|
|
95397416f3 | ||
|
|
8a86aae019 | ||
|
|
24c2c77057 | ||
|
|
5614984f06 | ||
|
|
4c1caa3733 | ||
|
|
12ab8f8f5f | ||
|
|
8ebbd12f21 | ||
|
|
07971759fa | ||
|
|
f5f79049c2 | ||
|
|
726bc647b2 | ||
|
|
af9039a167 | ||
|
|
07ed69bc37 | ||
|
|
0deb3767fc | ||
|
|
cb55fa9270 | ||
|
|
93bc9f17a1 | ||
|
|
536028c35a | ||
|
|
aedf3d1f38 | ||
|
|
91d927abc5 | ||
|
|
ba8df10a43 | ||
|
|
abf614804b | ||
|
|
a0dbbb23c4 | ||
|
|
0fd6278446 | ||
|
|
29fe07f0cc | ||
|
|
abfc73d31e | ||
|
|
5a5ca8e7ff | ||
|
|
f24a6f5988 | ||
|
|
fdbef6c95e | ||
|
|
24e43e3212 | ||
|
|
4cb42ca55e | ||
|
|
ec5e22ac85 | ||
|
|
ed89da92b4 | ||
|
|
a3297fed41 | ||
|
|
88c55199f8 | ||
|
|
b71c67b6ba |
@@ -135,3 +135,11 @@ Generate text from a model.
|
||||
```
|
||||
curl -X POST http://localhost:11434/api/generate -d '{"model": "llama2", "prompt":"Why is the sky blue?"}'
|
||||
```
|
||||
|
||||
### `POST /api/create`
|
||||
|
||||
Create a model from a `Modelfile`.
|
||||
|
||||
```
|
||||
curl -X POST http://localhost:11434/api/create -d '{"name": "my-model", "path": "/path/to/modelfile"}'
|
||||
```
|
||||
|
||||
@@ -131,7 +131,7 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
|
||||
}
|
||||
|
||||
if errorResponse.Error != "" {
|
||||
return fmt.Errorf("stream: %s", errorResponse.Error)
|
||||
return fmt.Errorf(errorResponse.Error)
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
@@ -189,11 +189,11 @@ func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc
|
||||
})
|
||||
}
|
||||
|
||||
type CreateProgressFunc func(CreateProgress) error
|
||||
type CreateProgressFunc func(ProgressResponse) error
|
||||
|
||||
func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error {
|
||||
return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error {
|
||||
var resp CreateProgress
|
||||
var resp ProgressResponse
|
||||
if err := json.Unmarshal(bts, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -210,6 +210,13 @@ func (c *Client) List(ctx context.Context) (*ListResponse, error) {
|
||||
return &lr, nil
|
||||
}
|
||||
|
||||
func (c *Client) Copy(ctx context.Context, req *CopyRequest) error {
|
||||
if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
|
||||
if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil {
|
||||
return err
|
||||
|
||||
@@ -40,14 +40,15 @@ type CreateRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type CreateProgress struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type DeleteRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CopyRequest struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
|
||||
type PullRequest struct {
|
||||
Name string `json:"name"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
|
||||
BIN
app/assets/iconTemplate.png
Normal file
|
After Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B |
BIN
app/assets/iconUpdateTemplate.png
Normal file
|
After Width: | Height: | Size: 443 B |
BIN
app/assets/iconUpdateTemplate@2x.png
Normal file
|
After Width: | Height: | Size: 844 B |
|
Before Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 445 B |
@@ -19,10 +19,10 @@ const config: ForgeConfig = {
|
||||
icon: './assets/icon.icns',
|
||||
extraResource: [
|
||||
'../ollama',
|
||||
path.join(__dirname, './assets/ollama_icon_16x16Template.png'),
|
||||
path.join(__dirname, './assets/ollama_icon_16x16Template@2x.png'),
|
||||
path.join(__dirname, './assets/ollama_outline_icon_16x16Template.png'),
|
||||
path.join(__dirname, './assets/ollama_outline_icon_16x16Template@2x.png'),
|
||||
path.join(__dirname, './assets/iconTemplate.png'),
|
||||
path.join(__dirname, './assets/iconTemplate@2x.png'),
|
||||
path.join(__dirname, './assets/iconUpdateTemplate.png'),
|
||||
path.join(__dirname, './assets/iconUpdateTemplate@2x.png'),
|
||||
...(process.platform === 'darwin' ? ['../llama/ggml-metal.metal'] : []),
|
||||
],
|
||||
...(process.env.SIGN
|
||||
|
||||
@@ -51,10 +51,15 @@ export default function () {
|
||||
<div className='mx-auto'>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await install()
|
||||
getCurrentWindow().show()
|
||||
getCurrentWindow().focus()
|
||||
setStep(Step.FINISH)
|
||||
try {
|
||||
await install()
|
||||
setStep(Step.FINISH)
|
||||
} catch (e) {
|
||||
console.error('could not install: ', e)
|
||||
} finally {
|
||||
getCurrentWindow().show()
|
||||
getCurrentWindow().focus()
|
||||
}
|
||||
}}
|
||||
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
||||
>
|
||||
|
||||
111
app/src/index.ts
@@ -1,5 +1,15 @@
|
||||
import { spawn } from 'child_process'
|
||||
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, nativeTheme } from 'electron'
|
||||
import {
|
||||
app,
|
||||
autoUpdater,
|
||||
dialog,
|
||||
Tray,
|
||||
Menu,
|
||||
BrowserWindow,
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
systemPreferences,
|
||||
} from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import winston from 'winston'
|
||||
import 'winston-daily-rotate-file'
|
||||
@@ -7,11 +17,12 @@ import * as path from 'path'
|
||||
|
||||
import { analytics, id } from './telemetry'
|
||||
import { installed } from './install'
|
||||
import { MenuItem } from '@electron/remote'
|
||||
|
||||
require('@electron/remote/main').initialize()
|
||||
|
||||
const store = new Store()
|
||||
let tray: Tray | null = null
|
||||
|
||||
let welcomeWindow: BrowserWindow | null = null
|
||||
|
||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string
|
||||
@@ -54,46 +65,45 @@ function firstRunWindow() {
|
||||
|
||||
// and load the index.html of the app.
|
||||
welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
|
||||
|
||||
welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
|
||||
|
||||
// for debugging
|
||||
// welcomeWindow.webContents.openDevTools()
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function createSystemtray() {
|
||||
let iconPath = nativeTheme.shouldUseDarkColors
|
||||
? path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')
|
||||
: path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png')
|
||||
let tray: Tray | null = null
|
||||
|
||||
if (app.isPackaged) {
|
||||
iconPath = nativeTheme.shouldUseDarkColors
|
||||
? path.join(process.resourcesPath, 'ollama_icon_16x16Template.png')
|
||||
: path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png')
|
||||
function setTray(updateAvailable: boolean) {
|
||||
const updateItems: MenuItemConstructorOptions[] = [
|
||||
{ label: 'An update is available', enabled: false },
|
||||
{
|
||||
label: 'Restart to update',
|
||||
click: () => autoUpdater.quitAndInstall(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
]
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
...(updateAvailable ? updateItems : []),
|
||||
{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' },
|
||||
])
|
||||
|
||||
const iconPath = app.isPackaged
|
||||
? updateAvailable
|
||||
? path.join(process.resourcesPath, 'iconUpdateTemplate.png')
|
||||
: path.join(process.resourcesPath, 'iconTemplate.png')
|
||||
: updateAvailable
|
||||
? path.join(__dirname, '..', '..', 'assets', 'iconUpdateTemplate.png')
|
||||
: path.join(__dirname, '..', '..', 'assets', 'iconTemplate.png')
|
||||
|
||||
if (!tray) {
|
||||
tray = new Tray(iconPath)
|
||||
}
|
||||
|
||||
tray = new Tray(iconPath)
|
||||
|
||||
nativeTheme.on('updated', function theThemeHasChanged() {
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
app.isPackaged
|
||||
? tray.setImage(path.join(process.resourcesPath, 'ollama_icon_16x16Template.png'))
|
||||
: tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png'))
|
||||
} else {
|
||||
app.isPackaged
|
||||
? tray.setImage(path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png'))
|
||||
: tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png'))
|
||||
}
|
||||
})
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' }])
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
tray.setToolTip('Ollama')
|
||||
tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama')
|
||||
tray.setContextMenu(menu)
|
||||
tray.setImage(iconPath)
|
||||
}
|
||||
|
||||
if (require('electron-squirrel-startup')) {
|
||||
@@ -132,6 +142,17 @@ if (process.platform === 'darwin') {
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
if (app.isPackaged) {
|
||||
heartbeat()
|
||||
autoUpdater.checkForUpdates()
|
||||
setInterval(() => {
|
||||
heartbeat()
|
||||
autoUpdater.checkForUpdates()
|
||||
}, 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
setTray(false)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (app.isPackaged) {
|
||||
if (!app.isInApplicationsFolder()) {
|
||||
@@ -167,7 +188,6 @@ app.on('ready', () => {
|
||||
}
|
||||
}
|
||||
|
||||
createSystemtray()
|
||||
server()
|
||||
|
||||
if (store.get('first-time-run') && installed()) {
|
||||
@@ -205,29 +225,10 @@ async function heartbeat() {
|
||||
})
|
||||
}
|
||||
|
||||
if (app.isPackaged) {
|
||||
heartbeat()
|
||||
autoUpdater.checkForUpdates()
|
||||
setInterval(() => {
|
||||
heartbeat()
|
||||
autoUpdater.checkForUpdates()
|
||||
}, 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
autoUpdater.on('error', e => {
|
||||
logger.error(`update check failed - ${e.message}`)
|
||||
console.error(`update check failed - ${e.message}`)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
buttons: ['Restart Now', 'Later'],
|
||||
title: 'New update available',
|
||||
message: process.platform === 'win32' ? releaseNotes : releaseName,
|
||||
detail: 'A new version of Ollama is available. Restart to apply the update.',
|
||||
})
|
||||
.then(returnValue => {
|
||||
if (returnValue.response === 0) autoUpdater.quitAndInstall()
|
||||
})
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
setTray(true)
|
||||
})
|
||||
|
||||
@@ -17,10 +17,5 @@ export async function install() {
|
||||
symlinkPath
|
||||
)} && ln -F -s ${ollama} ${symlinkPath}" with administrator privileges`
|
||||
|
||||
try {
|
||||
await exec(`osascript -e '${command}'`)
|
||||
} catch (error) {
|
||||
console.error(`cli: failed to install cli: ${error.message}`)
|
||||
return
|
||||
}
|
||||
await exec(`osascript -e '${command}'`)
|
||||
}
|
||||
|
||||
72
cmd/cmd.go
@@ -36,15 +36,32 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var spinner *Spinner
|
||||
|
||||
var currentDigest string
|
||||
var bar *progressbar.ProgressBar
|
||||
|
||||
request := api.CreateRequest{Name: args[0], Path: filename}
|
||||
fn := func(resp api.CreateProgress) error {
|
||||
if spinner != nil {
|
||||
spinner.Stop()
|
||||
fn := func(resp api.ProgressResponse) error {
|
||||
if resp.Digest != currentDigest && resp.Digest != "" {
|
||||
if spinner != nil {
|
||||
spinner.Stop()
|
||||
}
|
||||
currentDigest = resp.Digest
|
||||
bar = progressbar.DefaultBytes(
|
||||
int64(resp.Total),
|
||||
fmt.Sprintf("pulling %s...", resp.Digest[7:19]),
|
||||
)
|
||||
|
||||
bar.Set(resp.Completed)
|
||||
} else if resp.Digest == currentDigest && resp.Digest != "" {
|
||||
bar.Set(resp.Completed)
|
||||
} else {
|
||||
currentDigest = ""
|
||||
if spinner != nil {
|
||||
spinner.Stop()
|
||||
}
|
||||
spinner = NewSpinner(resp.Status)
|
||||
go spinner.Spin(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
spinner = NewSpinner(resp.Status)
|
||||
go spinner.Spin(100 * time.Millisecond)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -94,9 +111,25 @@ func PushHandler(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var currentDigest string
|
||||
var bar *progressbar.ProgressBar
|
||||
|
||||
request := api.PushRequest{Name: args[0], Insecure: insecure}
|
||||
fn := func(resp api.ProgressResponse) error {
|
||||
fmt.Println(resp.Status)
|
||||
if resp.Digest != currentDigest && resp.Digest != "" {
|
||||
currentDigest = resp.Digest
|
||||
bar = progressbar.DefaultBytes(
|
||||
int64(resp.Total),
|
||||
fmt.Sprintf("pushing %s...", resp.Digest[7:19]),
|
||||
)
|
||||
|
||||
bar.Set(resp.Completed)
|
||||
} else if resp.Digest == currentDigest && resp.Digest != "" {
|
||||
bar.Set(resp.Completed)
|
||||
} else {
|
||||
currentDigest = ""
|
||||
fmt.Println(resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -139,14 +172,25 @@ func ListHandler(cmd *cobra.Command, args []string) error {
|
||||
func DeleteHandler(cmd *cobra.Command, args []string) error {
|
||||
client := api.NewClient()
|
||||
|
||||
request := api.DeleteRequest{Name: args[0]}
|
||||
if err := client.Delete(context.Background(), &request); err != nil {
|
||||
req := api.DeleteRequest{Name: args[0]}
|
||||
if err := client.Delete(context.Background(), &req); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("deleted '%s'\n", args[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyHandler(cmd *cobra.Command, args []string) error {
|
||||
client := api.NewClient()
|
||||
|
||||
req := api.CopyRequest{Source: args[0], Destination: args[1]}
|
||||
if err := client.Copy(context.Background(), &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 {
|
||||
@@ -454,6 +498,13 @@ func NewCLI() *cobra.Command {
|
||||
RunE: ListHandler,
|
||||
}
|
||||
|
||||
copyCmd := &cobra.Command{
|
||||
Use: "cp",
|
||||
Short: "Copy a model",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
RunE: CopyHandler,
|
||||
}
|
||||
|
||||
deleteCmd := &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: "Remove a model",
|
||||
@@ -468,6 +519,7 @@ func NewCLI() *cobra.Command {
|
||||
pullCmd,
|
||||
pushCmd,
|
||||
listCmd,
|
||||
copyCmd,
|
||||
deleteCmd,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ Enable CGO:
|
||||
export CGO_ENABLED=1
|
||||
```
|
||||
|
||||
You will also need a C/C++ compiler such as GCC for MacOS and Linux or Mingw-w64 GCC for Windows.
|
||||
|
||||
Then build ollama:
|
||||
|
||||
```
|
||||
|
||||
8
examples/devops-engineer/Modelfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# Modelfile for creating a devops engineer assistant
|
||||
# Run `ollama create devops-engineer -f ./Modelfile` and then `ollama run devops-engineer` and enter a topic
|
||||
|
||||
FROM llama2:13b
|
||||
PARAMETER temperature 1
|
||||
SYSTEM """
|
||||
You are a senior devops engineer, acting as an assistant. You offer help with cloud technologies like: Terraform, AWS, kubernetes, python. You answer with code examples when possible
|
||||
"""
|
||||
@@ -1,9 +1,9 @@
|
||||
package llama
|
||||
|
||||
/*
|
||||
#cgo CPPFLAGS: -O3 -DNDEBUG=1
|
||||
#cgo CPPFLAGS: -O3 -DNDEBUG=1 -DGGML_USE_K_QUANTS
|
||||
#cgo CXXFLAGS: -std=c++11
|
||||
#cgo darwin CPPFLAGS: -DGGML_USE_METAL=1 -DGGML_METAL_NDEBUG=1
|
||||
#cgo darwin CPPFLAGS: -DGGML_USE_ACCELERATE -DGGML_USE_METAL -DGGML_METAL_NDEBUG
|
||||
#cgo darwin LDFLAGS: -framework Accelerate -framework Foundation -framework Metal -framework MetalKit -framework MetalPerformanceShaders
|
||||
#include <stdlib.h>
|
||||
#include "llama.h"
|
||||
@@ -168,7 +168,7 @@ func (llm *llama) tokenize(prompt string) []C.llama_token {
|
||||
cPrompt := C.CString(prompt)
|
||||
defer C.free(unsafe.Pointer(cPrompt))
|
||||
|
||||
tokens := make([]C.llama_token, llm.NumCtx)
|
||||
tokens := make([]C.llama_token, len(prompt)+1)
|
||||
if n := C.llama_tokenize(llm.ctx, cPrompt, unsafe.SliceData(tokens), C.int(len(tokens)), true); n > 0 {
|
||||
return tokens[:n]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
@@ -20,7 +20,6 @@ func (c *Command) Reset() {
|
||||
|
||||
func Parse(reader io.Reader) ([]Command, error) {
|
||||
var commands []Command
|
||||
|
||||
var command, modelCommand Command
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
@@ -29,7 +28,7 @@ func Parse(reader io.Reader) ([]Command, error) {
|
||||
line := scanner.Bytes()
|
||||
|
||||
fields := bytes.SplitN(line, []byte(" "), 2)
|
||||
if len(fields) == 0 {
|
||||
if len(fields) == 0 || len(fields[0]) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -47,6 +46,8 @@ func Parse(reader io.Reader) ([]Command, error) {
|
||||
command.Name = string(fields[0])
|
||||
command.Args = string(fields[1])
|
||||
default:
|
||||
// log a warning for unknown commands
|
||||
log.Printf("WARNING: Unknown command: %s", fields[0])
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -55,27 +56,32 @@ func Parse(reader io.Reader) ([]Command, error) {
|
||||
}
|
||||
|
||||
if modelCommand.Args == "" {
|
||||
return nil, fmt.Errorf("no FROM line for the model was specified")
|
||||
return nil, errors.New("no FROM line for the model was specified")
|
||||
}
|
||||
|
||||
return commands, scanner.Err()
|
||||
}
|
||||
|
||||
func scanModelfile(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
const multilineString = `"""`
|
||||
|
||||
newline := bytes.IndexByte(data, '\n')
|
||||
|
||||
if start := bytes.Index(data, []byte(`"""`)); start >= 0 && start < newline {
|
||||
end := bytes.Index(data[start+3:], []byte(`"""`))
|
||||
if start := bytes.Index(data, []byte(multilineString)); start >= 0 && start < newline {
|
||||
end := bytes.Index(data[start+len(multilineString):], []byte(multilineString))
|
||||
if end < 0 {
|
||||
if atEOF {
|
||||
return 0, nil, errors.New(`unterminated multiline string: """`)
|
||||
return 0, nil, errors.New("unterminated multiline string: " + multilineString)
|
||||
} else {
|
||||
return 0, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
n := start + 3 + end + 3
|
||||
return n, bytes.Replace(data[:n], []byte(`"""`), []byte(""), 2), nil
|
||||
n := start + len(multilineString) + end + len(multilineString)
|
||||
|
||||
newData := data[:start]
|
||||
newData = append(newData, data[start+len(multilineString):n-len(multilineString)]...)
|
||||
return n, newData, nil
|
||||
}
|
||||
|
||||
return bufio.ScanLines(data, atEOF)
|
||||
|
||||
@@ -17,15 +17,16 @@ npm --prefix app run make:sign
|
||||
# Create a new tag if it doesn't exist.
|
||||
if ! git rev-parse v$VERSION >/dev/null 2>&1; then
|
||||
git tag v$VERSION
|
||||
git push origin v$VERSION
|
||||
fi
|
||||
|
||||
git push origin v$VERSION
|
||||
|
||||
mkdir -p dist
|
||||
cp app/out/make/zip/${OS}/${ARCH}/Ollama-${OS}-${ARCH}-${VERSION}.zip dist/Ollama-${OS}-${ARCH}.zip
|
||||
cp ./ollama dist/ollama-${OS}-${ARCH}
|
||||
|
||||
# Create a new release.
|
||||
gh release create v$VERSION
|
||||
gh release create -p v$VERSION -t v$VERSION
|
||||
|
||||
# Upload the zip file.
|
||||
gh release upload v$VERSION ./dist/Ollama-${OS}-${ARCH}.zip
|
||||
|
||||
263
server/images.go
@@ -187,15 +187,15 @@ func GetModel(name string) (*Model, error) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func CreateModel(name string, path string, fn func(status string)) error {
|
||||
func CreateModel(name string, path string, fn func(resp api.ProgressResponse)) error {
|
||||
mf, err := os.Open(path)
|
||||
if err != nil {
|
||||
fn(fmt.Sprintf("couldn't open modelfile '%s'", path))
|
||||
fn(api.ProgressResponse{Status: fmt.Sprintf("couldn't open modelfile '%s'", path)})
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer mf.Close()
|
||||
|
||||
fn("parsing modelfile")
|
||||
fn(api.ProgressResponse{Status: "parsing modelfile"})
|
||||
commands, err := parser.Parse(mf)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -208,7 +208,7 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
log.Printf("[%s] - %s\n", c.Name, c.Args)
|
||||
switch c.Name {
|
||||
case "model":
|
||||
fn("looking for model")
|
||||
fn(api.ProgressResponse{Status: "looking for model"})
|
||||
mf, err := GetManifest(ParseModelPath(c.Args))
|
||||
if err != nil {
|
||||
fp := c.Args
|
||||
@@ -229,20 +229,40 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
fp = filepath.Join(filepath.Dir(path), fp)
|
||||
}
|
||||
|
||||
fn("creating model layer")
|
||||
file, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := os.Stat(fp); err != nil {
|
||||
// the model file does not exist, try pulling it
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
fn(api.ProgressResponse{Status: "pulling model file"})
|
||||
if err := PullModel(c.Args, &RegistryOptions{}, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
mf, err = GetManifest(ParseModelPath(c.Args))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file after pull: %v", err)
|
||||
}
|
||||
|
||||
l, err := CreateLayer(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create layer: %v", err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// create a model from this specified file
|
||||
fn(api.ProgressResponse{Status: "creating model layer"})
|
||||
|
||||
file, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
l, err := CreateLayer(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create layer: %v", err)
|
||||
}
|
||||
l.MediaType = "application/vnd.ollama.image.model"
|
||||
layers = append(layers, l)
|
||||
}
|
||||
l.MediaType = "application/vnd.ollama.image.model"
|
||||
layers = append(layers, l)
|
||||
} else {
|
||||
}
|
||||
if mf != nil {
|
||||
log.Printf("manifest = %#v", mf)
|
||||
for _, l := range mf.Layers {
|
||||
newLayer, err := GetLayerWithBufferFromLayer(l)
|
||||
@@ -253,7 +273,7 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
}
|
||||
}
|
||||
case "license", "template", "system", "prompt":
|
||||
fn(fmt.Sprintf("creating %s layer", c.Name))
|
||||
fn(api.ProgressResponse{Status: fmt.Sprintf("creating model %s layer", c.Name)})
|
||||
// remove the prompt layer if one exists
|
||||
mediaType := fmt.Sprintf("application/vnd.ollama.image.%s", c.Name)
|
||||
layers = removeLayerFromLayers(layers, mediaType)
|
||||
@@ -272,7 +292,7 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
|
||||
// Create a single layer for the parameters
|
||||
if len(params) > 0 {
|
||||
fn("creating parameter layer")
|
||||
fn(api.ProgressResponse{Status: "creating parameter layer"})
|
||||
layers = removeLayerFromLayers(layers, "application/vnd.ollama.image.params")
|
||||
paramData, err := paramsToReader(params)
|
||||
if err != nil {
|
||||
@@ -297,7 +317,7 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
}
|
||||
|
||||
// Create a layer for the config object
|
||||
fn("creating config layer")
|
||||
fn(api.ProgressResponse{Status: "creating config layer"})
|
||||
cfg, err := createConfigLayer(digests)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -310,13 +330,13 @@ func CreateModel(name string, path string, fn func(status string)) error {
|
||||
}
|
||||
|
||||
// Create the manifest
|
||||
fn("writing manifest")
|
||||
fn(api.ProgressResponse{Status: "writing manifest"})
|
||||
err = CreateManifest(name, cfg, manifestLayers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn("success")
|
||||
fn(api.ProgressResponse{Status: "success"})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -331,7 +351,7 @@ func removeLayerFromLayers(layers []*LayerReader, mediaType string) []*LayerRead
|
||||
return layers[:j]
|
||||
}
|
||||
|
||||
func SaveLayers(layers []*LayerReader, fn func(status string), force bool) error {
|
||||
func SaveLayers(layers []*LayerReader, fn func(resp api.ProgressResponse), force bool) error {
|
||||
// Write each of the layers to disk
|
||||
for _, layer := range layers {
|
||||
fp, err := GetBlobsPath(layer.Digest)
|
||||
@@ -341,7 +361,8 @@ func SaveLayers(layers []*LayerReader, fn func(status string), force bool) error
|
||||
|
||||
_, err = os.Stat(fp)
|
||||
if os.IsNotExist(err) || force {
|
||||
fn(fmt.Sprintf("writing layer %s", layer.Digest))
|
||||
fn(api.ProgressResponse{Status: fmt.Sprintf("writing layer %s", layer.Digest)})
|
||||
|
||||
out, err := os.Create(fp)
|
||||
if err != nil {
|
||||
log.Printf("couldn't create %s", fp)
|
||||
@@ -354,7 +375,7 @@ func SaveLayers(layers []*LayerReader, fn func(status string), force bool) error
|
||||
}
|
||||
|
||||
} else {
|
||||
fn(fmt.Sprintf("using already created layer %s", layer.Digest))
|
||||
fn(api.ProgressResponse{Status: fmt.Sprintf("using already created layer %s", layer.Digest)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +514,32 @@ func CreateLayer(f io.ReadSeeker) (*LayerReader, error) {
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
func CopyModel(src, dest string) error {
|
||||
srcPath, err := ParseModelPath(src).GetManifestPath(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath, err := ParseModelPath(dest).GetManifestPath(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy the file
|
||||
input, err := ioutil.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading file:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(destPath, input, 0o644)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading file:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteModel(name string) error {
|
||||
mp := ParseModelPath(name)
|
||||
|
||||
@@ -582,14 +629,10 @@ func PushModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
}
|
||||
|
||||
var layers []*Layer
|
||||
var total int
|
||||
var completed int
|
||||
for _, layer := range manifest.Layers {
|
||||
layers = append(layers, layer)
|
||||
total += layer.Size
|
||||
}
|
||||
layers = append(layers, &manifest.Config)
|
||||
total += manifest.Config.Size
|
||||
|
||||
for _, layer := range layers {
|
||||
exists, err := checkBlobExistence(mp, layer.Digest, regOpts)
|
||||
@@ -598,21 +641,20 @@ func PushModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
}
|
||||
|
||||
if exists {
|
||||
completed += layer.Size
|
||||
fn(api.ProgressResponse{
|
||||
Status: "using existing layer",
|
||||
Digest: layer.Digest,
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
Total: layer.Size,
|
||||
Completed: layer.Size,
|
||||
})
|
||||
log.Printf("Layer %s already exists", layer.Digest)
|
||||
continue
|
||||
}
|
||||
|
||||
fn(api.ProgressResponse{
|
||||
Status: "starting upload",
|
||||
Digest: layer.Digest,
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
Status: "starting upload",
|
||||
Digest: layer.Digest,
|
||||
Total: layer.Size,
|
||||
})
|
||||
|
||||
location, err := startUpload(mp, regOpts)
|
||||
@@ -621,25 +663,14 @@ func PushModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
return err
|
||||
}
|
||||
|
||||
err = uploadBlob(location, layer, regOpts)
|
||||
err = uploadBlobChunked(mp, location, layer, regOpts, fn)
|
||||
if err != nil {
|
||||
log.Printf("error uploading blob: %v", err)
|
||||
return err
|
||||
}
|
||||
completed += layer.Size
|
||||
fn(api.ProgressResponse{
|
||||
Status: "upload complete",
|
||||
Digest: layer.Digest,
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
})
|
||||
}
|
||||
|
||||
fn(api.ProgressResponse{
|
||||
Status: "pushing manifest",
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
})
|
||||
fn(api.ProgressResponse{Status: "pushing manifest"})
|
||||
url := fmt.Sprintf("%s/v2/%s/manifests/%s", mp.Registry, mp.GetNamespaceRepository(), mp.Tag)
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
@@ -659,14 +690,10 @@ func PushModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
// Check for success: For a successful upload, the Docker registry will respond with a 201 Created
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
return fmt.Errorf("on push registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
fn(api.ProgressResponse{
|
||||
Status: "success",
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
})
|
||||
fn(api.ProgressResponse{Status: "success"})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -678,7 +705,7 @@ func PullModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
|
||||
manifest, err := pullModelManifest(mp, regOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pull model manifest: %q", err)
|
||||
return fmt.Errorf("pull model manifest: %s", err)
|
||||
}
|
||||
|
||||
var layers []*Layer
|
||||
@@ -694,6 +721,17 @@ func PullModel(name string, regOpts *RegistryOptions, fn func(api.ProgressRespon
|
||||
fn(api.ProgressResponse{Status: "verifying sha256 digest"})
|
||||
for _, layer := range layers {
|
||||
if err := verifyBlob(layer.Digest); err != nil {
|
||||
if errors.Is(err, errDigestMismatch) {
|
||||
// something went wrong, delete the blob
|
||||
fp, err := GetBlobsPath(layer.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(fp); err != nil {
|
||||
// log this, but return the original error
|
||||
log.Printf("couldn't remove file with digest mismatch '%s': %v", fp, err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -736,8 +774,11 @@ func pullModelManifest(mp ModelPath, regOpts *RegistryOptions) (*ManifestV2, err
|
||||
|
||||
// Check for success: For a successful upload, the Docker registry will respond with a 201 Created
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("model not found")
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("registry responded with code %d: %s", resp.StatusCode, body)
|
||||
return nil, fmt.Errorf("on pull registry responded with code %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var m *ManifestV2
|
||||
@@ -801,7 +842,7 @@ func startUpload(mp ModelPath, regOpts *RegistryOptions) (string, error) {
|
||||
// Check for success
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("registry responded with code %d: %s", resp.StatusCode, body)
|
||||
return "", fmt.Errorf("on upload registry responded with code %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Extract UUID location from header
|
||||
@@ -828,19 +869,14 @@ func checkBlobExistence(mp ModelPath, digest string, regOpts *RegistryOptions) (
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
|
||||
func uploadBlob(location string, layer *Layer, regOpts *RegistryOptions) error {
|
||||
// Create URL
|
||||
url := fmt.Sprintf("%s&digest=%s", location, layer.Digest)
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Length"] = fmt.Sprintf("%d", layer.Size)
|
||||
headers["Content-Type"] = "application/octet-stream"
|
||||
|
||||
// TODO change from monolithic uploads to chunked uploads
|
||||
func uploadBlobChunked(mp ModelPath, location string, layer *Layer, regOpts *RegistryOptions, fn func(api.ProgressResponse)) error {
|
||||
// TODO allow resumability
|
||||
// TODO allow canceling uploads via DELETE
|
||||
// TODO allow cross repo blob mount
|
||||
|
||||
// Create URL
|
||||
url := fmt.Sprintf("%s", location)
|
||||
|
||||
fp, err := GetBlobsPath(layer.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -851,19 +887,72 @@ func uploadBlob(location string, layer *Layer, regOpts *RegistryOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := makeRequest("PUT", url, headers, f, regOpts)
|
||||
if err != nil {
|
||||
log.Printf("couldn't upload blob: %v", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Type"] = "application/octet-stream"
|
||||
|
||||
// Check for success: For a successful upload, the Docker registry will respond with a 201 Created
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
chunkSize := 1 << 20
|
||||
buf := make([]byte, chunkSize)
|
||||
var totalUploaded int
|
||||
|
||||
for {
|
||||
n, err := f.Read(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers["Content-Length"] = fmt.Sprintf("%d", n)
|
||||
headers["Content-Range"] = fmt.Sprintf("%d-%d", totalUploaded, totalUploaded+n-1)
|
||||
|
||||
fn(api.ProgressResponse{
|
||||
Status: fmt.Sprintf("uploading %s", layer.Digest),
|
||||
Digest: layer.Digest,
|
||||
Total: int(layer.Size),
|
||||
Completed: int(totalUploaded),
|
||||
})
|
||||
|
||||
// change the buffersize for the last chunk
|
||||
if n < chunkSize {
|
||||
buf = buf[:n]
|
||||
}
|
||||
resp, err := makeRequest("PATCH", url, headers, bytes.NewReader(buf), regOpts)
|
||||
if err != nil {
|
||||
log.Printf("couldn't upload blob: %v", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
url = resp.Header.Get("Location")
|
||||
|
||||
// Check for success: For a successful upload, the Docker registry will respond with a 201 Created
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
fn(api.ProgressResponse{
|
||||
Status: fmt.Sprintf("error uploading layer"),
|
||||
Digest: layer.Digest,
|
||||
Total: int(layer.Size),
|
||||
Completed: int(totalUploaded),
|
||||
})
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("on layer upload registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
totalUploaded += n
|
||||
if totalUploaded >= layer.Size {
|
||||
url = fmt.Sprintf("%s&digest=%s", url, layer.Digest)
|
||||
|
||||
// finish the upload
|
||||
resp, err := makeRequest("PUT", url, nil, nil, regOpts)
|
||||
if err != nil {
|
||||
log.Printf("couldn't finish upload: %v", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("on finish upload registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -885,6 +974,7 @@ func downloadBlob(mp ModelPath, digest string, regOpts *RegistryOptions, fn func
|
||||
}
|
||||
|
||||
var size int64
|
||||
chunkSize := 1024 * 1024 // 1 MiB in bytes
|
||||
|
||||
fi, err := os.Stat(fp + "-partial")
|
||||
switch {
|
||||
@@ -894,6 +984,13 @@ func downloadBlob(mp ModelPath, digest string, regOpts *RegistryOptions, fn func
|
||||
return fmt.Errorf("stat: %w", err)
|
||||
default:
|
||||
size = fi.Size()
|
||||
// Ensure the size is divisible by the chunk size by removing excess bytes
|
||||
size -= size % int64(chunkSize)
|
||||
|
||||
err := os.Truncate(fp+"-partial", size)
|
||||
if err != nil {
|
||||
return fmt.Errorf("truncate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2/%s/blobs/%s", mp.Registry, mp.GetNamespaceRepository(), digest)
|
||||
@@ -910,7 +1007,7 @@ func downloadBlob(mp ModelPath, digest string, regOpts *RegistryOptions, fn func
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
return fmt.Errorf("on download registry responded with code %d: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
err = os.MkdirAll(path.Dir(fp), 0o700)
|
||||
@@ -920,7 +1017,7 @@ func downloadBlob(mp ModelPath, digest string, regOpts *RegistryOptions, fn func
|
||||
|
||||
out, err := os.OpenFile(fp+"-partial", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
@@ -954,7 +1051,7 @@ func downloadBlob(mp ModelPath, digest string, regOpts *RegistryOptions, fn func
|
||||
break
|
||||
}
|
||||
|
||||
n, err := io.CopyN(out, resp.Body, 8192)
|
||||
n, err := io.CopyN(out, resp.Body, int64(chunkSize))
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
@@ -974,8 +1071,6 @@ func makeRequest(method, url string, headers map[string]string, body io.Reader,
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("url = %s", url)
|
||||
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1007,6 +1102,8 @@ func makeRequest(method, url string, headers map[string]string, body io.Reader,
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var errDigestMismatch = fmt.Errorf("digest mismatch, file must be downloaded again")
|
||||
|
||||
func verifyBlob(digest string) error {
|
||||
fp, err := GetBlobsPath(digest)
|
||||
if err != nil {
|
||||
@@ -1021,7 +1118,7 @@ func verifyBlob(digest string) error {
|
||||
|
||||
fileDigest, _ := GetSHA256Digest(f)
|
||||
if digest != fileDigest {
|
||||
return fmt.Errorf("digest mismatch: want %s, got %s", digest, fileDigest)
|
||||
return fmt.Errorf("%w: want %s, got %s", errDigestMismatch, digest, fileDigest)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -147,10 +147,8 @@ func CreateModelHandler(c *gin.Context) {
|
||||
ch := make(chan any)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
fn := func(status string) {
|
||||
ch <- api.CreateProgress{
|
||||
Status: status,
|
||||
}
|
||||
fn := func(resp api.ProgressResponse) {
|
||||
ch <- resp
|
||||
}
|
||||
|
||||
if err := CreateModel(req.Name, req.Path, fn); err != nil {
|
||||
@@ -228,6 +226,23 @@ func ListModelsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, api.ListResponse{models})
|
||||
}
|
||||
|
||||
func CopyModelHandler(c *gin.Context) {
|
||||
var req api.CopyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := CopyModel(req.Source, req.Destination); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("model '%s' not found", req.Source)})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Serve(ln net.Listener) error {
|
||||
config := cors.DefaultConfig()
|
||||
config.AllowWildcard = true
|
||||
@@ -254,6 +269,7 @@ func Serve(ln net.Listener) error {
|
||||
r.POST("/api/generate", GenerateHandler)
|
||||
r.POST("/api/create", CreateModelHandler)
|
||||
r.POST("/api/push", PushModelHandler)
|
||||
r.POST("/api/copy", CopyModelHandler)
|
||||
r.GET("/api/tags", ListModelsHandler)
|
||||
r.DELETE("/api/delete", DeleteModelHandler)
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ export async function GET(req: Request) {
|
||||
const res = await fetch('https://api.github.com/repos/jmorganca/ollama/releases', { next: { revalidate: 60 } })
|
||||
const data = await res.json()
|
||||
|
||||
if (data.length === 0) {
|
||||
const latest = data?.filter((f: any) => !f.prerelease)?.[0]
|
||||
|
||||
if (!latest) {
|
||||
return new Response('not found', { status: 404 })
|
||||
}
|
||||
|
||||
const latest = data[0]
|
||||
const assets = latest.assets || []
|
||||
|
||||
if (assets.length === 0) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link"
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Discord', href: 'https://discord.gg/MrfB5FbNWN' },
|
||||
{ name: 'Github', href: 'https://github.com/jmorganca/ollama' },
|
||||
{ name: 'GitHub', href: 'https://github.com/jmorganca/ollama' },
|
||||
{ name: 'Download', href: '/download' },
|
||||
]
|
||||
|
||||
@@ -21,6 +21,6 @@ export default function Header() {
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</header >
|
||||
</header>
|
||||
)
|
||||
}
|
||||