@@ -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+
15160const 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
61222type ollamaRequest struct {
0 commit comments