(model: string)
| 585 | } |
| 586 | |
| 587 | export function getModelCosts(model: string): ModelCosts | null { |
| 588 | // Try with provider prefix preserved (azure/gpt-5.4, openrouter/anthropic/claude-opus-4.6) |
| 589 | const withPrefix = model.replace(/@.*$/, '').replace(/-\d{8}$/, '') |
| 590 | const canonicalName = getCanonicalName(model) |
| 591 | const canonical = resolveAlias(canonicalName) |
| 592 | |
| 593 | const override = getPriceOverrideExact(model, withPrefix, canonicalName, canonical) |
| 594 | if (override) return override |
| 595 | |
| 596 | // An explicit alias for a bare (un-prefixed) model name is authoritative: it |
| 597 | // must win over a coincidental stripped reseller key of the same name. LiteLLM |
| 598 | // ships `snowflake/claude-4-opus` ($5), which the bundler strips to a bare |
| 599 | // `claude-4-opus` key; without this, that would shadow the curated alias |
| 600 | // `claude-4-opus -> claude-opus-4` ($15 official Anthropic price). |
| 601 | if (canonical !== canonicalName && withPrefix === canonicalName && pricingCache.has(canonical)) { |
| 602 | return pricingCache.get(canonical)! |
| 603 | } |
| 604 | |
| 605 | if (pricingCache.has(withPrefix)) return pricingCache.get(withPrefix)! |
| 606 | |
| 607 | if (pricingCache.has(canonical)) return pricingCache.get(canonical)! |
| 608 | |
| 609 | const prefixOverride = getPriceOverridePrefix(canonical) |
| 610 | if (prefixOverride) return prefixOverride |
| 611 | |
| 612 | // Iterate keys longest-first so a model id like `gpt-5-mini` matches the |
| 613 | // `gpt-5-mini` entry rather than collapsing to the shorter `gpt-5` entry |
| 614 | // due to dictionary insertion order. |
| 615 | for (const key of getSortedPricingKeys()) { |
| 616 | if (canonical.startsWith(key + '-') || canonical === key) { |
| 617 | return pricingCache.get(key)! |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | const caseInsensitiveOverride = getPriceOverrideCaseInsensitive(canonical, withPrefix) |
| 622 | if (caseInsensitiveOverride) return caseInsensitiveOverride |
| 623 | |
| 624 | // Case-insensitive fallback: gap-filled keys from OpenRouter are lowercase |
| 625 | // slugs (e.g. `minimax-m3`), but sessions report `MiniMax-M3`. Only consulted |
| 626 | // after the exact/canonical/prefix attempts, so it never changes a match that |
| 627 | // already resolved above. |
| 628 | const lowerIndex = getLowercasePricingIndex() |
| 629 | const byCanonical = lowerIndex.get(canonical.toLowerCase()) |
| 630 | if (byCanonical) return byCanonical |
| 631 | const byPrefix = lowerIndex.get(withPrefix.toLowerCase()) |
| 632 | if (byPrefix) return byPrefix |
| 633 | |
| 634 | return null |
| 635 | } |
| 636 | |
| 637 | // Warn at most once per unknown model name per process. Without this, a model |
| 638 | // missing from the pricing snapshot would silently price at $0 for every |
no test coverage detected