1// Package main provides a command-line tool to fetch models from io.net
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 xstrings "github.com/charmbracelet/x/exp/strings"
20)
21
22// Model represents a model from the io.net API.
23type Model struct {
24 ID string `json:"id"`
25 Name string `json:"name"`
26 ContextWindow int `json:"context_window"`
27 SupportsImagesInput bool `json:"supports_images_input"`
28 InputTokenPrice float64 `json:"input_token_price"`
29 OutputTokenPrice float64 `json:"output_token_price"`
30 CacheWriteTokenPrice float64 `json:"cache_write_token_price"`
31 CacheReadTokenPrice float64 `json:"cache_read_token_price"`
32}
33
34// Response is the response structure for the io.net models API.
35type Response struct {
36 Data []Model `json:"data"`
37}
38
39// This is used to generate the ionet.json config file.
40func main() {
41 provider := catwalk.Provider{
42 Name: "io.net",
43 ID: "ionet",
44 APIKey: "$IONET_API_KEY",
45 APIEndpoint: "https://api.intelligence.io.solutions/api/v1",
46 Type: catwalk.TypeOpenAICompat,
47 DefaultLargeModelID: "moonshotai/Kimi-K2.5",
48 DefaultSmallModelID: "zai-org/GLM-4.7-Flash",
49 }
50
51 resp, err := fetchModels(provider.APIEndpoint)
52 if err != nil {
53 log.Fatal("Error fetching io.net models:", err)
54 }
55
56 provider.Models = make([]catwalk.Model, 0, len(resp.Data))
57
58 modelIDSet := make(map[string]struct{})
59
60 for _, model := range resp.Data {
61 // Avoid duplicate entries
62 if _, ok := modelIDSet[model.ID]; ok {
63 continue
64 }
65 modelIDSet[model.ID] = struct{}{}
66
67 if model.ContextWindow < 20000 {
68 continue
69 }
70 if !supportsTools(model.ID) {
71 continue
72 }
73
74 canReason := isReasoningModel(model.ID)
75 var reasoningLevels []string
76 var defaultReasoning string
77 if canReason {
78 reasoningLevels = []string{"low", "medium", "high"}
79 defaultReasoning = "medium"
80 }
81
82 // Convert token prices (per token) to cost per 1M tokens
83 roundCost := func(v float64) float64 { return math.Round(v*1e5) / 1e5 }
84 costPer1MIn := roundCost(model.InputTokenPrice * 1_000_000)
85 costPer1MOut := roundCost(model.OutputTokenPrice * 1_000_000)
86 costPer1MInCached := roundCost(model.CacheReadTokenPrice * 1_000_000)
87 costPer1MOutCached := roundCost(model.CacheWriteTokenPrice * 1_000_000)
88
89 m := catwalk.Model{
90 ID: model.ID,
91 Name: model.Name,
92 CostPer1MIn: costPer1MIn,
93 CostPer1MOut: costPer1MOut,
94 CostPer1MInCached: costPer1MInCached,
95 CostPer1MOutCached: costPer1MOutCached,
96 ContextWindow: int64(model.ContextWindow),
97 DefaultMaxTokens: int64(model.ContextWindow) / 10,
98 CanReason: canReason,
99 ReasoningLevels: reasoningLevels,
100 DefaultReasoningEffort: defaultReasoning,
101 SupportsImages: model.SupportsImagesInput,
102 }
103
104 provider.Models = append(provider.Models, m)
105 fmt.Printf("Added model %s with context window %d\n", model.ID, model.ContextWindow)
106 }
107
108 slices.SortFunc(provider.Models, func(a catwalk.Model, b catwalk.Model) int {
109 return strings.Compare(a.Name, b.Name)
110 })
111
112 // Save the JSON in internal/providers/configs/ionet.json
113 data, err := json.MarshalIndent(provider, "", " ")
114 if err != nil {
115 log.Fatal("Error marshaling io.net provider:", err)
116 }
117 data = append(data, '\n')
118
119 if err := os.WriteFile("internal/providers/configs/ionet.json", data, 0o600); err != nil {
120 log.Fatal("Error writing io.net provider config:", err)
121 }
122
123 fmt.Printf("Generated ionet.json with %d models\n", len(provider.Models))
124}
125
126func fetchModels(apiEndpoint string) (*Response, error) {
127 client := &http.Client{Timeout: 30 * time.Second}
128
129 req, err := http.NewRequestWithContext(context.Background(), "GET", apiEndpoint+"/models", nil)
130 if err != nil {
131 return nil, fmt.Errorf("failed to create http request: %w", err)
132 }
133 req.Header.Set("User-Agent", "Charm-Catwalk/1.0")
134
135 resp, err := client.Do(req)
136 if err != nil {
137 return nil, fmt.Errorf("failed to do http request: %w", err)
138 }
139 defer resp.Body.Close() //nolint:errcheck
140
141 body, _ := io.ReadAll(resp.Body)
142
143 // for debugging
144 _ = os.MkdirAll("tmp", 0o700)
145 _ = os.WriteFile("tmp/io-net-response.json", body, 0o600)
146
147 if resp.StatusCode != http.StatusOK {
148 return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body)
149 }
150
151 var mr Response
152 if err := json.Unmarshal(body, &mr); err != nil {
153 return nil, fmt.Errorf("unable to unmarshal json: %w", err)
154 }
155 return &mr, nil
156}
157
158// isReasoningModel checks if the model ID indicates reasoning capability.
159func isReasoningModel(modelID string) bool {
160 return xstrings.ContainsAnyOf(
161 strings.ToLower(modelID),
162 "-thinking",
163 "deepseek",
164 "glm",
165 "gpt-oss",
166 "llama",
167 )
168}
169
170// supportsTools determines if a model supports tool calling based on its ID.
171func supportsTools(modelID string) bool {
172 return !xstrings.ContainsAnyOf(
173 strings.ToLower(modelID),
174 "deepseek",
175 "llama-4",
176 "mistral-nemo",
177 "qwen2.5",
178 )
179}