public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/ollama] rawhide: Add OLLAMA_HOST comments to ollama.service
@ 2026-06-07 15:14 Tom Rix
0 siblings, 0 replies; only message in thread
From: Tom Rix @ 2026-06-07 15:14 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/ollama
Branch : rawhide
Commit : a9c382e66c50748b9128f703d63161c06b14274c
Author : Tom Rix <Tom.Rix@amd.com>
Date : 2026-06-07T08:11:49-07:00
Stats : +17/-1002 in 3 file(s)
URL : https://src.fedoraproject.org/rpms/ollama/c/a9c382e66c50748b9128f703d63161c06b14274c?branch=rawhide
Log:
Add OLLAMA_HOST comments to ollama.service
These will aid the user with some common setup use cases.
Signed-off-by: Tom Rix <Tom.Rix@amd.com>
---
diff --git a/0001-ollama-crush-integration.patch b/0001-ollama-crush-integration.patch
deleted file mode 100644
index 5fbc0c7..0000000
--- a/0001-ollama-crush-integration.patch
+++ /dev/null
@@ -1,1001 +0,0 @@
-From 7fb9e6e1cd96661832f8de427390f0039083e07e Mon Sep 17 00:00:00 2001
-From: Tom Rix <Tom.Rix@amd.com>
-Date: Mon, 23 Feb 2026 19:10:10 -0800
-Subject: [PATCH] ollama crush integration
-
----
- cmd/config/crush.go | 285 ++++++++++++++++
- cmd/config/crush_test.go | 668 +++++++++++++++++++++++++++++++++++++
- cmd/config/integrations.go | 2 +
- 3 files changed, 955 insertions(+)
- create mode 100644 cmd/config/crush.go
- create mode 100644 cmd/config/crush_test.go
-
-diff --git a/cmd/config/crush.go b/cmd/config/crush.go
-new file mode 100644
-index 000000000000..f54591e180d3
---- /dev/null
-+++ b/cmd/config/crush.go
-@@ -0,0 +1,285 @@
-+package config
-+
-+import (
-+ "encoding/json"
-+ "fmt"
-+ "io/ioutil"
-+ "log"
-+ "maps"
-+ "os"
-+ "os/exec"
-+ "path/filepath"
-+ "slices"
-+ "github.com/ollama/ollama/envconfig"
-+)
-+
-+// ProviderConfig represents the configuration for a specific AI provider
-+type ProviderConfig struct {
-+ ID string `json:"id"`
-+ Name string `json:"name"`
-+ BaseURL string `json:"base_url"`
-+ Type string `json:"type"`
-+ APIKey string `json:"api_key"`
-+ OAuth interface{} `json:"oauth,omitempty"`
-+ Disable bool `json:"disable,omitempty"`
-+ SystemPromptPrefix string `json:"system_prompt_prefix,omitempty"`
-+ ExtraHeaders interface{} `json:"extra_headers,omitempty"`
-+ ExtraBody interface{} `json:"extra_body,omitempty"`
-+ ProviderOptions interface{} `json:"provider_options,omitempty"`
-+ Models []Model `json:"models,omitempty"`
-+}
-+
-+// Model represents an AI model configuration
-+type Model struct {
-+ ID string `json:"id"`
-+ Name string `json:"name"`
-+ CostPer1MIn float64 `json:"cost_per_1m_in"`
-+ CostPer1MOut float64 `json:"cost_per_1m_out"`
-+ CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
-+ CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
-+ ContextWindow int `json:"context_window"`
-+ DefaultMaxTokens int `json:"default_max_tokens"`
-+ CanReason bool `json:"can_reason"`
-+ ReasoningLevels []string `json:"reasoning_levels,omitempty"`
-+ DefaultReasoningEffort string `json:"default_reasoning_effort,omitempty"`
-+ SupportsAttachments bool `json:"supports_attachments"`
-+ Options ModelOptions `json:"options"`
-+}
-+
-+// ModelOptions represents options for a specific model
-+type ModelOptions struct {
-+ Temperature float64 `json:"temperature,omitempty"`
-+ TopP float64 `json:"top_p,omitempty"`
-+ TopK int `json:"top_k,omitempty"`
-+ FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
-+ PresencePenalty float64 `json:"presence_penalty,omitempty"`
-+ ProviderOptions interface{} `json:"provider_options,omitempty"`
-+}
-+
-+// Config represents the main configuration structure
-+type Config struct {
-+ Schema string `json:"$schema"`
-+ ID string `json:"$id"`
-+ Ref string `json:"$ref"`
-+ Defs interface{} `json:"$defs,omitempty"`
-+ Models map[string]Model `json:"models,omitempty"`
-+ Providers map[string]ProviderConfig `json:"providers,omitempty"`
-+ MCP interface{} `json:"mcp,omitempty"`
-+ LSP interface{} `json:"lsp,omitempty"`
-+ Options interface{} `json:"options,omitempty"`
-+ Permissions interface{} `json:"permissions,omitempty"`
-+ Tools interface{} `json:"tools,omitempty"`
-+}
-+
-+// ReadConfig reads the configuration from a JSON file into a Config struct
-+func ReadConfig(filename string) (*Config, error) {
-+ // Read file content
-+ data, err := ioutil.ReadFile(filename)
-+ if err != nil {
-+ // If file does not exist, return empty Config structure
-+ if os.IsNotExist(err) {
-+ return &Config{}, nil
-+ }
-+ return nil, fmt.Errorf("failed to read config file: %w", err)
-+ }
-+
-+ if err != nil {
-+ return nil, fmt.Errorf("failed to read config file: %w", err)
-+ }
-+
-+ // Parse JSON into Config struct
-+ var p Config
-+ if err := json.Unmarshal(data, &p); err != nil {
-+ return nil, fmt.Errorf("failed to unmarshal config: %w", err)
-+ }
-+
-+ return &p, nil
-+}
-+
-+// WriteConfig writes the configuration from a Config struct to a JSON file
-+func WriteConfig(config *Config, filename string) error {
-+ // Marshal the config struct to JSON
-+ data, err := json.MarshalIndent(config, "", " ")
-+ if err != nil {
-+ return fmt.Errorf("failed to marshal config: %w", err)
-+ }
-+
-+ // Write the JSON data to file
-+ err = ioutil.WriteFile(filename, data, 0644)
-+ if err != nil {
-+ return fmt.Errorf("failed to write config file: %w", err)
-+ }
-+
-+ return nil
-+}
-+
-+// AddProvider adds a new provider to the configuration
-+func (c *Config) AddProvider(id string, provider ProviderConfig) {
-+ if c.Providers == nil {
-+ c.Providers = make(map[string]ProviderConfig)
-+ }
-+ c.Providers[id] = provider
-+}
-+
-+// GetProvider retrieves a provider by ID
-+func (c *Config) GetProvider(id string) (*ProviderConfig, bool) {
-+ if c.Providers == nil {
-+ return nil, false
-+ }
-+ provider, exists := c.Providers[id]
-+ return &provider, exists
-+}
-+
-+// GetProviderOrDefault retrieves a provider by ID or returns a default provider if not found
-+func (c *Config) GetProviderOrDefault(id string, defaultProvider ProviderConfig) (*ProviderConfig, bool) {
-+ if c.Providers == nil {
-+ return &defaultProvider, false
-+ }
-+ provider, exists := c.Providers[id]
-+ if !exists {
-+ return &defaultProvider, false
-+ }
-+ return &provider, true
-+}
-+
-+// AddModel adds a new model to a specific provider
-+func (c *Config) AddModel(providerID string, model Model) error {
-+ provider, exists := c.GetProvider(providerID)
-+ if !exists {
-+ return fmt.Errorf("provider %s not found", providerID)
-+ }
-+
-+ // Check if model with same ID already exists
-+ if provider.Models != nil {
-+ for _, existingModel := range provider.Models {
-+ if existingModel.ID == model.ID {
-+ return nil
-+ }
-+ }
-+ }
-+
-+ if provider.Models == nil {
-+ provider.Models = []Model{}
-+ }
-+ provider.Models = append(provider.Models, model)
-+ c.Providers[providerID] = *provider
-+ return nil
-+}
-+
-+// Crush implements Runner and Editor for Crush integration
-+type Crush struct{}
-+
-+func (o *Crush) String() string { return "Crush" }
-+
-+func (o *Crush) Run(model string, args []string) error {
-+ if _, err := exec.LookPath("crush"); err != nil {
-+ return fmt.Errorf("crush is not installed, install from https://charm.land")
-+ }
-+
-+ // Call Edit() to ensure config is up-to-date before launch
-+ models := []string{model}
-+ if config, err := loadIntegration("crush"); err == nil && len(config.Models) > 0 {
-+ models = config.Models
-+ }
-+ if err := o.Edit(models); err != nil {
-+ return fmt.Errorf("setup failed: %w", err)
-+ }
-+
-+ cmd := exec.Command("crush", args...)
-+ cmd.Stdin = os.Stdin
-+ cmd.Stdout = os.Stdout
-+ cmd.Stderr = os.Stderr
-+ return cmd.Run()
-+}
-+
-+func (o *Crush) Paths() []string {
-+ home, err := os.UserHomeDir()
-+ if err != nil {
-+ return nil
-+ }
-+
-+ var paths []string
-+ p := filepath.Join(home, ".config", "crush", "crush.json")
-+ if _, err := os.Stat(p); err == nil {
-+ paths = append(paths, p)
-+ }
-+ sp := filepath.Join(home, ".local", "state", "crush", "model.json")
-+ if _, err := os.Stat(sp); err == nil {
-+ paths = append(paths, sp)
-+ }
-+ return paths
-+}
-+
-+func (o *Crush) Edit(modelList []string) error {
-+ if len(modelList) == 0 {
-+ return nil
-+ }
-+
-+ home, err := os.UserHomeDir()
-+ if err != nil {
-+ return err
-+ }
-+
-+ configPath := filepath.Join(home, ".config", "crush", "crush.json")
-+ if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
-+ return err
-+ }
-+
-+ config, err := ReadConfig(configPath)
-+ if err != nil {
-+ return err
-+ }
-+
-+ config.Schema = "https://charm.land/crush.json"
-+
-+ defaultProvider := ProviderConfig{
-+ ID: "ollama",
-+ Name: "Ollama (local)",
-+ BaseURL: envconfig.Host().String() + "/v1",
-+ Type: "openai-compat",
-+ }
-+
-+ config.GetProviderOrDefault("ollama", defaultProvider)
-+
-+ for _, model := range modelList {
-+ fmt.Printf("model: %s\n", model)
-+ newModel := Model{
-+ ID:model,
-+ }
-+ err = config.AddModel("ollama", newModel)
-+ if err != nil {
-+ log.Printf("Error adding model: %v", err)
-+ } else {
-+ fmt.Println("Model added successfully to provider")
-+ }
-+ }
-+
-+
-+
-+ err = WriteConfig(config, configPath)
-+ if err != nil {
-+ log.Fatal(err)
-+ }
-+ return err
-+}
-+
-+func (o *Crush) Models() []string {
-+ home, err := os.UserHomeDir()
-+ if err != nil {
-+ return nil
-+ }
-+ config, err := readJSONFile(filepath.Join(home, ".config", "crush", "crush.json"))
-+ if err != nil {
-+ return nil
-+ }
-+ provider, _ := config["provider"].(map[string]any)
-+ ollama, _ := provider["ollama"].(map[string]any)
-+ models, _ := ollama["models"].(map[string]any)
-+ if len(models) == 0 {
-+ return nil
-+ }
-+ keys := slices.Collect(maps.Keys(models))
-+ slices.Sort(keys)
-+ return keys
-+}
-+
-diff --git a/cmd/config/crush_test.go b/cmd/config/crush_test.go
-new file mode 100644
-index 000000000000..dfa6743abb39
---- /dev/null
-+++ b/cmd/config/crush_test.go
-@@ -0,0 +1,668 @@
-+package config
-+
-+import (
-+ "encoding/json"
-+ "fmt"
-+ "os"
-+ "path/filepath"
-+ "testing"
-+)
-+
-+func TestCrushIntegration(t *testing.T) {
-+ o := &Crush{}
-+
-+ t.Run("String", func(t *testing.T) {
-+ if got := o.String(); got != "Crush" {
-+ t.Errorf("String() = %q, want %q", got, "Crush")
-+ }
-+ })
-+
-+ t.Run("implements Runner", func(t *testing.T) {
-+ var _ Runner = o
-+ })
-+
-+ t.Run("implements Editor", func(t *testing.T) {
-+ var _ Editor = o
-+ })
-+}
-+
-+func TestCrushEdit(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+ stateDir := filepath.Join(tmpDir, ".local", "state", "crush")
-+ statePath := filepath.Join(stateDir, "model.json")
-+
-+ cleanup := func() {
-+ os.RemoveAll(configDir)
-+ os.RemoveAll(stateDir)
-+ }
-+
-+ t.Run("fresh install", func(t *testing.T) {
-+ cleanup()
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ assertCrushRecentModel(t, statePath, 0, "ollama", "llama3.2")
-+ })
-+
-+ t.Run("preserve other providers", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{"provider":{"anthropic":{"apiKey":"xxx"}}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+ provider := cfg["provider"].(map[string]any)
-+ if provider["anthropic"] == nil {
-+ t.Error("anthropic provider was removed")
-+ }
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ })
-+
-+ t.Run("preserve other models", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"mistral":{"name":"Mistral"}}}}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ assertCrushModelExists(t, configPath, "mistral")
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ })
-+
-+ t.Run("update existing model", func(t *testing.T) {
-+ cleanup()
-+ o.Edit([]string{"llama3.2"})
-+ o.Edit([]string{"llama3.2"})
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ })
-+
-+ t.Run("preserve top-level keys", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{"theme":"dark","keybindings":{}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+ if cfg["theme"] != "dark" {
-+ t.Error("theme was removed")
-+ }
-+ if cfg["keybindings"] == nil {
-+ t.Error("keybindings was removed")
-+ }
-+ })
-+
-+ t.Run("model state - insert at index 0", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(stateDir, 0o755)
-+ os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ assertCrushRecentModel(t, statePath, 0, "ollama", "llama3.2")
-+ assertCrushRecentModel(t, statePath, 1, "anthropic", "claude")
-+ })
-+
-+ t.Run("model state - preserve favorites and variants", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(stateDir, 0o755)
-+ os.WriteFile(statePath, []byte(`{"recent":[],"favorite":[{"providerID":"x","modelID":"y"}],"variant":{"a":"b"}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ data, _ := os.ReadFile(statePath)
-+ var state map[string]any
-+ json.Unmarshal(data, &state)
-+ if len(state["favorite"].([]any)) != 1 {
-+ t.Error("favorite was modified")
-+ }
-+ if state["variant"].(map[string]any)["a"] != "b" {
-+ t.Error("variant was modified")
-+ }
-+ })
-+
-+ t.Run("model state - deduplicate on re-add", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(stateDir, 0o755)
-+ os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"ollama","modelID":"llama3.2"},{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+ data, _ := os.ReadFile(statePath)
-+ var state map[string]any
-+ json.Unmarshal(data, &state)
-+ recent := state["recent"].([]any)
-+ if len(recent) != 2 {
-+ t.Errorf("expected 2 recent entries, got %d", len(recent))
-+ }
-+ assertCrushRecentModel(t, statePath, 0, "ollama", "llama3.2")
-+ })
-+
-+ t.Run("remove model", func(t *testing.T) {
-+ cleanup()
-+ // First add two models
-+ o.Edit([]string{"llama3.2", "mistral"})
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ assertCrushModelExists(t, configPath, "mistral")
-+
-+ // Then remove one by only selecting the other
-+ o.Edit([]string{"llama3.2"})
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ assertCrushModelNotExists(t, configPath, "mistral")
-+ })
-+
-+ t.Run("preserve user customizations on managed models", func(t *testing.T) {
-+ cleanup()
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ // Add custom fields to the model entry (simulating user edits)
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+ provider := cfg["provider"].(map[string]any)
-+ ollama := provider["ollama"].(map[string]any)
-+ models := ollama["models"].(map[string]any)
-+ entry := models["llama3.2"].(map[string]any)
-+ entry["_myPref"] = "custom-value"
-+ entry["_myNum"] = 42
-+ configData, _ := json.MarshalIndent(cfg, "", " ")
-+ os.WriteFile(configPath, configData, 0o644)
-+
-+ // Re-run Edit — should preserve custom fields
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ data, _ = os.ReadFile(configPath)
-+ json.Unmarshal(data, &cfg)
-+ provider = cfg["provider"].(map[string]any)
-+ ollama = provider["ollama"].(map[string]any)
-+ models = ollama["models"].(map[string]any)
-+ entry = models["llama3.2"].(map[string]any)
-+
-+ if entry["_myPref"] != "custom-value" {
-+ t.Errorf("_myPref was lost: got %v", entry["_myPref"])
-+ }
-+ if entry["_myNum"] != float64(42) {
-+ t.Errorf("_myNum was lost: got %v", entry["_myNum"])
-+ }
-+ if v, ok := entry["_launch"].(bool); !ok || !v {
-+ t.Errorf("_launch marker missing or false: got %v", entry["_launch"])
-+ }
-+ })
-+
-+ t.Run("migrate legacy [Ollama] suffix entries", func(t *testing.T) {
-+ cleanup()
-+ // Write a config with a legacy entry (has [Ollama] suffix but no _launch marker)
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"llama3.2":{"name":"llama3.2 [Ollama]"}}}}}`), 0o644)
-+
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+ provider := cfg["provider"].(map[string]any)
-+ ollama := provider["ollama"].(map[string]any)
-+ models := ollama["models"].(map[string]any)
-+ entry := models["llama3.2"].(map[string]any)
-+
-+ // _launch marker should be added
-+ if v, ok := entry["_launch"].(bool); !ok || !v {
-+ t.Errorf("_launch marker not added during migration: got %v", entry["_launch"])
-+ }
-+ // [Ollama] suffix should be stripped
-+ if name, ok := entry["name"].(string); !ok || name != "llama3.2" {
-+ t.Errorf("name suffix not stripped: got %q", entry["name"])
-+ }
-+ })
-+
-+ t.Run("remove model preserves non-ollama models", func(t *testing.T) {
-+ cleanup()
-+ os.MkdirAll(configDir, 0o755)
-+ // Add a non-Ollama model manually
-+ os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"external":{"name":"External Model"}}}}}`), 0o644)
-+
-+ o.Edit([]string{"llama3.2"})
-+ assertCrushModelExists(t, configPath, "llama3.2")
-+ assertCrushModelExists(t, configPath, "external") // Should be preserved
-+ })
-+}
-+
-+func assertCrushModelExists(t *testing.T, path, model string) {
-+ t.Helper()
-+ data, err := os.ReadFile(path)
-+ if err != nil {
-+ t.Fatal(err)
-+ }
-+ var cfg map[string]any
-+ if err := json.Unmarshal(data, &cfg); err != nil {
-+ t.Fatal(err)
-+ }
-+ provider, ok := cfg["provider"].(map[string]any)
-+ if !ok {
-+ t.Fatal("provider not found")
-+ }
-+ ollama, ok := provider["ollama"].(map[string]any)
-+ if !ok {
-+ t.Fatal("ollama provider not found")
-+ }
-+ models, ok := ollama["models"].(map[string]any)
-+ if !ok {
-+ t.Fatal("models not found")
-+ }
-+ if models[model] == nil {
-+ t.Errorf("model %s not found", model)
-+ }
-+}
-+
-+func assertCrushModelNotExists(t *testing.T, path, model string) {
-+ t.Helper()
-+ data, err := os.ReadFile(path)
-+ if err != nil {
-+ t.Fatal(err)
-+ }
-+ var cfg map[string]any
-+ if err := json.Unmarshal(data, &cfg); err != nil {
-+ t.Fatal(err)
-+ }
-+ provider, ok := cfg["provider"].(map[string]any)
-+ if !ok {
-+ return // No provider means no model
-+ }
-+ ollama, ok := provider["ollama"].(map[string]any)
-+ if !ok {
-+ return // No ollama means no model
-+ }
-+ models, ok := ollama["models"].(map[string]any)
-+ if !ok {
-+ return // No models means no model
-+ }
-+ if models[model] != nil {
-+ t.Errorf("model %s should not exist but was found", model)
-+ }
-+}
-+
-+func assertCrushRecentModel(t *testing.T, path string, index int, providerID, modelID string) {
-+ t.Helper()
-+ data, err := os.ReadFile(path)
-+ if err != nil {
-+ t.Fatal(err)
-+ }
-+ var state map[string]any
-+ if err := json.Unmarshal(data, &state); err != nil {
-+ t.Fatal(err)
-+ }
-+ recent, ok := state["recent"].([]any)
-+ if !ok {
-+ t.Fatal("recent not found")
-+ }
-+ if index >= len(recent) {
-+ t.Fatalf("index %d out of range (len=%d)", index, len(recent))
-+ }
-+ entry, ok := recent[index].(map[string]any)
-+ if !ok {
-+ t.Fatal("entry is not a map")
-+ }
-+ if entry["providerID"] != providerID {
-+ t.Errorf("expected providerID %s, got %s", providerID, entry["providerID"])
-+ }
-+ if entry["modelID"] != modelID {
-+ t.Errorf("expected modelID %s, got %s", modelID, entry["modelID"])
-+ }
-+}
-+
-+// Edge case tests for crush.go
-+
-+func TestCrushEdit_CorruptedConfigJSON(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{corrupted json content`), 0o644)
-+
-+ // Should not panic - corrupted JSON should be treated as empty
-+ err := o.Edit([]string{"llama3.2"})
-+ if err != nil {
-+ t.Fatalf("Edit failed with corrupted config: %v", err)
-+ }
-+
-+ // Verify valid JSON was created
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ if err := json.Unmarshal(data, &cfg); err != nil {
-+ t.Errorf("resulting config is not valid JSON: %v", err)
-+ }
-+}
-+
-+func TestCrushEdit_CorruptedStateJSON(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ stateDir := filepath.Join(tmpDir, ".local", "state", "crush")
-+ statePath := filepath.Join(stateDir, "model.json")
-+
-+ os.MkdirAll(stateDir, 0o755)
-+ os.WriteFile(statePath, []byte(`{corrupted state`), 0o644)
-+
-+ err := o.Edit([]string{"llama3.2"})
-+ if err != nil {
-+ t.Fatalf("Edit failed with corrupted state: %v", err)
-+ }
-+
-+ // Verify valid state was created
-+ data, _ := os.ReadFile(statePath)
-+ var state map[string]any
-+ if err := json.Unmarshal(data, &state); err != nil {
-+ t.Errorf("resulting state is not valid JSON: %v", err)
-+ }
-+}
-+
-+func TestCrushEdit_WrongTypeProvider(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{"provider": "not a map"}`), 0o644)
-+
-+ err := o.Edit([]string{"llama3.2"})
-+ if err != nil {
-+ t.Fatalf("Edit with wrong type provider failed: %v", err)
-+ }
-+
-+ // Verify provider is now correct type
-+ data, _ := os.ReadFile(configPath)
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+
-+ provider, ok := cfg["provider"].(map[string]any)
-+ if !ok {
-+ t.Fatalf("provider should be map after setup, got %T", cfg["provider"])
-+ }
-+ if provider["ollama"] == nil {
-+ t.Error("ollama provider should be created")
-+ }
-+}
-+
-+func TestCrushEdit_WrongTypeRecent(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ stateDir := filepath.Join(tmpDir, ".local", "state", "crush")
-+ statePath := filepath.Join(stateDir, "model.json")
-+
-+ os.MkdirAll(stateDir, 0o755)
-+ os.WriteFile(statePath, []byte(`{"recent": "not an array", "favorite": [], "variant": {}}`), 0o644)
-+
-+ err := o.Edit([]string{"llama3.2"})
-+ if err != nil {
-+ t.Fatalf("Edit with wrong type recent failed: %v", err)
-+ }
-+
-+ // The function should handle this gracefully
-+ data, _ := os.ReadFile(statePath)
-+ var state map[string]any
-+ json.Unmarshal(data, &state)
-+
-+ // recent should be properly set after setup
-+ recent, ok := state["recent"].([]any)
-+ if !ok {
-+ t.Logf("Note: recent type after setup is %T (documenting behavior)", state["recent"])
-+ } else if len(recent) == 0 {
-+ t.Logf("Note: recent is empty (documenting behavior)")
-+ }
-+}
-+
-+func TestCrushEdit_EmptyModels(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+
-+ os.MkdirAll(configDir, 0o755)
-+ originalContent := `{"provider":{"ollama":{"models":{"existing":{}}}}}`
-+ os.WriteFile(configPath, []byte(originalContent), 0o644)
-+
-+ // Empty models should be no-op
-+ err := o.Edit([]string{})
-+ if err != nil {
-+ t.Fatalf("Edit with empty models failed: %v", err)
-+ }
-+
-+ // Original content should be preserved (file not modified)
-+ data, _ := os.ReadFile(configPath)
-+ if string(data) != originalContent {
-+ t.Errorf("empty models should not modify file, but content changed")
-+ }
-+}
-+
-+func TestCrushEdit_SpecialCharsInModelName(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ // Model name with special characters (though unusual)
-+ specialModel := `model-with-"quotes"`
-+
-+ err := o.Edit([]string{specialModel})
-+ if err != nil {
-+ t.Fatalf("Edit with special chars failed: %v", err)
-+ }
-+
-+ // Verify it was stored correctly
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+ data, _ := os.ReadFile(configPath)
-+
-+ var cfg map[string]any
-+ if err := json.Unmarshal(data, &cfg); err != nil {
-+ t.Fatalf("resulting config is invalid JSON: %v", err)
-+ }
-+
-+ // Model should be accessible
-+ provider, _ := cfg["provider"].(map[string]any)
-+ ollama, _ := provider["ollama"].(map[string]any)
-+ models, _ := ollama["models"].(map[string]any)
-+
-+ if models[specialModel] == nil {
-+ t.Errorf("model with special chars not found in config")
-+ }
-+}
-+
-+func readCrushModel(t *testing.T, configPath, model string) map[string]any {
-+ t.Helper()
-+ data, err := os.ReadFile(configPath)
-+ if err != nil {
-+ t.Fatal(err)
-+ }
-+ var cfg map[string]any
-+ json.Unmarshal(data, &cfg)
-+ provider := cfg["provider"].(map[string]any)
-+ ollama := provider["ollama"].(map[string]any)
-+ models := ollama["models"].(map[string]any)
-+ entry, ok := models[model].(map[string]any)
-+ if !ok {
-+ t.Fatalf("model %s not found in config", model)
-+ }
-+ return entry
-+}
-+
-+func TestCrushEdit_LocalModelNoLimit(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configPath := filepath.Join(tmpDir, ".config", "crush", "crush.json")
-+
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ entry := readCrushModel(t, configPath, "llama3.2")
-+ if entry["limit"] != nil {
-+ t.Errorf("local model should not have limit set, got %v", entry["limit"])
-+ }
-+}
-+
-+func TestCrushEdit_PreservesUserLimit(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+
-+ // Set up a model with a user-configured limit
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(`{
-+ "provider": {
-+ "ollama": {
-+ "models": {
-+ "llama3.2": {
-+ "name": "llama3.2",
-+ "_launch": true,
-+ "limit": {"context": 8192, "output": 4096}
-+ }
-+ }
-+ }
-+ }
-+ }`), 0o644)
-+
-+ // Re-edit should preserve the user's limit (not delete it)
-+ if err := o.Edit([]string{"llama3.2"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ entry := readCrushModel(t, configPath, "llama3.2")
-+ limit, ok := entry["limit"].(map[string]any)
-+ if !ok {
-+ t.Fatal("user-configured limit was removed")
-+ }
-+ if limit["context"] != float64(8192) {
-+ t.Errorf("context limit changed: got %v, want 8192", limit["context"])
-+ }
-+ if limit["output"] != float64(4096) {
-+ t.Errorf("output limit changed: got %v, want 4096", limit["output"])
-+ }
-+}
-+
-+func TestCrushEdit_CloudModelLimitStructure(t *testing.T) {
-+ // Verify that when a cloud model entry has limits set (as Edit would do),
-+ // the structure matches what crush expects and re-edit preserves them.
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ configDir := filepath.Join(tmpDir, ".config", "crush")
-+ configPath := filepath.Join(configDir, "crush.json")
-+
-+ expected := cloudModelLimits["glm-4.7"]
-+
-+ // Simulate a cloud model that already has the limit set by a previous Edit
-+ os.MkdirAll(configDir, 0o755)
-+ os.WriteFile(configPath, []byte(fmt.Sprintf(`{
-+ "provider": {
-+ "ollama": {
-+ "models": {
-+ "glm-4.7:cloud": {
-+ "name": "glm-4.7:cloud",
-+ "_launch": true,
-+ "limit": {"context": %d, "output": %d}
-+ }
-+ }
-+ }
-+ }
-+ }`, expected.Context, expected.Output)), 0o644)
-+
-+ // Re-edit should preserve the cloud model limit
-+ if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil {
-+ t.Fatal(err)
-+ }
-+
-+ entry := readCrushModel(t, configPath, "glm-4.7:cloud")
-+ limit, ok := entry["limit"].(map[string]any)
-+ if !ok {
-+ t.Fatal("cloud model limit was removed on re-edit")
-+ }
-+ if limit["context"] != float64(expected.Context) {
-+ t.Errorf("context = %v, want %d", limit["context"], expected.Context)
-+ }
-+ if limit["output"] != float64(expected.Output) {
-+ t.Errorf("output = %v, want %d", limit["output"], expected.Output)
-+ }
-+}
-+
-+func TestLookupCloudModelLimit(t *testing.T) {
-+ tests := []struct {
-+ name string
-+ wantOK bool
-+ wantContext int
-+ wantOutput int
-+ }{
-+ {"glm-4.7", true, 202_752, 131_072},
-+ {"glm-4.7:cloud", true, 202_752, 131_072},
-+ {"kimi-k2.5", true, 262_144, 262_144},
-+ {"kimi-k2.5:cloud", true, 262_144, 262_144},
-+ {"deepseek-v3.2", true, 163_840, 65_536},
-+ {"deepseek-v3.2:cloud", true, 163_840, 65_536},
-+ {"qwen3-coder:480b", true, 262_144, 65_536},
-+ {"qwen3-coder-next:cloud", true, 262_144, 32_768},
-+ {"llama3.2", false, 0, 0},
-+ {"unknown-model:cloud", false, 0, 0},
-+ }
-+
-+ for _, tt := range tests {
-+ t.Run(tt.name, func(t *testing.T) {
-+ l, ok := lookupCloudModelLimit(tt.name)
-+ if ok != tt.wantOK {
-+ t.Errorf("lookupCloudModelLimit(%q) ok = %v, want %v", tt.name, ok, tt.wantOK)
-+ }
-+ if ok {
-+ if l.Context != tt.wantContext {
-+ t.Errorf("context = %d, want %d", l.Context, tt.wantContext)
-+ }
-+ if l.Output != tt.wantOutput {
-+ t.Errorf("output = %d, want %d", l.Output, tt.wantOutput)
-+ }
-+ }
-+ })
-+ }
-+}
-+
-+func TestCrushModels_NoConfig(t *testing.T) {
-+ o := &Crush{}
-+ tmpDir := t.TempDir()
-+ setTestHome(t, tmpDir)
-+
-+ models := o.Models()
-+ if len(models) > 0 {
-+ t.Errorf("expected nil/empty for missing config, got %v", models)
-+ }
-+}
-diff --git a/cmd/config/integrations.go b/cmd/config/integrations.go
-index e524c14ee6f7..22d2a39b4280 100644
---- a/cmd/config/integrations.go
-+++ b/cmd/config/integrations.go
-@@ -60,6 +60,7 @@ var integrations = map[string]Runner{
- "opencode": &OpenCode{},
- "openclaw": &Openclaw{},
- "pi": &Pi{},
-+ "crush": &Crush{},
- }
-
- // recommendedModels are shown when the user has no models or as suggestions.
-@@ -896,6 +897,7 @@ Supported integrations:
- opencode OpenCode
- openclaw OpenClaw (aliases: clawdbot, moltbot)
- pi Pi
-+ crush Crush
-
- Examples:
- ollama launch
---
-2.53.0
-
diff --git a/ollama.service b/ollama.service
index 5ddc9b1..467f014 100644
--- a/ollama.service
+++ b/ollama.service
@@ -3,6 +3,23 @@ Description=Ollama Service
After=network-online.target
[Service]
+# OLLAMA_HOST: Controls the IP address and port Ollama binds to for API requests.
+# Format: <IP_ADDRESS>:<PORT>
+#
+# Examples:
+# Default (localhost only, port 11434): 127.0.0.1:11434
+# Change to a custom port: 127.0.0.1:8080
+# Bind to all IPv4 interfaces (default port): 0.0.0.0:11434
+# Bind to all IPv6 interfaces (default port): [::]:11434
+#
+# ⚠️ Security: Binding to 0.0.0.0 or [::] exposes Ollama to your network.
+# Use firewall rules (ufw/iptables/nftables) or a reverse proxy (nginx/caddy)
+# to restrict access in production environments.
+Environment="OLLAMA_HOST=127.0.0.1:11434"
+#Environment="OLLAMA_HOST=127.0.0.1:<your port>"
+#Environment="OLLAMA_HOST=0.0.0.0:11434"
+#Environment="OLLAMA_HOST=[::]:11434"
+
ExecStart=/usr/bin/ollama serve
User=ollama
Group=ollama
diff --git a/ollama.spec b/ollama.spec
index 749adf8..72c31d2 100644
--- a/ollama.spec
+++ b/ollama.spec
@@ -40,7 +40,6 @@ Source11: ollama.sysusers
%endif
Patch1: 0001-ollama-handle-load.patch
-# Patch2: 0001-ollama-crush-integration.patch
BuildRequires: go-vendor-tools
BuildRequires: fdupes
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-06-07 15:14 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-07 15:14 [rpms/ollama] rawhide: Add OLLAMA_HOST comments to ollama.service Tom Rix
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox