main.go

  1// Package main provides a command-line tool to fetch models from Chutes
  2// and generate a configuration file for the provider.
  3package main
  4
  5import (
  6	"context"
  7	"encoding/json"
  8	"fmt"
  9	"io"
 10	"log"
 11	"math"
 12	"net/http"
 13	"os"
 14	"slices"
 15	"strings"
 16	"time"
 17
 18	"charm.land/catwalk/pkg/catwalk"
 19)
 20
 21type ChutesModel struct {
 22	ID                string   `json:"id"`
 23	ContextLength     int64    `json:"context_length"`
 24	MaxOutputLength   int64    `json:"max_output_length"`
 25	InputModalities   []string `json:"input_modalities"`
 26	OutputModalities  []string `json:"output_modalities"`
 27	SupportedFeatures []string `json:"supported_features"`
 28	Pricing           Pricing  `json:"pricing"`
 29}
 30
 31type Pricing struct {
 32	Prompt         float64 `json:"prompt"`
 33	Completion     float64 `json:"completion"`
 34	InputCacheRead float64 `json:"input_cache_read"`
 35}
 36
 37type ModelsResponse struct {
 38	Data []ChutesModel `json:"data"`
 39}
 40
 41func roundCost(v float64) float64 {
 42	return math.Round(v*1e5) / 1e5
 43}
 44
 45func hasFeature(m ChutesModel, feature string) bool {
 46	return slices.Contains(m.SupportedFeatures, feature)
 47}
 48
 49func hasModality(m ChutesModel, modality string) bool {
 50	return slices.Contains(m.InputModalities, modality)
 51}
 52
 53func modelDisplayName(id string) string {
 54	return strings.SplitN(id, "/", 2)[1]
 55}
 56
 57func main() {
 58	client := &http.Client{Timeout: 30 * time.Second}
 59	req, _ := http.NewRequestWithContext(
 60		context.Background(),
 61		"GET",
 62		"https://llm.chutes.ai/v1/models",
 63		nil,
 64	)
 65	req.Header.Set("User-Agent", "Crush-Client/1.0")
 66
 67	resp, err := client.Do(req)
 68	if err != nil {
 69		log.Fatal("Error fetching Chutes models:", err)
 70	}
 71	defer resp.Body.Close() //nolint:errcheck
 72
 73	body, err := io.ReadAll(resp.Body)
 74	if err != nil {
 75		log.Fatal("Error reading Chutes models response:", err)
 76	}
 77
 78	if resp.StatusCode != http.StatusOK {
 79		log.Fatalf("Error fetching Chutes models: status %d: %s", resp.StatusCode, body)
 80	}
 81
 82	_ = os.MkdirAll("tmp", 0o700)
 83	_ = os.WriteFile("tmp/chutes-response.json", body, 0o600)
 84
 85	var modelsResp ModelsResponse
 86	if err := json.Unmarshal(body, &modelsResp); err != nil {
 87		log.Fatal("Error parsing Chutes models response:", err)
 88	}
 89
 90	var models []catwalk.Model
 91	for _, m := range modelsResp.Data {
 92		if !hasFeature(m, "tools") {
 93			continue
 94		}
 95		if !hasModality(m, "text") {
 96			continue
 97		}
 98		if !slices.Contains(m.OutputModalities, "text") {
 99			continue
100		}
101
102		var (
103			canReason        = hasFeature(m, "reasoning")
104			reasoningLevels  []string
105			defaultReasoning string
106		)
107		if canReason {
108			reasoningLevels = []string{"low", "medium", "high"}
109			defaultReasoning = "medium"
110		}
111
112		model := catwalk.Model{
113			ID:                     m.ID,
114			Name:                   modelDisplayName(m.ID),
115			CostPer1MIn:            roundCost(m.Pricing.Prompt),
116			CostPer1MOut:           roundCost(m.Pricing.Completion),
117			CostPer1MInCached:      roundCost(m.Pricing.InputCacheRead),
118			ContextWindow:          m.ContextLength,
119			DefaultMaxTokens:       m.MaxOutputLength,
120			CanReason:              canReason,
121			DefaultReasoningEffort: defaultReasoning,
122			ReasoningLevels:        reasoningLevels,
123			SupportsImages:         hasModality(m, "image"),
124		}
125		models = append(models, model)
126		fmt.Printf("Added model %s\n", m.ID)
127	}
128
129	slices.SortFunc(models, func(a, b catwalk.Model) int {
130		return strings.Compare(a.Name, b.Name)
131	})
132
133	chutesProvider := catwalk.Provider{
134		Name:                "Chutes",
135		ID:                  "chutes",
136		APIKey:              "$CHUTES_API_KEY",
137		APIEndpoint:         "https://llm.chutes.ai/v1",
138		Type:                catwalk.TypeOpenAICompat,
139		DefaultLargeModelID: "zai-org/GLM-5-TEE",
140		DefaultSmallModelID: "zai-org/GLM-5-Turbo",
141		Models:              models,
142	}
143
144	data, err := json.MarshalIndent(chutesProvider, "", "  ")
145	if err != nil {
146		log.Fatal("Error marshaling Chutes provider:", err)
147	}
148	data = append(data, '\n')
149
150	if err := os.WriteFile("./internal/providers/configs/chutes.json", data, 0o600); err != nil {
151		log.Fatal("Error writing Chutes provider config:", err)
152	}
153
154	fmt.Println("Chutes provider configuration generated successfully!")
155}