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:
renato97
2026-07-12 19:58:30 -03:00
parent 05a52d29e2
commit 46127431a3
8 changed files with 1041 additions and 29 deletions
+47 -3
View File
@@ -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.