feat(models): support GPT-6.1 Sol

This commit is contained in:
lyy0709
2026-09-29 11:49:26 -07:00
parent a60a29549f
commit 9688571a83
30 changed files with 675 additions and 23 deletions
@@ -1499,9 +1499,9 @@ func TestGatewayModels_GPT6SolLunaDiscoveryRespectsGroupAndAccountRestrictions(t
restricted bool
want []string
}{
{"selected and ordered", []string{"gpt-6-luna", "gpt-6-sol"}, false, []string{"gpt-6-luna", "gpt-6-sol"}},
{"selected and ordered", []string{"gpt-6.1-sol", "gpt-6-luna", "gpt-6-sol"}, false, []string{"gpt-6.1-sol", "gpt-6-luna", "gpt-6-sol"}},
{"group excludes new models", []string{"gpt-5.6-sol"}, false, []string{"gpt-5.6-sol"}},
{"account restricts new models", []string{"gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol"}, true, []string{"gpt-5.6-sol"}},
{"account restricts new models", []string{"gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol"}, true, []string{"gpt-5.6-sol"}},
} {
t.Run(tc.name, func(t *testing.T) {
groupID := int64(25)
@@ -1979,3 +1979,16 @@ func TestMessageStartSSE_StopReasonIsJSONNull(t *testing.T) {
require.Contains(t, sse, `"stop_reason":null`)
require.NotContains(t, sse, `"stop_reason":""`)
}
func TestGPT61SolCacheOptionsAndBreakpointsSurviveChatBridge(t *testing.T) {
sampling := 0.7
for _, effort := range []string{"low", "medium", "high", "xhigh", "max"} {
out, err := ChatCompletionsToResponses(&ChatCompletionsRequest{Model: "gpt-6.1-sol", ReasoningEffort: effort, Temperature: &sampling, TopP: &sampling, PromptCacheOptions: json.RawMessage(`{"ttl":"30m","mode":"explicit"}`), Messages: []ChatMessage{{Role: "user", Content: json.RawMessage(`[{"type":"text","text":"prefix","prompt_cache_breakpoint":{"mode":"explicit"}}]`)}}})
require.NoError(t, err)
require.Nil(t, out.Temperature)
require.Nil(t, out.TopP)
require.Equal(t, effort, out.Reasoning.Effort)
require.JSONEq(t, `{"ttl":"30m","mode":"explicit"}`, string(out.PromptCacheOptions))
require.Contains(t, string(out.Input), "prompt_cache_breakpoint")
}
}
@@ -13,6 +13,9 @@ import (
// Chat Completions intermediary round-trip (e.g. thinking, cache_control,
// structured system prompts).
func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
if err := openai.ValidateGPT61SolReasoningEffort(req.Model, anthropicReasoningEffort(req)); err != nil {
return nil, err
}
input, err := convertAnthropicToResponsesInput(req.System, req.Messages)
if err != nil {
return nil, err
@@ -59,6 +62,9 @@ func AnthropicToResponses(req *AnthropicRequest) (*ResponsesRequest, error) {
// An explicit thinking disable takes precedence over output_config.effort.
effort := anthropicReasoningEffort(req)
if openai.IsGPT61SolModelSpelling(req.Model) && req.OutputConfig != nil && req.OutputConfig.Effort == "max" && effort != "none" {
effort = "max"
}
out.Reasoning = &ResponsesReasoning{
Effort: effort,
}
@@ -18,6 +18,9 @@ type chatMessageContent struct {
// true. store is always false and reasoning.encrypted_content is always
// included so that the response translator has full context.
func ChatCompletionsToResponses(req *ChatCompletionsRequest) (*ResponsesRequest, error) {
if err := openai.ValidateGPT61SolReasoningEffort(req.Model, req.ReasoningEffort); err != nil {
return nil, err
}
input, err := convertChatMessagesToResponsesInput(req.Messages)
if err != nil {
return nil, err
File diff suppressed because one or more lines are too long
+50
View File
@@ -3,6 +3,8 @@ package openai
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
)
@@ -23,6 +25,7 @@ var DefaultModels = []Model{
{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"},
{ID: "gpt-6.1-sol", Object: "model", Created: 1790640000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6.1 Sol"},
{ID: "gpt-6-sol", Object: "model", Created: 1790035200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 Sol"},
{ID: "gpt-6-luna", Object: "model", Created: 1790035200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 Luna"},
{ID: "gpt-6-astra", Object: "model", Created: 1788480000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-6 Astra"},
@@ -78,6 +81,12 @@ var instructionsGPT55 string
//go:embed instructions_gpt6_astra.txt
var instructionsGPT6Astra string
// CodexGPT61SolMetadata is the complete official descriptor from openai/codex
// b1e72963c3b71a9265a551e54beff078384efed9, codex-rs/models-manager/models.json.
//
//go:embed codex_gpt61_sol.json
var CodexGPT61SolMetadata []byte
// latestCodexInstructions 返回当前已知最新版本的 Codex base instructions,
// 当前为 GPT-5.5;若 5.5 prompt 意外为空则回退到 DefaultInstructions 保证非空。
func latestCodexInstructions() string {
@@ -140,6 +149,16 @@ func CanonicalizeOpenAIModelAliasSpelling(model string) string {
func CodexBaseInstructionsForModel(model string) string {
canonical := CanonicalizeOpenAIModelAliasSpelling(model)
switch {
case IsGPT61SolModelSpelling(canonical):
var metadata struct {
ModelMessages struct {
InstructionsTemplate string `json:"instructions_template"`
} `json:"model_messages"`
}
if err := json.Unmarshal(CodexGPT61SolMetadata, &metadata); err != nil {
panic(err)
}
return metadata.ModelMessages.InstructionsTemplate
case canonical == "gpt-6" || canonical == "gpt-6-astra" || strings.HasPrefix(canonical, "gpt-6-astra-"):
if v := strings.TrimSpace(instructionsGPT6Astra); v != "" {
return instructionsGPT6Astra
@@ -177,3 +196,34 @@ func IsGPT6SolOrLunaModelSpelling(model string) bool {
}
return false
}
// IsGPT61SolModelSpelling recognizes the published model and local effort/compact
// spellings. Invalid effort suffixes remain identifiable for request validation.
func IsGPT61SolModelSpelling(model string) bool {
canonical := CanonicalizeOpenAIModelAliasSpelling(model)
if canonical == "gpt-6.1-sol" {
return true
}
suffix, ok := strings.CutPrefix(canonical, "gpt-6.1-sol-")
if !ok {
return false
}
switch suffix {
case "none", "minimal", "low", "medium", "high", "xhigh", "max", "openai-compact":
return true
default:
return false
}
}
// ValidateGPT61SolReasoningEffort rejects disabled reasoning instead of silently
// increasing the client's requested effort on compatibility paths.
func ValidateGPT61SolReasoningEffort(model, effort string) error {
if IsGPT61SolModelSpelling(model) {
switch strings.ToLower(strings.TrimSpace(effort)) {
case "none", "minimal":
return fmt.Errorf("gpt-6.1-sol does not support reasoning effort %q; use low, medium, high, xhigh or max", effort)
}
}
return nil
}
@@ -42,3 +42,22 @@ func TestGPT6SolLunaModelIdentity(t *testing.T) {
require.False(t, IsGPT6SolOrLunaModelSpelling("gpt-6-solitude"))
require.False(t, IsGPT6SolOrLunaModelSpelling("gpt-6-luna-preview"))
}
func TestGPT61SolIdentityAndEffort(t *testing.T) {
require.Contains(t, DefaultModelIDs(), "gpt-6.1-sol")
for _, id := range []string{"gpt-6.1-sol", "openai/gpt-6.1-sol-max", "GPT_6.1_SOL", "gpt-6.1-sol-openai-compact"} {
require.True(t, IsGPT61SolModelSpelling(id), id)
require.False(t, IsGPT6SolOrLunaModelSpelling(id), id)
}
for _, id := range []string{"gpt-6.1", "gpt-6.1-solitude", "gpt-6.1-sol-preview", "gpt-6-sol"} {
require.False(t, IsGPT61SolModelSpelling(id), id)
}
for _, effort := range []string{"", "low", "medium", "high", "xhigh", "max"} {
require.NoError(t, ValidateGPT61SolReasoningEffort("gpt-6.1-sol", effort))
}
for _, effort := range []string{"none", "minimal"} {
require.Error(t, ValidateGPT61SolReasoningEffort("gpt-6.1-sol", effort))
require.NoError(t, ValidateGPT61SolReasoningEffort("gpt-6-sol", effort))
}
require.Contains(t, CodexBaseInstructionsForModel("gpt-6.1-sol"), "based on GPT-6")
}
+19 -5
View File
@@ -550,6 +550,20 @@ func (s *BillingService) initFallbackPricing() {
}
// GPT-6 Sol/Luna official rates, 2026-09-22.
s.fallbackPrices["gpt-6.1-sol"] = &ModelPricing{
InputPricePerToken: 2e-6,
InputPricePerTokenPriority: 4e-6,
OutputPricePerToken: 10e-6,
OutputPricePerTokenPriority: 20e-6,
CacheCreationPricePerToken: 2.5e-6,
CacheCreationPricePerTokenPriority: 5e-6,
CacheReadPricePerToken: 0.1e-6,
CacheReadPricePerTokenPriority: 0.2e-6,
CacheCreationPriceExplicit: true,
LongContextInputThreshold: 272_000,
LongContextInputMultiplier: 2,
LongContextOutputMultiplier: 1.5,
}
s.fallbackPrices["gpt-6-sol"] = &ModelPricing{
InputPricePerToken: 2e-6,
InputPricePerTokenPriority: 4e-6,
@@ -1168,7 +1182,7 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
// OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。
if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" {
switch normalized {
case "gpt-6-sol", "gpt-6-luna":
case "gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna":
return s.fallbackPrices[normalized]
case "gpt-6-astra":
return s.fallbackPrices["gpt-6-astra"]
@@ -1327,7 +1341,7 @@ func (s *BillingService) getModelPricingAt(model string, pricingAt time.Time) (*
InputPricePerTokenPriority: litellmPricing.InputCostPerTokenPriority,
OutputPricePerToken: litellmPricing.OutputCostPerToken,
OutputPricePerTokenPriority: litellmPricing.OutputCostPerTokenPriority,
CacheCreationPriceExplicit: openai.IsGPT6SolOrLunaModelSpelling(model) && litellmPricing.CacheCreationInputTokenCostExplicit,
CacheCreationPriceExplicit: (openai.IsGPT6SolOrLunaModelSpelling(model) || openai.IsGPT61SolModelSpelling(model)) && litellmPricing.CacheCreationInputTokenCostExplicit,
CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
CacheCreationPricePerTokenPriority: litellmPricing.CacheCreationInputTokenCostPriority,
CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
@@ -1845,7 +1859,7 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing
return &cloned
}
normalized := normalizeKnownOpenAICodexModel(model)
usesCacheWritePremium := isOpenAIGPT56Model(normalized) || openai.IsGPT6SolOrLunaModelSpelling(normalized)
usesCacheWritePremium := isOpenAIGPT56Model(normalized) || (openai.IsGPT6SolOrLunaModelSpelling(normalized) || openai.IsGPT61SolModelSpelling(normalized))
needsCacheCreationPolicy := usesCacheWritePremium && !pricing.CacheCreationPriceExplicit && (pricing.CacheCreationPricePerToken <= 0 ||
(pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0))
fastRatio := openAIModelFastPricingRatio(normalized)
@@ -1868,7 +1882,7 @@ func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing
}
if fastRatio > 0 {
enforceOpenAIFastPricingRatio(&cloned, fastRatio)
if openai.IsGPT6SolOrLunaModelSpelling(normalized) && cloned.CacheCreationPriceExplicit {
if (openai.IsGPT6SolOrLunaModelSpelling(normalized) || openai.IsGPT61SolModelSpelling(normalized)) && cloned.CacheCreationPriceExplicit {
cloned.CacheCreationPricePerTokenPriority = cloned.CacheCreationPricePerToken * fastRatio
}
}
@@ -1880,7 +1894,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", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna":
case "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna":
return 2.0
case "gpt-5.5":
return 2.5
@@ -1982,6 +1982,7 @@ func TestNewModelPricingCatalogFallbackAndContext(t *testing.T) {
model string
input, output, write, read float64
}{
{"gpt-6.1-sol", 2e-6, 10e-6, 2.5e-6, 0.1e-6},
{"gpt-6-sol", 2e-6, 10e-6, 2.5e-6, 0.2e-6},
{"gpt-6-luna", 0.1e-6, 0.5e-6, 0.125e-6, 0.01e-6},
} {
@@ -2045,7 +2046,7 @@ func TestNewModelPricingCatalogFallbackAndContext(t *testing.T) {
func TestNewModelPricingChannelOverridesAndFamilyIsolation(t *testing.T) {
svc := newTestBillingService()
for _, model := range []string{"gpt-6-sol", "gpt-6-luna", "claude-opus-5-5"} {
for _, model := range []string{"gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5"} {
t.Run(model, func(t *testing.T) {
zero := 0.0
prices, err := svc.GetModelPricingWithChannel(model, &ChannelModelPricing{InputPrice: &zero, OutputPrice: &zero, CacheWritePrice: &zero, CacheReadPrice: &zero})
@@ -2083,9 +2084,22 @@ func TestNewModelPricingExplicitZeroCacheWrite(t *testing.T) {
func TestNewModelPricingAliasesRetainExplicitOverrides(t *testing.T) {
zero := &LiteLLMModelPricing{}
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
"gpt-6-sol": zero, "gpt-6-luna": zero, "claude-opus-5-5": zero,
"gpt-6.1-sol": zero, "gpt-6-sol": zero, "gpt-6-luna": zero, "claude-opus-5-5": zero,
}}
for _, model := range []string{"gpt-6-sol-max", "openai/gpt-6-luna-openai-compact", "claude-opus-5-5-thinking"} {
for _, model := range []string{"gpt-6.1-sol-max", "gpt-6-sol-max", "openai/gpt-6-luna-openai-compact", "claude-opus-5-5-thinking"} {
require.Same(t, zero, svc.GetModelPricing(model))
}
}
func TestGPT61SolExplicitZeroCacheWriteAcrossTiers(t *testing.T) {
pricing := &PricingService{}
var err error
pricing.pricingData, err = pricing.parsePricingData([]byte(`{"gpt-6.1-sol":{"litellm_provider":"openai","input_cost_per_token":0.000002,"output_cost_per_token":0.00001,"input_cost_per_token_flex":0.000001,"cache_creation_input_token_cost":0,"cache_creation_input_token_cost_priority":0.000005}}`))
require.NoError(t, err)
svc := NewBillingService(&config.Config{}, pricing)
for _, tier := range []string{"", "fast", "priority", "flex"} {
cost, err := svc.CalculateCostWithServiceTier("openai/gpt-6.1-sol-max", UsageTokens{CacheCreationTokens: 300000}, 1, tier)
require.NoError(t, err)
require.Zero(t, cost.CacheCreationCost)
}
}
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"encoding/json"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"net/url"
"strings"
)
@@ -63,7 +64,7 @@ func accountCodexToolCapabilities(account *Account, modelID string) map[string]j
parsed, err := url.Parse(baseURL)
official := err == nil && (strings.EqualFold(parsed.Hostname(), "api.openai.com") ||
(account.IsOpenAIOAuth() && strings.EqualFold(parsed.Hostname(), "chatgpt.com")))
if account.IsOpenAI() && isOpenAIGPT6AstraModel(modelID) && official {
if account.IsOpenAI() && (isOpenAIGPT6AstraModel(modelID) || openai.IsGPT61SolModelSpelling(modelID)) && official {
defaults := map[string]json.RawMessage{
"supports_search_tool": json.RawMessage("true"),
"apply_patch_tool_type": json.RawMessage(`"freeform"`),
@@ -377,6 +377,7 @@ type configuredCodexModelMessages struct {
// emitted: unlike ordinary OpenAI /v1/models entries, the Codex manifest parser
// requires them to be present.
type configuredCodexModelDescriptor struct {
officialMetadata json.RawMessage
Slug string `json:"slug"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
@@ -510,7 +511,7 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr
descriptor.ServiceTiers = configuredCodexServiceTiersForModel(modelID)
if isOpenAICodexReasoningGPTModel(modelID) {
defaultReasoningLevel := "medium"
if getNormalizedCodexModel(modelID) == "gpt-5.6-sol" {
if getNormalizedCodexModel(modelID) == "gpt-5.6-sol" || openai.IsGPT61SolModelSpelling(modelID) {
defaultReasoningLevel = "low"
}
descriptor.DefaultReasoningLevel = &defaultReasoningLevel
@@ -519,7 +520,7 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr
descriptor.TruncationPolicy = configuredCodexTruncationPolicy{Mode: "tokens", Limit: configuredCodexToolOutputMaxTokens}
// GPT-6 Sol/Luna retain the existing 5.6 Codex window as an offline
// compatibility template; live account metadata remains authoritative.
if isOpenAIGPT56Model(modelID) || openai.IsGPT6SolOrLunaModelSpelling(modelID) {
if isOpenAIGPT56Model(modelID) || openai.IsGPT6SolOrLunaModelSpelling(modelID) || openai.IsGPT61SolModelSpelling(modelID) {
descriptor.MaxContextWindow = configuredCodexGPT56MaxContext
}
if isOpenAIGPT6AstraModel(modelID) {
@@ -539,6 +540,13 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr
}
}
if openai.IsGPT61SolModelSpelling(modelID) {
if err := json.Unmarshal(openai.CodexGPT61SolMetadata, &descriptor); err != nil {
panic(err)
}
descriptor.Slug = modelID
descriptor.officialMetadata = openai.CodexGPT61SolMetadata
}
return descriptor
}
@@ -638,7 +646,7 @@ func configuredCodexGPTReasoningLevels(modelID string) []configuredCodexReasonin
Description: "Maximum reasoning depth for complex tasks",
})
}
if isOpenAIGPT6AstraModel(modelID) || normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" {
if isOpenAIGPT6AstraModel(modelID) || openai.IsGPT61SolModelSpelling(modelID) || normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" {
levels = append(levels, configuredCodexReasoningLevel{
Effort: "ultra",
Description: "Maximum reasoning with automatic task delegation",
@@ -2062,6 +2070,7 @@ func CodexModelsManifestETag(body []byte) string {
}
var apiKeyCodexModelsWithoutResponsesLite = map[string]struct{}{
"gpt-6.1-sol": {},
"gpt-6-astra": {},
"gpt-5.6-sol": {},
"gpt-5.6-terra": {},
@@ -2632,3 +2641,42 @@ func buildCodexModelsManifestURL(endpoint string, appendModelsPath bool, clientV
requestURL.RawQuery = query.Encode()
return requestURL, nil
}
// MarshalJSON keeps fields added by the official client, including nested tool
// instructions, while the generated descriptor still controls routing metadata.
func (d configuredCodexModelDescriptor) MarshalJSON() ([]byte, error) {
type descriptor configuredCodexModelDescriptor
encoded, err := json.Marshal(descriptor(d))
if err != nil || len(d.officialMetadata) == 0 {
return encoded, err
}
var fields, official map[string]json.RawMessage
if err := json.Unmarshal(encoded, &fields); err != nil {
return nil, err
}
if err := json.Unmarshal(d.officialMetadata, &official); err != nil {
return nil, err
}
for key, value := range official {
if _, exists := fields[key]; !exists {
fields[key] = value
}
}
var messages, officialMessages map[string]json.RawMessage
if err := json.Unmarshal(fields["model_messages"], &messages); err != nil {
return nil, err
}
if err := json.Unmarshal(official["model_messages"], &officialMessages); err != nil {
return nil, err
}
for key, value := range officialMessages {
if _, exists := messages[key]; !exists {
messages[key] = value
}
}
fields["model_messages"], err = json.Marshal(messages)
if err != nil {
return nil, err
}
return json.Marshal(fields)
}
@@ -3768,7 +3768,7 @@ func TestCodexGPTIdentityPatternsCoverBundledPrompts(t *testing.T) {
}
func TestGPT6SolLunaCatalogKeepsAuthoritativeCapabilities(t *testing.T) {
for _, id := range []string{"gpt-6-sol", "gpt-6-luna"} {
for _, id := range []string{"gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna"} {
svc := &OpenAIGatewayService{}
manifest := &OpenAIModelsResponse{Body: []byte(`{"models":[{"slug":"` + id + `","supported_reasoning_levels":[{"effort":"ultra"}],"default_reasoning_level":"ultra","multi_agent_reasoning_effort":"xhigh","service_tiers":[{"id":"ultrafast"}],"context_window":300000,"max_context_window":900000,"supports_search_tool":false,"apply_patch_tool_type":null}]}`)}
account := newCodexModelsAPIKeyTestAccount("https://api.openai.com/v1")
@@ -3786,3 +3786,46 @@ func TestGPT6SolLunaCatalogKeepsAuthoritativeCapabilities(t *testing.T) {
require.Nil(t, models[0]["apply_patch_tool_type"])
}
}
func TestGPT61SolOfflineCodexCatalog(t *testing.T) {
body, err := BuildCodexModelsManifest([]string{"gpt-6.1-sol"})
require.NoError(t, err)
var catalog struct {
Models []map[string]any `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &catalog))
require.Len(t, catalog.Models, 1)
m := catalog.Models[0]
require.Equal(t, "gpt-6.1-sol", m["slug"])
require.Equal(t, "low", m["default_reasoning_level"])
require.EqualValues(t, 272000, m["context_window"])
require.EqualValues(t, 872000, m["max_context_window"])
require.Equal(t, "xhigh", m["multi_agent_reasoning_effort"])
require.Equal(t, "v2", m["multi_agent_version"])
require.Nil(t, m["default_service_tier"])
levels, ok := m["supported_reasoning_levels"].([]any)
require.True(t, ok)
require.NotEmpty(t, levels)
last, ok := levels[len(levels)-1].(map[string]any)
require.True(t, ok)
require.Equal(t, "ultra", last["effort"])
tiers, ok := m["service_tiers"].([]any)
require.True(t, ok)
for _, tier := range tiers {
fields, ok := tier.(map[string]any)
require.True(t, ok)
require.NotEqual(t, "ultrafast", fields["id"])
}
}
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)
require.NoError(t, err)
var catalog struct {
Models []map[string]any `json:"models"`
}
require.NoError(t, json.Unmarshal(body, &catalog))
require.Equal(t, false, catalog.Models[0]["use_responses_lite"])
require.Equal(t, "low", catalog.Models[0]["default_reasoning_level"])
}
@@ -11,6 +11,7 @@ import (
)
var codexModelMap = map[string]string{
"gpt-6.1-sol": "gpt-6.1-sol",
"gpt-6-sol": "gpt-6-sol",
"gpt-6-luna": "gpt-6-luna",
"gpt-6-astra": "gpt-6-astra",
@@ -62,6 +63,7 @@ var codexVersionModelPrefixes = []struct {
prefix string
target string
}{
{prefix: "gpt-6.1-sol", target: "gpt-6.1-sol"},
{prefix: "gpt-6-sol", target: "gpt-6-sol"},
{prefix: "gpt-6-luna", target: "gpt-6-luna"},
{prefix: "gpt-5.6-sol", target: "gpt-5.6-sol"},
@@ -4,9 +4,13 @@ import (
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
)
func NormalizeOpenAICompatRequestedModel(model string) string {
if openai.IsGPT61SolModelSpelling(model) {
return "gpt-6.1-sol"
}
trimmed := strings.TrimSpace(model)
if trimmed == "" {
return ""
@@ -20,6 +24,20 @@ func NormalizeOpenAICompatRequestedModel(model string) string {
}
func applyOpenAICompatModelNormalization(req *apicompat.AnthropicRequest) {
if req != nil && openai.IsGPT61SolModelSpelling(req.Model) {
canonical := openai.CanonicalizeOpenAIModelAliasSpelling(req.Model)
if effort, ok := strings.CutPrefix(canonical, "gpt-6.1-sol-"); ok && effort != "openai-compact" {
req.Model = "gpt-6.1-sol"
if req.OutputConfig == nil {
req.OutputConfig = &apicompat.AnthropicOutputConfig{}
}
if req.OutputConfig.Effort == "" {
req.OutputConfig.Effort = effort
}
return
}
}
if req == nil {
return
}
@@ -16,7 +16,7 @@ func shouldAutoInjectPromptCacheKeyForCompat(model string) bool {
trimmed := strings.TrimSpace(strings.ToLower(model))
canonical := canonicalizeOpenAIModelAliasSpelling(trimmed)
// GPT-6 Sol/Luna share the supported Responses cache identity contract.
if canonical == "gpt-6" || canonical == "gpt-6-astra" || openai.IsGPT6SolOrLunaModelSpelling(canonical) {
if canonical == "gpt-6" || canonical == "gpt-6-astra" || openai.IsGPT6SolOrLunaModelSpelling(canonical) || openai.IsGPT61SolModelSpelling(canonical) {
return true
}
// 仅对 Responses 兼容路径支持的 GPT-5 族开启自动注入,避免 normalizeCodexModel
@@ -166,10 +166,10 @@ func TestDeriveAnthropicCompatPromptCacheKey_UsesCacheControlAnchors(t *testing.
}
func TestGPT6SolLunaCompatCacheIdentity(t *testing.T) {
for _, model := range []string{"gpt-6-sol", "openai/gpt-6-luna", "gpt-6-sol-max"} {
for _, model := range []string{"gpt-6.1-sol", "openai/gpt-6.1-sol-max", "gpt-6-sol", "openai/gpt-6-luna", "gpt-6-sol-max"} {
require.True(t, shouldAutoInjectPromptCacheKeyForCompat(model), model)
}
for _, model := range []string{"gpt-6-sol-preview", "gpt-6-solitude", "gpt-6-luna-preview"} {
for _, model := range []string{"gpt-6.1-sol-preview", "gpt-6.1-solitude", "gpt-6-sol-preview", "gpt-6-solitude", "gpt-6-luna-preview"} {
require.False(t, shouldAutoInjectPromptCacheKeyForCompat(model), model)
}
}
@@ -205,6 +205,10 @@ func (s *OpenAIGatewayService) forwardAsChatCompletions(
// derive a stable seed from the final upstream model family.
billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
if err := validateGPT61SolCompatRequest(body, upstreamModel); err != nil {
writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
promptCacheKey = strings.TrimSpace(promptCacheKey)
compatPromptCacheInjected := false
@@ -74,6 +74,10 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
// 2. Resolve model mapping (same as ForwardAsChatCompletions)
billingModel := resolveOpenAIForwardModel(account, originalModel, defaultMappedModel)
upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
if err := validateGPT61SolCompatRequest(body, upstreamModel); err != nil {
writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
SetOpsUpstreamModel(c, upstreamModel)
grokCacheIdentity := ""
if account.Platform == PlatformGrok {
@@ -81,6 +85,11 @@ func (s *OpenAIGatewayService) forwardAsRawChatCompletions(
// anchored to the client's stable conversation prefix.
grokCacheIdentity = resolveGrokCacheIdentity(c, body, "", upstreamModel)
}
if openai.IsGPT61SolModelSpelling(upstreamModel) && (len(gjson.GetBytes(body, "tools").Array()) > 0 || len(gjson.GetBytes(body, "functions").Array()) > 0) {
err := fmt.Errorf("gpt-6.1-sol requires Responses for tool calls; this account only supports Chat Completions")
writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
if openai.IsGPT6SolOrLunaModelSpelling(upstreamModel) && (len(gjson.GetBytes(body, "tools").Array()) > 0 || len(gjson.GetBytes(body, "functions").Array()) > 0) && gjson.GetBytes(body, "reasoning_effort").String() != "none" {
err := fmt.Errorf("%s requires Responses for tool calls with reasoning; this account only supports Chat Completions. Use reasoning_effort=none or a Responses-capable account", upstreamModel)
writeChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
@@ -1213,7 +1213,7 @@ func TestGPT6ReasoningModeUsesMappedUpstream(t *testing.T) {
}
func TestGPT6MappedCompatibilityBridgesKeepReasoningAndTools(t *testing.T) {
for _, model := range []string{"gpt-6-sol", "gpt-6-luna"} {
for _, model := range []string{"gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna"} {
for _, messages := range []bool{false, true} {
body := []byte(`{"model":"public","reasoning_effort":"max","temperature":0.7,"top_p":0.9,"prompt_cache_options":{"ttl":"30m"},"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"messages":[{"role":"user","content":"hello"}]}`)
if messages {
@@ -1249,3 +1249,86 @@ func TestGPT6MappedCompatibilityBridgesKeepReasoningAndTools(t *testing.T) {
}
}
}
func TestGPT61SolRejectsDisabledReasoningBeforeForwarding(t *testing.T) {
for _, body := range []string{
`{"model":"public","reasoning_effort":"none"}`,
`{"model":"public","reasoning":{"effort":"minimal"}}`,
`{"model":"public","output_config":{"effort":"none"}}`,
`{"model":"public","thinking":{"type":"disabled"}}`,
`{"model":"gpt-6.1-sol-minimal"}`,
} {
require.Error(t, validateGPT61SolCompatRequest([]byte(body), "gpt-6.1-sol"))
require.NoError(t, validateGPT61SolCompatRequest([]byte(body), "gpt-6-sol"))
}
for _, field := range []string{`"reasoning_effort":"none"`, `"reasoning_effort":"minimal"`, `"tools":[{"type":"function","function":{"name":"lookup"}}]`} {
body := []byte(`{"model":"public",` + field + `,"messages":[{"role":"user","content":"hello"}]}`)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"model_mapping": map[string]any{"public": "gpt-6.1-sol"}}}
svc := &OpenAIGatewayService{cfg: &config.Config{}}
_, err := svc.forwardAsRawChatCompletions(context.Background(), c, account, body, "")
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, rec.Code)
require.Contains(t, rec.Body.String(), "gpt-6.1-sol")
}
}
func TestGPT61SolMappedReasoningModeAndSampling(t *testing.T) {
body := []byte(`{"model":"public","reasoning":{"mode":"pro","effort":"max"},"temperature":0.5,"top_p":0.9,"logprobs":true,"top_logprobs":2,"include":["message.output_text.logprobs","reasoning.encrypted_content"]}`)
out, _, err := normalizeOpenAIResponsesReasoningMode(body, "gpt-6.1-sol")
require.NoError(t, err)
require.Equal(t, "pro", gjson.GetBytes(out, "reasoning.mode").String())
require.Equal(t, "max", gjson.GetBytes(out, "reasoning.effort").String())
for _, field := range []string{"temperature", "top_p", "logprobs", "top_logprobs"} {
require.False(t, gjson.GetBytes(out, field).Exists(), field)
}
require.Equal(t, "reasoning.encrypted_content", gjson.GetBytes(out, "include.0").String())
}
func TestGPT61SolMessagesEffortAliasesPreserveIntent(t *testing.T) {
for _, effort := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"} {
req := &apicompat.AnthropicRequest{Model: "openai/gpt-6.1-sol-" + effort}
applyOpenAICompatModelNormalization(req)
require.Equal(t, "gpt-6.1-sol", req.Model)
require.Equal(t, effort, req.OutputConfig.Effort)
_, err := apicompat.AnthropicToResponses(req)
if effort == "none" || effort == "minimal" {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
}
func TestGPT61SolOnlyChatFallbackRejectsToolsAndDisabledReasoning(t *testing.T) {
for _, responses := range []bool{false, true} {
bodies := []string{
`{"model":"public","max_tokens":100,"messages":[{"role":"user","content":"hi"}],"tools":[{"name":"lookup","input_schema":{"type":"object"}}]}`,
`{"model":"public","max_tokens":100,"messages":[{"role":"user","content":"hi"}],"thinking":{"type":"disabled"}}`,
}
if responses {
bodies = []string{
`{"model":"public","input":"hi","tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`,
`{"model":"public","input":"hi","reasoning":{"effort":"none"}}`,
}
}
for _, body := range bodies {
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(body))
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Credentials: map[string]any{"model_mapping": map[string]any{"public": "gpt-6.1-sol"}}}
svc := &OpenAIGatewayService{cfg: &config.Config{}}
var err error
if responses {
_, err = svc.forwardResponsesViaRawChatCompletions(context.Background(), c, account, []byte(body))
} else {
_, err = svc.forwardAnthropicViaRawChatCompletions(context.Background(), c, account, []byte(body), "")
}
require.Error(t, err)
require.Equal(t, http.StatusBadRequest, rec.Code)
require.Contains(t, rec.Body.String(), "gpt-6.1-sol")
}
}
}
@@ -96,6 +96,10 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
// 2. Model mapping
billingModel := resolveOpenAIForwardModel(account, normalizedModel, defaultMappedModel)
upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
if err := validateGPT61SolCompatRequest(body, upstreamModel); err != nil {
writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
promptCacheKey = strings.TrimSpace(promptCacheKey)
apiKeyID := getAPIKeyIDFromContext(c)
anthropicDigestChain := ""
@@ -10,6 +10,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -59,6 +60,15 @@ func (s *OpenAIGatewayService) forwardAnthropicViaRawChatCompletions(
billingModel := resolveOpenAIForwardModel(account, anthropicReq.Model, defaultMappedModel)
upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
if err := validateGPT61SolCompatRequest(body, upstreamModel); err != nil {
writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
if openai.IsGPT61SolModelSpelling(upstreamModel) && len(anthropicReq.Tools) > 0 {
err := fmt.Errorf("gpt-6.1-sol requires Responses for tool calls; this account only supports Chat Completions")
writeAnthropicError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
chatReq.Model = upstreamModel
chatReq.ReasoningEffort = openAICompatAnthropicReasoningEffort(&anthropicReq, upstreamModel, chatReq.ReasoningEffort)
chatReq.Stream = clientStream
@@ -1333,7 +1333,7 @@ func normalizeOpenAIOAuthResponsesCompatibilityBody(body []byte) ([]byte, bool,
}
func normalizeGPT6ResponsesSampling(body []byte, model string) ([]byte, bool, error) {
if !openai.IsGPT6SolOrLunaModelSpelling(model) || gjson.GetBytes(body, "reasoning.effort").String() == "none" {
if (!openai.IsGPT6SolOrLunaModelSpelling(model) && !openai.IsGPT61SolModelSpelling(model)) || gjson.GetBytes(body, "reasoning.effort").String() == "none" {
return body, false, nil
}
out := body
@@ -2576,3 +2576,26 @@ func supportsOpenAIReasoningEffortMax(model string) bool {
return false
}
}
// validateGPT61SolCompatRequest runs after model mapping, before conversion can
// discard unsupported explicit effort selections or disabled thinking.
func validateGPT61SolCompatRequest(body []byte, model string) error {
if !openai.IsGPT61SolModelSpelling(model) {
return nil
}
for _, path := range []string{"reasoning.effort", "reasoning_effort", "output_config.effort"} {
if err := openai.ValidateGPT61SolReasoningEffort(model, gjson.GetBytes(body, path).String()); err != nil {
return err
}
}
if gjson.GetBytes(body, "thinking.type").String() == "disabled" {
return openai.ValidateGPT61SolReasoningEffort(model, "none")
}
requestedModel := gjson.GetBytes(body, "model").String()
for _, effort := range []string{"none", "minimal"} {
if strings.HasSuffix(strings.ToLower(requestedModel), "-"+effort) {
return openai.ValidateGPT61SolReasoningEffort(model, effort)
}
}
return nil
}
@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
@@ -71,6 +72,15 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions(
billingModel := resolveOpenAIForwardModel(account, originalModel, "")
upstreamModel := normalizeOpenAIModelForUpstream(account, billingModel)
if err := validateGPT61SolCompatRequest(body, upstreamModel); err != nil {
writeOpenAIResponsesFallbackError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
if openai.IsGPT61SolModelSpelling(upstreamModel) && len(effectiveTools) > 0 {
err := fmt.Errorf("gpt-6.1-sol requires Responses for tool calls; this account only supports Chat Completions")
writeOpenAIResponsesFallbackError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, err
}
reasoningEffort := extractOpenAIReasoningEffortFromBody(body, upstreamModel, billingModel, originalModel)
// 国产模型默认 effort 补充:需要 mappedModel 判定,推迟到 billingModel 算出之后。
reasoningEffort = ApplyThinkingEnabledFallback(reasoningEffort, body, billingModel)
@@ -37,6 +37,9 @@ func normalizeKnownOpenAICodexModel(model string) string {
}
}
if openai.IsGPT61SolModelSpelling(normalized) {
return "gpt-6.1-sol"
}
if openai.IsGPT6SolOrLunaModelSpelling(normalized) {
if strings.HasPrefix(normalized, "gpt-6-sol") {
return "gpt-6-sol"
@@ -160,5 +163,5 @@ func firstUsageBillingModel(candidates []string) string {
}
func isOpenAIGPT6Model(model string) bool {
return isOpenAIGPT6AstraModel(model) || openai.IsGPT6SolOrLunaModelSpelling(model)
return isOpenAIGPT6AstraModel(model) || openai.IsGPT6SolOrLunaModelSpelling(model) || openai.IsGPT61SolModelSpelling(model)
}
@@ -74,6 +74,23 @@ var (
Mode: "chat",
SupportsPromptCaching: true,
}
openAIGPT61SolFallbackPricing = &LiteLLMModelPricing{
InputCostPerToken: 2e-6,
InputCostPerTokenPriority: 4e-6,
OutputCostPerToken: 10e-6,
OutputCostPerTokenPriority: 20e-6,
CacheCreationInputTokenCost: 2.5e-6,
CacheCreationInputTokenCostPriority: 5e-6,
CacheReadInputTokenCost: 0.1e-6,
CacheReadInputTokenCostPriority: 0.2e-6,
LongContextInputTokenThreshold: 272_000,
LongContextInputCostMultiplier: 2,
LongContextOutputCostMultiplier: 1.5,
SupportsServiceTier: true,
LiteLLMProvider: "openai",
Mode: "chat",
SupportsPromptCaching: true,
}
openAIGPT6SolFallbackPricing = &LiteLLMModelPricing{
InputCostPerToken: 2e-6,
InputCostPerTokenPriority: 4e-6,
@@ -1522,6 +1539,12 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
}
}
if openai.IsGPT61SolModelSpelling(model) {
if pricing, ok := s.pricingData["gpt-6.1-sol"]; ok {
return pricing
}
return openAIGPT61SolFallbackPricing
}
if openai.IsGPT6SolOrLunaModelSpelling(model) {
if pricing, ok := s.pricingData[normalizeKnownOpenAICodexModel(model)]; ok {
return pricing
@@ -1,4 +1,58 @@
{
"gpt-6.1-sol": {
"cache_creation_input_token_cost": 2.5e-06,
"cache_creation_input_token_cost_batches": 1.25e-06,
"cache_creation_input_token_cost_flex": 1.25e-06,
"cache_creation_input_token_cost_priority": 5e-06,
"cache_read_input_token_cost": 1e-07,
"cache_read_input_token_cost_flex": 5e-08,
"cache_read_input_token_cost_priority": 2e-07,
"input_cost_per_token": 2e-06,
"input_cost_per_token_batches": 1e-06,
"input_cost_per_token_flex": 1e-06,
"input_cost_per_token_priority": 4e-06,
"long_context_input_token_threshold": 272000,
"long_context_input_cost_multiplier": 2.0,
"long_context_output_cost_multiplier": 1.5,
"litellm_provider": "openai",
"max_input_tokens": 922000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 1e-05,
"output_cost_per_token_batches": 5e-06,
"output_cost_per_token_flex": 5e-06,
"output_cost_per_token_priority": 2e-05,
"supported_endpoints": [
"/v1/chat/completions",
"/v1/batch",
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_minimal_reasoning_effort": false,
"supports_native_streaming": true,
"supports_none_reasoning_effort": false,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_service_tier": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_xhigh_reasoning_effort": true,
"cache_read_input_token_cost_batches": 5e-08,
"context_window": 1050000
},
"grok-4.7": {
"cache_read_input_token_cost": 5e-07,
"input_cost_per_token": 2e-06,
@@ -1403,6 +1403,23 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
max: {}
}
},
'gpt-6.1-sol': {
name: 'GPT-6.1 Sol',
limit: {
context: 1050000,
output: 128000
},
options: {
store: false
},
variants: {
low: {},
medium: {},
high: {},
xhigh: {},
max: {}
}
},
'gpt-6-sol': {
name: 'GPT-6 Sol',
limit: {
@@ -706,12 +706,13 @@ describe('UseKeyModal', () => {
const parsed = JSON.parse(wrapper.find('pre code').text())
const models = parsed.provider.openai.models
for (const model of ['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-6-sol', 'gpt-6-luna']) {
for (const model of ['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna']) {
expect(models[model]).toBeDefined()
expect(models[model].variants).toHaveProperty('max')
expect(models[model].variants).toHaveProperty('xhigh')
}
expect(models['gpt-5.6'].name).toBe('GPT-5.6 (Sol)')
expect(models['gpt-6.1-sol'].variants).not.toHaveProperty('none')
expect(models['gpt-6-sol'].variants).toHaveProperty('none')
expect(models['gpt-6-luna'].limit).toEqual({ context: 1050000, output: 128000 })
expect(models['gpt-6']).toEqual({
@@ -17,12 +17,14 @@ describe('useModelWhitelist', () => {
expect(models).toContain('gpt-5.6')
expect(models).toContain('gpt-6')
expect(models).toContain('gpt-6-astra')
expect(models).toContain('gpt-6.1-sol')
expect(models).toContain('gpt-6-sol')
expect(models).toContain('gpt-6-luna')
})
it('openai 预设映射包含 GPT-6 别名和 Astra', () => {
expect(getPresetMappingsByPlatform('openai')).toEqual(expect.arrayContaining([
expect.objectContaining({ label: 'GPT-6.1 Sol', from: 'gpt-6.1-sol', to: 'gpt-6.1-sol' }),
expect.objectContaining({ label: 'GPT-6', from: 'gpt-6', to: 'gpt-6' }),
expect.objectContaining({ label: 'GPT-6 Astra', from: 'gpt-6-astra', to: 'gpt-6-astra' })
]))
@@ -10,7 +10,7 @@ const openaiModels = [
// GPT-5.6 系列
'gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna',
// GPT-6 系列
'gpt-6', 'gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna',
'gpt-6', 'gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna',
// GPT-5.5 系列
'gpt-5.5',
// GPT-5.4 系列
@@ -310,6 +310,7 @@ const openaiPresetMappings = [
{ label: 'GPT-5.2', from: 'gpt-5.2', to: 'gpt-5.2', color: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' },
{ label: 'GPT-6', from: 'gpt-6', to: 'gpt-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },
{ label: 'GPT-5.6', from: 'gpt-5.6', to: 'gpt-5.6', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' },
{ label: 'GPT-6.1 Sol', from: 'gpt-6.1-sol', to: 'gpt-6.1-sol', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
{ label: 'GPT-6 Sol', from: 'gpt-6-sol', to: 'gpt-6-sol', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
{ label: 'GPT-5.6 Sol', from: 'gpt-5.6-sol', to: 'gpt-5.6-sol', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
{ label: 'GPT-5.6 Terra', from: 'gpt-5.6-terra', to: 'gpt-5.6-terra', color: 'bg-lime-100 text-lime-700 hover:bg-lime-200 dark:bg-lime-900/30 dark:text-lime-400' },