| package sync |
|
|
| import ( |
| "context" |
| "log/slog" |
| "net/http" |
| "strings" |
|
|
| "github.com/openmeterio/openmeter/openmeter/llmcost" |
| ) |
|
|
| |
| |
| type PriceFilterFunc func(llmcost.SourcePrice) bool |
|
|
| |
| |
| type SyncJob struct { |
| fetchers []Fetcher |
| normalizer ModelIDNormalizer |
| reconciler *Reconciler |
| filter PriceFilterFunc |
| logger *slog.Logger |
| } |
|
|
| |
| type SyncJobConfig struct { |
| HTTPClient *http.Client |
| Repo llmcost.Adapter |
| Logger *slog.Logger |
|
|
| |
| |
| Fetchers []Fetcher |
|
|
| |
| |
| MinSourceAgreement int |
|
|
| |
| |
| PriceTolerance float64 |
|
|
| |
| |
| Filter PriceFilterFunc |
| } |
|
|
| |
| func DefaultFetchers(client *http.Client) []Fetcher { |
| if client == nil { |
| client = http.DefaultClient |
| } |
|
|
| return []Fetcher{ |
| NewModelsDevFetcher(client), |
| } |
| } |
|
|
| |
| func NewSyncJob(config SyncJobConfig) *SyncJob { |
| normalizer := NewDefaultNormalizer() |
|
|
| fetchers := config.Fetchers |
| if fetchers == nil { |
| fetchers = DefaultFetchers(config.HTTPClient) |
| } |
|
|
| |
| |
| minAgreement := config.MinSourceAgreement |
| if minAgreement <= 0 { |
| minAgreement = DefaultMinSourceAgreement |
| } |
|
|
| if numFetchers := len(fetchers); numFetchers > 0 && minAgreement > numFetchers { |
| minAgreement = numFetchers |
| } |
|
|
| return &SyncJob{ |
| fetchers: fetchers, |
| normalizer: normalizer, |
| reconciler: NewReconciler(config.Repo, config.Logger, minAgreement, config.PriceTolerance), |
| filter: config.Filter, |
| logger: config.Logger, |
| } |
| } |
|
|
| |
| type sourceModelKey struct { |
| Source llmcost.PriceSource |
| Provider string |
| ModelID string |
| } |
|
|
| |
| |
| |
| func deduplicateSourcePrices(prices []llmcost.SourcePrice) []llmcost.SourcePrice { |
| seen := make(map[sourceModelKey]int, len(prices)) |
| result := make([]llmcost.SourcePrice, 0, len(prices)) |
|
|
| for _, p := range prices { |
| key := sourceModelKey{Source: p.Source, Provider: string(p.Provider), ModelID: p.ModelID} |
|
|
| if idx, exists := seen[key]; exists { |
| existing := result[idx] |
| existingHasPrefix := strings.Contains(existing.ModelName, "/") |
| newHasPrefix := strings.Contains(p.ModelName, "/") |
|
|
| |
| |
| if existingHasPrefix && !newHasPrefix || |
| existingHasPrefix == newHasPrefix && p.ModelName < existing.ModelName { |
| result[idx] = p |
| } |
|
|
| continue |
| } |
|
|
| seen[key] = len(result) |
| result = append(result, p) |
| } |
|
|
| return result |
| } |
|
|
| |
| func (j *SyncJob) Run(ctx context.Context) error { |
| var allPrices []llmcost.SourcePrice |
|
|
| |
| for _, f := range j.fetchers { |
| sourceName := f.Source() |
|
|
| j.logger.Info("fetching prices", "source", sourceName) |
|
|
| prices, err := f.Fetch(ctx) |
| if err != nil { |
| j.logger.Error("failed to fetch prices", |
| "source", sourceName, |
| "error", err) |
|
|
| continue |
| } |
|
|
| j.logger.Info("fetched prices", |
| "source", sourceName, |
| "count", len(prices)) |
|
|
| |
| for _, p := range prices { |
| provider, modelID := j.normalizer.Normalize(p.ModelID, string(p.Provider)) |
| p.Provider = llmcost.Provider(provider) |
| p.ModelID = modelID |
| allPrices = append(allPrices, p) |
| } |
| } |
|
|
| |
| |
| |
| |
| allPrices = deduplicateSourcePrices(allPrices) |
|
|
| j.logger.Info("deduplicated prices", "count", len(allPrices)) |
|
|
| |
| if j.filter != nil { |
| filtered := allPrices[:0] |
| for _, p := range allPrices { |
| if j.filter(p) { |
| filtered = append(filtered, p) |
| } |
| } |
|
|
| j.logger.Info("filtered prices", |
| "before", len(allPrices), |
| "after", len(filtered)) |
|
|
| allPrices = filtered |
| } |
|
|
| |
| j.logger.Info("starting reconciliation", "total_prices", len(allPrices)) |
|
|
| return j.reconciler.Reconcile(ctx, allPrices) |
| } |
|
|