@@ -14,10 +14,17 @@ import (
1414const settingKeyModelChecks = "recommendation.model_checks"
1515
1616const (
17- modelCheckInterval = 12 * time .Hour
18- modelCheckInitialDelay = 5 * time .Minute
19- modelCheckStaleAfter = 24 * time .Hour
20- modelCheckBatchLimit = 5
17+ SettingKeyModelCheckIntervalHours = "recommendation.model_check_interval_hours"
18+ SettingKeyModelCheckInitialDelayMins = "recommendation.model_check_initial_delay_minutes"
19+ SettingKeyModelCheckStaleHours = "recommendation.model_check_stale_hours"
20+ SettingKeyModelCheckBatchLimit = "recommendation.model_check_batch_limit"
21+ )
22+
23+ const (
24+ defaultModelCheckIntervalHours = 12
25+ defaultModelCheckInitialDelayMins = 5
26+ defaultModelCheckStaleHours = 24
27+ defaultModelCheckBatchLimit = 5
2128)
2229
2330type modelCheckStatus struct {
@@ -39,6 +46,30 @@ type modelCheckResponse struct {
3946 Status modelCheckStatus `json:"status"`
4047}
4148
49+ type modelCheckSummary struct {
50+ Total int `json:"total"`
51+ Verified int `json:"verified"`
52+ Degraded int `json:"degraded"`
53+ Blocked int `json:"blocked"`
54+ Unverified int `json:"unverified"`
55+ OldestCheck string `json:"oldest_check,omitempty"`
56+ NewestCheck string `json:"newest_check,omitempty"`
57+ }
58+
59+ type modelCheckSweepResult struct {
60+ Checked int `json:"checked"`
61+ Remaining int `json:"remaining"`
62+ Statuses map [string ]int `json:"statuses"`
63+ Models []modelCheckModel `json:"models,omitempty"`
64+ }
65+
66+ type modelCheckModel struct {
67+ ModelID string `json:"model_id"`
68+ Status string `json:"status"`
69+ LatencyMS int64 `json:"latency_ms,omitempty"`
70+ Error string `json:"error,omitempty"`
71+ }
72+
4273type modelProbeFunc func (ctx context.Context , providerType , modelID string , caps llm.Capabilities ) modelCheckStatus
4374
4475func modelCheckKey (provider , modelID string ) string {
@@ -82,6 +113,38 @@ func (s *Server) saveModelCheck(ctx context.Context, provider, modelID string, s
82113 return s .settings .PutSetting (ctx , settingKeyModelChecks , string (data ))
83114}
84115
116+ func (s * Server ) modelCheckInterval () time.Duration {
117+ return time .Duration (s .intSetting (SettingKeyModelCheckIntervalHours , defaultModelCheckIntervalHours , 1 , 168 )) * time .Hour
118+ }
119+
120+ func (s * Server ) modelCheckInitialDelay () time.Duration {
121+ return time .Duration (s .intSetting (SettingKeyModelCheckInitialDelayMins , defaultModelCheckInitialDelayMins , 0 , 1440 )) * time .Minute
122+ }
123+
124+ func (s * Server ) modelCheckStaleAfter () time.Duration {
125+ return time .Duration (s .intSetting (SettingKeyModelCheckStaleHours , defaultModelCheckStaleHours , 1 , 720 )) * time .Hour
126+ }
127+
128+ func (s * Server ) modelCheckBatchLimit () int {
129+ return s .intSetting (SettingKeyModelCheckBatchLimit , defaultModelCheckBatchLimit , 1 , 50 )
130+ }
131+
132+ func (s * Server ) intSetting (key string , def , min , max int ) int {
133+ if s .settings == nil {
134+ return def
135+ }
136+ ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Second )
137+ defer cancel ()
138+ v := llm .GetIntSetting (ctx , s .settings , key , def )
139+ if v < min {
140+ return min
141+ }
142+ if v > max {
143+ return max
144+ }
145+ return v
146+ }
147+
85148func (s * Server ) probeModel (ctx context.Context , providerType , modelID string , caps llm.Capabilities ) modelCheckStatus {
86149 if s .modelProbe != nil {
87150 return s .modelProbe (ctx , providerType , modelID , caps )
@@ -149,34 +212,41 @@ func (s *Server) startModelCheckScheduler() {
149212}
150213
151214func (s * Server ) modelCheckScheduler (ctx context.Context ) {
152- timer := time .NewTimer (modelCheckInitialDelay )
215+ timer := time .NewTimer (s . modelCheckInitialDelay () )
153216 defer timer .Stop ()
154217 for {
155218 select {
156219 case <- ctx .Done ():
157220 return
158221 case <- timer .C :
159222 s .runModelCheckSweep (ctx )
160- timer .Reset (modelCheckInterval )
223+ timer .Reset (s . modelCheckInterval () )
161224 }
162225 }
163226}
164227
165- func (s * Server ) runModelCheckSweep (ctx context.Context ) {
228+ func (s * Server ) runModelCheckSweep (ctx context.Context ) modelCheckSweepResult {
229+ return s .runModelCheckSweepProvider (ctx , "openrouter" )
230+ }
231+
232+ func (s * Server ) runModelCheckSweepProvider (ctx context.Context , provider string ) modelCheckSweepResult {
166233 sweepCtx , cancel := context .WithTimeout (ctx , 3 * time .Minute )
167234 defer cancel ()
168- candidates := s .freeModelCheckCandidates (sweepCtx , "openrouter" )
235+ candidates := s .freeModelCheckCandidates (sweepCtx , provider )
236+ result := modelCheckSweepResult {Statuses : map [string ]int {}}
169237 if len (candidates ) == 0 {
170- return
238+ return result
171239 }
172- if len (candidates ) > modelCheckBatchLimit {
173- candidates = candidates [:modelCheckBatchLimit ]
240+ limit := s .modelCheckBatchLimit ()
241+ if len (candidates ) > limit {
242+ result .Remaining = len (candidates ) - limit
243+ candidates = candidates [:limit ]
174244 }
175245 s .logger .Info ("free model check sweep started" , "count" , len (candidates ))
176246 for _ , candidate := range candidates {
177247 select {
178248 case <- sweepCtx .Done ():
179- return
249+ return result
180250 default :
181251 }
182252 checkCtx , checkCancel := context .WithTimeout (sweepCtx , 30 * time .Second )
@@ -186,8 +256,17 @@ func (s *Server) runModelCheckSweep(ctx context.Context) {
186256 s .logger .Warn ("free model check save failed" , "model" , candidate .modelID , "err" , err )
187257 continue
188258 }
259+ result .Checked ++
260+ result .Statuses [status .Status ]++
261+ result .Models = append (result .Models , modelCheckModel {
262+ ModelID : candidate .modelID ,
263+ Status : status .Status ,
264+ LatencyMS : status .LatencyMS ,
265+ Error : status .Error ,
266+ })
189267 s .logger .Info ("free model checked" , "model" , candidate .modelID , "status" , status .Status , "latency_ms" , status .LatencyMS )
190268 }
269+ return result
191270}
192271
193272type freeModelCandidate struct {
@@ -212,7 +291,7 @@ func (s *Server) freeModelCheckCandidates(ctx context.Context, provider string)
212291 }
213292 check := checks [modelCheckKey (provider , id )]
214293 checkedAt := parseCheckTime (check .CheckedAt )
215- if ! checkedAt .IsZero () && now .Sub (checkedAt ) < modelCheckStaleAfter {
294+ if ! checkedAt .IsZero () && now .Sub (checkedAt ) < s . modelCheckStaleAfter () {
216295 continue
217296 }
218297 out = append (out , freeModelCandidate {
@@ -234,6 +313,41 @@ func (s *Server) freeModelCheckCandidates(ctx context.Context, provider string)
234313 return out
235314}
236315
316+ func (s * Server ) buildModelCheckSummary (ctx context.Context ) modelCheckSummary {
317+ checks := s .loadModelChecks (ctx )
318+ out := modelCheckSummary {Total : len (checks )}
319+ var oldest , newest time.Time
320+ for _ , check := range checks {
321+ switch check .Status {
322+ case "free_verified" :
323+ out .Verified ++
324+ case "free_degraded" :
325+ out .Degraded ++
326+ case "free_blocked" :
327+ out .Blocked ++
328+ default :
329+ out .Unverified ++
330+ }
331+ checkedAt := parseCheckTime (check .CheckedAt )
332+ if checkedAt .IsZero () {
333+ continue
334+ }
335+ if oldest .IsZero () || checkedAt .Before (oldest ) {
336+ oldest = checkedAt
337+ }
338+ if newest .IsZero () || checkedAt .After (newest ) {
339+ newest = checkedAt
340+ }
341+ }
342+ if ! oldest .IsZero () {
343+ out .OldestCheck = oldest .Format (time .RFC3339 )
344+ }
345+ if ! newest .IsZero () {
346+ out .NewestCheck = newest .Format (time .RFC3339 )
347+ }
348+ return out
349+ }
350+
237351func parseCheckTime (raw string ) time.Time {
238352 if raw == "" {
239353 return time.Time {}
0 commit comments