Skip to content

Commit 53c2e3a

Browse files
authored
Merge pull request #3 from Dzarlax-AI/claude/ollama-cloud-models-tiEOJ
2 parents f2e419c + af06800 commit 53c2e3a

6 files changed

Lines changed: 305 additions & 1 deletion

File tree

cmd/agent/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ func main() {
9292
p, e := llm.NewOllama(cfg.Models.OllamaLocal)
9393
addProvider("ollama-local", p, e)
9494
}
95+
if cfg.Models.OllamaCloud.Model != "" {
96+
p, e := llm.NewOllama(cfg.Models.OllamaCloud)
97+
addProvider("ollama-cloud", p, e)
98+
llm.InitCloudModelCache("config/ollama_cloud_cache.json")
99+
}
95100
if cfg.Models.Claude.BaseURL != "" {
96101
p, e := llm.NewClaudeBridge(cfg.Models.Claude)
97102
addProvider("claude", p, e)

internal/agent/agent.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,16 @@ func (a *Agent) SetClassifierMinLen(n int) {
363363
a.router.SetClassifierMinLen(n)
364364
}
365365

366+
// SetOllamaCloudModel changes the model on the Ollama Cloud provider and persists.
367+
func (a *Agent) SetOllamaCloudModel(model string, vision bool) bool {
368+
return a.router.SetOllamaCloudModel(model, vision)
369+
}
370+
371+
// OllamaCloudModelName returns the current Ollama Cloud model name, or ("", false).
372+
func (a *Agent) OllamaCloudModelName() (string, bool) {
373+
return a.router.OllamaCloudModelName()
374+
}
375+
366376
type ToolInfo struct {
367377
Name string
368378
ServerName string

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type ModelsConfig struct {
9898
QwenMax ModelConfig `yaml:"qwen-max"`
9999
Ollama ModelConfig `yaml:"ollama"`
100100
OllamaLocal ModelConfig `yaml:"ollama-local"`
101+
OllamaCloud ModelConfig `yaml:"ollama-cloud"`
101102
Claude ModelConfig `yaml:"claude"`
102103
Local ModelConfig `yaml:"local"`
103104
}

internal/llm/ollama.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,156 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"os"
1011
"strings"
12+
"sync"
13+
"time"
1114

1215
"telegram-agent/internal/config"
1316
)
1417

18+
// --- Ollama Cloud model discovery ---
19+
20+
const ollamaRegistryURL = "https://ollama.com"
21+
22+
var (
23+
cloudMu sync.Mutex
24+
cloudCache *ollamaCloudCache
25+
cloudCachePath string // set via InitCloudModelCache
26+
cloudTTL = 24 * time.Hour
27+
)
28+
29+
// CloudModel represents a model available on Ollama Cloud.
30+
type CloudModel struct {
31+
Name string `json:"name"`
32+
Size int64 `json:"size"`
33+
}
34+
35+
// ollamaCloudCache is the on-disk + in-memory cache for cloud models and capabilities.
36+
type ollamaCloudCache struct {
37+
Models []CloudModel `json:"models"`
38+
Caps map[string][]string `json:"caps"`
39+
FetchedAt time.Time `json:"fetched_at"`
40+
}
41+
42+
// InitCloudModelCache sets the file path for persistent cloud model cache.
43+
// Call once at startup before any Fetch calls.
44+
func InitCloudModelCache(path string) {
45+
cloudMu.Lock()
46+
defer cloudMu.Unlock()
47+
cloudCachePath = path
48+
// Load existing cache from disk.
49+
data, err := os.ReadFile(path)
50+
if err != nil {
51+
return
52+
}
53+
var c ollamaCloudCache
54+
if json.Unmarshal(data, &c) == nil && len(c.Models) > 0 {
55+
cloudCache = &c
56+
}
57+
}
58+
59+
func saveCloudCache() {
60+
if cloudCachePath == "" || cloudCache == nil {
61+
return
62+
}
63+
data, err := json.Marshal(cloudCache)
64+
if err != nil {
65+
return
66+
}
67+
os.WriteFile(cloudCachePath, data, 0644) //nolint:errcheck
68+
}
69+
70+
// FetchOllamaCloudModels fetches the list of cloud-available models via /api/tags.
71+
// Uses in-memory cache (24h TTL), falls back to persistent disk cache.
72+
func FetchOllamaCloudModels() ([]CloudModel, error) {
73+
cloudMu.Lock()
74+
defer cloudMu.Unlock()
75+
76+
if cloudCache != nil && time.Since(cloudCache.FetchedAt) < cloudTTL && len(cloudCache.Models) > 0 {
77+
return cloudCache.Models, nil
78+
}
79+
80+
resp, err := http.Get(ollamaRegistryURL + "/api/tags")
81+
if err != nil {
82+
// Return stale cache on network failure.
83+
if cloudCache != nil && len(cloudCache.Models) > 0 {
84+
return cloudCache.Models, nil
85+
}
86+
return nil, fmt.Errorf("fetch ollama cloud models: %w", err)
87+
}
88+
defer resp.Body.Close()
89+
90+
var result struct {
91+
Models []struct {
92+
Name string `json:"name"`
93+
Size int64 `json:"size"`
94+
} `json:"models"`
95+
}
96+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
97+
if cloudCache != nil && len(cloudCache.Models) > 0 {
98+
return cloudCache.Models, nil
99+
}
100+
return nil, fmt.Errorf("parse ollama cloud models: %w", err)
101+
}
102+
103+
models := make([]CloudModel, 0, len(result.Models))
104+
for _, m := range result.Models {
105+
if m.Name != "" {
106+
models = append(models, CloudModel{Name: m.Name, Size: m.Size})
107+
}
108+
}
109+
110+
if len(models) > 0 {
111+
if cloudCache == nil {
112+
cloudCache = &ollamaCloudCache{Caps: make(map[string][]string)}
113+
}
114+
cloudCache.Models = models
115+
cloudCache.FetchedAt = time.Now()
116+
saveCloudCache()
117+
}
118+
return models, nil
119+
}
120+
121+
// FetchOllamaModelCaps fetches capabilities for a specific model via /api/show.
122+
// Returns a slice like ["completion","thinking","tools","vision"].
123+
// Results are cached per model persistently.
124+
func FetchOllamaModelCaps(model string) ([]string, error) {
125+
cloudMu.Lock()
126+
defer cloudMu.Unlock()
127+
128+
// Check in-memory/disk cache.
129+
if cloudCache != nil {
130+
if caps, ok := cloudCache.Caps[model]; ok {
131+
return caps, nil
132+
}
133+
}
134+
135+
body, err := json.Marshal(map[string]string{"model": model})
136+
if err != nil {
137+
return nil, err
138+
}
139+
resp, err := http.Post(ollamaRegistryURL+"/api/show", "application/json", bytes.NewReader(body))
140+
if err != nil {
141+
return nil, fmt.Errorf("fetch ollama model caps: %w", err)
142+
}
143+
defer resp.Body.Close()
144+
145+
var result struct {
146+
Capabilities []string `json:"capabilities"`
147+
}
148+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
149+
return nil, fmt.Errorf("parse ollama model caps: %w", err)
150+
}
151+
152+
if cloudCache == nil {
153+
cloudCache = &ollamaCloudCache{Caps: make(map[string][]string)}
154+
}
155+
cloudCache.Caps[model] = result.Capabilities
156+
saveCloudCache()
157+
return result.Capabilities, nil
158+
}
159+
15160
const defaultOllamaBaseURL = "http://localhost:11434"
16161

17162
// ollamaProvider implements the native Ollama /api/chat protocol,
@@ -56,6 +201,22 @@ func (p *ollamaProvider) SupportsVision() bool {
56201
return p.vision
57202
}
58203

204+
// SetModelAndCaps changes the model name and vision flag on a running provider.
205+
// Used by the router to switch Ollama Cloud models dynamically.
206+
func (p *ollamaProvider) SetModelAndCaps(model string, vision bool) {
207+
p.model = model
208+
p.vision = vision
209+
}
210+
211+
// OllamaCloudModel returns the current model name if this is a cloud provider
212+
// (base URL contains "ollama.com"), or ("", false) otherwise.
213+
func (p *ollamaProvider) OllamaCloudModel() (string, bool) {
214+
if strings.Contains(p.baseURL, "ollama.com") {
215+
return p.model, true
216+
}
217+
return "", false
218+
}
219+
59220
// --- Ollama-native request/response types ---
60221

61222
type ollamaRequest struct {

internal/llm/router.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type routingOverrides struct {
4646
Reasoner string `json:"reasoner,omitempty"`
4747
Classifier string `json:"classifier,omitempty"`
4848
ClassifierMinLen *int `json:"classifier_min_len,omitempty"`
49+
OllamaCloudModel string `json:"ollama_cloud_model,omitempty"`
50+
OllamaCloudVision *bool `json:"ollama_cloud_vision,omitempty"`
4951
}
5052

5153
// Router selects the appropriate LLM provider based on context.
@@ -121,6 +123,13 @@ func (r *Router) LoadPersistedOverrides() error {
121123
if ov.ClassifierMinLen != nil {
122124
r.cfg.ClassifierMinLen = *ov.ClassifierMinLen
123125
}
126+
// Restore Ollama Cloud model choice.
127+
if ov.OllamaCloudModel != "" {
128+
if oc := r.findOllamaCloud(); oc != nil {
129+
vision := ov.OllamaCloudVision != nil && *ov.OllamaCloudVision
130+
oc.SetModelAndCaps(ov.OllamaCloudModel, vision)
131+
}
132+
}
124133
return nil
125134
}
126135

@@ -139,6 +148,17 @@ func (r *Router) saveOverrides() {
139148
Classifier: r.cfg.Classifier,
140149
ClassifierMinLen: &minLen,
141150
}
151+
// Include Ollama Cloud model if present.
152+
if oc := r.findOllamaCloud(); oc != nil {
153+
if m, ok := oc.OllamaCloudModel(); ok {
154+
ov.OllamaCloudModel = m
155+
v := false
156+
if vp, ok2 := oc.(interface{ SupportsVision() bool }); ok2 {
157+
v = vp.SupportsVision()
158+
}
159+
ov.OllamaCloudVision = &v
160+
}
161+
}
142162
data, err := json.Marshal(ov)
143163
if err != nil {
144164
return
@@ -351,6 +371,46 @@ func (r *Router) ProviderNames() []string {
351371
return names
352372
}
353373

374+
// ollamaCloudSetter is implemented by Ollama providers that can switch models at runtime.
375+
type ollamaCloudSetter interface {
376+
SetModelAndCaps(model string, vision bool)
377+
OllamaCloudModel() (string, bool)
378+
}
379+
380+
// findOllamaCloud returns the first Ollama Cloud provider (base URL contains "ollama.com").
381+
func (r *Router) findOllamaCloud() ollamaCloudSetter {
382+
for _, p := range r.providers {
383+
if oc, ok := p.(ollamaCloudSetter); ok {
384+
if _, isCloud := oc.OllamaCloudModel(); isCloud {
385+
return oc
386+
}
387+
}
388+
}
389+
return nil
390+
}
391+
392+
// OllamaCloudModelName returns the current model of the Ollama Cloud provider,
393+
// or ("", false) if no such provider is configured.
394+
func (r *Router) OllamaCloudModelName() (string, bool) {
395+
if oc := r.findOllamaCloud(); oc != nil {
396+
return oc.OllamaCloudModel()
397+
}
398+
return "", false
399+
}
400+
401+
// SetOllamaCloudModel changes the model on the Ollama Cloud provider and persists the choice.
402+
func (r *Router) SetOllamaCloudModel(model string, vision bool) bool {
403+
oc := r.findOllamaCloud()
404+
if oc == nil {
405+
return false
406+
}
407+
oc.SetModelAndCaps(model, vision)
408+
r.mu.Lock()
409+
r.saveOverrides()
410+
r.mu.Unlock()
411+
return true
412+
}
413+
354414
func (r *Router) pick(ctx context.Context, messages []Message) Provider {
355415
r.mu.RLock()
356416
cfg := r.cfg

0 commit comments

Comments
 (0)