public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Tom Rix <Tom.Rix@amd.com>
To: git-commits@fedoraproject.org
Subject: [rpms/ollama] rawhide: Add OLLAMA_HOST comments to ollama.service
Date: Sun, 07 Jun 2026 15:14:08 GMT	[thread overview]
Message-ID: <178084524826.1.7135229797267872259.rpms-ollama-a9c382e66c50@fedoraproject.org> (raw)

            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

                 reply	other threads:[~2026-06-07 15:14 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178084524826.1.7135229797267872259.rpms-ollama-a9c382e66c50@fedoraproject.org \
    --to=tom.rix@amd.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox