mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
feat: add ollama as first-class provider
Adds --provider ollama with auto-detection of local ollama at localhost:11434. No API key required for local models. Optional --api-key and --base-url for remote/authenticated instances. Uses the OpenAI chat completions client internally. Unknown models are accepted without catalog entries. Updated README with ollama documentation.
This commit is contained in:
parent
b3a935fbe7
commit
cb9de10ec6
4 changed files with 108 additions and 24 deletions
44
README.md
44
README.md
|
|
@ -282,15 +282,55 @@ Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/
|
|||
}
|
||||
```
|
||||
|
||||
Supported fields per model: `id` (required), `name`, `reasoning`, `contextWindow`, `maxTokens`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`.
|
||||
Supported fields per model: `id` (required), `name`, `reasoning`, `contextWindow`, `maxTokens`, `baseUrl`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`.
|
||||
|
||||
Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`.
|
||||
|
||||
User-defined models show `source: user` in `--list-models` and take precedence over both the baked-in catalog and live-discovered models. Missing or invalid files are silently ignored.
|
||||
|
||||
### Local models with ollama
|
||||
|
||||
zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
|
||||
|
||||
Quick start:
|
||||
|
||||
```bash
|
||||
ollama pull qwen3.5:4b
|
||||
zot --provider ollama --model qwen3.5:4b
|
||||
```
|
||||
|
||||
That's it. No API key needed for local models. zot defaults to `http://localhost:11434`.
|
||||
|
||||
For a remote ollama instance or one behind auth:
|
||||
|
||||
```bash
|
||||
zot --provider ollama --model llama3 --base-url https://my-server.com/v1 --api-key my-token
|
||||
```
|
||||
|
||||
You can also add models to your `models.json` so you don't need flags every time:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"models": [
|
||||
{
|
||||
"id": "qwen3.5:4b",
|
||||
"name": "Qwen 3.5 4B",
|
||||
"contextWindow": 32768,
|
||||
"maxTokens": 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ollama` provider uses the OpenAI chat completions protocol internally, so it also works with any OpenAI-compatible server (vLLM, LM Studio, LocalAI, etc.).
|
||||
|
||||
## Inline images
|
||||
|
||||
When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **iTerm2**, **WezTerm**, **Kitty**, **Ghostty**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var:
|
||||
When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **Ghostty**, **Kitty**, **iTerm2**, **WezTerm**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var:
|
||||
|
||||
| Value | Effect |
|
||||
|---|---|
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ func PrintHelp(version string) {
|
|||
row{"zot tg ...", "short alias for telegram-bot"},
|
||||
)
|
||||
section("provider and model flags",
|
||||
row{"--provider", "provider to use (anthropic|openai)"},
|
||||
row{"--provider", "provider to use (anthropic|openai|ollama)"},
|
||||
row{"--model ID", "model id (see --list-models)"},
|
||||
row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
|
||||
row{"--base-url URL", "override provider api base url"},
|
||||
|
|
|
|||
|
|
@ -127,19 +127,32 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
|
|||
|
||||
// User-requested provider (explicit > config > default).
|
||||
provName := firstNonEmpty(args.Provider, cfg.Provider, "anthropic")
|
||||
if provName != "anthropic" && provName != "openai" {
|
||||
return Resolved{}, fmt.Errorf("provider must be anthropic or openai (got %q)", provName)
|
||||
if provName != "anthropic" && provName != "openai" && provName != "ollama" {
|
||||
return Resolved{}, fmt.Errorf("provider must be anthropic, openai, or ollama (got %q)", provName)
|
||||
}
|
||||
|
||||
// Try the requested provider first.
|
||||
cred, method, accountID, credErr := ResolveCredentialFull(provName, args.APIKey)
|
||||
var (
|
||||
cred string
|
||||
method string
|
||||
accountID string
|
||||
credErr error
|
||||
)
|
||||
if provName == "ollama" {
|
||||
if args.BaseURL == "" {
|
||||
args.BaseURL = "http://localhost:11434"
|
||||
}
|
||||
cred = firstNonEmpty(args.APIKey, "ollama")
|
||||
method = "apikey"
|
||||
} else {
|
||||
cred, method, accountID, credErr = ResolveCredentialFull(provName, args.APIKey)
|
||||
}
|
||||
|
||||
// If the user did NOT explicitly pick a provider and the default one
|
||||
// has no credentials, auto-fall-back to whichever provider is actually
|
||||
// logged in. That way running plain `zot` after `/login` (any provider)
|
||||
// never shows a "not logged in" banner.
|
||||
userPickedProvider := args.Provider != ""
|
||||
if credErr != nil && !userPickedProvider {
|
||||
if credErr != nil && !userPickedProvider && provName != "ollama" {
|
||||
other := "openai"
|
||||
if provName == "openai" {
|
||||
other = "anthropic"
|
||||
|
|
@ -152,25 +165,41 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
|
|||
|
||||
model := firstNonEmpty(args.Model, cfg.Model)
|
||||
if model == "" {
|
||||
if provName == "openai" {
|
||||
switch provName {
|
||||
case "openai":
|
||||
model = "gpt-5"
|
||||
} else {
|
||||
case "ollama":
|
||||
return Resolved{}, fmt.Errorf("ollama requires --model (e.g. --model llama3)")
|
||||
default:
|
||||
model = provider.DefaultModel.ID
|
||||
}
|
||||
}
|
||||
// If the resolved model belongs to a different provider (e.g. config
|
||||
// says gpt-5 but we fell back to anthropic), pick that provider's default.
|
||||
if m, err := provider.FindModel("", model); err == nil && m.Provider != provName {
|
||||
if provName == "openai" {
|
||||
model = "gpt-5"
|
||||
} else {
|
||||
model = provider.DefaultModel.ID
|
||||
if provName != "ollama" {
|
||||
if m, err := provider.FindModel("", model); err == nil && m.Provider != provName {
|
||||
if provName == "openai" {
|
||||
model = "gpt-5"
|
||||
} else {
|
||||
model = provider.DefaultModel.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
resolvedModel, err := provider.FindModel(provName, model)
|
||||
if err != nil {
|
||||
if err != nil && provName != "ollama" {
|
||||
return Resolved{}, err
|
||||
}
|
||||
if err != nil && provName == "ollama" {
|
||||
resolvedModel = provider.Model{
|
||||
Provider: "ollama",
|
||||
ID: model,
|
||||
DisplayName: model,
|
||||
ContextWindow: 32768,
|
||||
MaxOutput: 8192,
|
||||
BaseURL: args.BaseURL,
|
||||
Source: "ollama",
|
||||
}
|
||||
}
|
||||
|
||||
// If the model defines a base URL (e.g. local ollama) and the
|
||||
// user didn't pass --base-url, use the model's URL.
|
||||
|
|
@ -303,16 +332,20 @@ func (r Resolved) NewClient() provider.Client {
|
|||
if !r.HasCredential() {
|
||||
panic("NewClient called without credential; check HasCredential first")
|
||||
}
|
||||
if r.Provider == "openai" {
|
||||
switch r.Provider {
|
||||
case "ollama":
|
||||
return provider.NewOpenAI(r.Credential, r.BaseURL)
|
||||
case "openai":
|
||||
if r.AuthMethod == "oauth" {
|
||||
return provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)
|
||||
}
|
||||
return provider.NewOpenAI(r.Credential, r.BaseURL)
|
||||
default:
|
||||
if r.AuthMethod == "oauth" {
|
||||
return provider.NewAnthropicOAuth(r.Credential, r.BaseURL)
|
||||
}
|
||||
return provider.NewAnthropic(r.Credential, r.BaseURL)
|
||||
}
|
||||
if r.AuthMethod == "oauth" {
|
||||
return provider.NewAnthropicOAuth(r.Credential, r.BaseURL)
|
||||
}
|
||||
return provider.NewAnthropic(r.Credential, r.BaseURL)
|
||||
}
|
||||
|
||||
// UseSandbox replaces the sandbox pointer that every tool in r's
|
||||
|
|
@ -395,6 +428,8 @@ func envVarName(provider string) string {
|
|||
switch provider {
|
||||
case "openai":
|
||||
return "OPENAI"
|
||||
case "ollama":
|
||||
return "OLLAMA"
|
||||
default:
|
||||
return "ANTHROPIC"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,9 +111,18 @@ type oaiRequest struct {
|
|||
// ---- request building ----
|
||||
|
||||
func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) {
|
||||
m, err := FindModel("openai", req.Model)
|
||||
// Look up the model by id across all providers (not just openai)
|
||||
// because the OpenAI client is also used for ollama and other
|
||||
// OpenAI-compatible backends.
|
||||
m, err := FindModel("", req.Model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Unknown model: use sensible defaults so local/custom
|
||||
// models still work without a catalog entry.
|
||||
m = Model{
|
||||
ID: req.Model,
|
||||
ContextWindow: 32768,
|
||||
MaxOutput: 8192,
|
||||
}
|
||||
}
|
||||
out := &oaiRequest{
|
||||
Model: req.Model,
|
||||
|
|
@ -311,7 +320,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R
|
|||
defer close(out)
|
||||
defer resp.Body.Close()
|
||||
|
||||
model, _ := FindModel("openai", req.Model)
|
||||
model, _ := FindModel("", req.Model)
|
||||
out <- EventStart{Model: req.Model, Provider: "openai"}
|
||||
|
||||
raw := make(chan sseEvent, 16)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue