Repository navigation
Handle models missing from the pricing table #66552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
e5fbb5a
2732e85
c4751e5
89f9659
46f8220
b2742c0
2a75a62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,7 +225,11 @@ func configuredModelHasPricing(provider, model string, aliases map[string][]stri | |
| targetProvider, targetModel, ok := configuredModelPricingID(&workflow.WorkflowData{ | ||
| EngineConfig: &workflow.EngineConfig{LLMProvider: workflow.LLMProvider(provider)}, | ||
| }, target) | ||
| if !ok || !configuredModelHasPricing(targetProvider, targetModel, aliases, visited) { | ||
| hasPricing := ok && configuredModelHasPricing(targetProvider, targetModel, aliases, visited) | ||
| if ok && strings.ContainsAny(targetModel, "*[") { | ||
| hasPricing = hasCatalogPricingMatching(targetProvider, targetModel) | ||
| } | ||
| if !hasPricing { | ||
| delete(visited, alias) | ||
| return false | ||
| } | ||
|
|
@@ -255,6 +259,14 @@ func hasModelCostOverlay(costs map[string]any, provider, model string) bool { | |
| continue | ||
| } | ||
| for name := range models { | ||
| entry, ok := models[name].(map[string]any) | ||
| if !ok { | ||
| continue | ||
| } | ||
| cost, ok := entry["cost"] | ||
| if !ok || !hasNonEmptyCostMap(cost) { | ||
| continue | ||
| } | ||
| if strings.EqualFold(name, model) { | ||
| return true | ||
|
Comment on lines
+261
to
+271
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The schema permits a frontmatter model entry such as costs := map[string]any{"providers": map[string]any{"openai": map[string]any{"models": map[string]any{"custom-model": map[string]any{}}}}}
hasModelCostOverlay(costs, "openai", "custom-model") // => true (should be false — no cost present)This means a frontmatter stub entry with no actual price silently suppresses
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in |
||
| } | ||
|
|
@@ -263,6 +275,17 @@ func hasModelCostOverlay(costs map[string]any, provider, model string) bool { | |
| return false | ||
| } | ||
|
|
||
| func hasNonEmptyCostMap(value any) bool { | ||
| switch cost := value.(type) { | ||
| case map[string]any: | ||
| return len(cost) > 0 | ||
| case map[string]string: | ||
| return len(cost) > 0 | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| func stringSlice(value any) []string { | ||
| raw, ok := value.([]any) | ||
| if !ok { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package cli | |
| import ( | ||
| _ "embed" | ||
| "encoding/json" | ||
| "path" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
|
|
@@ -35,51 +36,44 @@ type modelPriceRecord struct { | |
| pricing map[string]float64 | ||
| } | ||
|
|
||
| var ( | ||
| modelPriceRecords []modelPriceRecord | ||
| modelPricesOnce sync.Once | ||
| ) | ||
| var modelPriceRecords = sync.OnceValue(loadModelPriceRecords) | ||
|
|
||
| func initModelPrices() { | ||
| modelPricesOnce.Do(func() { | ||
| var data modelsCatalogData | ||
| if err := json.Unmarshal(modelsJSON, &data); err != nil { | ||
| return | ||
| } | ||
| func loadModelPriceRecords() []modelPriceRecord { | ||
| var data modelsCatalogData | ||
| if err := json.Unmarshal(modelsJSON, &data); err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| modelPriceRecords = make([]modelPriceRecord, 0) | ||
| for providerName, providerData := range data.Providers { | ||
| normalizedProvider := strings.ToLower(strings.TrimSpace(providerName)) | ||
| if normalizedProvider == "" { //nolint:tolowerequalfold | ||
| records := make([]modelPriceRecord, 0) | ||
| for providerName, providerData := range data.Providers { | ||
| normalizedProvider := strings.ToLower(strings.TrimSpace(providerName)) | ||
| if normalizedProvider == "" { //nolint:tolowerequalfold | ||
| continue | ||
| } | ||
| for modelName, entry := range providerData.Models { | ||
| normalizedModel := strings.ToLower(strings.TrimSpace(modelName)) | ||
| if normalizedModel == "" { //nolint:tolowerequalfold | ||
| continue | ||
| } | ||
| for modelName, entry := range providerData.Models { | ||
| normalizedModel := strings.ToLower(strings.TrimSpace(modelName)) | ||
| if normalizedModel == "" { //nolint:tolowerequalfold | ||
| continue | ||
| } | ||
| normalizedID := normalizedProvider + "/" + normalizedModel | ||
| record := modelPriceRecord{ | ||
| id: normalizedID, | ||
| provider: normalizedProvider, | ||
| model: normalizedModel, | ||
| pricing: make(map[string]float64, len(entry.Cost)), | ||
| } | ||
| for key, value := range entry.Cost { | ||
| if parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64); err == nil { | ||
| record.pricing[key] = parsed | ||
| } | ||
| record := modelPriceRecord{ | ||
| id: path.Join(normalizedProvider, normalizedModel), | ||
| provider: normalizedProvider, | ||
| model: normalizedModel, | ||
| pricing: make(map[string]float64, len(entry.Cost)), | ||
| } | ||
| for key, value := range entry.Cost { | ||
| if parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64); err == nil { | ||
| record.pricing[key] = parsed | ||
| } | ||
| modelPriceRecords = append(modelPriceRecords, record) | ||
| } | ||
| records = append(records, record) | ||
| } | ||
| modelCostsLog.Printf("Initialized model price catalog: providers=%d, records=%d", len(data.Providers), len(modelPriceRecords)) | ||
| }) | ||
| } | ||
| modelCostsLog.Printf("Initialized model price catalog: providers=%d, records=%d", len(data.Providers), len(records)) | ||
| return records | ||
| } | ||
|
|
||
| func findModelPricing(provider, model string) (map[string]float64, bool) { | ||
| initModelPrices() | ||
|
|
||
| normalizedProvider := modelsdev.NormalizeProvider(provider) | ||
| normalizedModel := strings.ToLower(strings.TrimSpace(model)) | ||
| comparableModel := modelsdev.NormalizeComparableModelID(normalizedModel) | ||
|
|
@@ -89,23 +83,21 @@ func findModelPricing(provider, model string) (map[string]float64, bool) { | |
|
|
||
| fullID := normalizedModel | ||
| if !strings.Contains(fullID, "/") && normalizedProvider != "" { | ||
| fullID = normalizedProvider + "/" + normalizedModel | ||
| fullID = path.Join(normalizedProvider, normalizedModel) | ||
| } | ||
| comparableFullID := modelsdev.NormalizeComparableModelID(fullID) | ||
|
|
||
| for _, record := range modelPriceRecords { | ||
| if (fullID != "" && record.id == fullID) || (comparableFullID != "" && modelsdev.NormalizeComparableModelID(record.id) == comparableFullID) { | ||
| modelCostsLog.Printf("Exact pricing match: provider=%s, model=%s -> %s", provider, model, record.id) | ||
| return record.pricing, true | ||
| } | ||
| if pricing, ok := findExactCatalogPricing(modelPriceRecords(), fullID, comparableFullID); ok { | ||
| modelCostsLog.Printf("Exact pricing match: provider=%s, model=%s -> %s", provider, model, fullID) | ||
| return pricing, true | ||
| } | ||
|
|
||
| var bestProviderScoped map[string]float64 | ||
| bestProviderScopedLen := -1 | ||
| var bestGeneric map[string]float64 | ||
| bestGenericLen := -1 | ||
|
|
||
| for _, record := range modelPriceRecords { | ||
| for _, record := range modelPriceRecords() { | ||
| comparableRecordModel := modelsdev.NormalizeComparableModelID(record.model) | ||
| if record.model == normalizedModel || comparableRecordModel == comparableModel { | ||
| if normalizedProvider != "" && record.provider == normalizedProvider { | ||
|
|
@@ -141,15 +133,23 @@ func findModelPricing(provider, model string) (map[string]float64, bool) { | |
| return nil, false | ||
| } | ||
|
|
||
| func findExactModelPricing(provider, model string) (map[string]float64, bool) { | ||
| initModelPrices() | ||
| func findExactCatalogPricing(records []modelPriceRecord, fullID, comparableFullID string) (map[string]float64, bool) { | ||
| for _, record := range records { | ||
| if (fullID != "" && record.id == fullID) || (comparableFullID != "" && modelsdev.NormalizeComparableModelID(record.id) == comparableFullID) { | ||
| return record.pricing, true | ||
| } | ||
| } | ||
| return nil, false | ||
| } | ||
|
|
||
| func findExactModelPricing(provider, model string) (map[string]float64, bool) { | ||
| normalizedProvider := modelsdev.NormalizeProvider(provider) | ||
| model, _, _ = strings.Cut(strings.TrimSpace(model), "?") | ||
| comparableModel := modelsdev.NormalizeComparableModelID(model) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Query-parameter suffixes are not stripped before the exact-pricing lookup.
Reproduced locally: findExactModelPricing("openai", "gpt-6.1-sol?effort=high") // => (nil, false)
findExactModelPricing("openai", "gpt-6.1-sol") // => (pricing, true)Any workflow configuring
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handled in |
||
| if normalizedProvider == "" || comparableModel == "" { | ||
| return nil, false | ||
| } | ||
| for _, record := range modelPriceRecords { | ||
| for _, record := range modelPriceRecords() { | ||
| if record.provider == normalizedProvider && | ||
| modelsdev.NormalizeComparableModelID(record.model) == comparableModel && | ||
| len(record.pricing) > 0 { | ||
|
|
@@ -159,6 +159,24 @@ func findExactModelPricing(provider, model string) (map[string]float64, bool) { | |
| return nil, false | ||
| } | ||
|
|
||
| func hasCatalogPricingMatching(provider, pattern string) bool { | ||
| normalizedProvider := modelsdev.NormalizeProvider(provider) | ||
| comparablePattern := modelsdev.NormalizeComparableModelID(pattern) | ||
| if normalizedProvider == "" || comparablePattern == "" { | ||
| return false | ||
| } | ||
| for _, record := range modelPriceRecords() { | ||
| if record.provider != normalizedProvider || len(record.pricing) == 0 { | ||
| continue | ||
| } | ||
| matched, err := path.Match(comparablePattern, modelsdev.NormalizeComparableModelID(record.model)) | ||
| if err == nil && matched { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func usdToAIC(usd float64) float64 { | ||
| return usd / 0.01 | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alias glob patterns break
configuredModelHasPricingand emit false-positive warnings for every built-in model alias.Built-in aliases such as
sonnetandhaikuresolve to glob targets likecopilot/*sonnet*(seepkg/workflow/model_aliases.go), butconfiguredModelPricingID(and the exact-match lookup it feeds into) treats each target as a literal model ID. A glob pattern never matches any catalog record, soconfiguredModelHasPricingreturnsfalseand the new warning fires for every ordinary workflow using a built-in alias, even though AWF resolves these at runtime.Reproduced locally:
This fires spuriously for any workflow with
model: sonnetormodel: haiku, which is likely very common.Please either skip/short-circuit glob targets (treat them as "has pricing" since AWF resolves them dynamically) or expand the glob against the catalog before declaring no pricing. @copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handled in
b2742c01: alias glob targets are matched against priced catalog models, and built-insonnet/haikucases are covered by tests.