openmeter / llmcost /sync /fetcher_modelsdev.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 5)
cee2387 verified
Raw
History Blame Contribute Delete
3.8 kB
package sync
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/alpacahq/alpacadecimal"
"github.com/openmeterio/openmeter/openmeter/llmcost"
)
const modelsDevAPIURL = "https://models.dev/api.json"
// modelsDevProvider represents a provider entry from models.dev.
type modelsDevProvider struct {
ID string `json:"id"`
Name string `json:"name"`
Models map[string]modelsDevModel `json:"models"`
}
// modelsDevModel represents a model entry from models.dev.
type modelsDevModel struct {
ID string `json:"id"`
Name string `json:"name"`
Cost *modelsDevCost `json:"cost"`
}
type modelsDevCost struct {
Input *float64 `json:"input"`
Output *float64 `json:"output"`
CacheRead *float64 `json:"cache_read"`
CacheWrite *float64 `json:"cache_write"`
Reasoning *float64 `json:"reasoning"`
}
type modelsDevFetcher struct {
client *http.Client
}
func NewModelsDevFetcher(client *http.Client) Fetcher {
return &modelsDevFetcher{client: client}
}
func (f *modelsDevFetcher) Source() llmcost.PriceSource {
return "models_dev"
}
func (f *modelsDevFetcher) Fetch(ctx context.Context) ([]llmcost.SourcePrice, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsDevAPIURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := f.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch models.dev: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("models.dev returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var providers map[string]modelsDevProvider
if err := json.Unmarshal(body, &providers); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
now := time.Now().UTC()
perMillion := alpacadecimal.NewFromFloat(1_000_000)
var prices []llmcost.SourcePrice
for providerKey, prov := range providers {
provider := strings.ToLower(providerKey)
if provider == "" {
continue
}
for _, model := range prov.Models {
if model.Cost == nil || model.Cost.Input == nil || model.Cost.Output == nil {
continue
}
// models.dev provides prices per million tokens, convert to per-token
// models.dev uses "provider/model" format for IDs, strip the provider prefix
modelID := model.ID
if parts := strings.SplitN(modelID, "/", 2); len(parts) == 2 {
modelID = parts[1]
}
// Strip provider prefix from display name (e.g., "azure/gpt-3.5-turbo" → "gpt-3.5-turbo")
modelName := model.Name
if idx := strings.Index(modelName, "/"); idx > 0 && idx < len(modelName)-1 {
modelName = modelName[idx+1:]
}
sp := llmcost.SourcePrice{
Source: "models_dev",
Provider: llmcost.Provider(provider),
ModelID: modelID,
ModelName: modelName,
Pricing: llmcost.ModelPricing{
InputPerToken: alpacadecimal.NewFromFloat(*model.Cost.Input).Div(perMillion),
OutputPerToken: alpacadecimal.NewFromFloat(*model.Cost.Output).Div(perMillion),
},
FetchedAt: now,
}
if model.Cost.CacheRead != nil {
cached := alpacadecimal.NewFromFloat(*model.Cost.CacheRead).Div(perMillion)
sp.Pricing.CacheReadPerToken = &cached
}
if model.Cost.CacheWrite != nil {
cacheWrite := alpacadecimal.NewFromFloat(*model.Cost.CacheWrite).Div(perMillion)
sp.Pricing.CacheWritePerToken = &cacheWrite
}
if model.Cost.Reasoning != nil {
reasoning := alpacadecimal.NewFromFloat(*model.Cost.Reasoning).Div(perMillion)
sp.Pricing.ReasoningPerToken = &reasoning
}
prices = append(prices, sp)
}
}
return prices, nil
}