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:
@@ -0,0 +1,232 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KimchiBaseURL is the upstream Kimchi Dev API base URL.
|
||||
const KimchiBaseURL = "https://llm.kimchi.dev/openai/v1"
|
||||
|
||||
// KimchiBackend forwards to Kimchi Dev's free models.
|
||||
//
|
||||
// It is a Discoverer: it polls GET /openai/v1/models and merges the IDs with
|
||||
// the curated base layer. Kimchi's /models returns only IDs (no metadata, no
|
||||
// free flag). Because the Kimchi account is credit-based, every advertised
|
||||
// model is treated as usable: all discovered IDs not already curated are added
|
||||
// with minimal metadata (option B). Curated IDs keep their trusted metadata.
|
||||
type KimchiBackend struct {
|
||||
mu sync.RWMutex
|
||||
discovered []ModelInfo
|
||||
}
|
||||
|
||||
// kimchiCuratedModels is the curated base layer with full metadata.
|
||||
var kimchiCuratedModels = []ModelInfo{
|
||||
{
|
||||
ID: "deepseek-v4-flash",
|
||||
Name: "DeepSeek V4 Flash (Kimchi)",
|
||||
OwnedBy: "deepseek",
|
||||
ContextWindow: 1_048_576,
|
||||
MaxOutputTokens: 1_048_576,
|
||||
Description: "DeepSeek V4, 284B params, propósito general, razonamiento.",
|
||||
},
|
||||
{
|
||||
ID: "glm-5.2-fp8",
|
||||
Name: "GLM 5.2 FP8 (Kimchi)",
|
||||
OwnedBy: "zhipu",
|
||||
ContextWindow: 1_048_576,
|
||||
MaxOutputTokens: 1_048_576,
|
||||
Description: "GLM-5.2 cuantizado FP8, razonamiento, rendimiento.",
|
||||
},
|
||||
{
|
||||
ID: "kimi-k2.7",
|
||||
Name: "Kimi K2.7 (Kimchi)",
|
||||
OwnedBy: "moonshot",
|
||||
ContextWindow: 262_144,
|
||||
MaxOutputTokens: 262_144,
|
||||
Description: "Moonshot AI, multimodal con visión, razonamiento.",
|
||||
SupportsVision: true,
|
||||
},
|
||||
{
|
||||
ID: "minimax-m3",
|
||||
Name: "MiniMax M3 (Kimchi)",
|
||||
OwnedBy: "minimax",
|
||||
ContextWindow: 1_048_576,
|
||||
MaxOutputTokens: 1_048_576,
|
||||
Description: "MiniMax M3, multimodal texto+imagen, razonamiento.",
|
||||
SupportsVision: true,
|
||||
},
|
||||
{
|
||||
ID: "nemotron-3-ultra-fp4",
|
||||
Name: "Nemotron 3 Ultra FP4 (Kimchi)",
|
||||
OwnedBy: "nvidia",
|
||||
ContextWindow: 1_048_576,
|
||||
MaxOutputTokens: 1_048_576,
|
||||
Description: "NVIDIA Nemotron 3 Ultra 550B, FP4 cuantizado, razonamiento.",
|
||||
},
|
||||
}
|
||||
|
||||
// kimchiAliases lets clients use shorter names for Kimchi models.
|
||||
// Uses prefixed aliases to avoid colliding with opencode backend aliases.
|
||||
var kimchiAliases = map[string]string{
|
||||
"kimchi": "deepseek-v4-flash",
|
||||
"kimchi/deepseek": "deepseek-v4-flash",
|
||||
"kimchi/glm": "glm-5.2-fp8",
|
||||
"kimchi/kimi": "kimi-k2.7",
|
||||
"kimchi/minimax": "minimax-m3",
|
||||
"kimchi/nemotron": "nemotron-3-ultra-fp4",
|
||||
"deepseek-v4-flash": "deepseek-v4-flash",
|
||||
"glm-5.2-fp8": "glm-5.2-fp8",
|
||||
"kimi-k2.7": "kimi-k2.7",
|
||||
"minimax-m3": "minimax-m3",
|
||||
"nemotron-3-ultra-fp4": "nemotron-3-ultra-fp4",
|
||||
}
|
||||
|
||||
// Name returns the backend identifier.
|
||||
func (*KimchiBackend) Name() string { return "kimchi" }
|
||||
|
||||
// Models returns the Kimchi free models (discovered, or curated fallback).
|
||||
func (b *KimchiBackend) Models() []ModelInfo {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if len(b.discovered) == 0 {
|
||||
return kimchiCuratedModels
|
||||
}
|
||||
return b.discovered
|
||||
}
|
||||
|
||||
// Resolve maps a requested name to a Kimchi model ID.
|
||||
func (b *KimchiBackend) Resolve(model string) (string, bool) {
|
||||
if mapped, ok := kimchiAliases[model]; ok {
|
||||
return mapped, true
|
||||
}
|
||||
for _, m := range kimchiCuratedModels {
|
||||
if m.ID == model {
|
||||
return model, true
|
||||
}
|
||||
}
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
for _, m := range b.discovered {
|
||||
if m.ID == model {
|
||||
return model, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ChatURL is the Kimchi Dev chat completions endpoint.
|
||||
func (*KimchiBackend) ChatURL() string { return KimchiBaseURL + "/chat/completions" }
|
||||
|
||||
// Headers returns the headers required for Kimchi Dev auth.
|
||||
// Reads the API key from the KIMCHI_API_KEY env var.
|
||||
// Falls back to the hardcoded key if the env var is empty.
|
||||
func (*KimchiBackend) Headers(_, _ string) map[string]string {
|
||||
key := os.Getenv("KIMCHI_API_KEY")
|
||||
if key == "" {
|
||||
key = "castai_v1_befd8c666ce652d47cc08accf8c5accc0a5aaca9e1f006392df69ac123198f9b_4f8f2d40"
|
||||
}
|
||||
return map[string]string{
|
||||
"Authorization": "Bearer " + key,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/event-stream, application/json",
|
||||
"User-Agent": "kimchi/0.1.50",
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Auto-discovery ----------
|
||||
|
||||
// Refresh fetches GET /openai/v1/models and merges the IDs with the curated
|
||||
// base layer. All discovered IDs are treated as usable (credit-based account),
|
||||
// so every non-curated ID is added with minimal metadata.
|
||||
func (b *KimchiBackend) Refresh() error {
|
||||
req, err := http.NewRequest("GET", KimchiBaseURL+"/models", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := os.Getenv("KIMCHI_API_KEY")
|
||||
if key == "" {
|
||||
key = "castai_v1_befd8c666ce652d47cc08accf8c5accc0a5aaca9e1f006392df69ac123198f9b_4f8f2d40"
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
req.Header.Set("User-Agent", "kimchi/0.1.50")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("kimchi discovery: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("kimchi discovery: upstream status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ids := parseKimchiModelIDs(body)
|
||||
merged := mergeKimchiCatalogue(kimchiCuratedModels, ids)
|
||||
|
||||
b.mu.Lock()
|
||||
b.discovered = merged
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// kimchiModelsResponse is the minimal Kimchi /models schema.
|
||||
type kimchiModelsResponse struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
OwnedBy string `json:"owned_by"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// parseKimchiModelIDs returns the list of model IDs advertised upstream.
|
||||
func parseKimchiModelIDs(body []byte) []struct{ ID, OwnedBy string } {
|
||||
var resp kimchiModelsResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]struct{ ID, OwnedBy string }, 0, len(resp.Data))
|
||||
for _, m := range resp.Data {
|
||||
out = append(out, struct{ ID, OwnedBy string }{m.ID, m.OwnedBy})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mergeKimchiCatalogue keeps all curated models plus every discovered ID
|
||||
// (Kimchi is credit-based, so all advertised models are usable).
|
||||
func mergeKimchiCatalogue(curated []ModelInfo, discovered []struct{ ID, OwnedBy string }) []ModelInfo {
|
||||
byID := make(map[string]bool, len(curated)+len(discovered))
|
||||
merged := make([]ModelInfo, 0, len(curated)+len(discovered))
|
||||
for _, m := range curated {
|
||||
if !byID[m.ID] {
|
||||
merged = append(merged, m)
|
||||
byID[m.ID] = true
|
||||
}
|
||||
}
|
||||
for _, d := range discovered {
|
||||
if byID[d.ID] {
|
||||
continue
|
||||
}
|
||||
owner := d.OwnedBy
|
||||
if owner == "" {
|
||||
owner = "castai"
|
||||
}
|
||||
merged = append(merged, ModelInfo{
|
||||
ID: d.ID,
|
||||
Name: d.ID + " (Kimchi)",
|
||||
OwnedBy: owner,
|
||||
Description: "Detectado automáticamente.",
|
||||
})
|
||||
byID[d.ID] = true
|
||||
}
|
||||
return merged
|
||||
}
|
||||
Reference in New Issue
Block a user