feat: custom models with baseUrl + domain migration to www.zot.sh
Adds baseUrl support in models.json for local models (ollama, vLLM, etc). Migrates all install URLs and references from zot.patriceckhart.com to www.zot.sh.
- commit
- b25a2bc8543f9d8d602168d6f0e97792345bfc11
- author
- patriceckhart · · 2026-04-24 12:00 UTC
- parent
- dcc9ba0
- signature
- unsigned
- refs
- tag: v0.1.6
5 files · +33 -7
M README.md +3 -3
| @@ -18,19 +18,19 @@ Yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |||
| 18 | 18 | ### One-liner (macOS, Linux) | |
| 19 | 19 | ||
| 20 | 20 | ```bash | |
| 21 | - | curl -fsSL https://zot.patriceckhart.com/install.sh | bash | |
| 21 | + | curl -fsSL https://www.zot.sh/install.sh | bash | |
| 22 | 22 | ``` | |
| 23 | 23 | ||
| 24 | 24 | Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin: | |
| 25 | 25 | ||
| 26 | 26 | ```bash | |
| 27 | - | curl -fsSL https://zot.patriceckhart.com/install.sh | bash -s -- v0.0.1 ~/bin | |
| 27 | + | curl -fsSL https://www.zot.sh/install.sh | bash -s -- v0.0.1 ~/bin | |
| 28 | 28 | ``` | |
| 29 | 29 | ||
| 30 | 30 | ### One-liner (Windows, PowerShell) | |
| 31 | 31 | ||
| 32 | 32 | ```powershell | |
| 33 | - | iwr -useb https://zot.patriceckhart.com/install.ps1 | iex | |
| 33 | + | iwr -useb https://www.zot.sh/install.ps1 | iex | |
| 34 | 34 | ``` | |
| 35 | 35 | ||
| 36 | 36 | Drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards. | |
| @@ -18,19 +18,19 @@ Yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |||||
| 18 | ### One-liner (macOS, Linux) | 18 | ### One-liner (macOS, Linux) | ||
| 19 | 19 | ||||
| 20 | ```bash | 20 | ```bash | ||
| 21 | - | curl -fsSL https://zot.patriceckhart.com/install.sh | bash | 21 | + | curl -fsSL https://www.zot.sh/install.sh | bash |
| 22 | ``` | 22 | ``` | ||
| 23 | 23 | ||||
| 24 | Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin: | 24 | Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin: | ||
| 25 | 25 | ||||
| 26 | ```bash | 26 | ```bash | ||
| 27 | - | curl -fsSL https://zot.patriceckhart.com/install.sh | bash -s -- v0.0.1 ~/bin | 27 | + | curl -fsSL https://www.zot.sh/install.sh | bash -s -- v0.0.1 ~/bin |
| 28 | ``` | 28 | ``` | ||
| 29 | 29 | ||||
| 30 | ### One-liner (Windows, PowerShell) | 30 | ### One-liner (Windows, PowerShell) | ||
| 31 | 31 | ||||
| 32 | ```powershell | 32 | ```powershell | ||
| 33 | - | iwr -useb https://zot.patriceckhart.com/install.ps1 | iex | 33 | + | iwr -useb https://www.zot.sh/install.ps1 | iex |
| 34 | ``` | 34 | ``` | ||
| 35 | 35 | ||||
| 36 | Drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards. | 36 | Drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards. | ||
M internal/agent/build.go +16 -1
| @@ -167,10 +167,25 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||
| 167 | 167 | model = provider.DefaultModel.ID | |
| 168 | 168 | } | |
| 169 | 169 | } | |
| 170 | - | if _, err := provider.FindModel(provName, model); err != nil { | |
| 170 | + | resolvedModel, err := provider.FindModel(provName, model) | |
| 171 | + | if err != nil { | |
| 171 | 172 | return Resolved{}, err | |
| 172 | 173 | } | |
| 173 | 174 | ||
| 175 | + | // If the model defines a base URL (e.g. local ollama) and the | |
| 176 | + | // user didn't pass --base-url, use the model's URL. | |
| 177 | + | if args.BaseURL == "" && resolvedModel.BaseURL != "" { | |
| 178 | + | args.BaseURL = resolvedModel.BaseURL | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // If the model has a base URL, credentials are optional (local | |
| 182 | + | // models like ollama don't need real API keys). | |
| 183 | + | if resolvedModel.BaseURL != "" && credErr != nil { | |
| 184 | + | cred = "ollama" | |
| 185 | + | credErr = nil | |
| 186 | + | requireCred = false | |
| 187 | + | } | |
| 188 | + | ||
| 174 | 189 | if credErr != nil && requireCred { | |
| 175 | 190 | return Resolved{}, fmt.Errorf("%w; set %s_API_KEY, pass --api-key, or run `zot` and /login", | |
| 176 | 191 | credErr, envVarName(provName)) | |
| @@ -167,10 +167,25 @@ func Resolve(args Args, requireCred bool) (Resolved, error) { | |||||
| 167 | model = provider.DefaultModel.ID | 167 | model = provider.DefaultModel.ID | ||
| 168 | } | 168 | } | ||
| 169 | } | 169 | } | ||
| 170 | - | if _, err := provider.FindModel(provName, model); err != nil { | 170 | + | resolvedModel, err := provider.FindModel(provName, model) |
| 171 | + | if err != nil { | |||
| 171 | return Resolved{}, err | 172 | return Resolved{}, err | ||
| 172 | } | 173 | } | ||
| 173 | 174 | ||||
| 175 | + | // If the model defines a base URL (e.g. local ollama) and the | |||
| 176 | + | // user didn't pass --base-url, use the model's URL. | |||
| 177 | + | if args.BaseURL == "" && resolvedModel.BaseURL != "" { | |||
| 178 | + | args.BaseURL = resolvedModel.BaseURL | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // If the model has a base URL, credentials are optional (local | |||
| 182 | + | // models like ollama don't need real API keys). | |||
| 183 | + | if resolvedModel.BaseURL != "" && credErr != nil { | |||
| 184 | + | cred = "ollama" | |||
| 185 | + | credErr = nil | |||
| 186 | + | requireCred = false | |||
| 187 | + | } | |||
| 188 | + | ||||
| 174 | if credErr != nil && requireCred { | 189 | if credErr != nil && requireCred { | ||
| 175 | return Resolved{}, fmt.Errorf("%w; set %s_API_KEY, pass --api-key, or run `zot` and /login", | 190 | return Resolved{}, fmt.Errorf("%w; set %s_API_KEY, pass --api-key, or run `zot` and /login", | ||
| 176 | credErr, envVarName(provName)) | 191 | credErr, envVarName(provName)) | ||
M internal/agent/modes/update_banner.go +3 -3
| @@ -48,12 +48,12 @@ func renderUpdateBanner(th tui.Theme, info UpdateInfo, width int) []string { | |||
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | 50 | // recommendedUpdateCommand returns the one-liner appropriate for the | |
| 51 | - | // user's platform. Points at zot.patriceckhart.com, which 301s to | |
| 51 | + | // user's platform. Points at www.zot.sh, which 301s to | |
| 52 | 52 | // the raw install scripts on github — same surface, stable short | |
| 53 | 53 | // URL that's safe to hard-code even if the scripts later move. | |
| 54 | 54 | func recommendedUpdateCommand() string { | |
| 55 | 55 | if runtime.GOOS == "windows" { | |
| 56 | - | return `iwr -useb https://zot.patriceckhart.com/install.ps1 | iex` | |
| 56 | + | return `iwr -useb https://www.zot.sh/install.ps1 | iex` | |
| 57 | 57 | } | |
| 58 | - | return `curl -fsSL https://zot.patriceckhart.com/install.sh | bash` | |
| 58 | + | return `curl -fsSL https://www.zot.sh/install.sh | bash` | |
| 59 | 59 | } | |
| @@ -48,12 +48,12 @@ func renderUpdateBanner(th tui.Theme, info UpdateInfo, width int) []string { | |||||
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | // recommendedUpdateCommand returns the one-liner appropriate for the | 50 | // recommendedUpdateCommand returns the one-liner appropriate for the | ||
| 51 | - | // user's platform. Points at zot.patriceckhart.com, which 301s to | 51 | + | // user's platform. Points at www.zot.sh, which 301s to |
| 52 | // the raw install scripts on github — same surface, stable short | 52 | // the raw install scripts on github — same surface, stable short | ||
| 53 | // URL that's safe to hard-code even if the scripts later move. | 53 | // URL that's safe to hard-code even if the scripts later move. | ||
| 54 | func recommendedUpdateCommand() string { | 54 | func recommendedUpdateCommand() string { | ||
| 55 | if runtime.GOOS == "windows" { | 55 | if runtime.GOOS == "windows" { | ||
| 56 | - | return `iwr -useb https://zot.patriceckhart.com/install.ps1 | iex` | 56 | + | return `iwr -useb https://www.zot.sh/install.ps1 | iex` |
| 57 | } | 57 | } | ||
| 58 | - | return `curl -fsSL https://zot.patriceckhart.com/install.sh | bash` | 58 | + | return `curl -fsSL https://www.zot.sh/install.sh | bash` |
| 59 | } | 59 | } | ||
M internal/provider/models.go +6 -0
| @@ -25,6 +25,12 @@ type Model struct { | |||
| 25 | 25 | // today but start working the moment the provider flips the switch. | |
| 26 | 26 | Speculative bool | |
| 27 | 27 | ||
| 28 | + | // BaseURL overrides the provider's default API endpoint for this | |
| 29 | + | // model. Optional; when empty the provider's default (or the | |
| 30 | + | // --base-url flag) is used. Useful for local models served by | |
| 31 | + | // ollama, vLLM, LM Studio, etc. | |
| 32 | + | BaseURL string | |
| 33 | + | ||
| 28 | 34 | // Source is where this model entry came from: "catalog" (baked in), | |
| 29 | 35 | // "live" (discovered via /v1/models), or "cache" (loaded from the | |
| 30 | 36 | // on-disk cache). Informational. | |
| @@ -25,6 +25,12 @@ type Model struct { | |||||
| 25 | // today but start working the moment the provider flips the switch. | 25 | // today but start working the moment the provider flips the switch. | ||
| 26 | Speculative bool | 26 | Speculative bool | ||
| 27 | 27 | ||||
| 28 | + | // BaseURL overrides the provider's default API endpoint for this | |||
| 29 | + | // model. Optional; when empty the provider's default (or the | |||
| 30 | + | // --base-url flag) is used. Useful for local models served by | |||
| 31 | + | // ollama, vLLM, LM Studio, etc. | |||
| 32 | + | BaseURL string | |||
| 33 | + | ||||
| 28 | // Source is where this model entry came from: "catalog" (baked in), | 34 | // Source is where this model entry came from: "catalog" (baked in), | ||
| 29 | // "live" (discovered via /v1/models), or "cache" (loaded from the | 35 | // "live" (discovered via /v1/models), or "cache" (loaded from the | ||
| 30 | // on-disk cache). Informational. | 36 | // on-disk cache). Informational. | ||
M internal/provider/usermodels.go +5 -0
| @@ -49,6 +49,7 @@ type UserModel struct { | |||
| 49 | 49 | PriceOutput float64 `json:"priceOutput"` | |
| 50 | 50 | PriceCacheRead float64 `json:"priceCacheRead"` | |
| 51 | 51 | PriceCacheWrite float64 `json:"priceCacheWrite"` | |
| 52 | + | BaseURL string `json:"baseUrl,omitempty"` | |
| 52 | 53 | Input []string `json:"input"` // informational only | |
| 53 | 54 | API string `json:"api"` // informational only | |
| 54 | 55 | } | |
| @@ -91,6 +92,7 @@ func LoadUserModels(path string) []Model { | |||
| 91 | 92 | PriceOutput: um.PriceOutput, | |
| 92 | 93 | PriceCacheRead: um.PriceCacheRead, | |
| 93 | 94 | PriceCacheWrite: um.PriceCacheWrite, | |
| 95 | + | BaseURL: um.BaseURL, | |
| 94 | 96 | Source: "user", | |
| 95 | 97 | } | |
| 96 | 98 | if m.DisplayName == "" { | |
| @@ -147,6 +149,9 @@ func SetUserModels(models []Model) { | |||
| 147 | 149 | existing.MaxOutput = um.MaxOutput | |
| 148 | 150 | } | |
| 149 | 151 | existing.Reasoning = um.Reasoning | |
| 152 | + | if um.BaseURL != "" { | |
| 153 | + | existing.BaseURL = um.BaseURL | |
| 154 | + | } | |
| 150 | 155 | existing.Source = "user" | |
| 151 | 156 | existing.Speculative = false | |
| 152 | 157 | active[idx] = existing | |
| @@ -49,6 +49,7 @@ type UserModel struct { | |||||
| 49 | PriceOutput float64 `json:"priceOutput"` | 49 | PriceOutput float64 `json:"priceOutput"` | ||
| 50 | PriceCacheRead float64 `json:"priceCacheRead"` | 50 | PriceCacheRead float64 `json:"priceCacheRead"` | ||
| 51 | PriceCacheWrite float64 `json:"priceCacheWrite"` | 51 | PriceCacheWrite float64 `json:"priceCacheWrite"` | ||
| 52 | + | BaseURL string `json:"baseUrl,omitempty"` | |||
| 52 | Input []string `json:"input"` // informational only | 53 | Input []string `json:"input"` // informational only | ||
| 53 | API string `json:"api"` // informational only | 54 | API string `json:"api"` // informational only | ||
| 54 | } | 55 | } | ||
| @@ -91,6 +92,7 @@ func LoadUserModels(path string) []Model { | |||||
| 91 | PriceOutput: um.PriceOutput, | 92 | PriceOutput: um.PriceOutput, | ||
| 92 | PriceCacheRead: um.PriceCacheRead, | 93 | PriceCacheRead: um.PriceCacheRead, | ||
| 93 | PriceCacheWrite: um.PriceCacheWrite, | 94 | PriceCacheWrite: um.PriceCacheWrite, | ||
| 95 | + | BaseURL: um.BaseURL, | |||
| 94 | Source: "user", | 96 | Source: "user", | ||
| 95 | } | 97 | } | ||
| 96 | if m.DisplayName == "" { | 98 | if m.DisplayName == "" { | ||
| @@ -147,6 +149,9 @@ func SetUserModels(models []Model) { | |||||
| 147 | existing.MaxOutput = um.MaxOutput | 149 | existing.MaxOutput = um.MaxOutput | ||
| 148 | } | 150 | } | ||
| 149 | existing.Reasoning = um.Reasoning | 151 | existing.Reasoning = um.Reasoning | ||
| 152 | + | if um.BaseURL != "" { | |||
| 153 | + | existing.BaseURL = um.BaseURL | |||
| 154 | + | } | |||
| 150 | existing.Source = "user" | 155 | existing.Source = "user" | ||
| 151 | existing.Speculative = false | 156 | existing.Speculative = false | ||
| 152 | active[idx] = existing | 157 | active[idx] = existing | ||