校正 Grok 模型目录计费与工具出站

This commit is contained in:
IanShaw
2026-08-20 01:03:29 -07:00
parent 5ade094318
commit ed4207a16f
5 changed files with 62 additions and 24 deletions
+10 -11
View File
@@ -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
+7 -1
View File
@@ -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,
},
}