Files
sub2api/backend/internal/service/grok_upstream_url.go
T
shaw 221581400b feat(grok): 支持账号级自定义上游地址与请求头覆写
将账号级请求头覆写从 anthropic/openai 的 api_key 账号扩展到 Grok 的
api_key 与 oauth 账号,并放开 Grok OAuth 账号的自定义上游地址(仅作用于
转发端点,授权与 token 刷新链路不变)。

后端:
- IsHeaderOverrideEligible 扩展到 Grok(api_key+oauth),禁止名单新增
  x-grok-conv-id(逐请求会话路由头)。
- 所有 Grok 上游请求路径接线 ApplyHeaderOverrides(Responses/Chat 桥/
  媒体/配额探测/billing 探测/连通性测试),统一置于内置默认头之后。
- GetGrokBaseURL/GetGrokMediaBaseURL 放开 OAuth 自定义地址:官方地址
  视同未定制回落官方网关,仅显式第三方 host 改发转发流量。
- 非官方 host 允许任意 path 前缀,官方 host 仍强制 /v1。
- OAuth base_url 校验按 host 判定官方/自定义,自定义 host 恒受运营方
  URL 策略约束,不受 XAI_ALLOW_UNSAFE_URL_OVERRIDES 调试开关放宽。
- 给 GrokQuotaService 注入 config,使配额/billing 探测与转发共用同一
  URL 策略。

前端:
- Edit 模态为 Grok OAuth 账号新增「自定义上游地址」开关。
- 请求头表单新增 JSON 快速导入与按 JSON 一键复制(复用 useClipboard,
  兼容非安全上下文 HTTP 页面)。
- 门控改为平台×类型判定,Bulk 批量 base_url 增加格式校验,zh/en 文案同步。
2026-07-15 20:30:23 +08:00

119 lines
4.3 KiB
Go

package service
import (
"errors"
"fmt"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
)
func grokBaseURLValidator(account *Account, cfg *config.Config) (xai.BaseURLValidator, error) {
if account == nil || !account.IsGrok() {
return nil, fmt.Errorf("grok account is required")
}
switch account.Type {
case AccountTypeOAuth:
// Official gateway hosts are always trusted and always usable, even when
// the operator enables a restrictive URL allowlist. A custom forwarding
// host is vetted by the same operator policy as API-key accounts.
//
// The official-vs-custom decision is made on the host, not via
// ValidateTrustedBaseURL: that validator relaxes to accept-any under the
// XAI_ALLOW_UNSAFE_URL_OVERRIDES debug switch, which must never let an
// OAuth bearer token reach an arbitrary custom host.
policyValidator := grokOperatorPolicyValidator(cfg)
return redactedGrokBaseURLValidator(func(raw string) (string, error) {
if xai.IsOfficialBaseURL(raw) {
return xai.ValidateTrustedBaseURL(raw)
}
return policyValidator(raw)
}), nil
case AccountTypeAPIKey:
return redactedGrokBaseURLValidator(grokOperatorPolicyValidator(cfg)), nil
default:
return nil, fmt.Errorf("unsupported grok account type: %s", account.Type)
}
}
// grokOperatorPolicyValidator 按全局出站 URL 安全策略校验自定义 base_url:
// 白名单开启时强制 UpstreamHosts;关闭时仅做格式校验(HTTP 允许与否跟随配置)。
func grokOperatorPolicyValidator(cfg *config.Config) xai.BaseURLValidator {
if cfg == nil {
return xai.ValidateBaseURL
}
if !cfg.Security.URLAllowlist.Enabled {
return func(raw string) (string, error) {
return urlvalidator.ValidateURLFormat(raw, cfg.Security.URLAllowlist.AllowInsecureHTTP)
}
}
return func(raw string) (string, error) {
return urlvalidator.ValidateHTTPSURL(raw, urlvalidator.ValidationOptions{
AllowedHosts: cfg.Security.URLAllowlist.UpstreamHosts,
RequireAllowlist: true,
AllowPrivate: cfg.Security.URLAllowlist.AllowPrivateHosts,
})
}
}
func redactedGrokBaseURLValidator(validator xai.BaseURLValidator) xai.BaseURLValidator {
return func(raw string) (string, error) {
validated, err := validator(raw)
if err != nil {
return "", errors.New("base URL rejected by URL security policy")
}
return validated, nil
}
}
func buildGrokResponsesURL(account *Account, cfg *config.Config) (string, error) {
validator, err := grokBaseURLValidator(account, cfg)
if err != nil {
return "", err
}
return xai.BuildResponsesURLWithValidator(account.GetGrokBaseURL(), validator)
}
func buildGrokChatCompletionsURL(account *Account, cfg *config.Config) (string, error) {
validator, err := grokBaseURLValidator(account, cfg)
if err != nil {
return "", err
}
return xai.BuildChatCompletionsURLWithValidator(account.GetGrokBaseURL(), validator)
}
// buildGrokBillingURL 解析 billing 探测端点:跟随账号的转发 base_url,
// 未定制的账号仍指向官方 CLI 网关。
func buildGrokBillingURL(account *Account, cfg *config.Config, weekly bool) (string, error) {
validator, err := grokBaseURLValidator(account, cfg)
if err != nil {
return "", err
}
return xai.BuildBillingURLWithValidator(account.GetGrokBaseURL(), weekly, validator)
}
func buildGrokMediaURL(account *Account, cfg *config.Config, endpoint GrokMediaEndpoint, requestID string) (string, error) {
validator, err := grokBaseURLValidator(account, cfg)
if err != nil {
return "", err
}
baseURL := account.GetGrokMediaBaseURL()
switch endpoint {
case GrokMediaEndpointImagesGenerations:
return xai.BuildImagesGenerationsURLWithValidator(baseURL, validator)
case GrokMediaEndpointImagesEdits:
return xai.BuildImagesEditsURLWithValidator(baseURL, validator)
case GrokMediaEndpointVideosGenerations:
return xai.BuildVideosGenerationsURLWithValidator(baseURL, validator)
case GrokMediaEndpointVideosEdits:
return xai.BuildVideosEditsURLWithValidator(baseURL, validator)
case GrokMediaEndpointVideosExtensions:
return xai.BuildVideosExtensionsURLWithValidator(baseURL, validator)
case GrokMediaEndpointVideoStatus:
return xai.BuildVideoURLWithValidator(baseURL, requestID, validator)
default:
return "", fmt.Errorf("unsupported grok media endpoint: %s", endpoint)
}
}