mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(models): support GPT-6.1 Sol
This commit is contained in:
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user