feat: 新增 grok-4.6 目录、官方定价与请求路径支持
官方目录价:<200k 为 $2 / 缓存读取 $0.50 / $6,≥200k 全部 2 倍。 原先没有独立价卡,/models 宣称可配 reasoning 但请求路径会剥掉 effort。 - 目录与别名接入 grok-4.6 / grok-4.6-latest,默认文本模型仍为 grok-4.5 - 独立回退价卡含缓存读取价与 200k 长上下文倍率 - 保留 reasoning_effort;Chat→Responses 的 cache/vision 桥接同样识别 4.6
This commit is contained in:
1 parent
bb9e74285e
commit
a04ce49016
12 files changed
+75
-6
No files matched your search
@@ -1249,7 +1249,7 @@ func writeGrokModelsList(c *gin.Context, modelIDs []string) {
|
||||
|
||||
func grokModelSupportsConfigurableReasoning(modelID string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(modelID)) {
|
||||
case "grok-4.5", "grok-4.5-latest", "grok", "grok-latest", "grok-build", "grok-build-latest", "grok-build-0.1":
|
||||
case "grok-4.6", "grok-4.6-latest", "grok-4.5", "grok-4.5-latest", "grok", "grok-latest", "grok-build", "grok-build-latest", "grok-build-0.1":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -83,6 +83,7 @@ func (o ModelMappingOptions) defaultText() string {
|
||||
|
||||
var defaultModels = []Model{
|
||||
// Text
|
||||
{ID: "grok-4.6", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.6"},
|
||||
{ID: "grok-4.5", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.5"},
|
||||
{ID: "grok-4.3", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.3"},
|
||||
{ID: "grok-3-mini", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 3 Mini"},
|
||||
@@ -106,6 +107,8 @@ var defaultModels = []Model{
|
||||
var grokTextResponsesModelAliases = map[string]string{
|
||||
"grok": DefaultTextModel,
|
||||
"grok-latest": DefaultTextModel,
|
||||
"grok-4.6": "grok-4.6",
|
||||
"grok-4.6-latest": "grok-4.6",
|
||||
"grok-4.5": DefaultTextModel,
|
||||
"grok-4.5-latest": DefaultTextModel,
|
||||
"grok-4.3": "grok-4.3",
|
||||
|
||||
@@ -52,11 +52,20 @@ func TestCanonicalImagineVideoModel(t *testing.T) {
|
||||
func TestIsGrokModelID(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.True(t, IsGrokModelID("grok-4.5"))
|
||||
require.True(t, IsGrokModelID("grok-4.6"))
|
||||
require.True(t, IsGrokModelID("x-ai/grok-4.3"))
|
||||
require.False(t, IsGrokModelID("gpt-5"))
|
||||
require.False(t, IsGrokModelID("claude-sonnet-4"))
|
||||
}
|
||||
|
||||
func TestDefaultModelsIncludesGrok46(t *testing.T) {
|
||||
t.Parallel()
|
||||
ids := DefaultModelIDs()
|
||||
require.Contains(t, ids, "grok-4.6")
|
||||
require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok-4.6"))
|
||||
require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok-4.6-latest"))
|
||||
}
|
||||
|
||||
func TestResolveGrokTextResponsesModelID(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID(""))
|
||||
|
||||
@@ -352,6 +352,8 @@ func TestDefaultModelMappingIncludesGrokAliases(t *testing.T) {
|
||||
mapping := DefaultModelMapping()
|
||||
require.Equal(t, "grok-4.5", mapping["grok"])
|
||||
require.Equal(t, "grok-4.5", mapping["grok-latest"])
|
||||
require.Equal(t, "grok-4.6", mapping["grok-4.6"])
|
||||
require.Equal(t, "grok-4.6", mapping["grok-4.6-latest"])
|
||||
require.Equal(t, "grok-4.5", mapping["grok-4.5"])
|
||||
require.Equal(t, "grok-4.5", mapping["grok-4.5-latest"])
|
||||
require.Equal(t, "grok-build-0.1", mapping["grok-build"])
|
||||
|
||||
@@ -593,6 +593,19 @@ func (s *BillingService) initFallbackPricing() {
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
|
||||
// xAI Grok 4.6 (docs.x.ai/developers/models: $2 input / $0.50 cached input /
|
||||
// $6 output per MTok under 200k prompt tokens; ≥200k is 2× on input,
|
||||
// cached input, and output).
|
||||
s.fallbackPrices["grok-4.6"] = &ModelPricing{
|
||||
InputPricePerToken: 2e-6,
|
||||
OutputPricePerToken: 6e-6,
|
||||
CacheReadPricePerToken: 0.5e-6,
|
||||
SupportsCacheBreakdown: false,
|
||||
LongContextInputThreshold: 200000,
|
||||
LongContextInputMultiplier: 2,
|
||||
LongContextOutputMultiplier: 2,
|
||||
}
|
||||
|
||||
// xAI Grok 4.3 (official docs: $1.25 input / $2.50 output per MTok)
|
||||
s.fallbackPrices["grok-4.3"] = &ModelPricing{
|
||||
InputPricePerToken: 1.25e-6,
|
||||
@@ -805,6 +818,8 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
||||
switch modelLower {
|
||||
case "grok", "grok-latest", "grok-4.5", "grok-4.5-latest", "grok-build-latest":
|
||||
return s.fallbackPrices["grok-4.5"]
|
||||
case "grok-4.6", "grok-4.6-latest":
|
||||
return s.fallbackPrices["grok-4.6"]
|
||||
case "grok-4.3",
|
||||
"grok-4.20-0309-reasoning",
|
||||
"grok-4.20-0309-non-reasoning",
|
||||
|
||||
@@ -871,8 +871,8 @@ func TestComputeTokenBreakdown_GptImage2ImageEditIssue4386(t *testing.T) {
|
||||
|
||||
cost := svc.computeTokenBreakdown(pricing, tokens, 1.0, "", false)
|
||||
|
||||
wantTextInput := float64(19) * 5e-6 // 0.000095
|
||||
wantImageInput := float64(352) * 8e-6 // 0.002816
|
||||
wantTextInput := float64(19) * 5e-6 // 0.000095
|
||||
wantImageInput := float64(352) * 8e-6 // 0.002816
|
||||
wantImageOutput := float64(439) * 30e-6 // 0.013170
|
||||
require.InDelta(t, wantTextInput, cost.InputCost, 1e-15, "InputCost 仅含文本输入")
|
||||
require.InDelta(t, wantImageInput, cost.ImageInputCost, 1e-15, "图片输入按 $8/1M 独立计费")
|
||||
@@ -1166,6 +1166,25 @@ func TestGetModelPricing_Grok45OfficialFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelPricing_Grok46OfficialFallback(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
for _, model := range []string{"grok-4.6", "grok-4.6-latest"} {
|
||||
model := model
|
||||
t.Run(model, func(t *testing.T) {
|
||||
pricing, err := svc.GetModelPricing(model)
|
||||
require.NoError(t, err)
|
||||
require.InDelta(t, 2e-6, pricing.InputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 6e-6, pricing.OutputPricePerToken, 1e-12)
|
||||
require.InDelta(t, 0.5e-6, pricing.CacheReadPricePerToken, 1e-12)
|
||||
require.Equal(t, 200000, pricing.LongContextInputThreshold)
|
||||
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
|
||||
require.InDelta(t, 2.0, pricing.LongContextOutputMultiplier, 1e-12)
|
||||
require.False(t, pricing.SupportsCacheBreakdown)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetModelPricing_GrokCatalogFallbacks(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
|
||||
@@ -632,7 +632,8 @@ func normalizeGrokReasoningEffortValue(raw string) (string, bool) {
|
||||
func grokSupportsReasoningEffort(model string) bool {
|
||||
model = strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model)))
|
||||
switch model {
|
||||
case xai.DefaultTextModel, "grok-4.5-latest", "grok-4.3", "grok-4.3-latest",
|
||||
case xai.DefaultTextModel, "grok-4.5-latest", "grok-4.6", "grok-4.6-latest",
|
||||
"grok-4.3", "grok-4.3-latest",
|
||||
"grok-3-mini", "grok-3-mini-fast", "grok-4.20-0309-reasoning",
|
||||
"grok-4.20-reasoning", "grok-4.20-multi-agent-0309":
|
||||
return true
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -495,8 +496,17 @@ func grokChatResponsesCacheIntentBody(body []byte) ([]byte, error) {
|
||||
return json.Marshal(root)
|
||||
}
|
||||
|
||||
func grokChatResponsesBridgeModel(model string) bool {
|
||||
switch strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(model))) {
|
||||
case "grok-4.5", "grok-4.6", "grok-4.6-latest":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity string) bool {
|
||||
return strings.TrimSpace(upstreamModel) == "grok-4.5" && strings.TrimSpace(cacheIdentity) != ""
|
||||
return grokChatResponsesBridgeModel(upstreamModel) && strings.TrimSpace(cacheIdentity) != ""
|
||||
}
|
||||
|
||||
// forwardGrokChatCompletionsViaResponses converts a strictly compatible Chat
|
||||
@@ -527,7 +537,7 @@ func (s *OpenAIGatewayService) forwardGrokChatCompletionsViaResponses(
|
||||
// for non-composer models, so they would be silently dropped. Route them to
|
||||
// Responses even when no prompt-cache identity is available.
|
||||
hasImageInput := openAIJSONValueMayContainImageInput(gjson.GetBytes(body, "messages"))
|
||||
if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || strings.TrimSpace(upstreamModel) != "grok-4.5") {
|
||||
if !grokChatResponsesRuntimeEligible(upstreamModel, cacheIdentity) && (!hasImageInput || !grokChatResponsesBridgeModel(upstreamModel)) {
|
||||
return s.forwardAsRawChatCompletions(ctx, c, account, body, defaultMappedModel)
|
||||
}
|
||||
|
||||
|
||||
@@ -218,9 +218,12 @@ func TestGrokChatResponsesBridgeEligibility(t *testing.T) {
|
||||
func TestGrokChatResponsesRuntimeEligibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.True(t, grokChatResponsesRuntimeEligible("grok-4.5", "isolated-id"))
|
||||
require.True(t, grokChatResponsesRuntimeEligible("grok-4.6", "isolated-id"))
|
||||
require.True(t, grokChatResponsesRuntimeEligible("grok-4.6-latest", "isolated-id"))
|
||||
require.False(t, grokChatResponsesRuntimeEligible("grok-4.3", "isolated-id"))
|
||||
require.False(t, grokChatResponsesRuntimeEligible("grok-4.5-build-free", "isolated-id"))
|
||||
require.False(t, grokChatResponsesRuntimeEligible("grok-4.5", ""))
|
||||
require.False(t, grokChatResponsesRuntimeEligible("grok-4.6", ""))
|
||||
}
|
||||
|
||||
func TestForwardGrokChatViaResponsesNonStreamingCachesAndReturnsChat(t *testing.T) {
|
||||
|
||||
@@ -56,6 +56,8 @@ func TestPatchGrokResponsesBodySanitizesComposerReasoningParameters(t *testing.T
|
||||
{name: "composer legacy alias", upstreamModel: "composer-2.5"},
|
||||
{name: "provider-prefixed composer", upstreamModel: "xai/grok-composer-2.5-fast"},
|
||||
{name: "grok 4.5", upstreamModel: "grok-4.5", wantReasoning: true},
|
||||
{name: "grok 4.6", upstreamModel: "grok-4.6", wantReasoning: true},
|
||||
{name: "grok 4.6 latest", upstreamModel: "grok-4.6-latest", wantReasoning: true},
|
||||
}
|
||||
|
||||
bodyTemplate := []byte(`{
|
||||
|
||||
@@ -46,6 +46,8 @@ describe('useModelWhitelist', () => {
|
||||
it('xAI 模型列表包含 Grok 4.5 官方模型和别名', () => {
|
||||
const models = getModelsByPlatform('grok')
|
||||
|
||||
expect(models).toContain('grok-4.6')
|
||||
expect(models).toContain('grok-4.6-latest')
|
||||
expect(models).toContain('grok-4.5')
|
||||
expect(models).toContain('grok-4.5-latest')
|
||||
expect(models).toContain('grok-build-latest')
|
||||
|
||||
@@ -136,6 +136,7 @@ const metaModels = [
|
||||
|
||||
// xAI Grok
|
||||
const xaiModels = [
|
||||
'grok-4.6',
|
||||
'grok-4.5',
|
||||
'grok-4.3',
|
||||
'grok-build-0.1',
|
||||
@@ -147,6 +148,7 @@ const xaiModels = [
|
||||
'grok-4.20-multi-agent-latest',
|
||||
'grok-4.3-latest',
|
||||
'grok-latest',
|
||||
'grok-4.6-latest',
|
||||
'grok-4.5-latest',
|
||||
'grok-build-latest',
|
||||
'composer-2.5',
|
||||
@@ -303,6 +305,7 @@ const geminiPresetMappings = [
|
||||
]
|
||||
|
||||
const grokPresetMappings = [
|
||||
{ label: 'Grok 4.6', from: 'grok-4.6', to: 'grok-4.6', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' },
|
||||
{ label: 'Grok 4.5', from: 'grok-4.5', to: 'grok-4.5', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' },
|
||||
{ label: 'Grok 4.3', from: 'grok-4.3', to: 'grok-4.3', color: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800/50 dark:text-slate-300' },
|
||||
{ label: 'Grok Latest', from: 'grok-latest', to: 'grok-4.5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
|
||||
|
||||
Reference in new issue
Block a user