Compare commits
37 Commits
v0.0.5
...
list-model
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5595259996 | ||
![]() |
4a28a2f093 | ||
![]() |
1f45f7bb52 | ||
![]() |
2e6c64a8f9 | ||
![]() |
c7dd52271c | ||
![]() |
53d0052c6c | ||
![]() |
28a136e9a3 | ||
![]() |
529ff9ab6d | ||
![]() |
41aca47d43 | ||
![]() |
3862a51a6a | ||
![]() |
bcb612a30a | ||
![]() |
c05219aa0d | ||
![]() |
508ffbbb15 | ||
![]() |
59fa93cdd4 | ||
![]() |
952abe029b | ||
![]() |
f923855906 | ||
![]() |
9386073e96 | ||
![]() |
52ea4d4bb2 | ||
![]() |
c4ba192187 | ||
![]() |
fe758ca319 | ||
![]() |
08b933cc10 | ||
![]() |
6746a00af8 | ||
![]() |
2fb52261ad | ||
![]() |
6fdea03049 | ||
![]() |
38021ba494 | ||
![]() |
6c9fa573ae | ||
![]() |
40c9dc0a31 | ||
![]() |
0142660bd4 | ||
![]() |
743e957d88 | ||
![]() |
560f36e6c8 | ||
![]() |
e88dd25bab | ||
![]() |
567e74e7d7 | ||
![]() |
5ade3db040 | ||
![]() |
965f9ad033 | ||
![]() |
5d1c6b7499 | ||
![]() |
5fefaa5d4d | ||
![]() |
1775647f76 |
@@ -16,7 +16,7 @@ Run large language models with `llama.cpp`.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
- [Download](https://ollama.ai/download) for macOS
|
- [Download](https://ollama.ai/download) for macOS with Apple Silicon (Intel coming soon)
|
||||||
- Download for Windows (coming soon)
|
- Download for Windows (coming soon)
|
||||||
|
|
||||||
You can also build the [binary from source](#building).
|
You can also build the [binary from source](#building).
|
||||||
|
112
api/client.go
112
api/client.go
@@ -6,26 +6,31 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatusError struct {
|
type Client struct {
|
||||||
StatusCode int
|
base url.URL
|
||||||
Status string
|
HTTP http.Client
|
||||||
Message string
|
Headers http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e StatusError) Error() string {
|
func checkError(resp *http.Response, body []byte) error {
|
||||||
if e.Message != "" {
|
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||||
return fmt.Sprintf("%s: %s", e.Status, e.Message)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.Status
|
apiError := StatusError{StatusCode: resp.StatusCode}
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
err := json.Unmarshal(body, &apiError)
|
||||||
base url.URL
|
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 {
|
func NewClient(hosts ...string) *Client {
|
||||||
@@ -36,9 +41,60 @@ func NewClient(hosts ...string) *Client {
|
|||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
base: url.URL{Scheme: "http", Host: host},
|
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 {
|
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
|
||||||
var buf *bytes.Buffer
|
var buf *bytes.Buffer
|
||||||
if data != nil {
|
if data != nil {
|
||||||
@@ -116,3 +172,37 @@ func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc
|
|||||||
return fn(resp)
|
return fn(resp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PushProgressFunc func(PushProgress) error
|
||||||
|
|
||||||
|
func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc) error {
|
||||||
|
return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error {
|
||||||
|
var resp PushProgress
|
||||||
|
if err := json.Unmarshal(bts, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProgressFunc func(CreateProgress) 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
|
||||||
|
if err := json.Unmarshal(bts, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
74
api/types.go
74
api/types.go
@@ -7,29 +7,81 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PullRequest struct {
|
type StatusError struct {
|
||||||
Model string `json:"model"`
|
StatusCode int
|
||||||
|
Status string
|
||||||
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PullProgress struct {
|
func (e StatusError) Error() string {
|
||||||
Total int64 `json:"total"`
|
if e.Message != "" {
|
||||||
Completed int64 `json:"completed"`
|
return fmt.Sprintf("%s: %s", e.Status, e.Message)
|
||||||
Percent float64 `json:"percent"`
|
}
|
||||||
|
return e.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenerateRequest struct {
|
type GenerateRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
|
Context []int `json:"context,omitempty"`
|
||||||
|
|
||||||
Options `json:"options"`
|
Options `json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProgress struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PullRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PullProgress struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Digest string `json:"digest,omitempty"`
|
||||||
|
Total int `json:"total,omitempty"`
|
||||||
|
Completed int `json:"completed,omitempty"`
|
||||||
|
Percent float64 `json:"percent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushProgress struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Digest string `json:"digest,omitempty"`
|
||||||
|
Total int `json:"total,omitempty"`
|
||||||
|
Completed int `json:"completed,omitempty"`
|
||||||
|
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 {
|
type GenerateResponse struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Response string `json:"response,omitempty"`
|
Response string `json:"response,omitempty"`
|
||||||
|
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
|
Context []int `json:"context,omitempty"`
|
||||||
|
|
||||||
TotalDuration time.Duration `json:"total_duration,omitempty"`
|
TotalDuration time.Duration `json:"total_duration,omitempty"`
|
||||||
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
||||||
@@ -57,7 +109,7 @@ func (r *GenerateResponse) Summary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.EvalDuration > 0 {
|
if r.EvalDuration > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "eval duraiton: %s\n", r.EvalDuration)
|
fmt.Fprintf(os.Stderr, "eval duration: %s\n", r.EvalDuration)
|
||||||
fmt.Fprintf(os.Stderr, "eval rate: %.2f tokens/s\n", float64(r.EvalCount)/r.EvalDuration.Seconds())
|
fmt.Fprintf(os.Stderr, "eval rate: %.2f tokens/s\n", float64(r.EvalCount)/r.EvalDuration.Seconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +156,7 @@ func DefaultOptions() Options {
|
|||||||
|
|
||||||
UseNUMA: false,
|
UseNUMA: false,
|
||||||
|
|
||||||
NumCtx: 512,
|
NumCtx: 2048,
|
||||||
NumBatch: 512,
|
NumBatch: 512,
|
||||||
NumGPU: 1,
|
NumGPU: 1,
|
||||||
LowVRAM: false,
|
LowVRAM: false,
|
||||||
|
@@ -58,7 +58,7 @@ const config: ForgeConfig = {
|
|||||||
new AutoUnpackNativesPlugin({}),
|
new AutoUnpackNativesPlugin({}),
|
||||||
new WebpackPlugin({
|
new WebpackPlugin({
|
||||||
mainConfig,
|
mainConfig,
|
||||||
devContentSecurityPolicy: `default-src * 'unsafe-eval' 'unsafe-inline'`,
|
devContentSecurityPolicy: `default-src * 'unsafe-eval' 'unsafe-inline'; img-src data: 'self'`,
|
||||||
renderer: {
|
renderer: {
|
||||||
config: rendererConfig,
|
config: rendererConfig,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
2274
app/package-lock.json
generated
2274
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@
|
|||||||
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
|
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
|
||||||
"@electron-forge/plugin-webpack": "^6.2.1",
|
"@electron-forge/plugin-webpack": "^6.2.1",
|
||||||
"@electron-forge/publisher-github": "^6.2.1",
|
"@electron-forge/publisher-github": "^6.2.1",
|
||||||
|
"@svgr/webpack": "^8.0.1",
|
||||||
"@types/chmodr": "^1.0.0",
|
"@types/chmodr": "^1.0.0",
|
||||||
"@types/node": "^20.4.0",
|
"@types/node": "^20.4.0",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
@@ -54,17 +55,21 @@
|
|||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
|
"svg-inline-loader": "^0.8.2",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "~4.5.4",
|
"typescript": "~4.5.4",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.88.0",
|
"webpack": "^5.88.0",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^2.0.10",
|
"@electron/remote": "^2.0.10",
|
||||||
|
"@heroicons/react": "^2.0.18",
|
||||||
"@segment/analytics-node": "^1.0.0",
|
"@segment/analytics-node": "^1.0.0",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@@ -11,6 +11,10 @@ body {
|
|||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-drag {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
.blink {
|
.blink {
|
||||||
-webkit-animation: 1s blink step-end infinite;
|
-webkit-animation: 1s blink step-end infinite;
|
||||||
-moz-animation: 1s blink step-end infinite;
|
-moz-animation: 1s blink step-end infinite;
|
||||||
|
239
app/src/app.tsx
239
app/src/app.tsx
@@ -1,158 +1,109 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import path from 'path'
|
import copy from 'copy-to-clipboard'
|
||||||
import os from 'os'
|
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline'
|
||||||
import { dialog, getCurrentWindow } from '@electron/remote'
|
import Store from 'electron-store'
|
||||||
|
import { getCurrentWindow } from '@electron/remote'
|
||||||
|
|
||||||
const API_URL = 'http://127.0.0.1:7734'
|
import { install } from './install'
|
||||||
|
import OllamaIcon from './ollama.svg'
|
||||||
|
|
||||||
type Message = {
|
const store = new Store()
|
||||||
sender: 'bot' | 'human'
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = os.userInfo()
|
enum Step {
|
||||||
|
WELCOME = 0,
|
||||||
async function generate(prompt: string, model: string, callback: (res: string) => void) {
|
CLI,
|
||||||
const result = await fetch(`${API_URL}/generate`, {
|
FINISH,
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt,
|
|
||||||
model,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let reader = result.body.getReader()
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
let decoder = new TextDecoder()
|
|
||||||
let str = decoder.decode(value)
|
|
||||||
|
|
||||||
let re = /}\s*{/g
|
|
||||||
str = '[' + str.replace(re, '},{') + ']'
|
|
||||||
let messages = JSON.parse(str)
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
const choice = message.choices[0]
|
|
||||||
|
|
||||||
callback(choice.text)
|
|
||||||
|
|
||||||
if (choice.finish_reason === 'stop') {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const [prompt, setPrompt] = useState('')
|
const [step, setStep] = useState<Step>(Step.WELCOME)
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [commandCopied, setCommandCopied] = useState<boolean>(false)
|
||||||
const [model, setModel] = useState('')
|
|
||||||
const [generating, setGenerating] = useState(false)
|
const command = 'ollama run orca'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex min-h-screen flex-1 flex-col justify-between bg-white'>
|
<div className='drag'>
|
||||||
<header className='drag sticky top-0 z-50 flex h-14 w-full flex-row items-center border-b border-black/10 bg-white/75 backdrop-blur-md'>
|
<div className='mx-auto flex min-h-screen w-full flex-col justify-between bg-white px-4 pt-16'>
|
||||||
<div className='mx-auto w-full max-w-xl leading-none'>
|
{step === Step.WELCOME && (
|
||||||
<h1 className='text-sm font-medium'>{path.basename(model).replace('.bin', '')}</h1>
|
<>
|
||||||
</div>
|
<div className='mx-auto text-center'>
|
||||||
</header>
|
<h1 className='mb-6 mt-4 text-2xl tracking-tight text-gray-900'>Welcome to Ollama</h1>
|
||||||
{model ? (
|
<p className='mx-auto w-[65%] text-sm text-gray-400'>
|
||||||
<section className='mx-auto mb-10 w-full max-w-xl flex-1 break-words'>
|
Let's get you up and running with your own large language models.
|
||||||
{messages.map((m, i) => (
|
</p>
|
||||||
<div className='my-4 flex gap-4' key={i}>
|
<button
|
||||||
<div className='flex-none pr-1 text-lg'>
|
onClick={() => setStep(Step.CLI)}
|
||||||
{m.sender === 'human' ? (
|
className='no-drag rounded-dm mx-auto my-8 w-[40%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
||||||
<div className='mt-px flex h-6 w-6 items-center justify-center rounded-md bg-neutral-200 text-sm text-neutral-700'>
|
>
|
||||||
{userInfo.username[0].toUpperCase()}
|
Next
|
||||||
</div>
|
</button>
|
||||||
) : (
|
</div>
|
||||||
<div className='mt-0.5 flex h-6 w-6 items-center justify-center rounded-md bg-blue-600 text-sm text-white'>
|
<div className='mx-auto'>
|
||||||
{path.basename(model)[0].toUpperCase()}
|
<OllamaIcon />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
<div className='flex-1 text-gray-800'>
|
{step === Step.CLI && (
|
||||||
{m.content}
|
<>
|
||||||
{m.sender === 'bot' && generating && i === messages.length - 1 && (
|
<div className='mx-auto flex flex-col space-y-28 text-center'>
|
||||||
<span className='blink relative -top-[3px] left-1 text-[10px]'>█</span>
|
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Install the command line</h1>
|
||||||
)}
|
<pre className='mx-auto text-4xl text-gray-400'>> ollama</pre>
|
||||||
|
<div className='mx-auto'>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await install()
|
||||||
|
getCurrentWindow().show()
|
||||||
|
getCurrentWindow().focus()
|
||||||
|
setStep(Step.FINISH)
|
||||||
|
}}
|
||||||
|
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>
|
||||||
|
You will be prompted for administrator access
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</>
|
||||||
</section>
|
)}
|
||||||
) : (
|
{step === Step.FINISH && (
|
||||||
<section className='flex flex-1 select-none flex-col items-center justify-center pb-20'>
|
<>
|
||||||
<h2 className='text-3xl font-light text-neutral-400'>No model selected</h2>
|
<div className='mx-auto flex flex-col space-y-20 text-center'>
|
||||||
<button
|
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Run your first model</h1>
|
||||||
onClick={async () => {
|
<div className='flex flex-col'>
|
||||||
const res = await dialog.showOpenDialog(getCurrentWindow(), {
|
<div className='group relative flex items-center'>
|
||||||
properties: ['openFile', 'multiSelections'],
|
<pre className='language-none text-2xs w-full rounded-md bg-gray-100 px-4 py-3 text-start leading-normal'>
|
||||||
})
|
{command}
|
||||||
if (res.canceled) {
|
</pre>
|
||||||
return
|
<button
|
||||||
}
|
className={`no-drag absolute right-[5px] px-2 py-2 ${commandCopied ? 'text-gray-900 opacity-100 hover:cursor-auto' : 'text-gray-200 opacity-50 hover:cursor-pointer'} hover:text-gray-900 hover:font-bold group-hover:opacity-100`}
|
||||||
|
onClick={() => {
|
||||||
setModel(res.filePaths[0])
|
copy(command)
|
||||||
}}
|
setCommandCopied(true)
|
||||||
className='rounded-dm my-8 rounded-md bg-blue-600 px-4 py-2 text-sm text-white hover:brightness-110'
|
setTimeout(() => setCommandCopied(false), 3000)
|
||||||
>
|
}}
|
||||||
Open file...
|
>
|
||||||
</button>
|
{commandCopied ? (
|
||||||
</section>
|
<CheckIcon className='h-4 w-4 text-gray-500 font-bold' />
|
||||||
)}
|
) : (
|
||||||
<div className='sticky bottom-0 bg-gradient-to-b from-transparent to-white'>
|
<DocumentDuplicateIcon className='h-4 w-4 text-gray-500' />
|
||||||
{model && (
|
)}
|
||||||
<textarea
|
</button>
|
||||||
autoFocus
|
</div>
|
||||||
rows={1}
|
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>Run this command in your favorite terminal.</p>
|
||||||
value={prompt}
|
</div>
|
||||||
placeholder='Send a message...'
|
<button
|
||||||
onChange={e => setPrompt(e.target.value)}
|
onClick={() => {
|
||||||
className='mx-auto my-4 block w-full max-w-xl resize-none rounded-xl border border-gray-200 px-5 py-3.5 text-[15px] shadow-lg shadow-black/5 focus:outline-none'
|
store.set('first-time-run', true)
|
||||||
onKeyDownCapture={async e => {
|
window.close()
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
}}
|
||||||
e.preventDefault()
|
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
||||||
|
>
|
||||||
if (generating) {
|
Finish
|
||||||
return
|
</button>
|
||||||
}
|
</div>
|
||||||
|
</>
|
||||||
if (!prompt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await setMessages(messages => {
|
|
||||||
return [...messages, { sender: 'human', content: prompt }, { sender: 'bot', content: '' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
setPrompt('')
|
|
||||||
|
|
||||||
setGenerating(true)
|
|
||||||
await generate(prompt, model, res => {
|
|
||||||
setMessages(messages => {
|
|
||||||
let last = messages[messages.length - 1]
|
|
||||||
return [...messages.slice(0, messages.length - 1), { ...last, content: last.content + res }]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
setGenerating(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></textarea>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
4
app/src/declarations.d.ts
vendored
Normal file
4
app/src/declarations.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.svg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
114
app/src/index.ts
114
app/src/index.ts
@@ -1,17 +1,20 @@
|
|||||||
import { spawn, exec } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { app, autoUpdater, dialog, Tray, Menu } from 'electron'
|
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow } from 'electron'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
import winston from 'winston'
|
import winston from 'winston'
|
||||||
import 'winston-daily-rotate-file'
|
import 'winston-daily-rotate-file'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
import { analytics, id } from './telemetry'
|
import { analytics, id } from './telemetry'
|
||||||
|
import { installed } from './install'
|
||||||
|
|
||||||
require('@electron/remote/main').initialize()
|
require('@electron/remote/main').initialize()
|
||||||
|
|
||||||
const store = new Store()
|
const store = new Store()
|
||||||
let tray: Tray | null = null
|
let tray: Tray | null = null
|
||||||
|
let welcomeWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
declare const MAIN_WINDOW_WEBPACK_ENTRY: string
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
transports: [
|
transports: [
|
||||||
@@ -22,7 +25,7 @@ const logger = winston.createLogger({
|
|||||||
maxFiles: 5,
|
maxFiles: 5,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
format: winston.format.printf(info => `${info.message}`),
|
format: winston.format.printf(info => info.message),
|
||||||
})
|
})
|
||||||
|
|
||||||
const SingleInstanceLock = app.requestSingleInstanceLock()
|
const SingleInstanceLock = app.requestSingleInstanceLock()
|
||||||
@@ -30,7 +33,39 @@ if (!SingleInstanceLock) {
|
|||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSystemtray = () => {
|
function firstRunWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
welcomeWindow = new BrowserWindow({
|
||||||
|
width: 400,
|
||||||
|
height: 500,
|
||||||
|
frame: false,
|
||||||
|
fullscreenable: false,
|
||||||
|
resizable: false,
|
||||||
|
movable: true,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
alwaysOnTop: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
require('@electron/remote/main').enable(welcomeWindow.webContents)
|
||||||
|
|
||||||
|
// 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 = path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')
|
let iconPath = path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
@@ -49,8 +84,6 @@ if (require('electron-squirrel-startup')) {
|
|||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
const ollama = path.join(process.resourcesPath, 'ollama')
|
|
||||||
|
|
||||||
function server() {
|
function server() {
|
||||||
const binary = app.isPackaged
|
const binary = app.isPackaged
|
||||||
? path.join(process.resourcesPath, 'ollama')
|
? path.join(process.resourcesPath, 'ollama')
|
||||||
@@ -66,66 +99,25 @@ function server() {
|
|||||||
logger.error(data.toString().trim())
|
logger.error(data.toString().trim())
|
||||||
})
|
})
|
||||||
|
|
||||||
proc.on('exit', () => {
|
function restart() {
|
||||||
logger.info('Restarting the server...')
|
logger.info('Restarting the server...')
|
||||||
server()
|
server()
|
||||||
})
|
}
|
||||||
|
|
||||||
proc.on('disconnect', () => {
|
proc.on('exit', restart)
|
||||||
logger.info('Server disconnected. Reconnecting...')
|
|
||||||
server()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
app.on('before-quit', () => {
|
||||||
|
proc.off('exit', restart)
|
||||||
proc.kill()
|
proc.kill()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function installCLI() {
|
if (process.platform === 'darwin') {
|
||||||
const symlinkPath = '/usr/local/bin/ollama'
|
app.dock.hide()
|
||||||
|
|
||||||
if (fs.existsSync(symlinkPath) && fs.readlinkSync(symlinkPath) === ollama) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog
|
|
||||||
.showMessageBox({
|
|
||||||
type: 'info',
|
|
||||||
title: 'Ollama CLI installation',
|
|
||||||
message: 'To make the Ollama command work in your terminal, it needs administrator privileges.',
|
|
||||||
buttons: ['OK'],
|
|
||||||
})
|
|
||||||
.then(result => {
|
|
||||||
if (result.response === 0) {
|
|
||||||
const command = `
|
|
||||||
do shell script "ln -F -s ${ollama} /usr/local/bin/ollama" with administrator privileges
|
|
||||||
`
|
|
||||||
exec(`osascript -e '${command}'`, (error: Error | null, stdout: string, stderr: string) => {
|
|
||||||
if (error) {
|
|
||||||
logger.error(`cli: failed to install cli: ${error.message}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(stdout)
|
|
||||||
logger.error(stderr)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on('ready', () => {
|
app.on('ready', () => {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
app.dock.hide()
|
|
||||||
|
|
||||||
if (!store.has('first-time-run')) {
|
|
||||||
// This is the first run
|
|
||||||
app.setLoginItemSettings({ openAtLogin: true })
|
|
||||||
store.set('first-time-run', false)
|
|
||||||
} else {
|
|
||||||
// The app has been run before
|
|
||||||
app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
if (!app.isInApplicationsFolder()) {
|
if (!app.isInApplicationsFolder()) {
|
||||||
const chosen = dialog.showMessageBoxSync({
|
const chosen = dialog.showMessageBoxSync({
|
||||||
@@ -157,13 +149,21 @@ app.on('ready', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
installCLI()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createSystemtray()
|
createSystemtray()
|
||||||
server()
|
server()
|
||||||
|
|
||||||
|
if (store.get('first-time-run') && installed()) {
|
||||||
|
app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the first run or the CLI is no longer installed
|
||||||
|
app.setLoginItemSettings({ openAtLogin: true })
|
||||||
|
|
||||||
|
firstRunWindow()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
24
app/src/install.ts
Normal file
24
app/src/install.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import { exec as cbExec } from 'child_process'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const app = process && process.type === 'renderer' ? require('@electron/remote').app : require('electron').app
|
||||||
|
const ollama = app.isPackaged ? path.join(process.resourcesPath, 'ollama') : path.resolve(process.cwd(), '..', 'ollama')
|
||||||
|
const exec = promisify(cbExec)
|
||||||
|
const symlinkPath = '/usr/local/bin/ollama'
|
||||||
|
|
||||||
|
export function installed() {
|
||||||
|
return fs.existsSync(symlinkPath) && fs.readlinkSync(symlinkPath) === ollama
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function install() {
|
||||||
|
const command = `do shell script "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
|
||||||
|
}
|
||||||
|
}
|
9
app/src/ollama.svg
Normal file
9
app/src/ollama.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
@@ -28,4 +28,8 @@ export const rules: Required<ModuleOptions>['rules'] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: ['@svgr/webpack'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
163
cmd/cmd.go
163
cmd/cmd.go
@@ -9,15 +9,18 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/schollz/progressbar/v3"
|
"github.com/schollz/progressbar/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/jmorganca/ollama/api"
|
||||||
|
"github.com/jmorganca/ollama/format"
|
||||||
"github.com/jmorganca/ollama/server"
|
"github.com/jmorganca/ollama/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,11 +30,34 @@ func cacheDir() string {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.Join(home, ".ollama")
|
return filepath.Join(home, ".ollama")
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(cmd *cobra.Command, args []string) error {
|
||||||
|
filename, _ := cmd.Flags().GetString("file")
|
||||||
|
client := api.NewClient()
|
||||||
|
|
||||||
|
request := api.CreateRequest{Name: args[0], Path: filename}
|
||||||
|
fn := func(resp api.CreateProgress) error {
|
||||||
|
fmt.Println(resp.Status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Create(context.Background(), &request, fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunRun(cmd *cobra.Command, args []string) error {
|
func RunRun(cmd *cobra.Command, args []string) error {
|
||||||
_, err := os.Stat(args[0])
|
mp := server.ParseModelPath(args[0])
|
||||||
|
fp, err := mp.GetManifestPath(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(fp)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, os.ErrNotExist):
|
case errors.Is(err, os.ErrNotExist):
|
||||||
if err := pull(args[0]); err != nil {
|
if err := pull(args[0]); err != nil {
|
||||||
@@ -51,25 +77,84 @@ func RunRun(cmd *cobra.Command, args []string) error {
|
|||||||
return RunGenerate(cmd, args)
|
return RunGenerate(cmd, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func push(cmd *cobra.Command, args []string) error {
|
||||||
|
client := api.NewClient()
|
||||||
|
|
||||||
|
request := api.PushRequest{Name: args[0]}
|
||||||
|
fn := func(resp api.PushProgress) error {
|
||||||
|
fmt.Println(resp.Status)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Push(context.Background(), &request, fn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
func pull(model string) error {
|
func pull(model string) error {
|
||||||
client := api.NewClient()
|
client := api.NewClient()
|
||||||
|
|
||||||
var bar *progressbar.ProgressBar
|
var bar *progressbar.ProgressBar
|
||||||
return client.Pull(
|
|
||||||
context.Background(),
|
|
||||||
&api.PullRequest{Model: model},
|
|
||||||
func(progress api.PullProgress) error {
|
|
||||||
if bar == nil {
|
|
||||||
if progress.Percent >= 100 {
|
|
||||||
// already downloaded
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
bar = progressbar.DefaultBytes(progress.Total)
|
currentLayer := ""
|
||||||
|
request := api.PullRequest{Name: model}
|
||||||
|
fn := func(resp api.PullProgress) error {
|
||||||
|
if resp.Digest != currentLayer && resp.Digest != "" {
|
||||||
|
if currentLayer != "" {
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
currentLayer = resp.Digest
|
||||||
|
layerStr := resp.Digest[7:23] + "..."
|
||||||
|
bar = progressbar.DefaultBytes(
|
||||||
|
int64(resp.Total),
|
||||||
|
"pulling "+layerStr,
|
||||||
|
)
|
||||||
|
} else if resp.Digest == currentLayer && resp.Digest != "" {
|
||||||
|
bar.Set(resp.Completed)
|
||||||
|
} else {
|
||||||
|
currentLayer = ""
|
||||||
|
fmt.Println(resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return bar.Set64(progress.Completed)
|
if err := client.Pull(context.Background(), &request, fn); err != nil {
|
||||||
},
|
return err
|
||||||
)
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunGenerate(cmd *cobra.Command, args []string) error {
|
func RunGenerate(cmd *cobra.Command, args []string) error {
|
||||||
@@ -85,6 +170,8 @@ func RunGenerate(cmd *cobra.Command, args []string) error {
|
|||||||
return generateBatch(cmd, args[0])
|
return generateBatch(cmd, args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var generateContextKey struct{}
|
||||||
|
|
||||||
func generate(cmd *cobra.Command, model, prompt string) error {
|
func generate(cmd *cobra.Command, model, prompt string) error {
|
||||||
if len(strings.TrimSpace(prompt)) > 0 {
|
if len(strings.TrimSpace(prompt)) > 0 {
|
||||||
client := api.NewClient()
|
client := api.NewClient()
|
||||||
@@ -110,7 +197,12 @@ func generate(cmd *cobra.Command, model, prompt string) error {
|
|||||||
|
|
||||||
var latest api.GenerateResponse
|
var latest api.GenerateResponse
|
||||||
|
|
||||||
request := api.GenerateRequest{Model: model, Prompt: prompt}
|
generateContext, ok := cmd.Context().Value(generateContextKey).([]int)
|
||||||
|
if !ok {
|
||||||
|
generateContext = []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
request := api.GenerateRequest{Model: model, Prompt: prompt, Context: generateContext}
|
||||||
fn := func(resp api.GenerateResponse) error {
|
fn := func(resp api.GenerateResponse) error {
|
||||||
if !spinner.IsFinished() {
|
if !spinner.IsFinished() {
|
||||||
spinner.Finish()
|
spinner.Finish()
|
||||||
@@ -119,6 +211,8 @@ func generate(cmd *cobra.Command, model, prompt string) error {
|
|||||||
latest = resp
|
latest = resp
|
||||||
|
|
||||||
fmt.Print(resp.Response)
|
fmt.Print(resp.Response)
|
||||||
|
|
||||||
|
cmd.SetContext(context.WithValue(cmd.Context(), generateContextKey, resp.Context))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +294,21 @@ func NewCLI() *cobra.Command {
|
|||||||
},
|
},
|
||||||
PersistentPreRunE: func(_ *cobra.Command, args []string) error {
|
PersistentPreRunE: func(_ *cobra.Command, args []string) error {
|
||||||
// create the models directory and it's parent
|
// create the models directory and it's parent
|
||||||
return os.MkdirAll(path.Join(cacheDir(), "models"), 0o700)
|
return os.MkdirAll(filepath.Join(cacheDir(), "models"), 0o700)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cobra.EnableCommandSorting = false
|
cobra.EnableCommandSorting = false
|
||||||
|
|
||||||
|
createCmd := &cobra.Command{
|
||||||
|
Use: "create MODEL",
|
||||||
|
Short: "Create a model from a Modelfile",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: create,
|
||||||
|
}
|
||||||
|
|
||||||
|
createCmd.Flags().StringP("file", "f", "Modelfile", "Name of the Modelfile (default \"Modelfile\")")
|
||||||
|
|
||||||
runCmd := &cobra.Command{
|
runCmd := &cobra.Command{
|
||||||
Use: "run MODEL [PROMPT]",
|
Use: "run MODEL [PROMPT]",
|
||||||
Short: "Run a model",
|
Short: "Run a model",
|
||||||
@@ -222,9 +325,33 @@ func NewCLI() *cobra.Command {
|
|||||||
RunE: RunServer,
|
RunE: RunServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pullCmd := &cobra.Command{
|
||||||
|
Use: "pull MODEL",
|
||||||
|
Short: "Pull a model from a registry",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: RunPull,
|
||||||
|
}
|
||||||
|
|
||||||
|
pushCmd := &cobra.Command{
|
||||||
|
Use: "push MODEL",
|
||||||
|
Short: "Push a model to a registry",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: push,
|
||||||
|
}
|
||||||
|
|
||||||
|
listCmd := &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List models",
|
||||||
|
RunE: list,
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
serveCmd,
|
serveCmd,
|
||||||
|
createCmd,
|
||||||
runCmd,
|
runCmd,
|
||||||
|
pullCmd,
|
||||||
|
pushCmd,
|
||||||
|
listCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
return rootCmd
|
return rootCmd
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
# Python SDK
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```
|
|
||||||
pip install ollama
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
import ollama
|
|
||||||
ollama.generate("orca-mini-3b", "hi")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
### `ollama.generate(model, message)`
|
|
||||||
|
|
||||||
Generate a completion
|
|
||||||
|
|
||||||
```python
|
|
||||||
ollama.generate("./llama-7b-ggml.bin", "hi")
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ollama.models()`
|
|
||||||
|
|
||||||
List available local models
|
|
||||||
|
|
||||||
```python
|
|
||||||
models = ollama.models()
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ollama.load(model)`
|
|
||||||
|
|
||||||
Manually a model for generation
|
|
||||||
|
|
||||||
```python
|
|
||||||
ollama.load("model")
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ollama.unload(model)`
|
|
||||||
|
|
||||||
Unload a model
|
|
||||||
|
|
||||||
```python
|
|
||||||
ollama.unload("model")
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ollama.pull(model)`
|
|
||||||
|
|
||||||
Download a model
|
|
||||||
|
|
||||||
```python
|
|
||||||
ollama.pull("huggingface.co/thebloke/llama-7b-ggml")
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ollama.search(query)`
|
|
||||||
|
|
||||||
Search for compatible models that Ollama can run
|
|
||||||
|
|
||||||
```python
|
|
||||||
ollama.search("llama-7b")
|
|
||||||
```
|
|
141
format/time.go
Normal file
141
format/time.go
Normal file
@@ -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())
|
||||||
|
}
|
102
format/time_test.go
Normal file
102
format/time_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertEqual(t *testing.T, a interface{}, b interface{}) {
|
||||||
|
if a != b {
|
||||||
|
t.Errorf("Assert failed, expected %v, got %v", b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHumanDuration(t *testing.T) {
|
||||||
|
day := 24 * time.Hour
|
||||||
|
week := 7 * day
|
||||||
|
month := 30 * day
|
||||||
|
year := 365 * day
|
||||||
|
|
||||||
|
assertEqual(t, "Less than a second", HumanDuration(450*time.Millisecond))
|
||||||
|
assertEqual(t, "Less than a second", HumanDurationWithCase(450*time.Millisecond, true))
|
||||||
|
assertEqual(t, "less than a second", HumanDurationWithCase(450*time.Millisecond, false))
|
||||||
|
assertEqual(t, "1 second", HumanDuration(1*time.Second))
|
||||||
|
assertEqual(t, "45 seconds", HumanDuration(45*time.Second))
|
||||||
|
assertEqual(t, "46 seconds", HumanDuration(46*time.Second))
|
||||||
|
assertEqual(t, "59 seconds", HumanDuration(59*time.Second))
|
||||||
|
assertEqual(t, "About a minute", HumanDuration(60*time.Second))
|
||||||
|
assertEqual(t, "About a minute", HumanDurationWithCase(1*time.Minute, true))
|
||||||
|
assertEqual(t, "about a minute", HumanDurationWithCase(1*time.Minute, false))
|
||||||
|
assertEqual(t, "3 minutes", HumanDuration(3*time.Minute))
|
||||||
|
assertEqual(t, "35 minutes", HumanDuration(35*time.Minute))
|
||||||
|
assertEqual(t, "35 minutes", HumanDuration(35*time.Minute+40*time.Second))
|
||||||
|
assertEqual(t, "45 minutes", HumanDuration(45*time.Minute))
|
||||||
|
assertEqual(t, "45 minutes", HumanDuration(45*time.Minute+40*time.Second))
|
||||||
|
assertEqual(t, "46 minutes", HumanDuration(46*time.Minute))
|
||||||
|
assertEqual(t, "59 minutes", HumanDuration(59*time.Minute))
|
||||||
|
assertEqual(t, "About an hour", HumanDuration(1*time.Hour))
|
||||||
|
assertEqual(t, "About an hour", HumanDurationWithCase(1*time.Hour+29*time.Minute, true))
|
||||||
|
assertEqual(t, "about an hour", HumanDurationWithCase(1*time.Hour+29*time.Minute, false))
|
||||||
|
assertEqual(t, "2 hours", HumanDuration(1*time.Hour+31*time.Minute))
|
||||||
|
assertEqual(t, "2 hours", HumanDuration(1*time.Hour+59*time.Minute))
|
||||||
|
assertEqual(t, "3 hours", HumanDuration(3*time.Hour))
|
||||||
|
assertEqual(t, "3 hours", HumanDuration(3*time.Hour+29*time.Minute))
|
||||||
|
assertEqual(t, "4 hours", HumanDuration(3*time.Hour+31*time.Minute))
|
||||||
|
assertEqual(t, "4 hours", HumanDuration(3*time.Hour+59*time.Minute))
|
||||||
|
assertEqual(t, "4 hours", HumanDuration(3*time.Hour+60*time.Minute))
|
||||||
|
assertEqual(t, "24 hours", HumanDuration(24*time.Hour))
|
||||||
|
assertEqual(t, "36 hours", HumanDuration(1*day+12*time.Hour))
|
||||||
|
assertEqual(t, "2 days", HumanDuration(2*day))
|
||||||
|
assertEqual(t, "7 days", HumanDuration(7*day))
|
||||||
|
assertEqual(t, "13 days", HumanDuration(13*day+5*time.Hour))
|
||||||
|
assertEqual(t, "2 weeks", HumanDuration(2*week))
|
||||||
|
assertEqual(t, "2 weeks", HumanDuration(2*week+4*day))
|
||||||
|
assertEqual(t, "3 weeks", HumanDuration(3*week))
|
||||||
|
assertEqual(t, "4 weeks", HumanDuration(4*week))
|
||||||
|
assertEqual(t, "4 weeks", HumanDuration(4*week+3*day))
|
||||||
|
assertEqual(t, "4 weeks", HumanDuration(1*month))
|
||||||
|
assertEqual(t, "6 weeks", HumanDuration(1*month+2*week))
|
||||||
|
assertEqual(t, "2 months", HumanDuration(2*month))
|
||||||
|
assertEqual(t, "2 months", HumanDuration(2*month+2*week))
|
||||||
|
assertEqual(t, "3 months", HumanDuration(3*month))
|
||||||
|
assertEqual(t, "3 months", HumanDuration(3*month+1*week))
|
||||||
|
assertEqual(t, "5 months", HumanDuration(5*month+2*week))
|
||||||
|
assertEqual(t, "13 months", HumanDuration(13*month))
|
||||||
|
assertEqual(t, "23 months", HumanDuration(23*month))
|
||||||
|
assertEqual(t, "24 months", HumanDuration(24*month))
|
||||||
|
assertEqual(t, "2 years", HumanDuration(24*month+2*week))
|
||||||
|
assertEqual(t, "3 years", HumanDuration(3*year+2*month))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHumanTime(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
t.Run("zero value", func(t *testing.T) {
|
||||||
|
assertEqual(t, HumanTime(time.Time{}, "never"), "never")
|
||||||
|
})
|
||||||
|
t.Run("time in the future", func(t *testing.T) {
|
||||||
|
v := now.Add(48 * time.Hour)
|
||||||
|
assertEqual(t, HumanTime(v, ""), "2 days from now")
|
||||||
|
})
|
||||||
|
t.Run("time in the past", func(t *testing.T) {
|
||||||
|
v := now.Add(-48 * time.Hour)
|
||||||
|
assertEqual(t, HumanTime(v, ""), "2 days ago")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExactDuration(t *testing.T) {
|
||||||
|
assertEqual(t, "1 millisecond", ExactDuration(1*time.Millisecond))
|
||||||
|
assertEqual(t, "10 milliseconds", ExactDuration(10*time.Millisecond))
|
||||||
|
assertEqual(t, "1 second", ExactDuration(1*time.Second))
|
||||||
|
assertEqual(t, "10 seconds", ExactDuration(10*time.Second))
|
||||||
|
assertEqual(t, "1 minute", ExactDuration(1*time.Minute))
|
||||||
|
assertEqual(t, "10 minutes", ExactDuration(10*time.Minute))
|
||||||
|
assertEqual(t, "1 hour", ExactDuration(1*time.Hour))
|
||||||
|
assertEqual(t, "10 hours", ExactDuration(10*time.Hour))
|
||||||
|
assertEqual(t, "1 hour 1 second", ExactDuration(1*time.Hour+1*time.Second))
|
||||||
|
assertEqual(t, "1 hour 10 seconds", ExactDuration(1*time.Hour+10*time.Second))
|
||||||
|
assertEqual(t, "1 hour 1 minute", ExactDuration(1*time.Hour+1*time.Minute))
|
||||||
|
assertEqual(t, "1 hour 10 minutes", ExactDuration(1*time.Hour+10*time.Minute))
|
||||||
|
assertEqual(t, "1 hour 1 minute 1 second", ExactDuration(1*time.Hour+1*time.Minute+1*time.Second))
|
||||||
|
assertEqual(t, "10 hours 10 minutes 10 seconds", ExactDuration(10*time.Hour+10*time.Minute+10*time.Second))
|
||||||
|
}
|
4
go.mod
4
go.mod
@@ -3,7 +3,9 @@ module github.com/jmorganca/ollama
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
dario.cat/mergo v1.0.0
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
@@ -27,7 +30,6 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.8
|
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
40
go.sum
40
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
@@ -8,6 +10,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
@@ -38,11 +42,10 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
@@ -52,6 +55,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -80,54 +85,23 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
|
||||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
@@ -78,12 +78,14 @@ llama_token llama_sample(
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/jmorganca/ollama/api"
|
||||||
@@ -149,9 +151,14 @@ func (llm *llama) Close() {
|
|||||||
C.llama_print_timings(llm.ctx)
|
C.llama_print_timings(llm.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (llm *llama) Predict(prompt string, fn func(api.GenerateResponse)) error {
|
func (llm *llama) Predict(ctx []int, prompt string, fn func(api.GenerateResponse)) error {
|
||||||
if tokens := llm.tokenize(prompt); tokens != nil {
|
if input := llm.tokenize(prompt); input != nil {
|
||||||
return llm.generate(tokens, fn)
|
embd := make([]C.llama_token, len(ctx))
|
||||||
|
for i := range ctx {
|
||||||
|
embd[i] = C.llama_token(ctx[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return llm.generate(append(embd, input...), fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("llama: tokenize")
|
return errors.New("llama: tokenize")
|
||||||
@@ -194,6 +201,12 @@ func (llm *llama) generate(input []C.llama_token, fn func(api.GenerateResponse))
|
|||||||
|
|
||||||
output := deque[C.llama_token]{capacity: llm.NumCtx}
|
output := deque[C.llama_token]{capacity: llm.NumCtx}
|
||||||
|
|
||||||
|
context := deque[int]{capacity: llm.NumCtx / 2}
|
||||||
|
for _, in := range input {
|
||||||
|
context.PushLeft(int(in))
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
for C.llama_get_kv_cache_token_count(llm.ctx) < C.int(llm.NumCtx) {
|
for C.llama_get_kv_cache_token_count(llm.ctx) < C.int(llm.NumCtx) {
|
||||||
if retval := C.llama_eval(llm.ctx, unsafe.SliceData(input), C.int(len(input)), C.llama_get_kv_cache_token_count(llm.ctx), C.int(llm.NumThread)); retval != 0 {
|
if retval := C.llama_eval(llm.ctx, unsafe.SliceData(input), C.int(len(input)), C.llama_get_kv_cache_token_count(llm.ctx), C.int(llm.NumThread)); retval != 0 {
|
||||||
return errors.New("llama: eval")
|
return errors.New("llama: eval")
|
||||||
@@ -206,12 +219,17 @@ func (llm *llama) generate(input []C.llama_token, fn func(api.GenerateResponse))
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the callback
|
b.WriteString(llm.detokenize(token))
|
||||||
fn(api.GenerateResponse{
|
if utf8.Valid(b.Bytes()) || b.Len() >= utf8.UTFMax {
|
||||||
Response: llm.detokenize(token),
|
// call the callback
|
||||||
})
|
fn(api.GenerateResponse{
|
||||||
|
Response: b.String(),
|
||||||
|
})
|
||||||
|
|
||||||
output.PushLeft(token)
|
output.PushLeft(token)
|
||||||
|
context.PushLeft(int(token))
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
input = []C.llama_token{token}
|
input = []C.llama_token{token}
|
||||||
}
|
}
|
||||||
@@ -228,6 +246,7 @@ func (llm *llama) generate(input []C.llama_token, fn func(api.GenerateResponse))
|
|||||||
timings := C.llama_get_timings(llm.ctx)
|
timings := C.llama_get_timings(llm.ctx)
|
||||||
fn(api.GenerateResponse{
|
fn(api.GenerateResponse{
|
||||||
Done: true,
|
Done: true,
|
||||||
|
Context: context.Data(),
|
||||||
PromptEvalCount: int(timings.n_p_eval),
|
PromptEvalCount: int(timings.n_p_eval),
|
||||||
PromptEvalDuration: dur(float64(timings.t_p_eval_ms)),
|
PromptEvalDuration: dur(float64(timings.t_p_eval_ms)),
|
||||||
EvalCount: int(timings.n_eval),
|
EvalCount: int(timings.n_eval),
|
||||||
@@ -252,8 +271,8 @@ func (llm *llama) sample(output deque[C.llama_token], opts *C.struct_llama_sampl
|
|||||||
|
|
||||||
token := C.llama_sample(
|
token := C.llama_sample(
|
||||||
llm.ctx,
|
llm.ctx,
|
||||||
unsafe.SliceData(candidates.Data()), C.ulong(candidates.Len()),
|
unsafe.SliceData(candidates.Data()), C.size_t(candidates.Len()),
|
||||||
unsafe.SliceData(output.Data()), C.ulong(output.Len()),
|
unsafe.SliceData(output.Data()), C.size_t(output.Len()),
|
||||||
opts)
|
opts)
|
||||||
if token != C.llama_token_eos() {
|
if token != C.llama_token_eos() {
|
||||||
return token, nil
|
return token, nil
|
||||||
|
4
main.go
4
main.go
@@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/cmd"
|
"github.com/jmorganca/ollama/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.NewCLI().Execute()
|
cmd.NewCLI().ExecuteContext(context.Background())
|
||||||
}
|
}
|
||||||
|
77
parser/parser.go
Normal file
77
parser/parser.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Name string
|
||||||
|
Arg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(reader io.Reader) ([]Command, error) {
|
||||||
|
var commands []Command
|
||||||
|
var foundModel bool
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
multiline := false
|
||||||
|
var multilineCommand *Command
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if multiline {
|
||||||
|
// If we're in a multiline string and the line is """, end the multiline string.
|
||||||
|
if strings.TrimSpace(line) == `"""` {
|
||||||
|
multiline = false
|
||||||
|
commands = append(commands, *multilineCommand)
|
||||||
|
} else {
|
||||||
|
// Otherwise, append the line to the multiline string.
|
||||||
|
multilineCommand.Arg += "\n" + line
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
command := Command{}
|
||||||
|
switch strings.ToUpper(fields[0]) {
|
||||||
|
case "FROM":
|
||||||
|
command.Name = "model"
|
||||||
|
command.Arg = fields[1]
|
||||||
|
if command.Arg == "" {
|
||||||
|
return nil, fmt.Errorf("no model specified in FROM line")
|
||||||
|
}
|
||||||
|
foundModel = true
|
||||||
|
case "PROMPT":
|
||||||
|
command.Name = "prompt"
|
||||||
|
if fields[1] == `"""` {
|
||||||
|
multiline = true
|
||||||
|
multilineCommand = &command
|
||||||
|
multilineCommand.Arg = ""
|
||||||
|
} else {
|
||||||
|
command.Arg = strings.Join(fields[1:], " ")
|
||||||
|
}
|
||||||
|
case "PARAMETER":
|
||||||
|
command.Name = fields[1]
|
||||||
|
command.Arg = strings.Join(fields[2:], " ")
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !multiline {
|
||||||
|
commands = append(commands, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundModel {
|
||||||
|
return nil, fmt.Errorf("no FROM line for the model was specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
return nil, fmt.Errorf("unclosed multiline string")
|
||||||
|
}
|
||||||
|
return commands, scanner.Err()
|
||||||
|
}
|
837
server/images.go
Normal file
837
server/images.go
Normal file
@@ -0,0 +1,837 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmorganca/ollama/api"
|
||||||
|
"github.com/jmorganca/ollama/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ModelPath string
|
||||||
|
Prompt string
|
||||||
|
Options api.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManifestV2 struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Config Layer `json:"config"`
|
||||||
|
Layers []*Layer `json:"layers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layer struct {
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerWithBuffer struct {
|
||||||
|
Layer
|
||||||
|
|
||||||
|
Buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigV2 struct {
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
RootFS RootFS `json:"rootfs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootFS struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
DiffIDs []string `json:"diff_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ManifestV2) GetTotalSize() int {
|
||||||
|
var total int
|
||||||
|
for _, layer := range m.Layers {
|
||||||
|
total += layer.Size
|
||||||
|
}
|
||||||
|
total += m.Config.Size
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetManifest(mp ModelPath) (*ManifestV2, error) {
|
||||||
|
fp, err := mp.GetManifestPath(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err = os.Stat(fp); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("couldn't find model '%s'", mp.GetShortTagname())
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest *ManifestV2
|
||||||
|
|
||||||
|
f, err := os.Open(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't open file '%s'", fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(f)
|
||||||
|
err = decoder.Decode(&manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModel(name string) (*Model, error) {
|
||||||
|
mp := ParseModelPath(name)
|
||||||
|
|
||||||
|
manifest, err := GetManifest(mp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
model := &Model{
|
||||||
|
Name: mp.GetFullTagname(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layer := range manifest.Layers {
|
||||||
|
filename, err := GetBlobsPath(layer.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch layer.MediaType {
|
||||||
|
case "application/vnd.ollama.image.model":
|
||||||
|
model.ModelPath = filename
|
||||||
|
case "application/vnd.ollama.image.prompt":
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
model.Prompt = string(data)
|
||||||
|
case "application/vnd.ollama.image.params":
|
||||||
|
params, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer params.Close()
|
||||||
|
|
||||||
|
var opts api.Options
|
||||||
|
if err = json.NewDecoder(params).Decode(&opts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
model.Options = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAbsPath(fp string) (string, error) {
|
||||||
|
if strings.HasPrefix(fp, "~/") {
|
||||||
|
parts := strings.Split(fp, "/")
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = filepath.Join(home, filepath.Join(parts[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.ExpandEnv(fp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateModel(name string, mf io.Reader, fn func(status string)) error {
|
||||||
|
fn("parsing modelfile")
|
||||||
|
commands, err := parser.Parse(mf)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("error: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var layers []*LayerWithBuffer
|
||||||
|
params := make(map[string]string)
|
||||||
|
|
||||||
|
for _, c := range commands {
|
||||||
|
log.Printf("[%s] - %s\n", c.Name, c.Arg)
|
||||||
|
switch c.Name {
|
||||||
|
case "model":
|
||||||
|
fn("looking for model")
|
||||||
|
mf, err := GetManifest(ParseModelPath(c.Arg))
|
||||||
|
if err != nil {
|
||||||
|
// if we couldn't read the manifest, try getting the bin file
|
||||||
|
fp, err := getAbsPath(c.Arg)
|
||||||
|
if err != nil {
|
||||||
|
fn("error determing path. exiting.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("creating model layer")
|
||||||
|
file, err := os.Open(fp)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("couldn't find model '%s'", c.Arg))
|
||||||
|
return fmt.Errorf("failed to open file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
l, err := CreateLayer(file)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("couldn't create model layer: %v", err))
|
||||||
|
return fmt.Errorf("failed to create layer: %v", err)
|
||||||
|
}
|
||||||
|
l.MediaType = "application/vnd.ollama.image.model"
|
||||||
|
layers = append(layers, l)
|
||||||
|
} else {
|
||||||
|
log.Printf("manifest = %#v", mf)
|
||||||
|
for _, l := range mf.Layers {
|
||||||
|
newLayer, err := GetLayerWithBufferFromLayer(l)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("couldn't read layer: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layers = append(layers, newLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "prompt":
|
||||||
|
fn("creating prompt layer")
|
||||||
|
// remove the prompt layer if one exists
|
||||||
|
layers = removeLayerFromLayers(layers, "application/vnd.ollama.image.prompt")
|
||||||
|
|
||||||
|
prompt := strings.NewReader(c.Arg)
|
||||||
|
l, err := CreateLayer(prompt)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("couldn't create prompt layer: %v", err))
|
||||||
|
return fmt.Errorf("failed to create layer: %v", err)
|
||||||
|
}
|
||||||
|
l.MediaType = "application/vnd.ollama.image.prompt"
|
||||||
|
layers = append(layers, l)
|
||||||
|
default:
|
||||||
|
params[c.Name] = c.Arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single layer for the parameters
|
||||||
|
if len(params) > 0 {
|
||||||
|
fn("creating parameter layer")
|
||||||
|
layers = removeLayerFromLayers(layers, "application/vnd.ollama.image.params")
|
||||||
|
paramData, err := paramsToReader(params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create params json: %v", err)
|
||||||
|
}
|
||||||
|
l, err := CreateLayer(paramData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create layer: %v", err)
|
||||||
|
}
|
||||||
|
l.MediaType = "application/vnd.ollama.image.params"
|
||||||
|
layers = append(layers, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
digests, err := getLayerDigests(layers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestLayers []*Layer
|
||||||
|
for _, l := range layers {
|
||||||
|
manifestLayers = append(manifestLayers, &l.Layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a layer for the config object
|
||||||
|
fn("creating config layer")
|
||||||
|
cfg, err := createConfigLayer(digests)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
layers = append(layers, cfg)
|
||||||
|
|
||||||
|
err = SaveLayers(layers, fn, false)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("error saving layers: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the manifest
|
||||||
|
fn("writing manifest")
|
||||||
|
err = CreateManifest(name, cfg, manifestLayers)
|
||||||
|
if err != nil {
|
||||||
|
fn(fmt.Sprintf("error creating manifest: %v", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("success")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeLayerFromLayers(layers []*LayerWithBuffer, mediaType string) []*LayerWithBuffer {
|
||||||
|
j := 0
|
||||||
|
for _, l := range layers {
|
||||||
|
if l.MediaType != mediaType {
|
||||||
|
layers[j] = l
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return layers[:j]
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveLayers(layers []*LayerWithBuffer, fn func(status string), force bool) error {
|
||||||
|
// Write each of the layers to disk
|
||||||
|
for _, layer := range layers {
|
||||||
|
fp, err := GetBlobsPath(layer.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(fp)
|
||||||
|
if os.IsNotExist(err) || force {
|
||||||
|
fn(fmt.Sprintf("writing layer %s", layer.Digest))
|
||||||
|
out, err := os.Create(fp)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't create %s", fp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, layer.Buffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fn(fmt.Sprintf("using already created layer %s", layer.Digest))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateManifest(name string, cfg *LayerWithBuffer, layers []*Layer) error {
|
||||||
|
mp := ParseModelPath(name)
|
||||||
|
|
||||||
|
manifest := ManifestV2{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
Config: Layer{
|
||||||
|
MediaType: cfg.MediaType,
|
||||||
|
Size: cfg.Size,
|
||||||
|
Digest: cfg.Digest,
|
||||||
|
},
|
||||||
|
Layers: layers,
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestJSON, err := json.Marshal(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fp, err := mp.GetManifestPath(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(fp, manifestJSON, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLayerWithBufferFromLayer(layer *Layer) (*LayerWithBuffer, error) {
|
||||||
|
fp, err := GetBlobsPath(layer.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(fp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open blob: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
newLayer, err := CreateLayer(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newLayer.MediaType = layer.MediaType
|
||||||
|
return newLayer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramsToReader(params map[string]string) (io.Reader, error) {
|
||||||
|
opts := api.DefaultOptions()
|
||||||
|
typeOpts := reflect.TypeOf(opts)
|
||||||
|
|
||||||
|
// build map of json struct tags
|
||||||
|
jsonOpts := make(map[string]reflect.StructField)
|
||||||
|
for _, field := range reflect.VisibleFields(typeOpts) {
|
||||||
|
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||||
|
if jsonTag != "" {
|
||||||
|
jsonOpts[jsonTag] = field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valueOpts := reflect.ValueOf(&opts).Elem()
|
||||||
|
// iterate params and set values based on json struct tags
|
||||||
|
for key, val := range params {
|
||||||
|
if opt, ok := jsonOpts[key]; ok {
|
||||||
|
field := valueOpts.FieldByName(opt.Name)
|
||||||
|
if field.IsValid() && field.CanSet() {
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Float32:
|
||||||
|
floatVal, err := strconv.ParseFloat(val, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid float value %s", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.SetFloat(floatVal)
|
||||||
|
case reflect.Int:
|
||||||
|
intVal, err := strconv.ParseInt(val, 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid int value %s", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.SetInt(intVal)
|
||||||
|
case reflect.Bool:
|
||||||
|
boolVal, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid bool value %s", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.SetBool(boolVal)
|
||||||
|
case reflect.String:
|
||||||
|
field.SetString(val)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bts, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader(bts), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLayerDigests(layers []*LayerWithBuffer) ([]string, error) {
|
||||||
|
var digests []string
|
||||||
|
for _, l := range layers {
|
||||||
|
if l.Digest == "" {
|
||||||
|
return nil, fmt.Errorf("layer is missing a digest")
|
||||||
|
}
|
||||||
|
digests = append(digests, l.Digest)
|
||||||
|
}
|
||||||
|
return digests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLayer creates a Layer object from a given file
|
||||||
|
func CreateLayer(f io.Reader) (*LayerWithBuffer, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, err := io.Copy(buf, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, size := GetSHA256Digest(buf)
|
||||||
|
|
||||||
|
layer := &LayerWithBuffer{
|
||||||
|
Layer: Layer{
|
||||||
|
MediaType: "application/vnd.docker.image.rootfs.diff.tar",
|
||||||
|
Digest: digest,
|
||||||
|
Size: size,
|
||||||
|
},
|
||||||
|
Buffer: buf,
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PushModel(name, username, password string, fn func(status, digest string, Total, Completed int, Percent float64)) error {
|
||||||
|
mp := ParseModelPath(name)
|
||||||
|
|
||||||
|
fn("retrieving manifest", "", 0, 0, 0)
|
||||||
|
manifest, err := GetManifest(mp)
|
||||||
|
if err != nil {
|
||||||
|
fn("couldn't retrieve manifest", "", 0, 0, 0)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
completed += layer.Size
|
||||||
|
fn("using existing layer", layer.Digest, total, completed, float64(completed)/float64(total))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("starting upload", layer.Digest, total, completed, float64(completed)/float64(total))
|
||||||
|
|
||||||
|
location, err := startUpload(mp, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't start upload: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = uploadBlob(location, layer, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error uploading blob: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
completed += layer.Size
|
||||||
|
fn("upload complete", layer.Digest, total, completed, float64(completed)/float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("pushing manifest", "", total, completed, float64(completed/total))
|
||||||
|
url := fmt.Sprintf("%s://%s/v2/%s/manifests/%s", mp.ProtocolScheme, mp.Registry, mp.GetNamespaceRepository(), mp.Tag)
|
||||||
|
headers := map[string]string{
|
||||||
|
"Content-Type": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestJSON, err := json.Marshal(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := makeRequest("PUT", url, headers, bytes.NewReader(manifestJSON), username, password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("success", "", total, completed, 1.0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PullModel(name, username, password string, fn func(status, digest string, Total, Completed int, Percent float64)) error {
|
||||||
|
mp := ParseModelPath(name)
|
||||||
|
|
||||||
|
fn("pulling manifest", "", 0, 0, 0)
|
||||||
|
|
||||||
|
manifest, err := pullModelManifest(mp, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pull model manifest: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn("starting download", layer.Digest, total, completed, float64(completed)/float64(total))
|
||||||
|
if err := downloadBlob(mp, layer.Digest, username, password, fn); err != nil {
|
||||||
|
fn(fmt.Sprintf("error downloading: %v", err), layer.Digest, 0, 0, 0)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
completed += layer.Size
|
||||||
|
fn("download complete", layer.Digest, total, completed, float64(completed)/float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("writing manifest", "", total, completed, 1.0)
|
||||||
|
|
||||||
|
manifestJSON, err := json.Marshal(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fp, err := mp.GetManifestPath(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fp, manifestJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't write to %s", fp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn("success", "", total, completed, 1.0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pullModelManifest(mp ModelPath, username, password string) (*ManifestV2, error) {
|
||||||
|
url := fmt.Sprintf("%s://%s/v2/%s/manifests/%s", mp.ProtocolScheme, mp.Registry, mp.GetNamespaceRepository(), mp.Tag)
|
||||||
|
headers := map[string]string{
|
||||||
|
"Accept": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := makeRequest("GET", url, headers, nil, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't get manifest: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check for success: For a successful upload, the Docker registry will respond with a 201 Created
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("registry responded with code %d: %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var m *ManifestV2
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfigLayer(layers []string) (*LayerWithBuffer, error) {
|
||||||
|
// TODO change architecture and OS
|
||||||
|
config := ConfigV2{
|
||||||
|
Architecture: "arm64",
|
||||||
|
OS: "linux",
|
||||||
|
RootFS: RootFS{
|
||||||
|
Type: "layers",
|
||||||
|
DiffIDs: layers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configJSON, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(configJSON)
|
||||||
|
digest, size := GetSHA256Digest(buf)
|
||||||
|
|
||||||
|
layer := &LayerWithBuffer{
|
||||||
|
Layer: Layer{
|
||||||
|
MediaType: "application/vnd.docker.container.image.v1+json",
|
||||||
|
Digest: digest,
|
||||||
|
Size: size,
|
||||||
|
},
|
||||||
|
Buffer: buf,
|
||||||
|
}
|
||||||
|
return layer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSHA256Digest returns the SHA256 hash of a given buffer and returns it, and the size of buffer
|
||||||
|
func GetSHA256Digest(data *bytes.Buffer) (string, int) {
|
||||||
|
layerBytes := data.Bytes()
|
||||||
|
hash := sha256.Sum256(layerBytes)
|
||||||
|
return "sha256:" + hex.EncodeToString(hash[:]), len(layerBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startUpload(mp ModelPath, username string, password string) (string, error) {
|
||||||
|
url := fmt.Sprintf("%s://%s/v2/%s/blobs/uploads/", mp.ProtocolScheme, mp.Registry, mp.GetNamespaceRepository())
|
||||||
|
|
||||||
|
resp, err := makeRequest("POST", url, nil, nil, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't start upload: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract UUID location from header
|
||||||
|
location := resp.Header.Get("Location")
|
||||||
|
if location == "" {
|
||||||
|
return "", fmt.Errorf("location header is missing in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return location, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if a blob already exists in the Docker registry
|
||||||
|
func checkBlobExistence(mp ModelPath, digest string, username string, password string) (bool, error) {
|
||||||
|
url := fmt.Sprintf("%s://%s/v2/%s/blobs/%s", mp.ProtocolScheme, mp.Registry, mp.GetNamespaceRepository(), digest)
|
||||||
|
|
||||||
|
resp, err := makeRequest("HEAD", url, nil, nil, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't check for blob: %v", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check for success: If the blob exists, the Docker registry will respond with a 200 OK
|
||||||
|
return resp.StatusCode == http.StatusOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadBlob(location string, layer *Layer, username string, password string) 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
|
||||||
|
// TODO allow resumability
|
||||||
|
// TODO allow canceling uploads via DELETE
|
||||||
|
// TODO allow cross repo blob mount
|
||||||
|
|
||||||
|
fp, err := GetBlobsPath(layer.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(fp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := makeRequest("PUT", url, headers, f, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't upload blob: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadBlob(mp ModelPath, digest string, username, password string, fn func(status, digest string, Total, Completed int, Percent float64)) error {
|
||||||
|
fp, err := GetBlobsPath(digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(fp)
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
// we already have the file, so return
|
||||||
|
log.Printf("already have %s\n", digest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
fi, err := os.Stat(fp + "-partial")
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, os.ErrNotExist):
|
||||||
|
// noop, file doesn't exist so create it
|
||||||
|
case err != nil:
|
||||||
|
return fmt.Errorf("stat: %w", err)
|
||||||
|
default:
|
||||||
|
size = fi.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s://%s/v2/%s/blobs/%s", mp.ProtocolScheme, mp.Registry, mp.GetNamespaceRepository(), digest)
|
||||||
|
headers := map[string]string{
|
||||||
|
"Range": fmt.Sprintf("bytes=%d-", size),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := makeRequest("GET", url, headers, nil, username, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't download blob: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(path.Dir(fp), 0o700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("make blobs directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(fp+"-partial", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
remaining, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||||||
|
completed := size
|
||||||
|
total := remaining + completed
|
||||||
|
|
||||||
|
for {
|
||||||
|
fn(fmt.Sprintf("Downloading %s", digest), digest, int(total), int(completed), float64(completed)/float64(total))
|
||||||
|
if completed >= total {
|
||||||
|
if err := os.Rename(fp+"-partial", fp); err != nil {
|
||||||
|
fn(fmt.Sprintf("error renaming file: %v", err), digest, int(total), int(completed), 1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := io.CopyN(out, resp.Body, 8192)
|
||||||
|
if err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
completed += n
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("success getting %s\n", digest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRequest(method, url string, headers map[string]string, body io.Reader, username, password string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: better auth
|
||||||
|
if username != "" && password != "" {
|
||||||
|
req.SetBasicAuth(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= 10 {
|
||||||
|
return fmt.Errorf("too many redirects")
|
||||||
|
}
|
||||||
|
log.Printf("redirected to: %s\n", req.URL)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
115
server/modelpath.go
Normal file
115
server/modelpath.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModelPath struct {
|
||||||
|
ProtocolScheme string
|
||||||
|
Registry string
|
||||||
|
Namespace string
|
||||||
|
Repository string
|
||||||
|
Tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultRegistry = "registry.ollama.ai"
|
||||||
|
DefaultNamespace = "library"
|
||||||
|
DefaultTag = "latest"
|
||||||
|
DefaultProtocolScheme = "https"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseModelPath(name string) ModelPath {
|
||||||
|
slashParts := strings.Split(name, "/")
|
||||||
|
var registry, namespace, repository, tag string
|
||||||
|
|
||||||
|
switch len(slashParts) {
|
||||||
|
case 3:
|
||||||
|
registry = slashParts[0]
|
||||||
|
namespace = slashParts[1]
|
||||||
|
repository = strings.Split(slashParts[2], ":")[0]
|
||||||
|
case 2:
|
||||||
|
registry = DefaultRegistry
|
||||||
|
namespace = slashParts[0]
|
||||||
|
repository = strings.Split(slashParts[1], ":")[0]
|
||||||
|
case 1:
|
||||||
|
registry = DefaultRegistry
|
||||||
|
namespace = DefaultNamespace
|
||||||
|
repository = strings.Split(slashParts[0], ":")[0]
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid image format.")
|
||||||
|
return ModelPath{}
|
||||||
|
}
|
||||||
|
|
||||||
|
colonParts := strings.Split(name, ":")
|
||||||
|
if len(colonParts) == 2 {
|
||||||
|
tag = colonParts[1]
|
||||||
|
} else {
|
||||||
|
tag = DefaultTag
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModelPath{
|
||||||
|
ProtocolScheme: DefaultProtocolScheme,
|
||||||
|
Registry: registry,
|
||||||
|
Namespace: namespace,
|
||||||
|
Repository: repository,
|
||||||
|
Tag: tag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp ModelPath) GetNamespaceRepository() string {
|
||||||
|
return fmt.Sprintf("%s/%s", mp.Namespace, mp.Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp ModelPath) GetFullTagname() string {
|
||||||
|
return fmt.Sprintf("%s/%s/%s:%s", mp.Registry, mp.Namespace, mp.Repository, mp.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp ModelPath) GetShortTagname() string {
|
||||||
|
if mp.Registry == DefaultRegistry && mp.Namespace == DefaultNamespace {
|
||||||
|
return fmt.Sprintf("%s:%s", mp.Repository, mp.Tag)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s:%s", mp.Namespace, mp.Repository, mp.Tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp ModelPath) GetManifestPath(createDir bool) (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(home, ".ollama", "models", "manifests", mp.Registry, mp.Namespace, mp.Repository, mp.Tag)
|
||||||
|
if createDir {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetManifestPath() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(home, ".ollama", "models", "manifests"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBlobsPath(digest string) (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(home, ".ollama", "models", "blobs")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(path, digest), nil
|
||||||
|
}
|
128
server/models.go
128
server/models.go
@@ -1,128 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const directoryURL = "https://ollama.ai/api/models"
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
Parameters string `json:"parameters"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
ShortDescription string `json:"short_description"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
PublishedBy string `json:"published_by"`
|
|
||||||
OriginalAuthor string `json:"original_author"`
|
|
||||||
OriginalURL string `json:"original_url"`
|
|
||||||
License string `json:"license"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) FullName() string {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Join(home, ".ollama", "models", m.Name+".bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) TempFile() string {
|
|
||||||
fullName := m.FullName()
|
|
||||||
return path.Join(
|
|
||||||
path.Dir(fullName),
|
|
||||||
fmt.Sprintf(".%s.part", path.Base(fullName)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRemote(model string) (*Model, error) {
|
|
||||||
// resolve the model download from our directory
|
|
||||||
resp, err := http.Get(directoryURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get directory: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
|
||||||
}
|
|
||||||
var models []Model
|
|
||||||
err = json.Unmarshal(body, &models)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse directory: %w", err)
|
|
||||||
}
|
|
||||||
for _, m := range models {
|
|
||||||
if m.Name == model {
|
|
||||||
return &m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("model not found in directory: %s", model)
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveModel(model *Model, fn func(total, completed int64)) error {
|
|
||||||
// this models cache directory is created by the server on startup
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", model.URL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to download model: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var size int64
|
|
||||||
|
|
||||||
// completed file doesn't exist, check partial file
|
|
||||||
fi, err := os.Stat(model.TempFile())
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, os.ErrNotExist):
|
|
||||||
// noop, file doesn't exist so create it
|
|
||||||
case err != nil:
|
|
||||||
return fmt.Errorf("stat: %w", err)
|
|
||||||
default:
|
|
||||||
size = fi.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", size))
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to download model: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("failed to download model: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.OpenFile(model.TempFile(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
remaining, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
|
||||||
completed := size
|
|
||||||
|
|
||||||
total := remaining + completed
|
|
||||||
|
|
||||||
for {
|
|
||||||
fn(total, completed)
|
|
||||||
if completed >= total {
|
|
||||||
return os.Rename(model.TempFile(), model.FullName())
|
|
||||||
}
|
|
||||||
|
|
||||||
n , err := io.CopyN(out, resp.Body, 8192)
|
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
completed += n
|
|
||||||
}
|
|
||||||
}
|
|
252
server/routes.go
252
server/routes.go
@@ -1,104 +1,94 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dario.cat/mergo"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/jmorganca/ollama/api"
|
||||||
"github.com/jmorganca/ollama/llama"
|
"github.com/jmorganca/ollama/llama"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/*
|
|
||||||
var templatesFS embed.FS
|
|
||||||
var templates = template.Must(template.ParseFS(templatesFS, "templates/*.prompt"))
|
|
||||||
|
|
||||||
func cacheDir() string {
|
func cacheDir() string {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.Join(home, ".ollama")
|
return filepath.Join(home, ".ollama")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generate(c *gin.Context) {
|
func generate(c *gin.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
req := api.GenerateRequest{
|
var req api.GenerateRequest
|
||||||
Options: api.DefaultOptions(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if remoteModel, _ := getRemote(req.Model); remoteModel != nil {
|
model, err := GetModel(req.Model)
|
||||||
req.Model = remoteModel.FullName()
|
if err != nil {
|
||||||
}
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
if _, err := os.Stat(req.Model); err != nil {
|
return
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Model = path.Join(cacheDir(), "models", req.Model+".bin")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan any)
|
opts := api.DefaultOptions()
|
||||||
go stream(c, ch)
|
if err := mergo.Merge(&opts, model.Options, mergo.WithOverride); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
templateNames := make([]string, 0, len(templates.Templates()))
|
return
|
||||||
for _, template := range templates.Templates() {
|
|
||||||
templateNames = append(templateNames, template.Name())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match, _ := matchRankOne(path.Base(req.Model), templateNames)
|
if err := mergo.Merge(&opts, req.Options, mergo.WithOverride); err != nil {
|
||||||
if template := templates.Lookup(match); template != nil {
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
var sb strings.Builder
|
return
|
||||||
if err := template.Execute(&sb, req); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Prompt = sb.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
llm, err := llama.New(req.Model, req.Options)
|
templ, err := template.New("").Parse(model.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
if err = templ.Execute(&sb, req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Prompt = sb.String()
|
||||||
|
|
||||||
|
llm, err := llama.New(model.ModelPath, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer llm.Close()
|
defer llm.Close()
|
||||||
|
|
||||||
fn := func(r api.GenerateResponse) {
|
ch := make(chan any)
|
||||||
r.Model = req.Model
|
go func() {
|
||||||
r.CreatedAt = time.Now().UTC()
|
defer close(ch)
|
||||||
if r.Done {
|
llm.Predict(req.Context, req.Prompt, func(r api.GenerateResponse) {
|
||||||
r.TotalDuration = time.Since(start)
|
r.Model = req.Model
|
||||||
}
|
r.CreatedAt = time.Now().UTC()
|
||||||
|
if r.Done {
|
||||||
|
r.TotalDuration = time.Since(start)
|
||||||
|
}
|
||||||
|
|
||||||
ch <- r
|
ch <- r
|
||||||
}
|
})
|
||||||
|
}()
|
||||||
if err := llm.Predict(req.Prompt, fn); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
streamResponse(c, ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pull(c *gin.Context) {
|
func pull(c *gin.Context) {
|
||||||
@@ -108,45 +98,132 @@ func pull(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
remote, err := getRemote(req.Model)
|
ch := make(chan any)
|
||||||
if err != nil {
|
go func() {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
defer close(ch)
|
||||||
return
|
fn := func(status, digest string, total, completed int, percent float64) {
|
||||||
}
|
ch <- api.PullProgress{
|
||||||
|
Status: status,
|
||||||
|
Digest: digest,
|
||||||
|
Total: total,
|
||||||
|
Completed: completed,
|
||||||
|
Percent: percent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := PullModel(req.Name, req.Username, req.Password, fn); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// check if completed file exists
|
streamResponse(c, ch)
|
||||||
fi, err := os.Stat(remote.FullName())
|
}
|
||||||
switch {
|
|
||||||
case errors.Is(err, os.ErrNotExist):
|
|
||||||
// noop, file doesn't exist so create it
|
|
||||||
case err != nil:
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
c.JSON(http.StatusOK, api.PullProgress{
|
|
||||||
Total: fi.Size(),
|
|
||||||
Completed: fi.Size(),
|
|
||||||
Percent: 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
func push(c *gin.Context) {
|
||||||
|
var req api.PushRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan any)
|
ch := make(chan any)
|
||||||
go stream(c, ch)
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
fn := func(total, completed int64) {
|
fn := func(status, digest string, total, completed int, percent float64) {
|
||||||
ch <- api.PullProgress{
|
ch <- api.PushProgress{
|
||||||
Total: total,
|
Status: status,
|
||||||
Completed: completed,
|
Digest: digest,
|
||||||
Percent: float64(completed) / float64(total) * 100,
|
Total: total,
|
||||||
|
Completed: completed,
|
||||||
|
Percent: percent,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if err := PushModel(req.Name, req.Username, req.Password, fn); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
streamResponse(c, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(c *gin.Context) {
|
||||||
|
var req api.CreateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := saveModel(remote, fn); err != nil {
|
// NOTE consider passing the entire Modelfile in the json instead of the path to it
|
||||||
|
|
||||||
|
file, err := os.Open(req.Path)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
ch := make(chan any)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
fn := func(status string) {
|
||||||
|
ch <- api.CreateProgress{
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateModel(req.Name, file, fn); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
streamResponse(c, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(c *gin.Context) {
|
||||||
|
var models []api.ListResponseModel
|
||||||
|
fp, err := GetManifestPath()
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := path[len(fp)+1:]
|
||||||
|
slashIndex := strings.LastIndex(path, "/")
|
||||||
|
if slashIndex == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tag := path[:slashIndex] + ":" + path[slashIndex+1:]
|
||||||
|
mp := ParseModelPath(tag)
|
||||||
|
manifest, err := GetManifest(mp)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("couldn't get manifest: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
model := api.ListResponseModel{
|
||||||
|
Name: mp.GetShortTagname(),
|
||||||
|
Size: manifest.GetTotalSize(),
|
||||||
|
ModifiedAt: fi.ModTime(),
|
||||||
|
}
|
||||||
|
models = append(models, model)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, api.ListResponse{models})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Serve(ln net.Listener) error {
|
func Serve(ln net.Listener) error {
|
||||||
@@ -158,6 +235,9 @@ func Serve(ln net.Listener) error {
|
|||||||
|
|
||||||
r.POST("/api/pull", pull)
|
r.POST("/api/pull", pull)
|
||||||
r.POST("/api/generate", generate)
|
r.POST("/api/generate", generate)
|
||||||
|
r.POST("/api/create", create)
|
||||||
|
r.POST("/api/push", push)
|
||||||
|
r.GET("/api/tags", list)
|
||||||
|
|
||||||
log.Printf("Listening on %s", ln.Addr())
|
log.Printf("Listening on %s", ln.Addr())
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
@@ -167,19 +247,7 @@ func Serve(ln net.Listener) error {
|
|||||||
return s.Serve(ln)
|
return s.Serve(ln)
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchRankOne(source string, targets []string) (bestMatch string, bestRank int) {
|
func streamResponse(c *gin.Context, ch chan any) {
|
||||||
bestRank = math.MaxInt
|
|
||||||
for _, target := range targets {
|
|
||||||
if rank := fuzzy.LevenshteinDistance(source, target); bestRank > rank {
|
|
||||||
bestRank = rank
|
|
||||||
bestMatch = target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func stream(c *gin.Context, ch chan any) {
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
val, ok := <-ch
|
val, ok := <-ch
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
|
{{- if not .Context }}
|
||||||
Below is an instruction that describes a task. Write a response that appropriately completes the request.
|
Below is an instruction that describes a task. Write a response that appropriately completes the request.
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
### Instruction:
|
### Instruction:
|
||||||
{{ .Prompt }}
|
{{ .Prompt }}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
{{- if not .Context }}
|
||||||
A helpful assistant who helps the user with any questions asked.
|
A helpful assistant who helps the user with any questions asked.
|
||||||
|
{{- end }}
|
||||||
User: {{ .Prompt }}
|
User: {{ .Prompt }}
|
||||||
Assistant:
|
Assistant:
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
|
{{- if not .Context }}
|
||||||
Below is an instruction that describes a task. Write a response that appropriately completes the request. Be concise. Once the request is completed, include no other text.
|
Below is an instruction that describes a task. Write a response that appropriately completes the request. Be concise. Once the request is completed, include no other text.
|
||||||
|
{{- end }}
|
||||||
### Instruction:
|
### Instruction:
|
||||||
{{ .Prompt }}
|
{{ .Prompt }}
|
||||||
### Response:
|
### Response:
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
|
{{- if not .Context }}
|
||||||
### System:
|
### System:
|
||||||
You are an AI assistant that follows instruction extremely well. Help as much as you can.
|
You are an AI assistant that follows instruction extremely well. Help as much as you can.
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
### User:
|
### User:
|
||||||
{{ .Prompt }}
|
{{ .Prompt }}
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
|
{{ if not .Context }}
|
||||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
USER: {{ .Prompt }}
|
USER: {{ .Prompt }}
|
||||||
ASSISTANT:
|
ASSISTANT:
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
|
{{- if not .Context }}
|
||||||
Below is an instruction that describes a task. Write a response that appropriately completes the request
|
Below is an instruction that describes a task. Write a response that appropriately completes the request
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
### Instruction: {{ .Prompt }}
|
### Instruction: {{ .Prompt }}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user