fix grok usage guard for compatible accounts

This commit is contained in:
Rain
2026-08-05 17:59:20 +08:00
parent 5c52fa93d5
commit ba92d70422
5 changed files with 113 additions and 2 deletions
@@ -463,7 +463,7 @@ func (s *OpenAIGatewayService) handleChatBufferedStreamingResponse(
// When the terminal event has an empty output array, reconstruct from
// accumulated delta events so the client receives the full content.
acc.SupplementResponseOutput(finalResponse)
if account != nil && account.Platform == PlatformGrok && !hasBillableOpenAIUsage(usage) {
if requiresBillableGrokChatUsage(account, originalModel, billingModel, upstreamModel, finalResponse.Model) && !hasBillableOpenAIUsage(usage) {
return nil, s.newOpenAIStreamFailoverError(
c,
account,
@@ -431,7 +431,8 @@ func (s *OpenAIGatewayService) bufferRawChatCompletions(
if parsedUsage, ok := extractOpenAIUsageFromJSONBytes(respBody); ok {
usage = parsedUsage
}
if account != nil && account.Platform == PlatformGrok && !hasBillableOpenAIUsage(usage) {
responseModel := gjson.GetBytes(respBody, "model").String()
if requiresBillableGrokChatUsage(account, originalModel, billingModel, upstreamModel, responseModel) && !hasBillableOpenAIUsage(usage) {
return nil, s.newOpenAIStreamFailoverError(
c,
account,
@@ -14,6 +14,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
@@ -122,6 +123,41 @@ func TestForwardAsRawChatCompletions_ForcesStreamUsageUpstreamAndPassesUsageDown
require.Contains(t, rec.Body.String(), "data: [DONE]")
}
func TestForwardAsChatCompletions_OpenAICompatibleGrokRawMissingUsageFailsBeforeWrite(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"model":"grok-4.5","messages":[{"role":"user","content":"hello"}],"stream":false}`)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(body))
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"application/json"},
"X-Request-Id": []string{"rid-openai-compat-grok-no-usage"},
},
Body: io.NopCloser(strings.NewReader(
`{"id":"resp_missing_usage","object":"chat.completion","model":"grok-4.5","choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":"stop"}]}`,
)),
}}
svc := &OpenAIGatewayService{
cfg: rawChatCompletionsTestConfig(),
httpUpstream: upstream,
}
account := rawChatCompletionsTestAccount()
account.Name = "openai-compatible-grok"
account.Extra = map[string]any{openai_compat.ExtraKeyResponsesSupported: false}
result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body, "", "")
require.Nil(t, result)
var failoverErr *UpstreamFailoverError
require.ErrorAs(t, err, &failoverErr)
require.False(t, c.Writer.Written(), "unbilled Grok content must not be returned by an OpenAI-compatible account")
require.Empty(t, recorder.Body.String())
}
func TestForwardAsRawChatCompletions_PreservesMappedGPT56MaxEffort(t *testing.T) {
gin.SetMode(gin.TestMode)
@@ -14,3 +14,19 @@ func hasBillableOpenAIUsage(usage OpenAIUsage) bool {
usage.CacheReadInputTokens > 0 ||
usage.ImageOutputTokens > 0
}
// requiresBillableGrokChatUsage identifies Grok traffic by both account
// platform and model identity. Grok models may be served through generic
// OpenAI-compatible accounts, so account.Platform alone is not a safe billing
// boundary.
func requiresBillableGrokChatUsage(account *Account, models ...string) bool {
if account != nil && account.Platform == PlatformGrok {
return true
}
for _, model := range models {
if platform, ok := DetectModelPlatform(model); ok && platform == PlatformGrok {
return true
}
}
return false
}
@@ -0,0 +1,58 @@
//go:build unit
package service
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRequiresBillableGrokChatUsage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
account *Account
models []string
want bool
}{
{
name: "grok platform",
account: &Account{Platform: PlatformGrok},
models: []string{"alias"},
want: true,
},
{
name: "OpenAI-compatible requested Grok model",
account: &Account{Platform: PlatformOpenAI},
models: []string{"grok-4.5"},
want: true,
},
{
name: "OpenAI-compatible mapped Grok model",
account: &Account{Platform: PlatformOpenAI},
models: []string{"alias", "grok-4.5"},
want: true,
},
{
name: "xAI-qualified Grok model",
account: &Account{Platform: PlatformOpenAI},
models: []string{"xai/grok-4.5"},
want: true,
},
{
name: "ordinary OpenAI model",
account: &Account{Platform: PlatformOpenAI},
models: []string{"gpt-5.4"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, requiresBillableGrokChatUsage(tt.account, tt.models...))
})
}
}