feat(proxy): auto-discovery de modelos + backend Kimchi
- Nueva interfaz Discoverer con Refresh() por backend - Kilo: full auto (isFree + metadata completa del upstream) - OpenCode: heurística -free para descubrir nuevos free - Kimchi: agrega todos los IDs no-curados del upstream - discoveryLoop cada 10 min en el Proxy - Backends stateful (mutex + catálogo dinámico) - CREDENCIALES.md y KIMCHI_ANALISIS.md documentan el setup
This commit is contained in:
@@ -21,6 +21,11 @@ type ModelInfo struct {
|
||||
SupportsVision bool `json:"supports_vision"`
|
||||
}
|
||||
|
||||
// discoveryInterval is how often backends re-fetch their upstream /models
|
||||
// catalogue. Kept shorter than the health interval because model lists rotate
|
||||
// more frequently than rate-limit windows.
|
||||
const discoveryInterval = 10 * time.Minute
|
||||
|
||||
// healthCheckInterval is how often caged models get re-probed.
|
||||
const healthCheckInterval = 30 * time.Minute
|
||||
|
||||
@@ -59,15 +64,20 @@ type session struct {
|
||||
// the catch-all opencode aliases. The default backend is the fallback for
|
||||
// unknown models (pass-through as-is).
|
||||
func NewProxy(apiKey string) *Proxy {
|
||||
oc := OpenCodeBackend{}
|
||||
kl := KiloBackend{}
|
||||
// Backends are stateful (they hold a mutex + discovered catalogue), so we
|
||||
// store pointers to avoid copying the mutex when the Backend interface
|
||||
// value is passed around.
|
||||
oc := &OpenCodeBackend{}
|
||||
kl := &KiloBackend{}
|
||||
km := &KimchiBackend{}
|
||||
p := &Proxy{
|
||||
apiKey: apiKey,
|
||||
backends: []Backend{kl, oc},
|
||||
backends: []Backend{km, kl, oc},
|
||||
defaultBackend: oc,
|
||||
sessions: make(map[string]*session),
|
||||
health: make(map[string]*modelHealth),
|
||||
}
|
||||
go p.discoveryLoop()
|
||||
go p.healthLoop()
|
||||
return p
|
||||
}
|
||||
@@ -142,6 +152,40 @@ func randomHex(n int) string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// ---------- Model discovery ----------
|
||||
|
||||
// discoveryLoop runs an initial full refresh on startup, then re-fetches each
|
||||
// backend's upstream /models catalogue every discoveryInterval. Backends that
|
||||
// do not implement Discoverer are skipped (they stay on their curated list).
|
||||
func (p *Proxy) discoveryLoop() {
|
||||
p.refreshAllBackends()
|
||||
ticker := time.NewTicker(discoveryInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
p.refreshAllBackends()
|
||||
}
|
||||
}
|
||||
|
||||
// refreshAllBackends calls Refresh() on every backend that implements
|
||||
// Discoverer. Failures are logged but do not stop the loop; a failed refresh
|
||||
// leaves the backend on its previous (curated or last-good) catalogue.
|
||||
func (p *Proxy) refreshAllBackends() {
|
||||
var names []string
|
||||
for _, b := range p.backends {
|
||||
d, ok := b.(Discoverer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := d.Refresh(); err != nil {
|
||||
fmt.Printf("[discovery] backend '%s' refresh failed: %v\n", b.Name(), err)
|
||||
continue
|
||||
}
|
||||
names = append(names, b.Name())
|
||||
}
|
||||
total := len(p.Models())
|
||||
fmt.Printf("[discovery] refresh complete: %d backends, %d models\n", len(names), total)
|
||||
}
|
||||
|
||||
// ---------- Model health / rate-limit tracking ----------
|
||||
|
||||
// markRateLimited cages a model so it disappears from /v1/models until it recovers.
|
||||
|
||||
Reference in New Issue
Block a user