mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
fix(openai): support Astra Ultrafast capabilities and pricing
This commit is contained in:
@@ -96,6 +96,8 @@ type BillingCache interface {
|
||||
|
||||
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
||||
type ModelPricing struct {
|
||||
// UltrafastMultiplier is model-owned and independent of operator Fast pricing.
|
||||
UltrafastMultiplier float64
|
||||
InputPricePerToken float64 // 每token输入价格 (USD)
|
||||
InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD)
|
||||
ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken
|
||||
@@ -153,6 +155,9 @@ func serviceTierCostMultiplier(serviceTier string) float64 {
|
||||
|
||||
func configuredServiceTierMultiplier(serviceTier string, pricing *ModelPricing) float64 {
|
||||
if pricing != nil {
|
||||
if normalizeBillingServiceTier(serviceTier) == OpenAIFastTierUltrafast && pricing.UltrafastMultiplier > 0 {
|
||||
return pricing.UltrafastMultiplier
|
||||
}
|
||||
switch normalizeBillingServiceTier(serviceTier) {
|
||||
case "priority", "fast":
|
||||
if pricing.FastMultiplier != nil {
|
||||
@@ -1868,6 +1873,9 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing
|
||||
return pricing
|
||||
}
|
||||
cloned := *pricing
|
||||
if isOpenAIGPT6AstraModel(normalized) {
|
||||
cloned.UltrafastMultiplier = 6
|
||||
}
|
||||
if needsOpus55FastMultiplier {
|
||||
multiplier := 2.0
|
||||
cloned.FastMultiplier = &multiplier
|
||||
|
||||
@@ -2091,6 +2091,44 @@ func TestNewModelPricingAliasesRetainExplicitOverrides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstraUltrafastPricingUsesSixTimesStandard(t *testing.T) {
|
||||
data, err := os.ReadFile("../../resources/model-pricing/model_prices_and_context_window.json")
|
||||
require.NoError(t, err)
|
||||
catalog := &PricingService{}
|
||||
catalog.pricingData, err = catalog.parsePricingData(data)
|
||||
require.NoError(t, err)
|
||||
for _, svc := range []*BillingService{newTestBillingService(), NewBillingService(&config.Config{}, &PricingService{}), NewBillingService(&config.Config{}, catalog)} {
|
||||
for _, model := range []string{"gpt-6-astra", "gpt-6", "openai/gpt-6-astra"} {
|
||||
for _, n := range []int{271999, 272000, 272001} {
|
||||
tokens := UsageTokens{InputTokens: n - 3000, CacheReadTokens: 2000, CacheCreationTokens: 1000, OutputTokens: 500}
|
||||
cost, err := svc.CalculateCostWithServiceTier(model, tokens, 1, "ultrafast")
|
||||
require.NoError(t, err)
|
||||
im, om := 1.0, 1.0
|
||||
if n > 272000 {
|
||||
im, om = 2, 1.5
|
||||
}
|
||||
require.InDelta(t, float64(n-3000)*60e-6*im, cost.InputCost, 1e-10)
|
||||
require.InDelta(t, 2000*6e-6*im, cost.CacheReadCost, 1e-10)
|
||||
require.InDelta(t, 1000*75e-6*im, cost.CacheCreationCost, 1e-10)
|
||||
require.InDelta(t, 500*300e-6*om, cost.OutputCost, 1e-10)
|
||||
}
|
||||
}
|
||||
}
|
||||
svc := newTestBillingService()
|
||||
for _, custom := range []float64{0, 1e-6} {
|
||||
fast := 3.0
|
||||
p, err := svc.GetModelPricingWithChannel("gpt-6-astra", &ChannelModelPricing{InputPrice: &custom, OutputPrice: &custom, CacheWritePrice: &custom, CacheReadPrice: &custom, FastMultiplier: &fast})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 6.0, configuredServiceTierMultiplier("ultrafast", p))
|
||||
require.Equal(t, 3.0, configuredServiceTierMultiplier("priority", p))
|
||||
cost := svc.computeTokenBreakdown(p, UsageTokens{InputTokens: 1000, OutputTokens: 1000, CacheReadTokens: 1000, CacheCreationTokens: 1000}, 1, "ultrafast", false)
|
||||
require.InDelta(t, custom*24000, cost.TotalCost, 1e-10)
|
||||
}
|
||||
p, err := svc.GetModelPricing("gpt-6-sol")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2.0, configuredServiceTierMultiplier("ultrafast", p))
|
||||
}
|
||||
|
||||
func TestGPT61SolExplicitZeroCacheWriteAcrossTiers(t *testing.T) {
|
||||
pricing := &PricingService{}
|
||||
var err error
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var codexToolCapabilityFields = []string{
|
||||
"service_tiers",
|
||||
"supports_search_tool", "apply_patch_tool_type", "comp_hash", "tool_mode", "use_responses_lite",
|
||||
"multi_agent_reasoning_effort", "multi_agent_version",
|
||||
}
|
||||
@@ -22,7 +23,12 @@ func applyCodexToolCapabilities(dst, src map[string]json.RawMessage, overwrite b
|
||||
}
|
||||
// These Codex fields are nullable booleans or strings, never arbitrary objects.
|
||||
if !bytes.Equal(value, []byte("null")) {
|
||||
if field == "supports_search_tool" || field == "use_responses_lite" {
|
||||
if field == "service_tiers" {
|
||||
var tiers []configuredCodexServiceTier
|
||||
if json.Unmarshal(value, &tiers) != nil {
|
||||
continue
|
||||
}
|
||||
} else if field == "supports_search_tool" || field == "use_responses_lite" {
|
||||
if !bytes.Equal(value, []byte("true")) && !bytes.Equal(value, []byte("false")) {
|
||||
continue
|
||||
}
|
||||
@@ -88,6 +94,17 @@ func accountCodexToolCapabilities(account *Account, modelID string) map[string]j
|
||||
capabilities["use_responses_lite"] = json.RawMessage("false")
|
||||
}
|
||||
}
|
||||
// API Astra publicly supports Ultrafast. OAuth must advertise it in its
|
||||
// account manifest; a subscription label alone does not grant the capability.
|
||||
if account.IsOpenAIApiKey() && isOfficialOpenAIModelsBaseURL(baseURL) && isOpenAIGPT6AstraModel(modelID) {
|
||||
tiers := configuredCodexServiceTiersForModel(modelID)
|
||||
tiers = append(tiers, configuredCodexServiceTier{ID: OpenAIFastTierUltrafast, Name: "Ultrafast", Description: "Lowest latency; 6x Standard token pricing."})
|
||||
encoded, err := json.Marshal(tiers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
applyCodexToolCapabilities(capabilities, map[string]json.RawMessage{"service_tiers": encoded}, false)
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
|
||||
@@ -3818,6 +3818,28 @@ func TestGPT61SolOfflineCodexCatalog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAstraUltrafastCatalogUsesAccountCapabilities(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
accountType, base, model string
|
||||
want bool
|
||||
}{
|
||||
{AccountTypeAPIKey, "https://api.openai.com", "gpt-6-astra", true},
|
||||
{AccountTypeAPIKey, "https://api.openai.com", "gpt-6.1-sol", false},
|
||||
{AccountTypeAPIKey, "https://proxy.example", "gpt-6-astra", false},
|
||||
{AccountTypeOAuth, "https://chatgpt.com", "gpt-6-astra", false},
|
||||
} {
|
||||
account := &Account{Platform: PlatformOpenAI, Type: tc.accountType, Credentials: map[string]any{"base_url": tc.base, "plan_type": "promax"}}
|
||||
caps := accountCodexToolCapabilities(account, tc.model)
|
||||
require.Equal(t, tc.want, bytes.Contains(caps["service_tiers"], []byte("ultrafast")))
|
||||
}
|
||||
// Explicit native null/empty fields and account-provided tiers remain authoritative.
|
||||
for _, raw := range []string{"null", "[]", `[{"id":"ultrafast","name":"Ultrafast"}]`} {
|
||||
dst := map[string]json.RawMessage{"service_tiers": json.RawMessage(raw)}
|
||||
require.False(t, applyCodexToolCapabilities(dst, map[string]json.RawMessage{"service_tiers": json.RawMessage(`[{"id":"priority"}]`)}, false))
|
||||
require.JSONEq(t, raw, string(dst["service_tiers"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPT61SolAPIKeyCatalogUsesFullResponses(t *testing.T) {
|
||||
account := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"base_url": "https://api.openai.com"}}
|
||||
body, err := completeAPIKeyCodexModelsManifestMetadata([]byte(`{"models":[{"slug":"gpt-6.1-sol"}]}`), true, account)
|
||||
|
||||
@@ -53,6 +53,8 @@ func serviceTierCostRank(tier string) (rank int, known bool) {
|
||||
return 1, true
|
||||
case "priority", "fast":
|
||||
return 2, true
|
||||
case "ultrafast":
|
||||
return 3, true
|
||||
default:
|
||||
return 1, false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ func TestResolveBillingServiceTier(t *testing.T) {
|
||||
}{
|
||||
{name: "openai priority served as default", requested: "priority", observed: "default", billing: "default", downgraded: true},
|
||||
{name: "anthropic fast served as standard", requested: "fast", observed: "standard", billing: "standard", downgraded: true},
|
||||
{name: "ultrafast served as priority", requested: "ultrafast", observed: "priority", billing: "priority", downgraded: true},
|
||||
{name: "ultrafast served as default", requested: "ultrafast", observed: "default", billing: "default", downgraded: true},
|
||||
{name: "ultrafast honoured", requested: "ultrafast", observed: "ultrafast", billing: "ultrafast"},
|
||||
{name: "ultrafast missing declaration", requested: "ultrafast", billing: "ultrafast"},
|
||||
{name: "response cannot upgrade to ultrafast", requested: "priority", observed: "ultrafast", billing: "priority"},
|
||||
{name: "priority honoured", requested: "priority", observed: "priority", billing: "priority"},
|
||||
{name: "no declaration keeps request", requested: "priority", observed: "", billing: "priority"},
|
||||
{name: "no request no declaration", requested: "", observed: "", billing: ""},
|
||||
|
||||
Reference in New Issue
Block a user