mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:48:43 +08:00
fix(grok): restore base URL resolution and operator settings wiring
Honor account GetGrokBaseURLOr policy for official vs custom endpoints, and wire settings resolution used by responses/chat URL builders.
This commit is contained in:
@@ -193,11 +193,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
|
||||
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
|
||||
grokQuotaFetcher := service.NewGrokQuotaFetcher()
|
||||
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository)
|
||||
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository, settingService)
|
||||
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIGatewayService)
|
||||
usageCache := service.NewUsageCache()
|
||||
accountUsageService := service.ProvideAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, grokQuotaFetcher, grokQuotaService, openAIQuotaService, usageCache, identityCache, tlsFingerprintProfileService, openAIGatewayService)
|
||||
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService)
|
||||
accountTestService := service.ProvideAccountTestService(accountRepository, geminiTokenProvider, claudeTokenProvider, grokTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService, openAIGatewayService, settingService)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||
accountHandler := admin.ProvideAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, grokOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator, grokQuotaService)
|
||||
adminAnnouncementHandler := admin.NewAnnouncementHandler(announcementService)
|
||||
|
||||
@@ -374,6 +374,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
|
||||
GrokDefaultTextModel: settings.GrokDefaultTextModel,
|
||||
GrokCrossClientModelMapEnabled: settings.GrokCrossClientModelMapEnabled,
|
||||
GrokDefaultBaseURLMode: settings.GrokDefaultBaseURLMode,
|
||||
|
||||
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||
|
||||
|
||||
@@ -66,6 +66,18 @@ func TestUpdateSettingsSMTPFromAliasIsWritable(t *testing.T) {
|
||||
require.Equal(t, "new@example.com", repo.values[service.SettingKeySMTPFrom])
|
||||
}
|
||||
|
||||
func TestUpdateSettingsGrokDefaultBaseURLModeIsWritable(t *testing.T) {
|
||||
h, repo := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyGrokDefaultBaseURLMode: service.GrokDefaultBaseURLModeCLI,
|
||||
})
|
||||
|
||||
rec := doUpdateSettings(t, h, map[string]any{
|
||||
"grok_default_base_url_mode": service.GrokDefaultBaseURLModeEUWest1,
|
||||
}, nil)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, service.GrokDefaultBaseURLModeEUWest1, repo.values[service.SettingKeyGrokDefaultBaseURLMode])
|
||||
}
|
||||
|
||||
func TestUpdateSettingsRejectsTwoCaptchaProviders(t *testing.T) {
|
||||
h, _ := newStepUpSwitchTestHandler(t, map[string]string{
|
||||
service.SettingKeyTurnstileEnabled: "true",
|
||||
|
||||
@@ -333,6 +333,7 @@ type UpdateSettingsRequest struct {
|
||||
// Grok model mapping policy
|
||||
GrokDefaultTextModel *string `json:"grok_default_text_model"`
|
||||
GrokCrossClientModelMapEnabled *bool `json:"grok_cross_client_model_map_enabled"`
|
||||
GrokDefaultBaseURLMode *string `json:"grok_default_base_url_mode"`
|
||||
|
||||
// Available Channels feature switch (user-facing)
|
||||
AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
|
||||
@@ -1880,6 +1881,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.GrokCrossClientModelMapEnabled
|
||||
}(),
|
||||
GrokDefaultBaseURLMode: func() string {
|
||||
if req.GrokDefaultBaseURLMode != nil {
|
||||
return strings.TrimSpace(*req.GrokDefaultBaseURLMode)
|
||||
}
|
||||
return previousSettings.GrokDefaultBaseURLMode
|
||||
}(),
|
||||
AvailableChannelsEnabled: func() bool {
|
||||
if req.AvailableChannelsEnabled != nil {
|
||||
return *req.AvailableChannelsEnabled
|
||||
@@ -2315,6 +2322,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
|
||||
GrokDefaultTextModel: updatedSettings.GrokDefaultTextModel,
|
||||
GrokCrossClientModelMapEnabled: updatedSettings.GrokCrossClientModelMapEnabled,
|
||||
GrokDefaultBaseURLMode: updatedSettings.GrokDefaultBaseURLMode,
|
||||
|
||||
AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
|
||||
|
||||
@@ -2324,11 +2332,11 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
|
||||
AffiliateEnabled: updatedSettings.AffiliateEnabled,
|
||||
|
||||
RiskControlEnabled: updatedSettings.RiskControlEnabled,
|
||||
CyberSessionBlockEnabled: updatedSettings.CyberSessionBlockEnabled,
|
||||
CyberSessionBlockTTLSeconds: updatedSettings.CyberSessionBlockTTLSeconds,
|
||||
AccountSchedulingThresholds: updatedSettings.AccountSchedulingThresholds,
|
||||
AllowUserViewErrorRequests: updatedSettings.AllowUserViewErrorRequests,
|
||||
RiskControlEnabled: updatedSettings.RiskControlEnabled,
|
||||
CyberSessionBlockEnabled: updatedSettings.CyberSessionBlockEnabled,
|
||||
CyberSessionBlockTTLSeconds: updatedSettings.CyberSessionBlockTTLSeconds,
|
||||
AccountSchedulingThresholds: updatedSettings.AccountSchedulingThresholds,
|
||||
AllowUserViewErrorRequests: updatedSettings.AllowUserViewErrorRequests,
|
||||
}
|
||||
if fastPolicy, err := h.settingService.GetOpenAIFastPolicySettings(c.Request.Context()); err != nil {
|
||||
slog.Error("openai_fast_policy_settings_get_failed", "error", err)
|
||||
|
||||
@@ -306,6 +306,7 @@ type SystemSettings struct {
|
||||
// Grok model mapping policy (admin settings; empty account mapping falls back to these).
|
||||
GrokDefaultTextModel string `json:"grok_default_text_model"`
|
||||
GrokCrossClientModelMapEnabled bool `json:"grok_cross_client_model_map_enabled"`
|
||||
GrokDefaultBaseURLMode string `json:"grok_default_base_url_mode"`
|
||||
|
||||
// Available Channels feature switch (user-facing aggregate view)
|
||||
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -1310,25 +1311,67 @@ func (a *Account) GetOpenAIRefreshToken() string {
|
||||
// traffic (OAuth authorization and token refresh) always uses the official
|
||||
// auth endpoints regardless of this value.
|
||||
func (a *Account) GetGrokBaseURL() string {
|
||||
if !a.IsGrok() {
|
||||
if a == nil || !a.IsGrok() {
|
||||
return ""
|
||||
}
|
||||
baseURL := strings.TrimSpace(a.GetCredential("base_url"))
|
||||
if a.IsGrokOAuth() {
|
||||
// Operators switch subscription traffic between the official CLI
|
||||
// gateway, the official/regional API hosts and third-party relays
|
||||
// (individual endpoints go down from time to time), so a stored
|
||||
// value is always honored as-is. Only empty or unparseable values
|
||||
// fall back to the default CLI gateway.
|
||||
if baseURL == "" || !xai.IsParseableBaseURL(baseURL) {
|
||||
return xai.DefaultCLIBaseURL
|
||||
return a.GetGrokBaseURLOr(xai.DefaultCLIBaseURL)
|
||||
}
|
||||
return a.GetGrokBaseURLOr(xai.DefaultBaseURL)
|
||||
}
|
||||
|
||||
// GetGrokBaseURLOr resolves an explicit account endpoint, falling back to the
|
||||
// supplied default. Official OAuth endpoints are normalized here; custom
|
||||
// endpoints are retained for the request builder's operator URL policy.
|
||||
func (a *Account) GetGrokBaseURLOr(defaultBaseURL string) string {
|
||||
if a == nil || !a.IsGrok() {
|
||||
return ""
|
||||
}
|
||||
defaultBaseURL = strings.TrimRight(strings.TrimSpace(defaultBaseURL), "/")
|
||||
if defaultBaseURL == "" {
|
||||
if a.IsGrokOAuth() {
|
||||
defaultBaseURL = xai.DefaultCLIBaseURL
|
||||
} else {
|
||||
defaultBaseURL = xai.DefaultBaseURL
|
||||
}
|
||||
}
|
||||
baseURL := strings.TrimSpace(a.GetCredential("base_url"))
|
||||
if baseURL == "" {
|
||||
return defaultBaseURL
|
||||
}
|
||||
if !a.IsGrokOAuth() {
|
||||
return baseURL
|
||||
}
|
||||
if baseURL != "" {
|
||||
return baseURL
|
||||
// Explicit regional/API or custom values remain pinned. Custom endpoints are checked by the
|
||||
// operator URL policy at the request builder, which has access to config.
|
||||
if validated, err := xai.ValidateTrustedBaseURL(baseURL); err == nil {
|
||||
return validated
|
||||
}
|
||||
return xai.DefaultBaseURL
|
||||
if parsed, err := url.Parse(baseURL); err == nil && parsed.Scheme != "" && parsed.Host != "" &&
|
||||
parsed.User == nil && parsed.RawQuery == "" && parsed.Fragment == "" {
|
||||
return strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
return defaultBaseURL
|
||||
}
|
||||
|
||||
func isOfficialGrokAPIBaseURL(raw string) bool {
|
||||
return isOfficialGrokBaseURL(raw, xai.DefaultBaseURL)
|
||||
}
|
||||
|
||||
func isOfficialGrokBaseURL(raw, expected string) bool {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
if err != nil || parsed == nil || parsed.Opaque != "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return false
|
||||
}
|
||||
want, err := url.Parse(expected)
|
||||
if err != nil || !strings.EqualFold(parsed.Scheme, want.Scheme) || !strings.EqualFold(parsed.Hostname(), want.Hostname()) {
|
||||
return false
|
||||
}
|
||||
if port := parsed.Port(); port != "" && port != "443" {
|
||||
return false
|
||||
}
|
||||
path := strings.TrimRight(parsed.Path, "/")
|
||||
return path == "" || path == strings.TrimRight(want.Path, "/")
|
||||
}
|
||||
|
||||
// GetGrokMediaBaseURL selects the upstream used by Grok Imagine APIs.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type grokBaseURLSettingRepoStub struct{ values map[string]string }
|
||||
|
||||
func (r *grokBaseURLSettingRepoStub) GetValue(_ context.Context, key string) (string, error) {
|
||||
if value, ok := r.values[key]; ok {
|
||||
return value, nil
|
||||
}
|
||||
return "", fmt.Errorf("setting %s not found", key)
|
||||
}
|
||||
func (r *grokBaseURLSettingRepoStub) Get(context.Context, string) (*Setting, error) {
|
||||
return nil, fmt.Errorf("unused")
|
||||
}
|
||||
func (r *grokBaseURLSettingRepoStub) Set(context.Context, string, string) error { return nil }
|
||||
func (r *grokBaseURLSettingRepoStub) GetMultiple(context.Context, []string) (map[string]string, error) {
|
||||
return r.values, nil
|
||||
}
|
||||
func (r *grokBaseURLSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (r *grokBaseURLSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
|
||||
return r.values, nil
|
||||
}
|
||||
func (r *grokBaseURLSettingRepoStub) Delete(context.Context, string) error { return nil }
|
||||
|
||||
func TestGrokBaseURLForMode(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
mode string
|
||||
want string
|
||||
}{
|
||||
{"api", xai.DefaultBaseURL},
|
||||
{"us-east-1", xai.DefaultUSEast1BaseURL},
|
||||
{"us-west-2", xai.DefaultUSWest2BaseURL},
|
||||
{"eu-west-1", xai.DefaultEUWest1BaseURL},
|
||||
{"cli", xai.DefaultCLIBaseURL},
|
||||
{"invalid", xai.DefaultCLIBaseURL},
|
||||
} {
|
||||
t.Run(tc.mode, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, GrokBaseURLForMode(tc.mode))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingServiceResolveGrokBaseURLHonorsModeAndExplicitPins(t *testing.T) {
|
||||
repo := &grokBaseURLSettingRepoStub{values: map[string]string{SettingKeyGrokDefaultBaseURLMode: GrokDefaultBaseURLModeUSWest2}}
|
||||
svc := NewSettingService(repo, nil)
|
||||
account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{}}
|
||||
require.Equal(t, xai.DefaultUSWest2BaseURL, svc.ResolveGrokBaseURL(context.Background(), account))
|
||||
|
||||
// An explicit official endpoint remains pinned.
|
||||
account.Credentials["base_url"] = xai.DefaultBaseURL
|
||||
require.Equal(t, xai.DefaultBaseURL, svc.ResolveGrokBaseURL(context.Background(), account))
|
||||
|
||||
// An explicit regional pin remains authoritative.
|
||||
account.Credentials["base_url"] = xai.DefaultEUWest1BaseURL
|
||||
require.Equal(t, xai.DefaultEUWest1BaseURL, svc.ResolveGrokBaseURL(context.Background(), account))
|
||||
}
|
||||
|
||||
func TestAccountGetGrokBaseURLOrPreservesCustomOAuthURLForPolicyValidation(t *testing.T) {
|
||||
account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{
|
||||
"base_url": "https://attacker.invalid/v1",
|
||||
}}
|
||||
require.Equal(t, "https://attacker.invalid/v1", account.GetGrokBaseURLOr(xai.DefaultCLIBaseURL))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -68,20 +69,28 @@ func redactedGrokBaseURLValidator(validator xai.BaseURLValidator) xai.BaseURLVal
|
||||
}
|
||||
}
|
||||
|
||||
func buildGrokResponsesURL(account *Account, cfg *config.Config) (string, error) {
|
||||
func buildGrokResponsesURL(account *Account, cfg *config.Config, settings ...*SettingService) (string, error) {
|
||||
validator, err := grokBaseURLValidator(account, cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return xai.BuildResponsesURLWithValidator(account.GetGrokBaseURL(), validator)
|
||||
baseURL := account.GetGrokBaseURL()
|
||||
if len(settings) > 0 && settings[0] != nil {
|
||||
baseURL = settings[0].ResolveGrokBaseURL(context.Background(), account)
|
||||
}
|
||||
return xai.BuildResponsesURLWithValidator(baseURL, validator)
|
||||
}
|
||||
|
||||
func buildGrokChatCompletionsURL(account *Account, cfg *config.Config) (string, error) {
|
||||
func buildGrokChatCompletionsURL(account *Account, cfg *config.Config, settings ...*SettingService) (string, error) {
|
||||
validator, err := grokBaseURLValidator(account, cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return xai.BuildChatCompletionsURLWithValidator(account.GetGrokBaseURL(), validator)
|
||||
baseURL := account.GetGrokBaseURL()
|
||||
if len(settings) > 0 && settings[0] != nil {
|
||||
baseURL = settings[0].ResolveGrokBaseURL(context.Background(), account)
|
||||
}
|
||||
return xai.BuildChatCompletionsURLWithValidator(baseURL, validator)
|
||||
}
|
||||
|
||||
// buildGrokBillingURL 解析 billing 探测端点:跟随账号的转发 base_url,
|
||||
@@ -91,7 +100,14 @@ func buildGrokBillingURL(account *Account, cfg *config.Config, weekly bool) (str
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return xai.BuildBillingURLWithValidator(account.GetGrokBaseURL(), weekly, validator)
|
||||
baseURL := account.GetGrokBaseURL()
|
||||
// Official public/regional API hosts do not expose Grok Build billing.
|
||||
// Keep custom relays on their configured host because they may proxy the CLI
|
||||
// billing path alongside inference.
|
||||
if xai.IsOfficialBaseURL(baseURL) && !isGrokCLIProxyBaseURL(baseURL) {
|
||||
baseURL = xai.DefaultCLIBaseURL
|
||||
}
|
||||
return xai.BuildBillingURLWithValidator(baseURL, weekly, validator)
|
||||
}
|
||||
|
||||
func buildGrokMediaURL(account *Account, cfg *config.Config, endpoint GrokMediaEndpoint, requestID string) (string, error) {
|
||||
|
||||
@@ -255,6 +255,26 @@ func TestGrokOAuthURLPolicy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildGrokBillingURLUsesCLIForOfficialAPIHosts(t *testing.T) {
|
||||
for _, baseURL := range []string{xai.DefaultBaseURL, "https://us-west-2.api.x.ai/v1"} {
|
||||
account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"base_url": baseURL}}
|
||||
|
||||
weekly, err := buildGrokBillingURL(account, &config.Config{}, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, xai.DefaultCLIBaseURL+xai.BillingWeeklyPath, weekly)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGrokBillingURLKeepsCustomRelay(t *testing.T) {
|
||||
account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{
|
||||
"base_url": "https://relay.example.test/xai/v1",
|
||||
}}
|
||||
|
||||
monthly, err := buildGrokBillingURL(account, &config.Config{}, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://relay.example.test/xai/v1"+xai.BillingMonthlyPath, monthly)
|
||||
}
|
||||
|
||||
func TestGrokBillingURLFollowsAccountBaseURL(t *testing.T) {
|
||||
t.Run("oauth default stays on CLI gateway", func(t *testing.T) {
|
||||
account := &Account{
|
||||
|
||||
@@ -191,6 +191,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
// Grok: safe defaults — no cross-vendor model rewrite unless operators enable it.
|
||||
SettingKeyGrokDefaultTextModel: "grok-4.5",
|
||||
SettingKeyGrokCrossClientModelMapEnabled: "false",
|
||||
SettingKeyGrokDefaultBaseURLMode: GrokDefaultBaseURLModeCLI,
|
||||
|
||||
// Available channels feature (default disabled; opt-in)
|
||||
SettingKeyAvailableChannelsEnabled: "false",
|
||||
@@ -796,6 +797,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
result.GrokDefaultTextModel = "grok-4.5"
|
||||
}
|
||||
result.GrokCrossClientModelMapEnabled = settings[SettingKeyGrokCrossClientModelMapEnabled] == "true"
|
||||
result.GrokDefaultBaseURLMode = normalizeGrokDefaultBaseURLMode(settings[SettingKeyGrokDefaultBaseURLMode])
|
||||
|
||||
// Available channels feature (default: disabled; strict true)
|
||||
result.AvailableChannelsEnabled = settings[SettingKeyAvailableChannelsEnabled] == "true"
|
||||
|
||||
@@ -5,13 +5,83 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
const (
|
||||
GrokDefaultBaseURLModeAPI = "api"
|
||||
GrokDefaultBaseURLModeUSEast1 = "us-east-1"
|
||||
GrokDefaultBaseURLModeUSWest2 = "us-west-2"
|
||||
GrokDefaultBaseURLModeEUWest1 = "eu-west-1"
|
||||
GrokDefaultBaseURLModeCLI = "cli"
|
||||
)
|
||||
|
||||
func normalizeGrokDefaultBaseURLMode(mode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case GrokDefaultBaseURLModeAPI:
|
||||
return GrokDefaultBaseURLModeAPI
|
||||
case GrokDefaultBaseURLModeUSEast1:
|
||||
return GrokDefaultBaseURLModeUSEast1
|
||||
case GrokDefaultBaseURLModeUSWest2:
|
||||
return GrokDefaultBaseURLModeUSWest2
|
||||
case GrokDefaultBaseURLModeEUWest1:
|
||||
return GrokDefaultBaseURLModeEUWest1
|
||||
case GrokDefaultBaseURLModeCLI:
|
||||
return GrokDefaultBaseURLModeCLI
|
||||
default:
|
||||
return GrokDefaultBaseURLModeCLI
|
||||
}
|
||||
}
|
||||
|
||||
func GrokBaseURLForMode(mode string) string {
|
||||
switch normalizeGrokDefaultBaseURLMode(mode) {
|
||||
case GrokDefaultBaseURLModeAPI:
|
||||
return xai.DefaultBaseURL
|
||||
case GrokDefaultBaseURLModeUSEast1:
|
||||
return xai.DefaultUSEast1BaseURL
|
||||
case GrokDefaultBaseURLModeUSWest2:
|
||||
return xai.DefaultUSWest2BaseURL
|
||||
case GrokDefaultBaseURLModeEUWest1:
|
||||
return xai.DefaultEUWest1BaseURL
|
||||
default:
|
||||
return xai.DefaultCLIBaseURL
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingService) GetGrokDefaultBaseURLMode(ctx context.Context) string {
|
||||
if s == nil || s.settingRepo == nil {
|
||||
return GrokDefaultBaseURLModeCLI
|
||||
}
|
||||
dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), gatewayForwardingDBTimeout)
|
||||
defer cancel()
|
||||
raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyGrokDefaultBaseURLMode)
|
||||
if err != nil {
|
||||
return GrokDefaultBaseURLModeCLI
|
||||
}
|
||||
return normalizeGrokDefaultBaseURLMode(raw)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetGrokDefaultBaseURL(ctx context.Context) string {
|
||||
return GrokBaseURLForMode(s.GetGrokDefaultBaseURLMode(ctx))
|
||||
}
|
||||
|
||||
func (s *SettingService) ResolveGrokBaseURL(ctx context.Context, account *Account) string {
|
||||
def := xai.DefaultCLIBaseURL
|
||||
if s != nil {
|
||||
def = s.GetGrokDefaultBaseURL(ctx)
|
||||
}
|
||||
if account == nil {
|
||||
return def
|
||||
}
|
||||
return account.GetGrokBaseURLOr(def)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRegistrationDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
|
||||
ErrSettingNotFound = infraerrors.NotFound("SETTING_NOT_FOUND", "setting not found")
|
||||
|
||||
@@ -422,6 +422,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
||||
updates[SettingKeyGrokDefaultTextModel] = "grok-4.5"
|
||||
}
|
||||
updates[SettingKeyGrokCrossClientModelMapEnabled] = strconv.FormatBool(settings.GrokCrossClientModelMapEnabled)
|
||||
updates[SettingKeyGrokDefaultBaseURLMode] = normalizeGrokDefaultBaseURLMode(settings.GrokDefaultBaseURLMode)
|
||||
|
||||
// Available channels feature switch
|
||||
updates[SettingKeyAvailableChannelsEnabled] = strconv.FormatBool(settings.AvailableChannelsEnabled)
|
||||
|
||||
@@ -202,6 +202,7 @@ type SystemSettings struct {
|
||||
// Grok model mapping policy (admin settings; empty mapping falls back to these).
|
||||
GrokDefaultTextModel string `json:"grok_default_text_model"`
|
||||
GrokCrossClientModelMapEnabled bool `json:"grok_cross_client_model_map_enabled"`
|
||||
GrokDefaultBaseURLMode string `json:"grok_default_base_url_mode"`
|
||||
|
||||
// Available Channels feature (user-facing aggregate view)
|
||||
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||
@@ -375,6 +376,7 @@ type PublicSettings struct {
|
||||
// Grok model mapping policy (admin settings).
|
||||
GrokDefaultTextModel string `json:"grok_default_text_model"`
|
||||
GrokCrossClientModelMapEnabled bool `json:"grok_cross_client_model_map_enabled"`
|
||||
GrokDefaultBaseURLMode string `json:"grok_default_base_url_mode"`
|
||||
|
||||
// Available Channels feature (user-facing aggregate view)
|
||||
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||
|
||||
@@ -229,6 +229,7 @@ func ProvideAccountTestService(
|
||||
cfg *config.Config,
|
||||
tlsFPProfileService *TLSFingerprintProfileService,
|
||||
openAIGatewayService *OpenAIGatewayService,
|
||||
settingService *SettingService,
|
||||
) *AccountTestService {
|
||||
service := NewAccountTestService(
|
||||
accountRepo,
|
||||
@@ -241,6 +242,7 @@ func ProvideAccountTestService(
|
||||
tlsFPProfileService,
|
||||
)
|
||||
service.agentIdentityWS = openAIGatewayService
|
||||
service.SetSettingService(settingService)
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -251,8 +253,11 @@ func ProvideGrokQuotaService(
|
||||
httpUpstream HTTPUpstream,
|
||||
cfg *config.Config,
|
||||
usageLogRepo UsageLogRepository,
|
||||
settingService *SettingService,
|
||||
) *GrokQuotaService {
|
||||
return NewGrokQuotaService(accountRepo, proxyRepo, tokenProvider, httpUpstream, cfg, usageLogRepo)
|
||||
service := NewGrokQuotaService(accountRepo, proxyRepo, tokenProvider, httpUpstream, cfg, usageLogRepo)
|
||||
service.SetSettingService(settingService)
|
||||
return service
|
||||
}
|
||||
|
||||
// ProvideGeminiTokenProvider creates GeminiTokenProvider with OAuthRefreshAPI injection
|
||||
|
||||
@@ -444,9 +444,9 @@ gateway:
|
||||
# Enabled by default because free detection requires an explicit subscription_tier/plan_type of "free".
|
||||
# Stats/query failures fail open so DB issues do not block all Grok traffic.
|
||||
grok:
|
||||
# Experimental admin-only email/password authorization. Disabled by default.
|
||||
# Requires YESCAPTCHA_CLIENT_KEY (or legacy YESCAPTCHA_API_KEY) in the server environment.
|
||||
# TurnstileTaskProxyless uses solver egress independent from the optional account proxy.
|
||||
# Email/password authorization is hidden and hard-disabled. Use SSO cookie,
|
||||
# browser OAuth, or refresh_token re-auth instead. The flag is retained for
|
||||
# config compatibility only and is ignored by the server.
|
||||
password_auth_enabled: false
|
||||
free_quota_soft_gate_enabled: true
|
||||
free_quota_token_limit: 1000000
|
||||
|
||||
@@ -590,6 +590,7 @@ export interface SystemSettings {
|
||||
fallback_model_antigravity: string;
|
||||
grok_default_text_model: string;
|
||||
grok_cross_client_model_map_enabled: boolean;
|
||||
grok_default_base_url_mode: string;
|
||||
|
||||
// Per-platform account auto-pause thresholds (100 = disabled)
|
||||
account_scheduling_thresholds: AccountSchedulingThresholdsMap;
|
||||
@@ -911,6 +912,7 @@ export interface UpdateSettingsRequest {
|
||||
fallback_model_antigravity?: string;
|
||||
grok_default_text_model?: string;
|
||||
grok_cross_client_model_map_enabled?: boolean;
|
||||
grok_default_base_url_mode?: string;
|
||||
account_scheduling_thresholds?: AccountSchedulingThresholdsMap;
|
||||
enable_identity_patch?: boolean;
|
||||
identity_patch_prompt?: string;
|
||||
|
||||
@@ -445,6 +445,13 @@ export default {
|
||||
grokDefaultTextModelHint: 'Used for empty model values and, only when the switch is enabled, requests from other client model namespaces. Custom Grok model IDs are accepted.',
|
||||
grokCrossClientMap: 'Map other clients to Grok',
|
||||
grokCrossClientMapHint: 'Disabled by default. When enabled, GPT, Codex, o-series, and Claude model IDs are routed to the default Grok text model above.',
|
||||
grokDefaultBaseURLMode: 'Default Grok upstream',
|
||||
grokDefaultBaseURLModeHint: 'Used only when a Grok account has no explicit base URL. Media and voice endpoints continue to use their official API hosts.',
|
||||
grokBaseURLModeCLI: 'CLI chat proxy',
|
||||
grokBaseURLModeAPI: 'Public API',
|
||||
grokBaseURLModeUSEast1: 'Regional API (us-east-1)',
|
||||
grokBaseURLModeUSWest2: 'Regional API (us-west-2)',
|
||||
grokBaseURLModeEUWest1: 'Regional API (eu-west-1)',
|
||||
fingerprintUnification: 'Fingerprint Unification',
|
||||
fingerprintUnificationHint: 'Unify X-Stainless-* headers across users sharing the same OAuth account. Disabling passes through each client\'s original headers.',
|
||||
metadataPassthrough: 'Metadata Passthrough',
|
||||
|
||||
@@ -438,6 +438,13 @@ export default {
|
||||
grokDefaultTextModelHint: '用于空模型值;仅在右侧开关开启时也用于其他客户端模型命名空间。允许填写自定义 Grok 模型 ID。',
|
||||
grokCrossClientMap: '映射其他客户端模型到 Grok',
|
||||
grokCrossClientMapHint: '默认关闭。开启后,GPT、Codex、o 系列和 Claude 模型 ID 会路由到左侧默认 Grok 文本模型。',
|
||||
grokDefaultBaseURLMode: '默认 Grok 上游',
|
||||
grokDefaultBaseURLModeHint: '仅用于 Grok 账号未配置显式 base URL 的文本请求;媒体和语音仍使用官方 API 主机。',
|
||||
grokBaseURLModeCLI: 'CLI 聊天代理',
|
||||
grokBaseURLModeAPI: '公共 API',
|
||||
grokBaseURLModeUSEast1: '区域 API(us-east-1)',
|
||||
grokBaseURLModeUSWest2: '区域 API(us-west-2)',
|
||||
grokBaseURLModeEUWest1: '区域 API(eu-west-1)',
|
||||
fingerprintUnification: '指纹统一化',
|
||||
fingerprintUnificationHint: '统一共享同一 OAuth 账号的用户的 X-Stainless-* 请求头。关闭后透传客户端原始请求头。',
|
||||
metadataPassthrough: 'Metadata 透传',
|
||||
|
||||
@@ -991,6 +991,9 @@ export interface TempUnschedulableState {
|
||||
matched_keyword: string
|
||||
rule_index: number
|
||||
error_message: string
|
||||
trigger_count?: number
|
||||
trigger_threshold?: number
|
||||
trigger_window_minutes?: number
|
||||
}
|
||||
|
||||
export interface TempUnschedulableStatus {
|
||||
|
||||
@@ -5178,7 +5178,30 @@
|
||||
data-testid="grok-cross-client-model-map-toggle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label
|
||||
for="grok-default-base-url-mode"
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.settings.gatewayForwarding.grokDefaultBaseURLMode") }}
|
||||
</label>
|
||||
<select
|
||||
id="grok-default-base-url-mode"
|
||||
v-model="form.grok_default_base_url_mode"
|
||||
class="input mt-2 w-full"
|
||||
data-testid="grok-default-base-url-mode"
|
||||
>
|
||||
<option value="cli">{{ t("admin.settings.gatewayForwarding.grokBaseURLModeCLI") }}</option>
|
||||
<option value="api">{{ t("admin.settings.gatewayForwarding.grokBaseURLModeAPI") }}</option>
|
||||
<option value="us-east-1">{{ t("admin.settings.gatewayForwarding.grokBaseURLModeUSEast1") }}</option>
|
||||
<option value="us-west-2">{{ t("admin.settings.gatewayForwarding.grokBaseURLModeUSWest2") }}</option>
|
||||
<option value="eu-west-1">{{ t("admin.settings.gatewayForwarding.grokBaseURLModeEUWest1") }}</option>
|
||||
</select>
|
||||
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.settings.gatewayForwarding.grokDefaultBaseURLModeHint") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Fingerprint Unification -->
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -9520,6 +9543,7 @@ const form = reactive<SettingsForm>({
|
||||
fallback_model_antigravity: "gemini-2.5-pro",
|
||||
grok_default_text_model: "grok-4.5",
|
||||
grok_cross_client_model_map_enabled: false,
|
||||
grok_default_base_url_mode: "cli",
|
||||
// Identity patch (Claude -> Gemini)
|
||||
enable_identity_patch: true,
|
||||
identity_patch_prompt: "",
|
||||
@@ -11102,6 +11126,7 @@ async function saveSettings() {
|
||||
form.grok_default_text_model.trim() || "grok-4.5",
|
||||
grok_cross_client_model_map_enabled:
|
||||
form.grok_cross_client_model_map_enabled,
|
||||
grok_default_base_url_mode: form.grok_default_base_url_mode,
|
||||
enable_identity_patch: form.enable_identity_patch,
|
||||
identity_patch_prompt: form.identity_patch_prompt,
|
||||
min_claude_code_version: form.min_claude_code_version,
|
||||
|
||||
Reference in New Issue
Block a user