feat(openai): 增加 gpt-5.4 模型支持与定价配置
- 接入 gpt-5.4 模型识别与规范化,补充默认模型列表 - 增加 gpt-5.4 输入/缓存命中/输出价格与计费兜底逻辑 - 同步前端模型白名单与 OpenCode 上下文窗口(1050000/128000) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 924476dcac6181cd0f3ee731ec7b73672ff03793)
This commit is contained in:
@@ -15,6 +15,7 @@ type Model struct {
|
||||
|
||||
// DefaultModels OpenAI models list
|
||||
var DefaultModels = []Model{
|
||||
{ID: "gpt-5.4", Object: "model", Created: 1738368000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.4"},
|
||||
{ID: "gpt-5.3-codex", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex"},
|
||||
{ID: "gpt-5.3-codex-spark", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex Spark"},
|
||||
{ID: "gpt-5.2", Object: "model", Created: 1733875200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.2"},
|
||||
|
||||
@@ -161,6 +161,32 @@ func (s *BillingService) initFallbackPricing() {
|
||||
CacheReadPricePerToken: 0.2e-6, // $0.20 per MTok
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
|
||||
// OpenAI GPT-5.1(本地兜底,防止动态定价不可用时拒绝计费)
|
||||
s.fallbackPrices["gpt-5.1"] = &ModelPricing{
|
||||
InputPricePerToken: 1.25e-6, // $1.25 per MTok
|
||||
OutputPricePerToken: 10e-6, // $10 per MTok
|
||||
CacheCreationPricePerToken: 1.25e-6, // $1.25 per MTok
|
||||
CacheReadPricePerToken: 0.125e-6,
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
// OpenAI GPT-5.4(业务指定价格)
|
||||
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
|
||||
InputPricePerToken: 2.5e-6, // $2.5 per MTok
|
||||
OutputPricePerToken: 15e-6, // $15 per MTok
|
||||
CacheCreationPricePerToken: 2.5e-6, // $2.5 per MTok
|
||||
CacheReadPricePerToken: 0.25e-6, // $0.25 per MTok
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
// Codex 族兜底统一按 GPT-5.1 Codex 价格计费
|
||||
s.fallbackPrices["gpt-5.1-codex"] = &ModelPricing{
|
||||
InputPricePerToken: 1.5e-6, // $1.5 per MTok
|
||||
OutputPricePerToken: 12e-6, // $12 per MTok
|
||||
CacheCreationPricePerToken: 1.5e-6, // $1.5 per MTok
|
||||
CacheReadPricePerToken: 0.15e-6,
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
s.fallbackPrices["gpt-5.3-codex"] = s.fallbackPrices["gpt-5.1-codex"]
|
||||
}
|
||||
|
||||
// getFallbackPricing 根据模型系列获取回退价格
|
||||
@@ -189,12 +215,30 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
||||
}
|
||||
return s.fallbackPrices["claude-3-haiku"]
|
||||
}
|
||||
// Claude 未知型号统一回退到 Sonnet,避免计费中断。
|
||||
if strings.Contains(modelLower, "claude") {
|
||||
return s.fallbackPrices["claude-sonnet-4"]
|
||||
}
|
||||
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
|
||||
return s.fallbackPrices["gemini-3.1-pro"]
|
||||
}
|
||||
|
||||
// 默认使用Sonnet价格
|
||||
return s.fallbackPrices["claude-sonnet-4"]
|
||||
// OpenAI 仅匹配已知 GPT-5/Codex 族,避免未知 OpenAI 型号误计价。
|
||||
if strings.Contains(modelLower, "gpt-5") || strings.Contains(modelLower, "codex") {
|
||||
normalized := normalizeCodexModel(modelLower)
|
||||
switch normalized {
|
||||
case "gpt-5.4":
|
||||
return s.fallbackPrices["gpt-5.4"]
|
||||
case "gpt-5.3-codex":
|
||||
return s.fallbackPrices["gpt-5.3-codex"]
|
||||
case "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", "codex-mini-latest":
|
||||
return s.fallbackPrices["gpt-5.1-codex"]
|
||||
case "gpt-5.1":
|
||||
return s.fallbackPrices["gpt-5.1"]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModelPricing 获取模型价格配置
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestGetModelPricing_CaseInsensitive(t *testing.T) {
|
||||
require.Equal(t, p1.InputPricePerToken, p2.InputPricePerToken)
|
||||
}
|
||||
|
||||
func TestGetModelPricing_UnknownModelFallsBackToSonnet(t *testing.T) {
|
||||
func TestGetModelPricing_UnknownClaudeModelFallsBackToSonnet(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
// 不包含 opus/sonnet/haiku 关键词的 Claude 模型会走默认 Sonnet 价格
|
||||
@@ -142,6 +142,71 @@ func TestGetModelPricing_UnknownModelFallsBackToSonnet(t *testing.T) {
|
||||
require.InDelta(t, 3e-6, pricing.InputPricePerToken, 1e-12)
|
||||
}
|
||||
|
||||
func TestGetModelPricing_UnknownOpenAIModelReturnsError(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
pricing, err := svc.GetModelPricing("gpt-unknown-model")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, pricing)
|
||||
require.Contains(t, err.Error(), "pricing not found")
|
||||
}
|
||||
|
||||
func TestGetModelPricing_OpenAIGPT51Fallback(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
pricing, err := svc.GetModelPricing("gpt-5.1")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, 1.25e-6, pricing.InputPricePerToken, 1e-12)
|
||||
}
|
||||
|
||||
func TestGetModelPricing_OpenAIGPT54Fallback(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
pricing, err := svc.GetModelPricing("gpt-5.4")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, 2.5e-6, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 15e-6, pricing.OutputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 0.25e-6, pricing.CacheReadPricePerToken, 1e-12)
|
||||
}
|
||||
|
||||
func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
expectedInput float64
|
||||
expectNilPricing bool
|
||||
}{
|
||||
{name: "empty model", model: " ", expectNilPricing: true},
|
||||
{name: "claude opus 4.6", model: "claude-opus-4.6-20260201", expectedInput: 5e-6},
|
||||
{name: "claude opus 4.5 alt separator", model: "claude-opus-4-5-20260101", expectedInput: 5e-6},
|
||||
{name: "claude generic model fallback sonnet", model: "claude-foo-bar", expectedInput: 3e-6},
|
||||
{name: "gemini explicit fallback", model: "gemini-3-1-pro", expectedInput: 2e-6},
|
||||
{name: "gemini unknown no fallback", model: "gemini-2.0-pro", expectNilPricing: true},
|
||||
{name: "openai gpt5.1", model: "gpt-5.1", expectedInput: 1.25e-6},
|
||||
{name: "openai gpt5.4", model: "gpt-5.4", expectedInput: 2.5e-6},
|
||||
{name: "openai gpt5.3 codex", model: "gpt-5.3-codex", expectedInput: 1.5e-6},
|
||||
{name: "openai gpt5.1 codex max alias", model: "gpt-5.1-codex-max", expectedInput: 1.5e-6},
|
||||
{name: "openai codex mini latest alias", model: "codex-mini-latest", expectedInput: 1.5e-6},
|
||||
{name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true},
|
||||
{name: "non supported family", model: "qwen-max", expectNilPricing: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pricing := svc.getFallbackPricing(tt.model)
|
||||
if tt.expectNilPricing {
|
||||
require.Nil(t, pricing)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, pricing)
|
||||
require.InDelta(t, tt.expectedInput, pricing.InputPricePerToken, 1e-12)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestCalculateCostWithLongContext_BelowThreshold(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ import (
|
||||
var codexCLIInstructions string
|
||||
|
||||
var codexModelMap = map[string]string{
|
||||
"gpt-5.4": "gpt-5.4",
|
||||
"gpt-5.4-none": "gpt-5.4",
|
||||
"gpt-5.4-low": "gpt-5.4",
|
||||
"gpt-5.4-medium": "gpt-5.4",
|
||||
"gpt-5.4-high": "gpt-5.4",
|
||||
"gpt-5.4-xhigh": "gpt-5.4",
|
||||
"gpt-5.4-chat-latest": "gpt-5.4",
|
||||
"gpt-5.3": "gpt-5.3-codex",
|
||||
"gpt-5.3-none": "gpt-5.3-codex",
|
||||
"gpt-5.3-low": "gpt-5.3-codex",
|
||||
@@ -154,6 +161,9 @@ func normalizeCodexModel(model string) string {
|
||||
|
||||
normalized := strings.ToLower(modelID)
|
||||
|
||||
if strings.Contains(normalized, "gpt-5.4") || strings.Contains(normalized, "gpt 5.4") {
|
||||
return "gpt-5.4"
|
||||
}
|
||||
if strings.Contains(normalized, "gpt-5.2-codex") || strings.Contains(normalized, "gpt 5.2 codex") {
|
||||
return "gpt-5.2-codex"
|
||||
}
|
||||
|
||||
@@ -167,6 +167,10 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
|
||||
|
||||
func TestNormalizeCodexModel_Gpt53(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"gpt-5.4": "gpt-5.4",
|
||||
"gpt-5.4-high": "gpt-5.4",
|
||||
"gpt-5.4-chat-latest": "gpt-5.4",
|
||||
"gpt 5.4": "gpt-5.4",
|
||||
"gpt-5.3": "gpt-5.3-codex",
|
||||
"gpt-5.3-codex": "gpt-5.3-codex",
|
||||
"gpt-5.3-codex-xhigh": "gpt-5.3-codex",
|
||||
|
||||
@@ -21,8 +21,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
openAIModelDatePattern = regexp.MustCompile(`-\d{8}$`)
|
||||
openAIModelBasePattern = regexp.MustCompile(`^(gpt-\d+(?:\.\d+)?)(?:-|$)`)
|
||||
openAIModelDatePattern = regexp.MustCompile(`-\d{8}$`)
|
||||
openAIModelBasePattern = regexp.MustCompile(`^(gpt-\d+(?:\.\d+)?)(?:-|$)`)
|
||||
openAIGPT54FallbackPricing = &LiteLLMModelPricing{
|
||||
InputCostPerToken: 2.5e-06, // $2.5 per MTok
|
||||
OutputCostPerToken: 1.5e-05, // $15 per MTok
|
||||
CacheReadInputTokenCost: 2.5e-07, // $0.25 per MTok
|
||||
LiteLLMProvider: "openai",
|
||||
Mode: "chat",
|
||||
SupportsPromptCaching: true,
|
||||
}
|
||||
)
|
||||
|
||||
// LiteLLMModelPricing LiteLLM价格数据结构
|
||||
@@ -660,7 +668,8 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
|
||||
// 2. gpt-5.2-codex -> gpt-5.2(去掉后缀如 -codex, -mini, -max 等)
|
||||
// 3. gpt-5.2-20251222 -> gpt-5.2(去掉日期版本号)
|
||||
// 4. gpt-5.3-codex -> gpt-5.2-codex
|
||||
// 5. 最终回退到 DefaultTestModel (gpt-5.1-codex)
|
||||
// 5. gpt-5.4* -> 业务静态兜底价
|
||||
// 6. 最终回退到 DefaultTestModel (gpt-5.1-codex)
|
||||
func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
||||
if strings.HasPrefix(model, "gpt-5.3-codex-spark") {
|
||||
if pricing, ok := s.pricingData["gpt-5.1-codex"]; ok {
|
||||
@@ -690,6 +699,12 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(model, "gpt-5.4") {
|
||||
logger.With(zap.String("component", "service.pricing")).
|
||||
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4(static)"))
|
||||
return openAIGPT54FallbackPricing
|
||||
}
|
||||
|
||||
// 最终回退到 DefaultTestModel
|
||||
defaultModel := strings.ToLower(openai.DefaultTestModel)
|
||||
if pricing, ok := s.pricingData[defaultModel]; ok {
|
||||
|
||||
@@ -51,3 +51,17 @@ func TestGetModelPricing_OpenAIFallbackMatchedLoggedAsInfo(t *testing.T) {
|
||||
require.True(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "info"))
|
||||
require.False(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "warn"))
|
||||
}
|
||||
|
||||
func TestGetModelPricing_Gpt54UsesStaticFallbackWhenRemoteMissing(t *testing.T) {
|
||||
svc := &PricingService{
|
||||
pricingData: map[string]*LiteLLMModelPricing{
|
||||
"gpt-5.1-codex": &LiteLLMModelPricing{InputCostPerToken: 1.25e-6},
|
||||
},
|
||||
}
|
||||
|
||||
got := svc.GetModelPricing("gpt-5.4")
|
||||
require.NotNil(t, got)
|
||||
require.InDelta(t, 2.5e-6, got.InputCostPerToken, 1e-12)
|
||||
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
|
||||
require.InDelta(t, 2.5e-7, got.CacheReadInputTokenCost, 1e-12)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user