1// Package main provides a command-line tool to fetch models from Avian
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 "net/http"
12 "os"
13 "slices"
14 "strings"
15 "time"
16
17 "charm.land/catwalk/pkg/catwalk"
18)
19
20// Model represents a model from the Avian API.
21type Model struct {
22 ID string `json:"id"`
23 DisplayName string `json:"display_name"`
24 ContextLength int64 `json:"context_length"`
25 MaxOutput int64 `json:"max_output"`
26 Reasoning bool `json:"reasoning"`
27 Pricing Pricing `json:"pricing"`
28}
29
30// Pricing contains the pricing information for a model.
31type Pricing struct {
32 InputPerMillion float64 `json:"input_per_million"`
33 OutputPerMillion float64 `json:"output_per_million"`
34 CacheReadPerMillion float64 `json:"cache_read_per_million"`
35}
36
37// ModelsResponse is the response structure for the Avian models API.
38type ModelsResponse struct {
39 Data []Model `json:"data"`
40}
41
42func fetchAvianModels() (*ModelsResponse, error) {
43 client := &http.Client{Timeout: 30 * time.Second}
44 req, _ := http.NewRequestWithContext(
45 context.Background(),
46 "GET",
47 "https://api.avian.io/v1/models",
48 nil,
49 )
50 req.Header.Set("User-Agent", "Crush-Client/1.0")
51 resp, err := client.Do(req)
52 if err != nil {
53 return nil, err //nolint:wrapcheck
54 }
55 defer resp.Body.Close() //nolint:errcheck
56 if resp.StatusCode != 200 {
57 body, _ := io.ReadAll(resp.Body)
58 return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
59 }
60 var mr ModelsResponse
61 if err := json.NewDecoder(resp.Body).Decode(&mr); err != nil {
62 return nil, err //nolint:wrapcheck
63 }
64 return &mr, nil
65}
66
67func main() {
68 modelsResp, err := fetchAvianModels()
69 if err != nil {
70 log.Fatal("Error fetching Avian models:", err)
71 }
72
73 avianProvider := catwalk.Provider{
74 Name: "Avian",
75 ID: catwalk.InferenceProviderAvian,
76 APIKey: "$AVIAN_API_KEY",
77 APIEndpoint: "https://api.avian.io/v1",
78 Type: catwalk.TypeOpenAICompat,
79 DefaultLargeModelID: "moonshotai/kimi-k2.5",
80 DefaultSmallModelID: "deepseek/deepseek-v3.2",
81 Models: []catwalk.Model{},
82 }
83
84 for _, model := range modelsResp.Data {
85 var reasoningLevels []string
86 var defaultReasoning string
87 if model.Reasoning {
88 reasoningLevels = []string{"low", "medium", "high"}
89 defaultReasoning = "medium"
90 }
91
92 m := catwalk.Model{
93 ID: model.ID,
94 Name: model.DisplayName,
95 CostPer1MIn: model.Pricing.InputPerMillion,
96 CostPer1MOut: model.Pricing.OutputPerMillion,
97 CostPer1MInCached: model.Pricing.CacheReadPerMillion,
98 CostPer1MOutCached: 0,
99 ContextWindow: model.ContextLength,
100 DefaultMaxTokens: model.MaxOutput,
101 CanReason: model.Reasoning,
102 ReasoningLevels: reasoningLevels,
103 DefaultReasoningEffort: defaultReasoning,
104 SupportsImages: false,
105 }
106
107 avianProvider.Models = append(avianProvider.Models, m)
108 fmt.Printf("Added model %s with context window %d\n", model.ID, model.ContextLength)
109 }
110
111 slices.SortFunc(avianProvider.Models, func(a catwalk.Model, b catwalk.Model) int {
112 return strings.Compare(a.Name, b.Name)
113 })
114
115 // Save the JSON in internal/providers/configs/avian.json
116 data, err := json.MarshalIndent(avianProvider, "", " ")
117 if err != nil {
118 log.Fatal("Error marshaling Avian provider:", err)
119 }
120 data = append(data, '\n')
121
122 if err := os.WriteFile("internal/providers/configs/avian.json", data, 0o600); err != nil {
123 log.Fatal("Error writing Avian provider config:", err)
124 }
125
126 fmt.Printf("Generated avian.json with %d models\n", len(avianProvider.Models))
127}