main.go

  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}