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}