models.go

v0.7.0
Doc Versions Source
1
package openrouter
2
3
import (
4
	"encoding/json"
5
	"fmt"
6
	"net/http"
7
	"slices"
8
	"sort"
9
	"strconv"
10
	"strings"
11
)
12
13
const modelsURL = "https://openrouter.ai/api/v1/models"
14
15
// EnterByHandCategory is the special category label for manual model ID entry.
16
const EnterByHandCategory = "✏ Enter model ID"
17
18
// ORModel represents a model available via the OpenRouter API.
19
type ORModel struct {
20
	ID            string  `json:"id"`
21
	Name          string  `json:"name"`
22
	Created       float64 `json:"created"`
23
	ContextLength int     `json:"context_length"`
24
	Pricing       struct {
25
		Prompt     string `json:"prompt"`
26
		Completion string `json:"completion"`
27
	} `json:"pricing"`
28
	SupportedParameters []string `json:"supported_parameters"`
29
	Architecture        struct {
30
		OutputModalities []string `json:"output_modalities"`
31
	} `json:"architecture"`
32
}
33
34
// DisplayName returns a human-friendly name for the model.
35
func (m ORModel) DisplayName() string {
36
	if m.Name != "" {
37
		return m.Name
38
	}
39
	return m.ID
40
}
41
42
// isAgentCompatible returns true if the model supports tool calling
43
// and produces text output, making it suitable for coding agents.
44
func (m ORModel) isAgentCompatible() bool {
45
	hasTools := slices.Contains(m.SupportedParameters, "tools")
46
	if !hasTools {
47
		return false
48
	}
49
	hasText := slices.Contains(m.Architecture.OutputModalities, "text")
50
	return hasText
51
}
52
53
func (m ORModel) isFree() bool {
54
	p, _ := strconv.ParseFloat(m.Pricing.Prompt, 64)
55
	c, _ := strconv.ParseFloat(m.Pricing.Completion, 64)
56
	return p == 0 && c == 0
57
}
58
59
var popularPrefixes = []string{
60
	"anthropic/",
61
	"openai/",
62
	"google/",
63
	"meta-llama/",
64
	"deepseek/",
65
	"mistralai/",
66
	"x-ai/",
67
	"cohere/",
68
	"qwen/",
69
}
70
71
func (m ORModel) isPopular() bool {
72
	for _, prefix := range popularPrefixes {
73
		if strings.HasPrefix(m.ID, prefix) {
74
			return true
75
		}
76
	}
77
	return false
78
}
79
80
// ListModels fetches available models from the OpenRouter API.
81
func ListModels(apiKey string) ([]ORModel, error) {
82
	req, err := http.NewRequest("GET", modelsURL, nil)
83
	if err != nil {
84
		return nil, err
85
	}
86
	if apiKey != "" {
87
		req.Header.Set("Authorization", "Bearer "+apiKey)
88
	}
89
	req.Header.Set("Accept", "application/json")
90
91
	resp, err := http.DefaultClient.Do(req)
92
	if err != nil {
93
		return nil, fmt.Errorf("fetching models: %w", err)
94
	}
95
	defer resp.Body.Close()
96
97
	if resp.StatusCode != http.StatusOK {
98
		return nil, fmt.Errorf("models endpoint returned %s", resp.Status)
99
	}
100
101
	var result struct {
102
		Data []ORModel `json:"data"`
103
	}
104
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
105
		return nil, fmt.Errorf("parsing models: %w", err)
106
	}
107
108
	return FilterAgentCompatible(result.Data), nil
109
}
110
111
// FilterAgentCompatible returns only models that support tool calling
112
// and produce text output, suitable for coding agents.
113
func FilterAgentCompatible(models []ORModel) []ORModel {
114
	var out []ORModel
115
	for _, m := range models {
116
		if m.isAgentCompatible() {
117
			out = append(out, m)
118
		}
119
	}
120
	return out
121
}
122
123
// CategorizeModels groups models into Free, Popular, and Newest categories.
124
// Returns category names and a map of category → models.
125
func CategorizeModels(models []ORModel) ([]string, map[string][]ORModel) {
126
	categories := []string{"Free", "Popular", "Newest", EnterByHandCategory}
127
	grouped := make(map[string][]ORModel)
128
129
	var free []ORModel
130
	for _, m := range models {
131
		if m.isFree() {
132
			free = append(free, m)
133
		}
134
	}
135
136
	sort.Slice(free, func(i, j int) bool {
137
		return strings.ToLower(free[i].Name) < strings.ToLower(free[j].Name)
138
	})
139
140
	// Newest: sort by created desc, take top 40.
141
	newest := make([]ORModel, len(models))
142
	copy(newest, models)
143
	sort.Slice(newest, func(i, j int) bool {
144
		return newest[i].Created > newest[j].Created
145
	})
146
	if len(newest) > 40 {
147
		newest = newest[:40]
148
	}
149
150
	grouped["Free"] = free
151
	grouped["Newest"] = newest
152
153
	return categories, grouped
154
}
155
156
// Provider display names and ordering for the Popular drill-down.
157
var providerDisplayNames = map[string]string{
158
	"anthropic":  "Anthropic",
159
	"openai":     "OpenAI",
160
	"google":     "Google",
161
	"meta-llama": "Meta",
162
	"deepseek":   "DeepSeek",
163
	"mistralai":  "Mistral",
164
	"x-ai":       "xAI",
165
	"cohere":     "Cohere",
166
	"qwen":       "Qwen",
167
}
168
169
var providerOrder = []string{
170
	"anthropic", "openai", "google", "meta-llama",
171
	"deepseek", "mistralai", "x-ai", "cohere", "qwen",
172
}
173
174
// GroupPopularByProvider groups popular models by their provider prefix.
175
// Returns ordered provider display names and a map of name → models.
176
func GroupPopularByProvider(models []ORModel) ([]string, map[string][]ORModel) {
177
	grouped := make(map[string][]ORModel)
178
	for _, m := range models {
179
		if !m.isPopular() {
180
			continue
181
		}
182
		prefix := providerPrefix(m.ID)
183
		name := providerDisplayName(prefix)
184
		grouped[name] = append(grouped[name], m)
185
	}
186
	for name := range grouped {
187
		sort.Slice(grouped[name], func(i, j int) bool {
188
			return grouped[name][i].Created > grouped[name][j].Created
189
		})
190
	}
191
	var providers []string
192
	for _, prefix := range providerOrder {
193
		name := providerDisplayName(prefix)
194
		if _, ok := grouped[name]; ok {
195
			providers = append(providers, name)
196
		}
197
	}
198
	return providers, grouped
199
}
200
201
func providerPrefix(id string) string {
202
	if i := strings.Index(id, "/"); i > 0 {
203
		return id[:i]
204
	}
205
	return id
206
}
207
208
func providerDisplayName(prefix string) string {
209
	if name, ok := providerDisplayNames[prefix]; ok {
210
		return name
211
	}
212
	return prefix
213
}
214
215
// AllModelIDs returns a set of all model IDs for validation.
216
func AllModelIDs(models []ORModel) map[string]bool {
217
	ids := make(map[string]bool, len(models))
218
	for _, m := range models {
219
		ids[m.ID] = true
220
	}
221
	return ids
222
}
223

Source Files