feat(models): support user-defined models via models.json
Reads $ZOT_HOME/models.json at startup and merges user-defined models into the active catalog with highest precedence. Provider keys like openai-codex are normalized. Documented in README.
- commit
- b245be02e5ce1ff5f93b69003e7ecd6cef4af274
- author
- patriceckhart · · 2026-04-23 21:09 UTC
- parent
- 47c1549
- signature
- unsigned
- refs
- tag: v0.1.4
4 files · +199 -0
M README.md +28 -0
| @@ -260,6 +260,34 @@ Every interactive or print/json run (unless `--no-session`) writes a JSONL trans | |||
| 260 | 260 | ||
| 261 | 261 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | |
| 262 | 262 | ||
| 263 | + | ### Custom models | |
| 264 | + | ||
| 265 | + | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | |
| 266 | + | ||
| 267 | + | ```json | |
| 268 | + | { | |
| 269 | + | "providers": { | |
| 270 | + | "openai": { | |
| 271 | + | "models": [ | |
| 272 | + | { | |
| 273 | + | "id": "gpt-5.5", | |
| 274 | + | "name": "GPT-5.5", | |
| 275 | + | "reasoning": true, | |
| 276 | + | "contextWindow": 400000, | |
| 277 | + | "maxTokens": 128000 | |
| 278 | + | } | |
| 279 | + | ] | |
| 280 | + | } | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ``` | |
| 284 | + | ||
| 285 | + | Supported fields per model: `id` (required), `name`, `reasoning`, `contextWindow`, `maxTokens`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`. | |
| 286 | + | ||
| 287 | + | Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`. | |
| 288 | + | ||
| 289 | + | 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. | |
| 290 | + | ||
| 263 | 291 | ## Inline images | |
| 264 | 292 | ||
| 265 | 293 | 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: | |
| @@ -260,6 +260,34 @@ Every interactive or print/json run (unless `--no-session`) writes a JSONL trans | |||||
| 260 | 260 | ||||
| 261 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | 261 | The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed. | ||
| 262 | 262 | ||||
| 263 | + | ### Custom models | |||
| 264 | + | ||||
| 265 | + | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | |||
| 266 | + | ||||
| 267 | + | ```json | |||
| 268 | + | { | |||
| 269 | + | "providers": { | |||
| 270 | + | "openai": { | |||
| 271 | + | "models": [ | |||
| 272 | + | { | |||
| 273 | + | "id": "gpt-5.5", | |||
| 274 | + | "name": "GPT-5.5", | |||
| 275 | + | "reasoning": true, | |||
| 276 | + | "contextWindow": 400000, | |||
| 277 | + | "maxTokens": 128000 | |||
| 278 | + | } | |||
| 279 | + | ] | |||
| 280 | + | } | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | ``` | |||
| 284 | + | ||||
| 285 | + | Supported fields per model: `id` (required), `name`, `reasoning`, `contextWindow`, `maxTokens`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`. | |||
| 286 | + | ||||
| 287 | + | Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`. | |||
| 288 | + | ||||
| 289 | + | 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. | |||
| 290 | + | ||||
| 263 | ## Inline images | 291 | ## Inline images | ||
| 264 | 292 | ||||
| 265 | 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: | 293 | 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: | ||
M internal/agent/cli.go +1 -0
| @@ -172,6 +172,7 @@ func Run(rawArgs []string, version string) error { | |||
| 172 | 172 | // Model catalog: load any cached discovery data before we inspect | |
| 173 | 173 | // the model list (list-models, print/json, interactive). | |
| 174 | 174 | LoadCachedModels() | |
| 175 | + | LoadUserModels() | |
| 175 | 176 | ||
| 176 | 177 | if args.ListModels { | |
| 177 | 178 | printModels() | |
| @@ -172,6 +172,7 @@ func Run(rawArgs []string, version string) error { | |||||
| 172 | // Model catalog: load any cached discovery data before we inspect | 172 | // Model catalog: load any cached discovery data before we inspect | ||
| 173 | // the model list (list-models, print/json, interactive). | 173 | // the model list (list-models, print/json, interactive). | ||
| 174 | LoadCachedModels() | 174 | LoadCachedModels() | ||
| 175 | + | LoadUserModels() | |||
| 175 | 176 | ||||
| 176 | if args.ListModels { | 177 | if args.ListModels { | ||
| 177 | printModels() | 178 | printModels() | ||
M internal/agent/modelsync.go +12 -0
| @@ -13,6 +13,11 @@ func ModelCachePath() string { | |||
| 13 | 13 | return filepath.Join(ZotHome(), "models-cache.json") | |
| 14 | 14 | } | |
| 15 | 15 | ||
| 16 | + | // UserModelsPath returns the path to the user's models.json override. | |
| 17 | + | func UserModelsPath() string { | |
| 18 | + | return filepath.Join(ZotHome(), "models.json") | |
| 19 | + | } | |
| 20 | + | ||
| 16 | 21 | // LoadCachedModels loads the cache file and applies it to the provider | |
| 17 | 22 | // package so FindModel / ModelsForProvider see live ids immediately. | |
| 18 | 23 | // Safe to call before any credentials are known. | |
| @@ -26,6 +31,13 @@ func LoadCachedModels() { | |||
| 26 | 31 | } | |
| 27 | 32 | } | |
| 28 | 33 | ||
| 34 | + | // LoadUserModels reads $ZOT_HOME/models.json and merges any user-defined | |
| 35 | + | // models into the active catalog. User models take highest precedence. | |
| 36 | + | func LoadUserModels() { | |
| 37 | + | models := provider.LoadUserModels(UserModelsPath()) | |
| 38 | + | provider.SetUserModels(models) | |
| 39 | + | } | |
| 40 | + | ||
| 29 | 41 | // RefreshModelsAsync kicks a background discovery for every provider | |
| 30 | 42 | // we have credentials for. Refreshed results are merged into the | |
| 31 | 43 | // active catalog and persisted to the on-disk cache. | |
| @@ -13,6 +13,11 @@ func ModelCachePath() string { | |||||
| 13 | return filepath.Join(ZotHome(), "models-cache.json") | 13 | return filepath.Join(ZotHome(), "models-cache.json") | ||
| 14 | } | 14 | } | ||
| 15 | 15 | ||||
| 16 | + | // UserModelsPath returns the path to the user's models.json override. | |||
| 17 | + | func UserModelsPath() string { | |||
| 18 | + | return filepath.Join(ZotHome(), "models.json") | |||
| 19 | + | } | |||
| 20 | + | ||||
| 16 | // LoadCachedModels loads the cache file and applies it to the provider | 21 | // LoadCachedModels loads the cache file and applies it to the provider | ||
| 17 | // package so FindModel / ModelsForProvider see live ids immediately. | 22 | // package so FindModel / ModelsForProvider see live ids immediately. | ||
| 18 | // Safe to call before any credentials are known. | 23 | // Safe to call before any credentials are known. | ||
| @@ -26,6 +31,13 @@ func LoadCachedModels() { | |||||
| 26 | } | 31 | } | ||
| 27 | } | 32 | } | ||
| 28 | 33 | ||||
| 34 | + | // LoadUserModels reads $ZOT_HOME/models.json and merges any user-defined | |||
| 35 | + | // models into the active catalog. User models take highest precedence. | |||
| 36 | + | func LoadUserModels() { | |||
| 37 | + | models := provider.LoadUserModels(UserModelsPath()) | |||
| 38 | + | provider.SetUserModels(models) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 29 | // RefreshModelsAsync kicks a background discovery for every provider | 41 | // RefreshModelsAsync kicks a background discovery for every provider | ||
| 30 | // we have credentials for. Refreshed results are merged into the | 42 | // we have credentials for. Refreshed results are merged into the | ||
| 31 | // active catalog and persisted to the on-disk cache. | 43 | // active catalog and persisted to the on-disk cache. | ||
A internal/provider/usermodels.go +158 -0
| @@ -0,0 +1,158 @@ | |||
| 1 | + | package provider | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "os" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // UserModelsFile is the JSON format for user-defined models. | |
| 9 | + | // Place a models.json in $ZOT_HOME to add models that aren't in the | |
| 10 | + | // baked-in catalog or to override catalog entries. | |
| 11 | + | // | |
| 12 | + | // Example: | |
| 13 | + | // | |
| 14 | + | // { | |
| 15 | + | // "providers": { | |
| 16 | + | // "openai": { | |
| 17 | + | // "models": [ | |
| 18 | + | // { | |
| 19 | + | // "id": "gpt-5.5", | |
| 20 | + | // "name": "GPT-5.5", | |
| 21 | + | // "reasoning": true, | |
| 22 | + | // "contextWindow": 400000, | |
| 23 | + | // "maxTokens": 128000, | |
| 24 | + | // "priceInput": 2.50, | |
| 25 | + | // "priceOutput": 15.00, | |
| 26 | + | // "priceCacheRead": 0.25 | |
| 27 | + | // } | |
| 28 | + | // ] | |
| 29 | + | // } | |
| 30 | + | // } | |
| 31 | + | // } | |
| 32 | + | type UserModelsFile struct { | |
| 33 | + | Providers map[string]UserProvider `json:"providers"` | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // UserProvider groups models under a provider key. | |
| 37 | + | type UserProvider struct { | |
| 38 | + | Models []UserModel `json:"models"` | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // UserModel is a single model entry in the user's models.json. | |
| 42 | + | type UserModel struct { | |
| 43 | + | ID string `json:"id"` | |
| 44 | + | Name string `json:"name"` | |
| 45 | + | Reasoning bool `json:"reasoning"` | |
| 46 | + | ContextWindow int `json:"contextWindow"` | |
| 47 | + | MaxTokens int `json:"maxTokens"` | |
| 48 | + | PriceInput float64 `json:"priceInput"` | |
| 49 | + | PriceOutput float64 `json:"priceOutput"` | |
| 50 | + | PriceCacheRead float64 `json:"priceCacheRead"` | |
| 51 | + | PriceCacheWrite float64 `json:"priceCacheWrite"` | |
| 52 | + | Input []string `json:"input"` // informational only | |
| 53 | + | API string `json:"api"` // informational only | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // LoadUserModels reads a models.json file and returns the models | |
| 57 | + | // converted to the internal Model type. Returns nil on any error | |
| 58 | + | // (missing file, bad JSON, etc.) so the caller can treat it as | |
| 59 | + | // optional without error handling. | |
| 60 | + | func LoadUserModels(path string) []Model { | |
| 61 | + | data, err := os.ReadFile(path) | |
| 62 | + | if err != nil { | |
| 63 | + | return nil | |
| 64 | + | } | |
| 65 | + | var file UserModelsFile | |
| 66 | + | if err := json.Unmarshal(data, &file); err != nil { | |
| 67 | + | return nil | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | var out []Model | |
| 71 | + | for providerName, prov := range file.Providers { | |
| 72 | + | // Normalize provider name: strip suffixes like "-codex" so | |
| 73 | + | // "openai-codex" maps to "openai". | |
| 74 | + | normalized := providerName | |
| 75 | + | switch providerName { | |
| 76 | + | case "openai-codex", "openai-responses": | |
| 77 | + | normalized = "openai" | |
| 78 | + | case "anthropic-messages": | |
| 79 | + | normalized = "anthropic" | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | for _, um := range prov.Models { | |
| 83 | + | m := Model{ | |
| 84 | + | Provider: normalized, | |
| 85 | + | ID: um.ID, | |
| 86 | + | DisplayName: um.Name, | |
| 87 | + | ContextWindow: um.ContextWindow, | |
| 88 | + | MaxOutput: um.MaxTokens, | |
| 89 | + | Reasoning: um.Reasoning, | |
| 90 | + | PriceInput: um.PriceInput, | |
| 91 | + | PriceOutput: um.PriceOutput, | |
| 92 | + | PriceCacheRead: um.PriceCacheRead, | |
| 93 | + | PriceCacheWrite: um.PriceCacheWrite, | |
| 94 | + | Source: "user", | |
| 95 | + | } | |
| 96 | + | if m.DisplayName == "" { | |
| 97 | + | m.DisplayName = m.ID | |
| 98 | + | } | |
| 99 | + | out = append(out, m) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | return out | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // SetUserModels merges user-defined models into the active catalog. | |
| 106 | + | // User models take precedence over both the baked-in catalog and | |
| 107 | + | // live-discovered models. | |
| 108 | + | func SetUserModels(models []Model) { | |
| 109 | + | if len(models) == 0 { | |
| 110 | + | return | |
| 111 | + | } | |
| 112 | + | activeMu.Lock() | |
| 113 | + | defer activeMu.Unlock() | |
| 114 | + | ||
| 115 | + | // Build index of current active models. | |
| 116 | + | byKey := func(p, id string) string { return p + "/" + id } | |
| 117 | + | index := make(map[string]int, len(active)) | |
| 118 | + | for i, m := range active { | |
| 119 | + | index[byKey(m.Provider, m.ID)] = i | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | for _, um := range models { | |
| 123 | + | k := byKey(um.Provider, um.ID) | |
| 124 | + | if idx, ok := index[k]; ok { | |
| 125 | + | // Override existing entry but keep prices from user if | |
| 126 | + | // they provided them, otherwise keep catalog prices. | |
| 127 | + | existing := active[idx] | |
| 128 | + | if um.PriceInput > 0 { | |
| 129 | + | existing.PriceInput = um.PriceInput | |
| 130 | + | } | |
| 131 | + | if um.PriceOutput > 0 { | |
| 132 | + | existing.PriceOutput = um.PriceOutput | |
| 133 | + | } | |
| 134 | + | if um.PriceCacheRead > 0 { | |
| 135 | + | existing.PriceCacheRead = um.PriceCacheRead | |
| 136 | + | } | |
| 137 | + | if um.PriceCacheWrite > 0 { | |
| 138 | + | existing.PriceCacheWrite = um.PriceCacheWrite | |
| 139 | + | } | |
| 140 | + | if um.DisplayName != "" { | |
| 141 | + | existing.DisplayName = um.DisplayName | |
| 142 | + | } | |
| 143 | + | if um.ContextWindow > 0 { | |
| 144 | + | existing.ContextWindow = um.ContextWindow | |
| 145 | + | } | |
| 146 | + | if um.MaxOutput > 0 { | |
| 147 | + | existing.MaxOutput = um.MaxOutput | |
| 148 | + | } | |
| 149 | + | existing.Reasoning = um.Reasoning | |
| 150 | + | existing.Source = "user" | |
| 151 | + | existing.Speculative = false | |
| 152 | + | active[idx] = existing | |
| 153 | + | } else { | |
| 154 | + | // New model not in catalog. | |
| 155 | + | active = append(active, um) | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | } | |
| @@ -0,0 +1,158 @@ | |||||
| 1 | + | package provider | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/json" | |||
| 5 | + | "os" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // UserModelsFile is the JSON format for user-defined models. | |||
| 9 | + | // Place a models.json in $ZOT_HOME to add models that aren't in the | |||
| 10 | + | // baked-in catalog or to override catalog entries. | |||
| 11 | + | // | |||
| 12 | + | // Example: | |||
| 13 | + | // | |||
| 14 | + | // { | |||
| 15 | + | // "providers": { | |||
| 16 | + | // "openai": { | |||
| 17 | + | // "models": [ | |||
| 18 | + | // { | |||
| 19 | + | // "id": "gpt-5.5", | |||
| 20 | + | // "name": "GPT-5.5", | |||
| 21 | + | // "reasoning": true, | |||
| 22 | + | // "contextWindow": 400000, | |||
| 23 | + | // "maxTokens": 128000, | |||
| 24 | + | // "priceInput": 2.50, | |||
| 25 | + | // "priceOutput": 15.00, | |||
| 26 | + | // "priceCacheRead": 0.25 | |||
| 27 | + | // } | |||
| 28 | + | // ] | |||
| 29 | + | // } | |||
| 30 | + | // } | |||
| 31 | + | // } | |||
| 32 | + | type UserModelsFile struct { | |||
| 33 | + | Providers map[string]UserProvider `json:"providers"` | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // UserProvider groups models under a provider key. | |||
| 37 | + | type UserProvider struct { | |||
| 38 | + | Models []UserModel `json:"models"` | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // UserModel is a single model entry in the user's models.json. | |||
| 42 | + | type UserModel struct { | |||
| 43 | + | ID string `json:"id"` | |||
| 44 | + | Name string `json:"name"` | |||
| 45 | + | Reasoning bool `json:"reasoning"` | |||
| 46 | + | ContextWindow int `json:"contextWindow"` | |||
| 47 | + | MaxTokens int `json:"maxTokens"` | |||
| 48 | + | PriceInput float64 `json:"priceInput"` | |||
| 49 | + | PriceOutput float64 `json:"priceOutput"` | |||
| 50 | + | PriceCacheRead float64 `json:"priceCacheRead"` | |||
| 51 | + | PriceCacheWrite float64 `json:"priceCacheWrite"` | |||
| 52 | + | Input []string `json:"input"` // informational only | |||
| 53 | + | API string `json:"api"` // informational only | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // LoadUserModels reads a models.json file and returns the models | |||
| 57 | + | // converted to the internal Model type. Returns nil on any error | |||
| 58 | + | // (missing file, bad JSON, etc.) so the caller can treat it as | |||
| 59 | + | // optional without error handling. | |||
| 60 | + | func LoadUserModels(path string) []Model { | |||
| 61 | + | data, err := os.ReadFile(path) | |||
| 62 | + | if err != nil { | |||
| 63 | + | return nil | |||
| 64 | + | } | |||
| 65 | + | var file UserModelsFile | |||
| 66 | + | if err := json.Unmarshal(data, &file); err != nil { | |||
| 67 | + | return nil | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | var out []Model | |||
| 71 | + | for providerName, prov := range file.Providers { | |||
| 72 | + | // Normalize provider name: strip suffixes like "-codex" so | |||
| 73 | + | // "openai-codex" maps to "openai". | |||
| 74 | + | normalized := providerName | |||
| 75 | + | switch providerName { | |||
| 76 | + | case "openai-codex", "openai-responses": | |||
| 77 | + | normalized = "openai" | |||
| 78 | + | case "anthropic-messages": | |||
| 79 | + | normalized = "anthropic" | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | for _, um := range prov.Models { | |||
| 83 | + | m := Model{ | |||
| 84 | + | Provider: normalized, | |||
| 85 | + | ID: um.ID, | |||
| 86 | + | DisplayName: um.Name, | |||
| 87 | + | ContextWindow: um.ContextWindow, | |||
| 88 | + | MaxOutput: um.MaxTokens, | |||
| 89 | + | Reasoning: um.Reasoning, | |||
| 90 | + | PriceInput: um.PriceInput, | |||
| 91 | + | PriceOutput: um.PriceOutput, | |||
| 92 | + | PriceCacheRead: um.PriceCacheRead, | |||
| 93 | + | PriceCacheWrite: um.PriceCacheWrite, | |||
| 94 | + | Source: "user", | |||
| 95 | + | } | |||
| 96 | + | if m.DisplayName == "" { | |||
| 97 | + | m.DisplayName = m.ID | |||
| 98 | + | } | |||
| 99 | + | out = append(out, m) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | return out | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // SetUserModels merges user-defined models into the active catalog. | |||
| 106 | + | // User models take precedence over both the baked-in catalog and | |||
| 107 | + | // live-discovered models. | |||
| 108 | + | func SetUserModels(models []Model) { | |||
| 109 | + | if len(models) == 0 { | |||
| 110 | + | return | |||
| 111 | + | } | |||
| 112 | + | activeMu.Lock() | |||
| 113 | + | defer activeMu.Unlock() | |||
| 114 | + | ||||
| 115 | + | // Build index of current active models. | |||
| 116 | + | byKey := func(p, id string) string { return p + "/" + id } | |||
| 117 | + | index := make(map[string]int, len(active)) | |||
| 118 | + | for i, m := range active { | |||
| 119 | + | index[byKey(m.Provider, m.ID)] = i | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | for _, um := range models { | |||
| 123 | + | k := byKey(um.Provider, um.ID) | |||
| 124 | + | if idx, ok := index[k]; ok { | |||
| 125 | + | // Override existing entry but keep prices from user if | |||
| 126 | + | // they provided them, otherwise keep catalog prices. | |||
| 127 | + | existing := active[idx] | |||
| 128 | + | if um.PriceInput > 0 { | |||
| 129 | + | existing.PriceInput = um.PriceInput | |||
| 130 | + | } | |||
| 131 | + | if um.PriceOutput > 0 { | |||
| 132 | + | existing.PriceOutput = um.PriceOutput | |||
| 133 | + | } | |||
| 134 | + | if um.PriceCacheRead > 0 { | |||
| 135 | + | existing.PriceCacheRead = um.PriceCacheRead | |||
| 136 | + | } | |||
| 137 | + | if um.PriceCacheWrite > 0 { | |||
| 138 | + | existing.PriceCacheWrite = um.PriceCacheWrite | |||
| 139 | + | } | |||
| 140 | + | if um.DisplayName != "" { | |||
| 141 | + | existing.DisplayName = um.DisplayName | |||
| 142 | + | } | |||
| 143 | + | if um.ContextWindow > 0 { | |||
| 144 | + | existing.ContextWindow = um.ContextWindow | |||
| 145 | + | } | |||
| 146 | + | if um.MaxOutput > 0 { | |||
| 147 | + | existing.MaxOutput = um.MaxOutput | |||
| 148 | + | } | |||
| 149 | + | existing.Reasoning = um.Reasoning | |||
| 150 | + | existing.Source = "user" | |||
| 151 | + | existing.Speculative = false | |||
| 152 | + | active[idx] = existing | |||
| 153 | + | } else { | |||
| 154 | + | // New model not in catalog. | |||
| 155 | + | active = append(active, um) | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | } | |||