mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 15:38:22 +08:00
校正 Grok 模型目录计费与工具出站
This commit is contained in:
@@ -57,9 +57,10 @@ const DefaultTextModel = "grok-4.5"
|
||||
const (
|
||||
DefaultImagineImageQualityModel = "grok-imagine-image-quality"
|
||||
DefaultImagineImageFastModel = "grok-imagine-image"
|
||||
DefaultImagineImage20Model = "grok-imagine-image-2.0"
|
||||
DefaultImagineVideoModel = "grok-imagine-video"
|
||||
DefaultImagineVideo15LegacyModel = "grok-imagine-video-1.5"
|
||||
DefaultImagineVideo15Model = "grok-imagine-video-1.5-preview"
|
||||
DefaultImagineVideo15Model = "grok-imagine-video-1.5"
|
||||
DefaultImagineVideo15LegacyModel = "grok-imagine-video-1.5-preview"
|
||||
)
|
||||
|
||||
// ModelMappingOptions controls optional expansions of the default mapping.
|
||||
@@ -86,8 +87,6 @@ var defaultModels = []Model{
|
||||
{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"},
|
||||
{ID: "grok-3-mini-fast", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 3 Mini Fast"},
|
||||
{ID: "grok-build-0.1", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Build 0.1"},
|
||||
{ID: "grok-composer-2.5-fast", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Composer 2.5 Fast"},
|
||||
{ID: "grok-4.20-0309-reasoning", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.20 Reasoning"},
|
||||
@@ -96,9 +95,9 @@ var defaultModels = []Model{
|
||||
// Imagine
|
||||
{ID: DefaultImagineImageQualityModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image Quality"},
|
||||
{ID: DefaultImagineImageFastModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image"},
|
||||
{ID: DefaultImagineImage20Model, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image 2.0"},
|
||||
{ID: DefaultImagineVideoModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video"},
|
||||
{ID: DefaultImagineVideo15Model, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video 1.5 Preview"},
|
||||
{ID: DefaultImagineVideo15LegacyModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video 1.5 Legacy"},
|
||||
{ID: DefaultImagineVideo15Model, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video 1.5"},
|
||||
}
|
||||
|
||||
// grokTextResponsesModelAliases is the source of truth for Grok text models
|
||||
@@ -109,8 +108,8 @@ var grokTextResponsesModelAliases = map[string]string{
|
||||
"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.5": "grok-4.5",
|
||||
"grok-4.5-latest": "grok-4.5",
|
||||
"grok-4.3": "grok-4.3",
|
||||
"grok-4.3-latest": "grok-4.3",
|
||||
"grok-3-mini": "grok-3-mini",
|
||||
@@ -162,7 +161,7 @@ func ModelMappingWithOptions(opts ModelMappingOptions) map[string]string {
|
||||
}
|
||||
for alias, canonical := range grokTextResponsesModelAliases {
|
||||
// Remap aliases that pointed at DefaultTextModel constant to runtime default.
|
||||
if canonical == DefaultTextModel {
|
||||
if (alias == "grok" || alias == "grok-latest" || alias == "grok-build-latest") && canonical == DefaultTextModel {
|
||||
mapping[alias] = defaultText
|
||||
} else {
|
||||
mapping[alias] = canonical
|
||||
@@ -179,7 +178,7 @@ func ModelMappingWithOptions(opts ModelMappingOptions) map[string]string {
|
||||
// Keep official IDs as identity so client-requested model strings are not
|
||||
// rewritten on the wire (pricing still canonicalizes 1.5* via CanonicalImagineVideoModel).
|
||||
mapping["grok-imagine-video"] = DefaultImagineVideoModel
|
||||
mapping["grok-imagine-video-1.5"] = DefaultImagineVideo15LegacyModel
|
||||
mapping["grok-imagine-video-1.5"] = DefaultImagineVideo15Model
|
||||
mapping["grok-imagine-video-1.5-preview"] = DefaultImagineVideo15Model
|
||||
// Informal alias only:
|
||||
mapping["grok-video-1.5"] = DefaultImagineVideo15Model
|
||||
@@ -273,7 +272,7 @@ func ResolveGrokTextResponsesModelID(model string, defaultText ...string) string
|
||||
}
|
||||
normalized := strings.ToLower(StripGrokProviderPrefix(trimmed))
|
||||
if canonical, ok := grokTextResponsesModelAliases[normalized]; ok {
|
||||
if canonical == DefaultTextModel {
|
||||
if (normalized == "grok" || normalized == "grok-latest" || normalized == "grok-build-latest") && canonical == DefaultTextModel {
|
||||
return fallback
|
||||
}
|
||||
return canonical
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestDefaultModelMappingExcludesCrossClientWildcards(t *testing.T) {
|
||||
require.Equal(t, "grok-build-0.1", mapping["grok-build"])
|
||||
require.Equal(t, DefaultTextModel, mapping["grok-build-latest"])
|
||||
require.Equal(t, DefaultImagineImageQualityModel, mapping["grok-imagine-edit"])
|
||||
require.Equal(t, DefaultImagineVideo15LegacyModel, mapping["grok-imagine-video-1.5"])
|
||||
require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5"])
|
||||
require.Equal(t, DefaultImagineVideo15Model, mapping["grok-imagine-video-1.5-preview"])
|
||||
require.Equal(t, "grok-4.5", mapping["xai/grok"])
|
||||
|
||||
@@ -72,3 +72,9 @@ func TestResolveGrokTextResponsesModelID(t *testing.T) {
|
||||
require.Equal(t, "grok-4.3", ResolveGrokTextResponsesModelID("grok", "grok-4.3"))
|
||||
require.Equal(t, "grok-4.20-multi-agent-0309", ResolveGrokTextResponsesModelID("grok-4.20-multi-agent"))
|
||||
}
|
||||
|
||||
func TestExplicitGrok45DoesNotFollowRuntimeDefault(t *testing.T) {
|
||||
require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID("grok-4.5", "grok-4.6"))
|
||||
require.Equal(t, "grok-4.5", ResolveGrokTextResponsesModelID("grok-4.5-latest", "grok-4.6"))
|
||||
require.Equal(t, "grok-4.6", ResolveGrokTextResponsesModelID("grok", "grok-4.6"))
|
||||
}
|
||||
|
||||
@@ -704,6 +704,21 @@ func (s *BillingService) initFallbackPricing() {
|
||||
LongContextInputMultiplier: 2,
|
||||
LongContextOutputMultiplier: 2,
|
||||
}
|
||||
|
||||
// Keep legacy Grok 3 Mini requests on their own historical xAI price card;
|
||||
// otherwise the generic Grok fallback bills them as Grok 4.5.
|
||||
s.fallbackPrices["grok-3-mini"] = &ModelPricing{
|
||||
InputPricePerToken: 0.30e-6,
|
||||
OutputPricePerToken: 0.50e-6,
|
||||
CacheReadPricePerToken: 0.075e-6,
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
s.fallbackPrices["grok-3-mini-fast"] = &ModelPricing{
|
||||
InputPricePerToken: 0.60e-6,
|
||||
OutputPricePerToken: 4e-6,
|
||||
CacheReadPricePerToken: 0.15e-6,
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
// xAI Grok Build 0.1 (official docs: $1 input / $0.20 cached input /
|
||||
// $2 output per MTok). Composer is available only through Grok Build and
|
||||
// has no standalone public API rate card, so its aliases use this coding
|
||||
@@ -913,6 +928,10 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
||||
return s.fallbackPrices["grok-4.5"]
|
||||
case "grok-4.6", "grok-4.6-latest":
|
||||
return s.fallbackPrices["grok-4.6"]
|
||||
case "grok-3-mini":
|
||||
return s.fallbackPrices["grok-3-mini"]
|
||||
case "grok-3-mini-fast":
|
||||
return s.fallbackPrices["grok-3-mini-fast"]
|
||||
case "grok-4.3",
|
||||
"grok-4.20-0309-reasoning",
|
||||
"grok-4.20-0309-non-reasoning",
|
||||
|
||||
@@ -173,11 +173,14 @@ func (s *OpenAIGatewayService) forwardGrokResponses(
|
||||
markGrokTeamModelRateLimit(account, upstreamModel, resolveGrokTeamRateLimitUntil(time.Now().Add(grokTeamRateLimitDefaultTTL), time.Now()))
|
||||
}
|
||||
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
|
||||
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
|
||||
return nil, &UpstreamFailoverError{
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseBody: respBody,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseBody: respBody,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
RetryableOnSameAccount: retryable,
|
||||
SameAccountRetryDelay: retryDelay,
|
||||
SameAccountRetryDeadline: retryDeadline,
|
||||
}
|
||||
}
|
||||
return s.handleErrorResponse(ctx, resp, c, account, patchedBody, upstreamModel)
|
||||
@@ -899,7 +902,14 @@ func sanitizeGrokResponsesTools(body []byte) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
if !tools.IsArray() {
|
||||
return body, nil
|
||||
// xAI rejects tool_choice when tools is null/object. Treat malformed
|
||||
// tool collections as absent at egress rather than forwarding a pair
|
||||
// that cannot be interpreted by the Grok Responses endpoint.
|
||||
body, err := sjson.DeleteBytes(body, "tools")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sjson.DeleteBytes(body, "tool_choice")
|
||||
}
|
||||
|
||||
rawTools := tools.Array()
|
||||
@@ -1173,11 +1183,14 @@ func (s *OpenAIGatewayService) describeGrokComposerImage(
|
||||
})
|
||||
s.handleGrokAccountUpstreamError(withGrokTeamRateLimitModel(ctx, grokComposerImageBridgeVisionModel), account, resp.StatusCode, resp.Header, respBody)
|
||||
if s.shouldFailoverGrokUpstreamError(resp.StatusCode, respBody) {
|
||||
retryable, retryDelay, retryDeadline := grokSameAccountRetryMetadata(account, resp.StatusCode, respBody)
|
||||
return "", OpenAIUsage{}, &UpstreamFailoverError{
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseBody: respBody,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
RetryableOnSameAccount: account.IsPoolMode() && account.IsPoolModeRetryableStatus(resp.StatusCode),
|
||||
StatusCode: resp.StatusCode,
|
||||
ResponseBody: respBody,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
RetryableOnSameAccount: retryable,
|
||||
SameAccountRetryDelay: retryDelay,
|
||||
SameAccountRetryDeadline: retryDeadline,
|
||||
}
|
||||
}
|
||||
return "", OpenAIUsage{}, fmt.Errorf("grok composer image bridge upstream error: %s", upstreamMsg)
|
||||
@@ -1314,6 +1327,7 @@ func buildGrokResponsesRequest(ctx context.Context, c *gin.Context, account *Acc
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(WithHTTPUpstreamProfile(req.Context(), HTTPUpstreamProfileGrok))
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||
|
||||
@@ -406,10 +406,10 @@ func TestSanitizeGrokResponsesToolsKeepsToolChoiceOnlyWithSupportedTools(t *test
|
||||
wantToolChoice: true,
|
||||
},
|
||||
{
|
||||
name: "malformed non-array tools remain untouched",
|
||||
name: "malformed non-array tools are removed",
|
||||
body: `{"input":"hello","tools":{"type":"function","name":"lookup"},"tool_choice":"auto"}`,
|
||||
wantTools: true,
|
||||
wantToolChoice: true,
|
||||
wantTools: false,
|
||||
wantToolChoice: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user