package ai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // OllamaModelInfo holds detailed info about an installed Ollama model. type OllamaModelInfo struct { Name string `json:"name"` Size int64 `json:"size"` ModifiedAt string `json:"modified_at"` Details struct { ParameterSize string `json:"parameter_size"` QuantizationLevel string `json:"quantization_level"` Family string `json:"family"` } `json:"details"` } // OllamaProvider implements Provider for Ollama and also exposes model management operations. type OllamaProvider struct { endpoint string model string client *http.Client } func newOllama(endpoint, model string) *OllamaProvider { if endpoint == "" { endpoint = "http://ollama:11434" } if model == "" { model = "llama3" } return &OllamaProvider{ endpoint: endpoint, model: model, client: &http.Client{}, } } // NewOllamaManager creates an OllamaProvider for model management (pull/delete/list). func NewOllamaManager(endpoint string) *OllamaProvider { return newOllama(endpoint, "") } func (p *OllamaProvider) Name() string { return "ollama" } func (p *OllamaProvider) Summarize(ctx context.Context, prompt string, opts GenOptions) (string, error) { numCtx := 32768 if opts.NumCtx > 0 { numCtx = opts.NumCtx } body := map[string]interface{}{ "model": p.model, "prompt": prompt, "stream": false, "think": opts.Think, "options": map[string]interface{}{ "num_ctx": numCtx, }, } b, _ := json.Marshal(body) req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint+"/api/generate", bytes.NewReader(b)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") resp, err := p.client.Do(req) if err != nil { return "", err } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("ollama API error %d: %s", resp.StatusCode, raw) } var result struct { Response string `json:"response"` } if err := json.Unmarshal(raw, &result); err != nil { return "", err } return result.Response, nil } func (p *OllamaProvider) ListModels(ctx context.Context) ([]string, error) { infos, err := p.ListModelsDetailed(ctx) if err != nil { return nil, err } names := make([]string, len(infos)) for i, m := range infos { names[i] = m.Name } return names, nil } func (p *OllamaProvider) ListModelsDetailed(ctx context.Context) ([]OllamaModelInfo, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.endpoint+"/api/tags", nil) if err != nil { return nil, err } resp, err := p.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) var result struct { Models []OllamaModelInfo `json:"models"` } if err := json.Unmarshal(raw, &result); err != nil { return nil, err } return result.Models, nil } // PullModel pulls (downloads) a model from Ollama Hub. Blocks until complete. func (p *OllamaProvider) PullModel(ctx context.Context, name string) error { body, _ := json.Marshal(map[string]interface{}{"name": name, "stream": false}) // Use a long-timeout client since model downloads can take many minutes client := &http.Client{Timeout: 60 * time.Minute} req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint+"/api/pull", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() raw, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return fmt.Errorf("ollama pull error %d: %s", resp.StatusCode, raw) } return nil } // DeleteModel removes a model from local storage. func (p *OllamaProvider) DeleteModel(ctx context.Context, name string) error { body, _ := json.Marshal(map[string]string{"name": name}) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, p.endpoint+"/api/delete", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := p.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { raw, _ := io.ReadAll(resp.Body) return fmt.Errorf("ollama delete error %d: %s", resp.StatusCode, raw) } return nil }