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 }