feat(openai): add GPT-6 Astra support

This commit is contained in:
lyy0709
2026-09-04 10:04:44 +08:00
parent b1748c4ea9
commit 3c8be00131
17 changed files with 258 additions and 13 deletions
+2
View File
@@ -19,6 +19,8 @@ type Model struct {
// DefaultModels OpenAI models list
var DefaultModels = []Model{
{ID: "gpt-5.6-sol", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Sol"},
{ID: "gpt-6-astra", Object: "model", Created: 1788480000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 Astra"},
{ID: "gpt-6", Object: "model", Created: 1788480000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 (Astra)"},
{ID: "gpt-5.6", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 (Sol)"},
{ID: "gpt-5.6-terra", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Terra"},
{ID: "gpt-5.6-luna", Object: "model", Created: 1780876800, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.6 Luna"},
@@ -10,6 +10,11 @@ func TestDefaultModelsIncludeBareGPT56Alias(t *testing.T) {
require.Contains(t, DefaultModelIDs(), "gpt-5.6")
}
func TestDefaultModelsIncludeGPT6Astra(t *testing.T) {
require.Contains(t, DefaultModelIDs(), "gpt-6-astra")
require.Contains(t, DefaultModelIDs(), "gpt-6")
}
func TestDefaultModelsPreferConcreteGPT56SolForAccountTests(t *testing.T) {
require.NotEmpty(t, DefaultModels)
require.Equal(t, "gpt-5.6-sol", DefaultModels[0].ID)
+17 -1
View File
@@ -431,6 +431,20 @@ func (s *BillingService) initFallbackPricing() {
SupportsCacheBreakdown: false,
}
s.fallbackPrices["gpt-6-astra"] = &ModelPricing{
InputPricePerToken: 10e-6,
InputPricePerTokenPriority: 20e-6,
OutputPricePerToken: 50e-6,
OutputPricePerTokenPriority: 100e-6,
CacheCreationPricePerToken: 12.5e-6,
CacheCreationPricePerTokenPriority: 25e-6,
CacheReadPricePerToken: 1e-6,
CacheReadPricePerTokenPriority: 2e-6,
LongContextInputThreshold: 272_000,
LongContextInputMultiplier: 2,
LongContextOutputMultiplier: 1.5,
}
// OpenAI GPT-5.6 官方价格(USD/token)。缓存写入为输入价的 1.25 倍。
s.fallbackPrices["gpt-5.6-sol"] = &ModelPricing{
InputPricePerToken: 5e-6,
@@ -976,6 +990,8 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
// OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。
if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" {
switch normalized {
case "gpt-6-astra":
return s.fallbackPrices["gpt-6-astra"]
case "gpt-5.6-sol":
return s.fallbackPrices["gpt-5.6-sol"]
case "gpt-5.6-terra":
@@ -1653,7 +1669,7 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing
// 档的模型(如 gpt-5.5-pro、gpt-5.4-mini/nano)返回 0。
func openAIModelFastPricingRatio(normalized string) float64 {
switch normalized {
case "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna":
case "gpt-6-astra", "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna":
return 2.0
case "gpt-5.5":
return 2.5
@@ -311,6 +311,7 @@ const (
configuredCodexGrokContext = 500_000
configuredCodexGrokBuildContext = 256_000
configuredCodexGPT56MaxContext = 872_000
configuredCodexGPT6AstraContext = 1_050_000
configuredCodexToolOutputMaxTokens = 10_000
)
@@ -490,6 +491,10 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr
if isOpenAIGPT56Model(modelID) {
descriptor.MaxContextWindow = configuredCodexGPT56MaxContext
}
if isOpenAIGPT6AstraModel(modelID) {
descriptor.ContextWindow = configuredCodexGPT6AstraContext
descriptor.MaxContextWindow = configuredCodexGPT6AstraContext
}
}
if SupportsVerbosity(modelID) {
defaultVerbosity := "low"
@@ -503,6 +508,9 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr
func configuredCodexSupportsPriorityServiceTier(modelID string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
if isOpenAIGPT6AstraModel(normalized) {
return true
}
for _, family := range []string{"gpt-5.4", "gpt-5.5", "gpt-5.6"} {
if normalized == family || strings.HasPrefix(normalized, family+"-") {
return true
@@ -567,7 +575,7 @@ func configuredCodexGPTReasoningLevels(modelID string) []configuredCodexReasonin
{Effort: "xhigh", Description: "Extra-high reasoning depth for difficult tasks"},
}
normalized := getNormalizedCodexModel(modelID)
if isOpenAIGPT56Model(modelID) {
if isOpenAIGPT6AstraModel(modelID) || isOpenAIGPT56Model(modelID) {
levels = append(levels, configuredCodexReasoningLevel{
Effort: "max",
Description: "Maximum reasoning depth for complex tasks",
@@ -592,12 +600,13 @@ func isOpenAICodexGPTModel(modelID string) bool {
func isOpenAICodexReasoningGPTModel(modelID string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
return strings.HasPrefix(normalized, "gpt-5")
return isOpenAIGPT6AstraModel(normalized) || strings.HasPrefix(normalized, "gpt-5")
}
func isOpenAICodexImageInputModel(modelID string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
return strings.HasPrefix(normalized, "gpt-5") ||
return isOpenAIGPT6AstraModel(normalized) ||
strings.HasPrefix(normalized, "gpt-5") ||
strings.HasPrefix(normalized, "gpt-4o") ||
strings.HasPrefix(normalized, "gpt-4.1") ||
strings.HasPrefix(normalized, "gpt-4.5") ||
@@ -1834,6 +1843,7 @@ func CodexModelsManifestETag(body []byte) string {
}
var apiKeyCodexModelsWithoutResponsesLite = map[string]struct{}{
"gpt-6-astra": {},
"gpt-5.6-sol": {},
"gpt-5.6-terra": {},
"gpt-5.6-luna": {},
@@ -349,6 +349,25 @@ func TestNewConfiguredCodexModelDescriptorUsesProviderMetadataAndSafeFallback(t
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(gpt56Luna.SupportedReasoningLevels))
require.Equal(t, "medium", *gpt56Luna.DefaultReasoningLevel)
astra := newConfiguredCodexModelDescriptor("gpt-6-astra")
require.Equal(t, "GPT-6 Astra", astra.DisplayName)
require.NotNil(t, astra.DefaultReasoningLevel)
require.Equal(t, "medium", *astra.DefaultReasoningLevel)
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(astra.SupportedReasoningLevels))
require.NotContains(t, astra.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "none"})
require.NotContains(t, astra.SupportedReasoningLevels, configuredCodexReasoningLevel{Effort: "minimal"})
require.Len(t, astra.ServiceTiers, 1)
require.Equal(t, "priority", astra.ServiceTiers[0].ID)
require.True(t, astra.SupportsParallelToolCalls)
require.True(t, astra.SupportVerbosity)
require.Equal(t, int64(1_050_000), astra.ContextWindow)
require.Equal(t, int64(1_050_000), astra.MaxContextWindow)
require.Equal(t, configuredCodexTruncationPolicy{Mode: "tokens", Limit: 10_000}, astra.TruncationPolicy)
gpt6 := newConfiguredCodexModelDescriptor("gpt-6")
require.Equal(t, "GPT-6 (Astra)", gpt6.DisplayName)
require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(gpt6.SupportedReasoningLevels))
require.Equal(t, int64(1_050_000), gpt6.ContextWindow)
gpt55 := newConfiguredCodexModelDescriptor("gpt-5.5")
require.Equal(t, "GPT-5.5", gpt55.DisplayName)
require.NotNil(t, gpt55.DefaultReasoningLevel)
@@ -2129,12 +2148,12 @@ func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTIma
t.Parallel()
svc := &OpenAIGatewayService{}
manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o"},{"slug":"gpt-3.5-turbo"},{"slug":"gpt-4"}]}`)}
manifest := &CodexModelsManifest{Body: []byte(`{"models":[{"slug":"gpt-6-astra"},{"slug":"gpt-5.6-sol"},{"slug":"gpt-4o"},{"slug":"gpt-3.5-turbo"},{"slug":"gpt-4"}]}`)}
account := newCodexModelsAPIKeyTestAccount("")
require.NoError(t, svc.CompleteAPIKeyCodexModelsManifestForClient(manifest, account))
models := decodeCodexManifestModels(t, manifest.Body)
require.Len(t, models, 4)
require.Len(t, models, 5)
bySlug := make(map[string]map[string]any, len(models))
for _, model := range models {
@@ -2142,7 +2161,7 @@ func TestCompleteAPIKeyCodexModelsManifestForClientMarksOnlyOfficialVisionGPTIma
require.True(t, ok)
bySlug[slug] = model
}
for _, slug := range []string{"gpt-5.6-sol", "gpt-4o"} {
for _, slug := range []string{"gpt-6-astra", "gpt-5.6-sol", "gpt-4o"} {
require.Equal(t, []any{"text", "image"}, bySlug[slug]["input_modalities"])
require.Equal(t, true, bySlug[slug]["supports_image_detail_original"])
}
@@ -2173,8 +2192,8 @@ func TestAdjustAPIKeyCodexModelsManifest(t *testing.T) {
}{
{
name: "affected models disable responses lite and preserve unknown fields",
body: `{"models":[{"slug":"gpt-5.6-sol","use_responses_lite":true,"unknown_model":{"enabled":true}},{"slug":"gpt-5.6-terra","use_responses_lite":true},{"slug":"gpt-5.6-luna","use_responses_lite":true}],"unknown_top":{"version":1}}`,
want: `{"models":[{"slug":"gpt-5.6-sol","unknown_model":{"enabled":true},"use_responses_lite":false},{"slug":"gpt-5.6-terra","use_responses_lite":false},{"slug":"gpt-5.6-luna","use_responses_lite":false}],"unknown_top":{"version":1}}`,
body: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":true},{"slug":"gpt-5.6-sol","use_responses_lite":true,"unknown_model":{"enabled":true}},{"slug":"gpt-5.6-terra","use_responses_lite":true},{"slug":"gpt-5.6-luna","use_responses_lite":true}],"unknown_top":{"version":1}}`,
want: `{"models":[{"slug":"gpt-6-astra","use_responses_lite":false},{"slug":"gpt-5.6-sol","unknown_model":{"enabled":true},"use_responses_lite":false},{"slug":"gpt-5.6-terra","use_responses_lite":false},{"slug":"gpt-5.6-luna","use_responses_lite":false}],"unknown_top":{"version":1}}`,
},
{
name: "unaffected model unchanged",
@@ -11,6 +11,7 @@ import (
)
var codexModelMap = map[string]string{
"gpt-6-astra": "gpt-6-astra",
"gpt-5.6-sol": "gpt-5.6-sol",
"gpt-5.6-terra": "gpt-5.6-terra",
"gpt-5.6-luna": "gpt-5.6-luna",
@@ -1361,6 +1361,10 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
func TestNormalizeCodexModel_Gpt53(t *testing.T) {
cases := map[string]string{
"gpt-6-astra": "gpt-6-astra",
"openai/gpt-6-astra": "gpt-6-astra",
"gpt-6": "gpt-6-astra",
"openai/gpt-6": "gpt-6-astra",
"gpt-5.4": "gpt-5.4",
"gpt5.5": "gpt-5.5",
"openai/gpt5.5": "gpt-5.5",
@@ -2162,7 +2162,7 @@ func normalizeOpenAIReasoningEffortForModel(raw, model string) string {
// supportsOpenAIReasoningEffortMax reports model families whose upstream scale
// has a distinct max level. Other models keep the legacy max -> xhigh behavior.
func supportsOpenAIReasoningEffortMax(model string) bool {
if isOpenAIGPT56Model(model) {
if isOpenAIGPT6AstraModel(model) || isOpenAIGPT56Model(model) {
return true
}
@@ -14,13 +14,14 @@ import (
"github.com/tidwall/gjson"
)
func TestNormalizeOpenAIReasoningEffortForGPT56(t *testing.T) {
func TestNormalizeOpenAIReasoningEffortForMaxCapableModels(t *testing.T) {
tests := []struct {
name string
raw string
model string
want string
}{
{name: "Astra 保留 max", raw: "max", model: "gpt-6-astra", want: "max"},
{name: "Sol 保留 max", raw: "max", model: "gpt-5.6-sol", want: "max"},
{name: "Terra 保留 max", raw: "max", model: "openai/gpt-5.6-terra", want: "max"},
{name: "Luna 后缀保留 max", raw: "max", model: "gpt-5.6-luna-2026-07-09", want: "max"},
@@ -65,6 +65,8 @@ func normalizeKnownOpenAICodexModel(model string) string {
}
switch {
case isOpenAIGPT6AstraModel(normalized):
return "gpt-6-astra"
case strings.Contains(normalized, "gpt-5.6-sol"):
return "gpt-5.6-sol"
case strings.Contains(normalized, "gpt-5.6-terra"):
@@ -106,6 +108,11 @@ func normalizeKnownOpenAICodexModel(model string) string {
}
}
func isOpenAIGPT6AstraModel(model string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(model)
return normalized == "gpt-6" || normalized == "gpt-6-astra"
}
// isOpenAIGPT56Model 判断是否 GPT-5.6 系列模型;入参可为原始模型名
// (含大小写/路径/后缀变体)或已归一化的基名,两者均能正确识别。
func isOpenAIGPT56Model(model string) bool {
@@ -6,6 +6,12 @@ import (
"github.com/stretchr/testify/require"
)
func TestNormalizeKnownOpenAICodexModelGPT6Astra(t *testing.T) {
for _, model := range []string{"gpt-6-astra", "openai/gpt-6-astra", "gpt-6", "openai/gpt-6"} {
require.Equal(t, "gpt-6-astra", normalizeKnownOpenAICodexModel(model))
}
}
func TestNormalizeKnownOpenAICodexModel_BareGPT56RoutesToSol(t *testing.T) {
tests := map[string]string{
"gpt-5.6": "gpt-5.6-sol",
@@ -44,6 +44,23 @@ var (
Mode: "chat",
SupportsPromptCaching: true,
}
openAIGPT6AstraFallbackPricing = &LiteLLMModelPricing{
InputCostPerToken: 1e-05,
InputCostPerTokenPriority: 2e-05,
OutputCostPerToken: 5e-05,
OutputCostPerTokenPriority: 1e-04,
CacheCreationInputTokenCost: 1.25e-05,
CacheCreationInputTokenCostPriority: 2.5e-05,
CacheReadInputTokenCost: 1e-06,
CacheReadInputTokenCostPriority: 2e-06,
LongContextInputTokenThreshold: 272_000,
LongContextInputCostMultiplier: 2,
LongContextOutputCostMultiplier: 1.5,
SupportsServiceTier: true,
LiteLLMProvider: "openai",
Mode: "chat",
SupportsPromptCaching: true,
}
openAIGPT56SolFallbackPricing = &LiteLLMModelPricing{
InputCostPerToken: 5e-06,
InputCostPerTokenPriority: 1e-05,
@@ -1100,6 +1117,9 @@ func normalizeModelNameForPricing(model string) string {
model = strings.TrimLeft(model, "/")
if canonical := canonicalizeOpenAIModelAliasSpelling(model); canonical != "" {
if canonical == "gpt-6" {
return "gpt-6-astra"
}
if canonical == "gpt-5.6" {
return "gpt-5.6-sol"
}
@@ -1300,6 +1320,12 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
}
}
if isOpenAIGPT6AstraModel(model) {
logger.With(zap.String("component", "service.pricing")).
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-6-astra(static)"))
return openAIGPT6AstraFallbackPricing
}
if strings.HasPrefix(model, "gpt-5.6-sol") {
logger.With(zap.String("component", "service.pricing")).
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.6-sol(static)"))
@@ -77,6 +77,95 @@ func TestParsePricingData_ParsesPriorityAndServiceTierFields(t *testing.T) {
require.True(t, pricing.SupportsServiceTier)
}
const gpt6AstraCatalogJSON = `{
"gpt-6-astra": {
"litellm_provider": "openai",
"mode": "chat",
"input_cost_per_token": 1e-05,
"input_cost_per_token_priority": 2e-05,
"output_cost_per_token": 5e-05,
"output_cost_per_token_priority": 1e-04,
"cache_creation_input_token_cost": 1.25e-05,
"cache_creation_input_token_cost_priority": 2.5e-05,
"cache_read_input_token_cost": 1e-06,
"cache_read_input_token_cost_priority": 2e-06,
"input_cost_per_token_above_272k_tokens": 2e-05,
"output_cost_per_token_above_272k_tokens": 7.5e-05,
"cache_creation_input_token_cost_above_272k_tokens": 2.5e-05,
"cache_read_input_token_cost_above_272k_tokens": 2e-06
}
}`
func TestBillingServiceGPT6AstraUsesOfficialPricingAcrossTiersAndLongContext(t *testing.T) {
svc := NewBillingService(&config.Config{}, newStubPricingServiceFromJSON(t, gpt6AstraCatalogJSON))
boundaryTokens := UsageTokens{InputTokens: 100_000, CacheCreationTokens: 100_000, CacheReadTokens: 72_000, OutputTokens: 10}
boundary, err := svc.CalculateCost("gpt-6-astra", boundaryTokens, 1)
require.NoError(t, err)
require.False(t, boundary.LongContextBillingApplied)
require.InDelta(t, 100_000*10e-6, boundary.InputCost, 1e-12)
require.InDelta(t, 100_000*12.5e-6, boundary.CacheCreationCost, 1e-12)
require.InDelta(t, 72_000*1e-6, boundary.CacheReadCost, 1e-12)
require.InDelta(t, 10*50e-6, boundary.OutputCost, 1e-12)
tokens := UsageTokens{InputTokens: 100_000, CacheCreationTokens: 100_000, CacheReadTokens: 73_000, OutputTokens: 10}
tiers := []struct {
name string
serviceTier string
priceScale float64
}{
{name: "standard", priceScale: 1},
{name: "fast", serviceTier: "priority", priceScale: 2},
{name: "flex", serviceTier: "flex", priceScale: 0.5},
}
for _, tier := range tiers {
t.Run(tier.name, func(t *testing.T) {
cost, err := svc.CalculateCostWithServiceTier("gpt-6-astra", tokens, 1, tier.serviceTier)
require.NoError(t, err)
require.True(t, cost.LongContextBillingApplied)
require.InDelta(t, 100_000*10e-6*tier.priceScale*2, cost.InputCost, 1e-12)
require.InDelta(t, 100_000*12.5e-6*tier.priceScale*2, cost.CacheCreationCost, 1e-12)
require.InDelta(t, 73_000*1e-6*tier.priceScale*2, cost.CacheReadCost, 1e-12)
require.InDelta(t, 10*50e-6*tier.priceScale*1.5, cost.OutputCost, 1e-12)
})
}
}
func TestGPT6AstraDedicatedFallbacksUseOfficialRates(t *testing.T) {
tests := []struct {
name string
svc *BillingService
}{
{name: "pricing_service", svc: NewBillingService(&config.Config{}, &PricingService{pricingData: map[string]*LiteLLMModelPricing{}})},
{name: "billing_service", svc: NewBillingService(&config.Config{}, nil)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pricing, err := tt.svc.GetModelPricing("gpt-6-astra")
require.NoError(t, err)
require.InDelta(t, 10e-6, pricing.InputPricePerToken, 1e-12)
require.InDelta(t, 20e-6, pricing.InputPricePerTokenPriority, 1e-12)
require.InDelta(t, 50e-6, pricing.OutputPricePerToken, 1e-12)
require.InDelta(t, 100e-6, pricing.OutputPricePerTokenPriority, 1e-12)
require.InDelta(t, 12.5e-6, pricing.CacheCreationPricePerToken, 1e-12)
require.InDelta(t, 25e-6, pricing.CacheCreationPricePerTokenPriority, 1e-12)
require.InDelta(t, 1e-6, pricing.CacheReadPricePerToken, 1e-12)
require.InDelta(t, 2e-6, pricing.CacheReadPricePerTokenPriority, 1e-12)
require.Equal(t, 272_000, pricing.LongContextInputThreshold)
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
})
}
}
func TestPricingServiceBareGPT6AliasUsesAstra(t *testing.T) {
astraPricing := &LiteLLMModelPricing{InputCostPerToken: 123e-6, OutputCostPerToken: 456e-6}
pricingSvc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{"gpt-6-astra": astraPricing}}
for _, model := range []string{"gpt-6", "openai/gpt-6"} {
pricing := pricingSvc.GetModelPricing(model)
require.Same(t, astraPricing, pricing)
}
}
func TestBillingService_GPT56CacheWritePricingUsesOfficialMultiplier(t *testing.T) {
tests := []struct {
model string