Merge pull request #6139 from xz-dev/fix/configurable-model-list-read-limit

feat(gateway): configure model list read limit
This commit is contained in:
Wesley Liddick
2026-08-24 15:04:50 +08:00
committed by GitHub
14 changed files with 240 additions and 31 deletions
+1 -1
View File
@@ -194,7 +194,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
groupCapacityService := service.NewGroupCapacityService(accountRepository, groupRepository, concurrencyService, sessionLimitCache, rpmCache)
groupHandler := admin.NewGroupHandler(adminService, dashboardService, groupCapacityService)
claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository, configConfig)
grokQuotaFetcher := service.NewGrokQuotaFetcher()
grokQuotaService := service.ProvideGrokQuotaService(accountRepository, proxyRepository, grokTokenProvider, httpUpstream, configConfig, usageLogRepository, settingService)
openAIQuotaService := service.ProvideOpenAIQuotaService(accountRepository, proxyRepository, openAITokenProvider, privacyClientFactory, openAIGatewayService)
+10
View File
@@ -61,6 +61,10 @@ const (
// 可通过 gateway.upstream_response_read_max_bytes 配置项覆盖。
const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
// DefaultModelsListReadMaxBytes 上游模型列表响应体的默认读取上限。
// 可通过 gateway.models_list_read_max_bytes 配置项覆盖。
const DefaultModelsListReadMaxBytes int64 = 8 * 1024 * 1024
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig `mapstructure:"log"`
@@ -960,6 +964,8 @@ type GatewayConfig struct {
TextMaxBodySize int64 `mapstructure:"text_max_body_size"`
// 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
// 上游模型列表响应体读取上限(字节)
ModelsListReadMaxBytes int64 `mapstructure:"models_list_read_max_bytes"`
// 代理探测响应体读取上限(字节)
ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
// Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
@@ -2456,6 +2462,7 @@ func setDefaults() {
viper.SetDefault("gateway.max_body_size", int64(256*1024*1024))
viper.SetDefault("gateway.text_max_body_size", int64(32*1024*1024))
viper.SetDefault("gateway.upstream_response_read_max_bytes", DefaultUpstreamResponseReadMaxBytes)
viper.SetDefault("gateway.models_list_read_max_bytes", DefaultModelsListReadMaxBytes)
viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
viper.SetDefault("gateway.gemini_debug_response_headers", false)
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
@@ -3265,6 +3272,9 @@ func (c *Config) Validate() error {
if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
}
if c.Gateway.ModelsListReadMaxBytes <= 0 {
return fmt.Errorf("gateway.models_list_read_max_bytes must be positive")
}
if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
}
+12
View File
@@ -23,6 +23,13 @@ func resetViperWithJWTSecret(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
}
func TestLoadDefaultModelsListReadMaxBytes(t *testing.T) {
resetViperWithJWTSecret(t)
cfg, err := Load()
require.NoError(t, err)
require.Equal(t, DefaultModelsListReadMaxBytes, cfg.Gateway.ModelsListReadMaxBytes)
}
func TestLoadTimezonePrecedence(t *testing.T) {
tests := []struct {
name string
@@ -1803,6 +1810,11 @@ func TestValidateConfigErrors(t *testing.T) {
mutate: func(c *Config) { c.Gateway.TextMaxBodySize = c.Gateway.MaxBodySize + 1 },
wantErr: "gateway.text_max_body_size",
},
{
name: "gateway models list read limit",
mutate: func(c *Config) { c.Gateway.ModelsListReadMaxBytes = 0 },
wantErr: "gateway.models_list_read_max_bytes",
},
{
name: "gateway response header timeout",
mutate: func(c *Config) { c.Gateway.ResponseHeaderTimeout = -1 },
+7 -6
View File
@@ -255,8 +255,6 @@ const (
proxyTLSHandshakeTimeout = 5 * time.Second
// clientTimeout 整体请求超时(含连接、发送、等待响应、读取 body)
clientTimeout = 10 * time.Second
// fetchAvailableModelsBodyLimit limits model-list responses to avoid unbounded memory use.
fetchAvailableModelsBodyLimit int64 = 8 << 20
)
func NewClient(proxyURL string) (*Client, error) {
@@ -656,10 +654,13 @@ type FetchAvailableModelsResponse struct {
// FetchAvailableModels 获取可用模型和配额信息,返回解析后的结构体和原始 JSON
// 支持 URL fallback:sandbox → daily → prod
func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectID string) (*FetchAvailableModelsResponse, map[string]any, error) {
func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectID string, bodyLimit int64) (*FetchAvailableModelsResponse, map[string]any, error) {
if c == nil || c.httpClient == nil {
return nil, nil, errors.New("antigravity client is not configured")
}
if bodyLimit <= 0 {
return nil, nil, errors.New("fetchAvailableModels body limit must be positive")
}
reqBody := FetchAvailableModelsRequest{Project: projectID}
bodyBytes, err := json.Marshal(reqBody)
@@ -693,13 +694,13 @@ func (c *Client) FetchAvailableModels(ctx context.Context, accessToken, projectI
return nil, nil, lastErr
}
respBodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, fetchAvailableModelsBodyLimit+1))
respBodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, bodyLimit+1))
_ = resp.Body.Close() // 立即关闭,避免循环内 defer 导致的资源泄漏
if err != nil {
return nil, nil, fmt.Errorf("读取响应失败: %w", err)
}
if int64(len(respBodyBytes)) > fetchAvailableModelsBodyLimit {
return nil, nil, fmt.Errorf("响应超过 %d 字节", fetchAvailableModelsBodyLimit)
if int64(len(respBodyBytes)) > bodyLimit {
return nil, nil, fmt.Errorf("响应超过 %d 字节", bodyLimit)
}
// 检查是否需要 URL 降级
@@ -15,6 +15,8 @@ import (
"time"
)
const defaultFetchAvailableModelsBodyLimit int64 = 8 << 20
// ---------------------------------------------------------------------------
// NewAPIRequestWithURL
// ---------------------------------------------------------------------------
@@ -1517,7 +1519,7 @@ func TestClient_FetchAvailableModels_Success_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := mustNewClient(t, "")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "test-token", "project-abc")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "test-token", "project-abc", defaultFetchAvailableModelsBodyLimit)
if err != nil {
t.Fatalf("FetchAvailableModels 失败: %v", err)
}
@@ -1562,6 +1564,34 @@ func TestClient_FetchAvailableModels_Success_RealCall(t *testing.T) {
}
}
func TestClient_FetchAvailableModels_RejectsNonPositiveBodyLimit(t *testing.T) {
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", 0)
if err == nil || !strings.Contains(err.Error(), "body limit must be positive") {
t.Fatalf("非正读取上限应返回调用错误: %v", err)
}
}
func TestClient_FetchAvailableModels_UsesConfiguredBodyLimit(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"models":{"model-a":{}}}`))
}))
defer server.Close()
withMockBaseURLs(t, []string{server.URL})
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", 8)
if err == nil {
t.Fatal("响应超过配置上限时应返回错误")
}
if !strings.Contains(err.Error(), "响应超过 8 字节") {
t.Fatalf("错误应包含配置上限: %v", err)
}
}
func TestClient_FetchAvailableModels_HTTPError_RealCall(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
@@ -1572,7 +1602,7 @@ func TestClient_FetchAvailableModels_HTTPError_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "bad-token", "proj")
_, _, err := client.FetchAvailableModels(context.Background(), "bad-token", "proj", defaultFetchAvailableModelsBodyLimit)
if err == nil {
t.Fatal("服务器返回 403 时应返回错误")
}
@@ -1592,7 +1622,7 @@ func TestClient_FetchAvailableModels_InvalidJSON_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err == nil {
t.Fatal("无效 JSON 响应应返回错误")
}
@@ -1622,7 +1652,7 @@ func TestClient_FetchAvailableModels_URLFallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := mustNewClient(t, "")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err != nil {
t.Fatalf("FetchAvailableModels 应在 fallback 后成功: %v", err)
}
@@ -1650,7 +1680,7 @@ func TestClient_FetchAvailableModels_AllURLsFail_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err == nil {
t.Fatal("所有 URL 都失败时应返回错误")
}
@@ -1669,7 +1699,7 @@ func TestClient_FetchAvailableModels_ContextCanceled_RealCall(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err := client.FetchAvailableModels(ctx, "token", "proj")
_, _, err := client.FetchAvailableModels(ctx, "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err == nil {
t.Fatal("context 取消时应返回错误")
}
@@ -1686,7 +1716,7 @@ func TestClient_FetchAvailableModels_EmptyModels_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := mustNewClient(t, "")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "token", "proj")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err != nil {
t.Fatalf("FetchAvailableModels 失败: %v", err)
}
@@ -1748,7 +1778,7 @@ func TestClient_FetchAvailableModels_404Fallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := mustNewClient(t, "")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj", defaultFetchAvailableModelsBodyLimit)
if err != nil {
t.Fatalf("FetchAvailableModels 应在 404 fallback 后成功: %v", err)
}
@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
)
@@ -28,11 +29,12 @@ const (
// AntigravityQuotaFetcher 从 Antigravity API 获取额度
type AntigravityQuotaFetcher struct {
proxyRepo ProxyRepository
cfg *config.Config
}
// NewAntigravityQuotaFetcher 创建 AntigravityQuotaFetcher
func NewAntigravityQuotaFetcher(proxyRepo ProxyRepository) *AntigravityQuotaFetcher {
return &AntigravityQuotaFetcher{proxyRepo: proxyRepo}
func NewAntigravityQuotaFetcher(proxyRepo ProxyRepository, cfg *config.Config) *AntigravityQuotaFetcher {
return &AntigravityQuotaFetcher{proxyRepo: proxyRepo, cfg: cfg}
}
// CanFetch 检查是否可以获取此账户的额度
@@ -55,7 +57,7 @@ func (f *AntigravityQuotaFetcher) FetchQuota(ctx context.Context, account *Accou
}
// 调用 API 获取配额
modelsResp, modelsRaw, err := client.FetchAvailableModels(ctx, accessToken, projectID)
modelsResp, modelsRaw, err := client.FetchAvailableModels(ctx, accessToken, projectID, resolveModelsListReadLimit(f.cfg))
if err != nil {
// 403 Forbidden: 不报错,返回 is_forbidden 标记
var forbiddenErr *antigravity.ForbiddenError
@@ -3,11 +3,16 @@
package service
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
)
@@ -390,6 +395,36 @@ func TestBuildUsageInfo_AICredits(t *testing.T) {
require.Equal(t, 5.0, info.AICredits[0].MinimumBalance)
}
func TestFetchQuotaUsesConfiguredModelsListBodyLimit(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"models":{"model-a":{}}}`))
}))
defer server.Close()
oldBaseURLs := append([]string(nil), antigravity.BaseURLs...)
oldAvailability := antigravity.DefaultURLAvailability
t.Cleanup(func() {
antigravity.BaseURLs = oldBaseURLs
antigravity.DefaultURLAvailability = oldAvailability
})
antigravity.BaseURLs = []string{server.URL}
antigravity.DefaultURLAvailability = antigravity.NewURLAvailability(time.Minute)
cfg := &config.Config{}
cfg.Gateway.ModelsListReadMaxBytes = 8
fetcher := NewAntigravityQuotaFetcher(nil, cfg)
_, err := fetcher.FetchQuota(context.Background(), &Account{
Platform: PlatformAntigravity,
Type: AccountTypeOAuth,
Credentials: map[string]any{
"access_token": "token",
"project_id": "project",
},
}, "")
require.ErrorContains(t, err, "响应超过 8 字节")
}
func TestFetchQuota_ForbiddenReturnsIsForbidden(t *testing.T) {
// 模拟 FetchQuota 遇到 403 时的行为:
// FetchAvailableModels 返回 ForbiddenError → FetchQuota 应返回 is_forbidden=true
@@ -0,0 +1,10 @@
package service
import "github.com/Wei-Shaw/sub2api/internal/config"
func resolveModelsListReadLimit(cfg *config.Config) int64 {
if cfg != nil && cfg.Gateway.ModelsListReadMaxBytes > 0 {
return cfg.Gateway.ModelsListReadMaxBytes
}
return config.DefaultModelsListReadMaxBytes
}
@@ -0,0 +1,25 @@
package service
import (
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
func TestResolveModelsListReadLimit(t *testing.T) {
t.Run("nil config uses default", func(t *testing.T) {
require.Equal(t, config.DefaultModelsListReadMaxBytes, resolveModelsListReadLimit(nil))
})
t.Run("configured limit wins", func(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.ModelsListReadMaxBytes = 16 << 20
require.Equal(t, int64(16<<20), resolveModelsListReadLimit(cfg))
})
t.Run("non-positive limit uses default", func(t *testing.T) {
cfg := &config.Config{}
require.Equal(t, config.DefaultModelsListReadMaxBytes, resolveModelsListReadLimit(cfg))
})
}
@@ -27,12 +27,11 @@ import (
var chatgptCodexModelsURL = "https://chatgpt.com/backend-api/codex/models"
const (
codexModelsManifestBodyLimit int64 = 8 << 20
codexModelsManifestCacheBodyLimit = 1 << 20
codexModelsManifestCacheMaxEntries = 64
codexModelsManifestCacheTTL = 30 * time.Second
codexModelsManifestCacheStaleTTL = 5 * time.Minute
codexModelsManifestRequestTimeout = 15 * time.Second
codexModelsManifestCacheBodyLimit = 1 << 20
codexModelsManifestCacheMaxEntries = 64
codexModelsManifestCacheTTL = 30 * time.Second
codexModelsManifestCacheStaleTTL = 5 * time.Minute
codexModelsManifestRequestTimeout = 15 * time.Second
)
// CodexModelsManifest carries the client representation plus caching metadata.
@@ -515,13 +514,20 @@ func (s *OpenAIGatewayService) fetchCodexModelsManifestUpstream(ctx context.Cont
}
}
body, err := io.ReadAll(io.LimitReader(resp.Body, codexModelsManifestBodyLimit))
bodyLimit := resolveModelsListReadLimit(s.cfg)
body, err := io.ReadAll(io.LimitReader(resp.Body, bodyLimit+1))
if err != nil {
return nil, &codexModelsManifestUpstreamError{
err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "read codex models manifest response: %v", err),
retryable: isRetryableCodexModelsManifestTransportError(err),
}
}
if int64(len(body)) > bodyLimit {
return nil, &codexModelsManifestUpstreamError{
err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "codex models manifest response exceeds %d bytes", bodyLimit),
retryable: true,
}
}
upstreamBody := body
if request.useAPIKeyUpstream {
body = convertOpenAIModelListToCodexManifest(body)
@@ -1,6 +1,7 @@
package service
import (
"bytes"
"context"
"errors"
"fmt"
@@ -623,6 +624,51 @@ func TestConvertOpenAIModelListToCodexManifest(t *testing.T) {
}
}
func TestFetchCodexModelsManifestUsesConfiguredBodyLimit(t *testing.T) {
upstream := &codexModelsHTTPUpstreamStub{do: func(_ *http.Request, _ string, _ int64, _ int) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"models":[{"slug":"gpt-5.6"}]}`)),
}, nil
}}
s := newCodexModelsAPIKeyTestService(upstream)
s.cfg.Gateway.ModelsListReadMaxBytes = 8
_, err := s.FetchCodexModelsManifest(
context.Background(),
newCodexModelsAPIKeyTestAccount("https://upstream.example"),
"0.144.0",
"",
)
require.Error(t, err)
require.Equal(t, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", infraerrors.Reason(err))
require.Contains(t, err.Error(), "response exceeds 8 bytes")
require.True(t, IsRetryableCodexModelsManifestError(err))
}
func TestFetchCodexModelsManifestAcceptsConfiguredLimitAboveLegacyBoundary(t *testing.T) {
manifestBody := `{"models":[{"slug":"gpt-5.6","display_name":"` + strings.Repeat("x", (8<<20)+1024) + `"}]}`
require.Greater(t, len(manifestBody), 8<<20)
require.Less(t, len(manifestBody), 16<<20)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, manifestBody)
}))
defer server.Close()
original := chatgptCodexModelsURL
chatgptCodexModelsURL = server.URL
defer func() { chatgptCodexModelsURL = original }()
s := &OpenAIGatewayService{cfg: &config.Config{}}
s.cfg.Gateway.ModelsListReadMaxBytes = 16 << 20
manifest, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "0.144.0", "")
require.NoError(t, err)
require.True(t, bytes.Equal([]byte(manifestBody), manifest.Body), "manifest body must be returned intact")
}
func TestFetchCodexModelsManifestRejectsInvalidEnvelope(t *testing.T) {
tests := []struct {
name string
+10 -6
View File
@@ -14,8 +14,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
)
const upstreamModelsBodyLimit int64 = 8 << 20
// UpstreamModelSyncErrorKind classifies model sync failures for safe HTTP mapping.
type UpstreamModelSyncErrorKind string
@@ -101,12 +99,13 @@ func (s *AccountTestService) FetchUpstreamSupportedModels(ctx context.Context, a
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(io.LimitReader(resp.Body, upstreamModelsBodyLimit+1))
bodyLimit := resolveModelsListReadLimit(s.cfg)
body, err := io.ReadAll(io.LimitReader(resp.Body, bodyLimit+1))
if err != nil {
return nil, newUpstreamModelSyncUpstreamError("Failed to read upstream model list", err)
}
if int64(len(body)) > upstreamModelsBodyLimit {
return nil, newUpstreamModelSyncUpstreamError("Upstream model list response is too large", fmt.Errorf("response exceeds %d bytes", upstreamModelsBodyLimit))
if int64(len(body)) > bodyLimit {
return nil, newUpstreamModelSyncUpstreamError("Upstream model list response is too large", fmt.Errorf("response exceeds %d bytes", bodyLimit))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
@@ -506,7 +505,12 @@ func (s *AccountTestService) fetchAntigravityOAuthUpstreamModels(ctx context.Con
if err != nil {
return nil, newUpstreamModelSyncConfigError("Failed to configure Antigravity client", err)
}
modelsResp, _, err := client.FetchAvailableModels(ctx, accessToken, strings.TrimSpace(account.GetCredential("project_id")))
modelsResp, _, err := client.FetchAvailableModels(
ctx,
accessToken,
strings.TrimSpace(account.GetCredential("project_id")),
resolveModelsListReadLimit(s.cfg),
)
if err != nil {
return nil, newUpstreamModelSyncUpstreamError("Failed to fetch Antigravity available models", err)
}
@@ -394,6 +394,31 @@ func TestFetchUpstreamSupportedModelsParsesOpenAIResponse(t *testing.T) {
require.Equal(t, "Bearer openai-key", upstream.lastReq.Header.Get("Authorization"))
}
func TestFetchUpstreamSupportedModelsUsesConfiguredBodyLimit(t *testing.T) {
t.Parallel()
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"gpt-5"}]}`)),
}}
cfg := upstreamModelSyncTestConfig()
cfg.Gateway.ModelsListReadMaxBytes = 8
svc := &AccountTestService{httpUpstream: upstream, cfg: cfg}
_, err := svc.FetchUpstreamSupportedModels(context.Background(), &Account{
ID: 7,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "openai-key",
"base_url": "https://openai.example.com/v1",
},
})
require.Error(t, err)
require.Contains(t, err.Error(), "response exceeds 8 bytes")
}
func TestFetchUpstreamSupportedModelsParsesGrokAPIKeyResponse(t *testing.T) {
t.Parallel()
+3
View File
@@ -235,6 +235,9 @@ gateway:
# Max bytes to read for non-stream upstream responses (default: 8MB)
# 非流式上游响应体读取上限(默认 8MB)
upstream_response_read_max_bytes: 8388608
# Max bytes to read for upstream model-list responses (default: 8MB)
# 上游模型列表响应体读取上限(默认 8MB)
models_list_read_max_bytes: 8388608
# Max bytes to read for proxy probe responses (default: 1MB)
# 代理探测响应体读取上限(默认 1MB)
proxy_probe_response_read_max_bytes: 1048576