Skip to content

Commit a225a24

Browse files
authored
Merge pull request Wei-Shaw#1162 from remxcode/main
feat(openai): 增加 gpt-5.4-mini/nano 模型支持与定价配置
2 parents 553a486 + 578608d commit a225a24

12 files changed

Lines changed: 310 additions & 1 deletion

File tree

backend/internal/pkg/openai/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type Model struct {
1616
// DefaultModels OpenAI models list
1717
var DefaultModels = []Model{
1818
{ID: "gpt-5.4", Object: "model", Created: 1738368000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.4"},
19+
{ID: "gpt-5.4-mini", Object: "model", Created: 1738368000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.4 Mini"},
20+
{ID: "gpt-5.4-nano", Object: "model", Created: 1738368000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.4 Nano"},
1921
{ID: "gpt-5.3-codex", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex"},
2022
{ID: "gpt-5.3-codex-spark", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex Spark"},
2123
{ID: "gpt-5.2", Object: "model", Created: 1733875200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.2"},

backend/internal/service/billing_service.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ func (s *BillingService) initFallbackPricing() {
221221
LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier,
222222
LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier,
223223
}
224+
s.fallbackPrices["gpt-5.4-mini"] = &ModelPricing{
225+
InputPricePerToken: 7.5e-7,
226+
OutputPricePerToken: 4.5e-6,
227+
CacheReadPricePerToken: 7.5e-8,
228+
SupportsCacheBreakdown: false,
229+
}
230+
s.fallbackPrices["gpt-5.4-nano"] = &ModelPricing{
231+
InputPricePerToken: 2e-7,
232+
OutputPricePerToken: 1.25e-6,
233+
CacheReadPricePerToken: 2e-8,
234+
SupportsCacheBreakdown: false,
235+
}
224236
// OpenAI GPT-5.2(本地兜底)
225237
s.fallbackPrices["gpt-5.2"] = &ModelPricing{
226238
InputPricePerToken: 1.75e-6,
@@ -294,6 +306,10 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
294306
if strings.Contains(modelLower, "gpt-5") || strings.Contains(modelLower, "codex") {
295307
normalized := normalizeCodexModel(modelLower)
296308
switch normalized {
309+
case "gpt-5.4-mini":
310+
return s.fallbackPrices["gpt-5.4-mini"]
311+
case "gpt-5.4-nano":
312+
return s.fallbackPrices["gpt-5.4-nano"]
297313
case "gpt-5.4":
298314
return s.fallbackPrices["gpt-5.4"]
299315
case "gpt-5.2":

backend/internal/service/billing_service_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ func TestGetModelPricing_OpenAIGPT54Fallback(t *testing.T) {
174174
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
175175
}
176176

177+
func TestGetModelPricing_OpenAIGPT54MiniFallback(t *testing.T) {
178+
svc := newTestBillingService()
179+
180+
pricing, err := svc.GetModelPricing("gpt-5.4-mini")
181+
require.NoError(t, err)
182+
require.NotNil(t, pricing)
183+
require.InDelta(t, 7.5e-7, pricing.InputPricePerToken, 1e-12)
184+
require.InDelta(t, 4.5e-6, pricing.OutputPricePerToken, 1e-12)
185+
require.InDelta(t, 7.5e-8, pricing.CacheReadPricePerToken, 1e-12)
186+
require.Zero(t, pricing.LongContextInputThreshold)
187+
}
188+
189+
func TestGetModelPricing_OpenAIGPT54NanoFallback(t *testing.T) {
190+
svc := newTestBillingService()
191+
192+
pricing, err := svc.GetModelPricing("gpt-5.4-nano")
193+
require.NoError(t, err)
194+
require.NotNil(t, pricing)
195+
require.InDelta(t, 2e-7, pricing.InputPricePerToken, 1e-12)
196+
require.InDelta(t, 1.25e-6, pricing.OutputPricePerToken, 1e-12)
197+
require.InDelta(t, 2e-8, pricing.CacheReadPricePerToken, 1e-12)
198+
require.Zero(t, pricing.LongContextInputThreshold)
199+
}
200+
177201
func TestCalculateCost_OpenAIGPT54LongContextAppliesWholeSessionMultipliers(t *testing.T) {
178202
svc := newTestBillingService()
179203

@@ -210,6 +234,8 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
210234
{name: "gemini unknown no fallback", model: "gemini-2.0-pro", expectNilPricing: true},
211235
{name: "openai gpt5.1", model: "gpt-5.1", expectedInput: 1.25e-6},
212236
{name: "openai gpt5.4", model: "gpt-5.4", expectedInput: 2.5e-6},
237+
{name: "openai gpt5.4 mini", model: "gpt-5.4-mini", expectedInput: 7.5e-7},
238+
{name: "openai gpt5.4 nano", model: "gpt-5.4-nano", expectedInput: 2e-7},
213239
{name: "openai gpt5.3 codex", model: "gpt-5.3-codex", expectedInput: 1.5e-6},
214240
{name: "openai gpt5.1 codex max alias", model: "gpt-5.1-codex-max", expectedInput: 1.5e-6},
215241
{name: "openai codex mini latest alias", model: "codex-mini-latest", expectedInput: 1.5e-6},
@@ -564,6 +590,40 @@ func TestCalculateCostWithServiceTier_FlexAppliesHalfMultiplier(t *testing.T) {
564590
require.InDelta(t, baseCost.TotalCost*0.5, flexCost.TotalCost, 1e-10)
565591
}
566592

593+
func TestCalculateCostWithServiceTier_Gpt54MiniPriorityFallsBackToTierMultiplier(t *testing.T) {
594+
svc := newTestBillingService()
595+
tokens := UsageTokens{InputTokens: 120, OutputTokens: 30, CacheCreationTokens: 12, CacheReadTokens: 8}
596+
597+
baseCost, err := svc.CalculateCost("gpt-5.4-mini", tokens, 1.0)
598+
require.NoError(t, err)
599+
600+
priorityCost, err := svc.CalculateCostWithServiceTier("gpt-5.4-mini", tokens, 1.0, "priority")
601+
require.NoError(t, err)
602+
603+
require.InDelta(t, baseCost.InputCost*2, priorityCost.InputCost, 1e-10)
604+
require.InDelta(t, baseCost.OutputCost*2, priorityCost.OutputCost, 1e-10)
605+
require.InDelta(t, baseCost.CacheCreationCost*2, priorityCost.CacheCreationCost, 1e-10)
606+
require.InDelta(t, baseCost.CacheReadCost*2, priorityCost.CacheReadCost, 1e-10)
607+
require.InDelta(t, baseCost.TotalCost*2, priorityCost.TotalCost, 1e-10)
608+
}
609+
610+
func TestCalculateCostWithServiceTier_Gpt54NanoFlexAppliesHalfMultiplier(t *testing.T) {
611+
svc := newTestBillingService()
612+
tokens := UsageTokens{InputTokens: 100, OutputTokens: 50, CacheCreationTokens: 40, CacheReadTokens: 20}
613+
614+
baseCost, err := svc.CalculateCost("gpt-5.4-nano", tokens, 1.0)
615+
require.NoError(t, err)
616+
617+
flexCost, err := svc.CalculateCostWithServiceTier("gpt-5.4-nano", tokens, 1.0, "flex")
618+
require.NoError(t, err)
619+
620+
require.InDelta(t, baseCost.InputCost*0.5, flexCost.InputCost, 1e-10)
621+
require.InDelta(t, baseCost.OutputCost*0.5, flexCost.OutputCost, 1e-10)
622+
require.InDelta(t, baseCost.CacheCreationCost*0.5, flexCost.CacheCreationCost, 1e-10)
623+
require.InDelta(t, baseCost.CacheReadCost*0.5, flexCost.CacheReadCost, 1e-10)
624+
require.InDelta(t, baseCost.TotalCost*0.5, flexCost.TotalCost, 1e-10)
625+
}
626+
567627
func TestCalculateCostWithServiceTier_PriorityFallsBackToTierMultiplierWithoutExplicitPriorityPrice(t *testing.T) {
568628
svc := newTestBillingService()
569629
tokens := UsageTokens{InputTokens: 120, OutputTokens: 30, CacheCreationTokens: 12, CacheReadTokens: 8}

backend/internal/service/openai_codex_transform.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
var codexModelMap = map[string]string{
99
"gpt-5.4": "gpt-5.4",
10+
"gpt-5.4-mini": "gpt-5.4-mini",
11+
"gpt-5.4-nano": "gpt-5.4-nano",
1012
"gpt-5.4-none": "gpt-5.4",
1113
"gpt-5.4-low": "gpt-5.4",
1214
"gpt-5.4-medium": "gpt-5.4",
@@ -225,6 +227,12 @@ func normalizeCodexModel(model string) string {
225227

226228
normalized := strings.ToLower(modelID)
227229

230+
if strings.Contains(normalized, "gpt-5.4-mini") || strings.Contains(normalized, "gpt 5.4 mini") {
231+
return "gpt-5.4-mini"
232+
}
233+
if strings.Contains(normalized, "gpt-5.4-nano") || strings.Contains(normalized, "gpt 5.4 nano") {
234+
return "gpt-5.4-nano"
235+
}
228236
if strings.Contains(normalized, "gpt-5.4") || strings.Contains(normalized, "gpt 5.4") {
229237
return "gpt-5.4"
230238
}

backend/internal/service/openai_codex_transform_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,10 @@ func TestNormalizeCodexModel_Gpt53(t *testing.T) {
238238
"gpt-5.4-high": "gpt-5.4",
239239
"gpt-5.4-chat-latest": "gpt-5.4",
240240
"gpt 5.4": "gpt-5.4",
241+
"gpt-5.4-mini": "gpt-5.4-mini",
242+
"gpt 5.4 mini": "gpt-5.4-mini",
243+
"gpt-5.4-nano": "gpt-5.4-nano",
244+
"gpt 5.4 nano": "gpt-5.4-nano",
241245
"gpt-5.3": "gpt-5.3-codex",
242246
"gpt-5.3-codex": "gpt-5.3-codex",
243247
"gpt-5.3-codex-xhigh": "gpt-5.3-codex",

backend/internal/service/pricing_service.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ var (
3434
Mode: "chat",
3535
SupportsPromptCaching: true,
3636
}
37+
openAIGPT54MiniFallbackPricing = &LiteLLMModelPricing{
38+
InputCostPerToken: 7.5e-07,
39+
OutputCostPerToken: 4.5e-06,
40+
CacheReadInputTokenCost: 7.5e-08,
41+
LiteLLMProvider: "openai",
42+
Mode: "chat",
43+
SupportsPromptCaching: true,
44+
}
45+
openAIGPT54NanoFallbackPricing = &LiteLLMModelPricing{
46+
InputCostPerToken: 2e-07,
47+
OutputCostPerToken: 1.25e-06,
48+
CacheReadInputTokenCost: 2e-08,
49+
LiteLLMProvider: "openai",
50+
Mode: "chat",
51+
SupportsPromptCaching: true,
52+
}
3753
)
3854

3955
// LiteLLMModelPricing LiteLLM价格数据结构
@@ -723,6 +739,18 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
723739
}
724740
}
725741

742+
if strings.HasPrefix(model, "gpt-5.4-mini") {
743+
logger.With(zap.String("component", "service.pricing")).
744+
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4-mini(static)"))
745+
return openAIGPT54MiniFallbackPricing
746+
}
747+
748+
if strings.HasPrefix(model, "gpt-5.4-nano") {
749+
logger.With(zap.String("component", "service.pricing")).
750+
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4-nano(static)"))
751+
return openAIGPT54NanoFallbackPricing
752+
}
753+
726754
if strings.HasPrefix(model, "gpt-5.4") {
727755
logger.With(zap.String("component", "service.pricing")).
728756
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4(static)"))

backend/internal/service/pricing_service_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ func TestGetModelPricing_Gpt54UsesStaticFallbackWhenRemoteMissing(t *testing.T)
9898
require.InDelta(t, 1.5, got.LongContextOutputCostMultiplier, 1e-12)
9999
}
100100

101+
func TestGetModelPricing_Gpt54MiniUsesDedicatedStaticFallbackWhenRemoteMissing(t *testing.T) {
102+
svc := &PricingService{
103+
pricingData: map[string]*LiteLLMModelPricing{
104+
"gpt-5.1-codex": {InputCostPerToken: 1.25e-6},
105+
},
106+
}
107+
108+
got := svc.GetModelPricing("gpt-5.4-mini")
109+
require.NotNil(t, got)
110+
require.InDelta(t, 7.5e-7, got.InputCostPerToken, 1e-12)
111+
require.InDelta(t, 4.5e-6, got.OutputCostPerToken, 1e-12)
112+
require.InDelta(t, 7.5e-8, got.CacheReadInputTokenCost, 1e-12)
113+
require.Zero(t, got.LongContextInputTokenThreshold)
114+
}
115+
116+
func TestGetModelPricing_Gpt54NanoUsesDedicatedStaticFallbackWhenRemoteMissing(t *testing.T) {
117+
svc := &PricingService{
118+
pricingData: map[string]*LiteLLMModelPricing{
119+
"gpt-5.1-codex": {InputCostPerToken: 1.25e-6},
120+
},
121+
}
122+
123+
got := svc.GetModelPricing("gpt-5.4-nano")
124+
require.NotNil(t, got)
125+
require.InDelta(t, 2e-7, got.InputCostPerToken, 1e-12)
126+
require.InDelta(t, 1.25e-6, got.OutputCostPerToken, 1e-12)
127+
require.InDelta(t, 2e-8, got.CacheReadInputTokenCost, 1e-12)
128+
require.Zero(t, got.LongContextInputTokenThreshold)
129+
}
130+
101131
func TestParsePricingData_PreservesPriorityAndServiceTierFields(t *testing.T) {
102132
raw := map[string]any{
103133
"gpt-5.4": map[string]any{

backend/resources/model-pricing/model_prices_and_context_window.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5173,6 +5173,71 @@
51735173
"supports_tool_choice": true,
51745174
"supports_vision": true
51755175
},
5176+
"gpt-5.4-mini": {
5177+
"cache_read_input_token_cost": 7.5e-08,
5178+
"input_cost_per_token": 7.5e-07,
5179+
"litellm_provider": "openai",
5180+
"max_input_tokens": 400000,
5181+
"max_output_tokens": 128000,
5182+
"max_tokens": 128000,
5183+
"mode": "chat",
5184+
"output_cost_per_token": 4.5e-06,
5185+
"supported_endpoints": [
5186+
"/v1/chat/completions",
5187+
"/v1/batch",
5188+
"/v1/responses"
5189+
],
5190+
"supported_modalities": [
5191+
"text",
5192+
"image"
5193+
],
5194+
"supported_output_modalities": [
5195+
"text"
5196+
],
5197+
"supports_function_calling": true,
5198+
"supports_native_streaming": true,
5199+
"supports_parallel_function_calling": true,
5200+
"supports_pdf_input": true,
5201+
"supports_prompt_caching": true,
5202+
"supports_reasoning": true,
5203+
"supports_response_schema": true,
5204+
"supports_service_tier": true,
5205+
"supports_system_messages": true,
5206+
"supports_tool_choice": true,
5207+
"supports_vision": true
5208+
},
5209+
"gpt-5.4-nano": {
5210+
"cache_read_input_token_cost": 2e-08,
5211+
"input_cost_per_token": 2e-07,
5212+
"litellm_provider": "openai",
5213+
"max_input_tokens": 400000,
5214+
"max_output_tokens": 128000,
5215+
"max_tokens": 128000,
5216+
"mode": "chat",
5217+
"output_cost_per_token": 1.25e-06,
5218+
"supported_endpoints": [
5219+
"/v1/chat/completions",
5220+
"/v1/batch",
5221+
"/v1/responses"
5222+
],
5223+
"supported_modalities": [
5224+
"text",
5225+
"image"
5226+
],
5227+
"supported_output_modalities": [
5228+
"text"
5229+
],
5230+
"supports_function_calling": true,
5231+
"supports_native_streaming": true,
5232+
"supports_parallel_function_calling": true,
5233+
"supports_pdf_input": true,
5234+
"supports_prompt_caching": true,
5235+
"supports_reasoning": true,
5236+
"supports_response_schema": true,
5237+
"supports_system_messages": true,
5238+
"supports_tool_choice": true,
5239+
"supports_vision": true
5240+
},
51765241
"gpt-5.3-codex": {
51775242
"cache_read_input_token_cost": 1.75e-07,
51785243
"cache_read_input_token_cost_priority": 3.5e-07,

frontend/src/components/keys/UseKeyModal.vue

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,38 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
709709
xhigh: {}
710710
}
711711
},
712+
'gpt-5.4-mini': {
713+
name: 'GPT-5.4 Mini',
714+
limit: {
715+
context: 400000,
716+
output: 128000
717+
},
718+
options: {
719+
store: false
720+
},
721+
variants: {
722+
low: {},
723+
medium: {},
724+
high: {},
725+
xhigh: {}
726+
}
727+
},
728+
'gpt-5.4-nano': {
729+
name: 'GPT-5.4 Nano',
730+
limit: {
731+
context: 400000,
732+
output: 128000
733+
},
734+
options: {
735+
store: false
736+
},
737+
variants: {
738+
low: {},
739+
medium: {},
740+
high: {},
741+
xhigh: {}
742+
}
743+
},
712744
'gpt-5.3-codex-spark': {
713745
name: 'GPT-5.3 Codex Spark',
714746
limit: {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import { nextTick } from 'vue'
4+
5+
vi.mock('vue-i18n', () => ({
6+
useI18n: () => ({
7+
t: (key: string) => key
8+
})
9+
}))
10+
11+
vi.mock('@/composables/useClipboard', () => ({
12+
useClipboard: () => ({
13+
copyToClipboard: vi.fn().mockResolvedValue(true)
14+
})
15+
}))
16+
17+
import UseKeyModal from '../UseKeyModal.vue'
18+
19+
describe('UseKeyModal', () => {
20+
it('renders updated GPT-5.4 mini/nano names in OpenCode config', async () => {
21+
const wrapper = mount(UseKeyModal, {
22+
props: {
23+
show: true,
24+
apiKey: 'sk-test',
25+
baseUrl: 'https://example.com/v1',
26+
platform: 'openai'
27+
},
28+
global: {
29+
stubs: {
30+
BaseDialog: {
31+
template: '<div><slot /><slot name="footer" /></div>'
32+
},
33+
Icon: {
34+
template: '<span />'
35+
}
36+
}
37+
}
38+
})
39+
40+
const opencodeTab = wrapper.findAll('button').find((button) =>
41+
button.text().includes('keys.useKeyModal.cliTabs.opencode')
42+
)
43+
44+
expect(opencodeTab).toBeDefined()
45+
await opencodeTab!.trigger('click')
46+
await nextTick()
47+
48+
const codeBlock = wrapper.find('pre code')
49+
expect(codeBlock.exists()).toBe(true)
50+
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
51+
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Nano"')
52+
})
53+
})

0 commit comments

Comments
 (0)