refactor(platform-seam): gatewayplatform 接缝——Provider/Registry + 两处 Forward 分发替换

经双视角评审裁决的收窄版 Phase-3 接缝(零行为变更):
- internal/gatewayplatform:Provider{Platform,Forward} 两方法 v1 接口 +
  ForwardRequest{Parsed,Body,IsStickySession,SessionGroupID,SessionKey} +
  构造期注册 Registry(重复 panic,运行期并发只读)
- 3 个单语句直返 adapter(anthropic/antigravity/gemini,错误零包裹——
  BetaBlockedError/PromptTooLong 的 errors.As 链由 T4 特征化守护)
- Messages 端点两处平台分发改 registry 查找;Type!=APIKey 条件保留调用点
- OpenAI(独立 handler)、v1beta 端点、内核 internal/plugin 零触碰

门禁:T1-T5 特征化全绿、安全网 45 包、bench allocs 全部持平(266/329/93/134)。
This commit is contained in:
shaw
2026-06-12 00:27:10 +08:00
parent 57a2e7184b
commit f08f9111e5
11 changed files with 369 additions and 13 deletions
+2 -1
View File
@@ -251,11 +251,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
runtime := plugin.ProvideModuleRuntime(host, pluginConfig)
moduleHandler := admin.NewModuleHandler(runtime)
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, paymentHandler, affiliateHandler, complianceHandler, moduleHandler)
gatewayplatformRegistry := handler.ProvideGatewayPlatformRegistry(gatewayService, antigravityGatewayService)
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
chain := handler.ProvideGatewayHookChain(contentModerationService)
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig)
gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, chain, userMessageQueueService, configConfig, settingService)
gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, gatewayplatformRegistry, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, chain, userMessageQueueService, configConfig, settingService)
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, chain, configConfig)
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo, notificationEmailService)
totpHandler := handler.NewTotpHandler(totpService)
@@ -0,0 +1,91 @@
package gatewayplatform
// 本文件的三个 adapter 仅包装现有 service 的 Forward 入口(实现零移动)。
//
// 错误透传铁律(SEAM-DESIGN 裁决 / T4 特征化锁定):底层返回的 error 一律
// 原样 return,禁止任何包裹——handler 对 *service.BetaBlockedError /
// *service.PromptTooLongError / *service.UpstreamFailoverError 的 errors.As
// 断言链依赖错误原值。
import (
"context"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// geminiForwardAction 是 /v1/messages gemini 分支传给 ForwardGemini 的 action
// 常量(SEAM-DESIGN 裁决:硬编码 "generateContent" + 测试锁定;该参数仅决定
// 客户端响应形态合法性,上游恒为 streamGenerateContent)。
const geminiForwardAction = "generateContent"
// AnthropicProvider 包装 GatewayService.Forward(Claude 协议主路径;
// :794 条件不命中侧,含 antigravity 平台的 APIKey 账号)。
type AnthropicProvider struct {
gateway *service.GatewayService
}
// NewAnthropicProvider 构造 anthropic 平台 Provider。
func NewAnthropicProvider(gateway *service.GatewayService) *AnthropicProvider {
return &AnthropicProvider{gateway: gateway}
}
// Platform 返回 service.PlatformAnthropic。
func (p *AnthropicProvider) Platform() string { return service.PlatformAnthropic }
// Forward 转发到 GatewayService.Forward;error 原样透传。
func (p *AnthropicProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) {
return p.gateway.Forward(ctx, c, account, req.Parsed)
}
// AntigravityProvider 包装 AntigravityGatewayService.Forward(Claude 协议 →
// antigravity 上游;:794 条件命中侧:Platform == Antigravity && Type != APIKey)。
type AntigravityProvider struct {
antigravity *service.AntigravityGatewayService
}
// NewAntigravityProvider 构造 antigravity 平台 Provider。
func NewAntigravityProvider(antigravity *service.AntigravityGatewayService) *AntigravityProvider {
return &AntigravityProvider{antigravity: antigravity}
}
// Platform 返回 service.PlatformAntigravity。
func (p *AntigravityProvider) Platform() string { return service.PlatformAntigravity }
// Forward 转发到 AntigravityGatewayService.Forward;error 原样透传。
func (p *AntigravityProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) {
return p.antigravity.Forward(ctx, c, account, req.Body, req.IsStickySession)
}
// GeminiProvider 包装 AntigravityGatewayService.ForwardGemini(gemini 平台
// 分组下的 antigravity 账号,:444 命中侧;gemini 平台的非 antigravity 账号
// 走 GeminiMessagesCompatService,映射逻辑保留在调用点)。
type GeminiProvider struct {
antigravity *service.AntigravityGatewayService
}
// NewGeminiProvider 构造 gemini 平台 Provider。
func NewGeminiProvider(antigravity *service.AntigravityGatewayService) *GeminiProvider {
return &GeminiProvider{antigravity: antigravity}
}
// Platform 返回 service.PlatformGemini。
func (p *GeminiProvider) Platform() string { return service.PlatformGemini }
// Forward 转发到 AntigravityGatewayService.ForwardGemini;error 原样透传。
// model/stream 取自 req.Parsed(与原调用点 reqModel/reqStream 同源),
// 粘性会话维度经 WithForwardGeminiSession 透传。
func (p *GeminiProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) {
return p.antigravity.ForwardGemini(
ctx,
c,
account,
req.Parsed.Model,
geminiForwardAction,
req.Parsed.Stream,
req.Body,
req.IsStickySession,
service.WithForwardGeminiSession(req.SessionGroupID, req.SessionKey),
)
}
@@ -0,0 +1,52 @@
// Package gatewayplatform 定义 /v1/messages 平台 Forward 分发的 Provider 接缝
// (Phase-3 SEAM-DESIGN v2 裁决记录)。
//
// 设计要点:
// - Provider v1 接口仅 Platform/Forward 两方法(DefaultModels / FallbackModel /
// InvalidateToken 等经评审裁决不进接口);
// - adapter 实现零移动:仅包装现有 service 的 Forward 入口;底层 error 一律
// 原样透传(handler 对 BetaBlockedError / PromptTooLongError 等的
// errors.As 断言链依赖错误原值,禁止任何包裹);
// - Registry 构造期注册、运行期并发只读;平台 → Provider 的映射逻辑
// (如 :794 的 Type != APIKey 条件)保留在调用点,registry 只做查找;
// - OpenAI 走独立 OpenAIGatewayHandler 与独立计费管线,不进本注册表;
// 禁止统一跨平台返回类型(OpenAIForwardResult 载荷计费字段会丢失)。
package gatewayplatform
import (
"context"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// ForwardRequest 是平台 Forward 的统一入参面。字段集为两处分发点现有入参的
// 并集 {Parsed, Body, IsStickySession, SessionGroupID, SessionKey},不为未接入
// 平台虚构维度;各调用点保留自己的入参表达式,按各 Provider 的消费面填充。
type ForwardRequest struct {
// Parsed 为已解析的网关请求(anthropic / gemini Provider 消费;
// gemini Provider 读取 Parsed.Model 与 Parsed.Stream)。
Parsed *service.ParsedRequest
// Body 为转发请求体原文(antigravity / gemini Provider 消费)。
Body []byte
// IsStickySession 表示请求已命中粘性会话绑定(antigravity / gemini
// Provider 消费)。
IsStickySession bool
// SessionGroupID 与 SessionKey 为粘性会话维度(仅 gemini Provider 消费,
// 经 WithForwardGeminiSession 透传到模型限流切换时的粘性绑定清除)。
SessionGroupID int64
SessionKey string
}
// Provider 是单一平台的 Forward 入口。
//
// 契约:
// - Platform 返回 service.Platform* 现有常量,作为 Registry 的查找键;
// - Forward 将请求转发到对应平台 service,error 必须原样透传(禁止包裹)。
type Provider interface {
// Platform 返回该 Provider 服务的平台标识(service.Platform* 常量)。
Platform() string
// Forward 执行平台转发。
Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error)
}
@@ -0,0 +1,38 @@
package gatewayplatform
import "fmt"
// Registry 是 platform → Provider 的只读查找表:构造期一次性注册,
// 构造完成后并发只读(无锁)。
type Registry struct {
providers map[string]Provider
}
// NewRegistry 构造注册表。providers 中出现 nil Provider、空 Platform() 或
// 重复平台时 panic——装配错误必须在启动期(Wire 装配)暴露,而非请求期。
func NewRegistry(providers ...Provider) *Registry {
m := make(map[string]Provider, len(providers))
for _, p := range providers {
if p == nil {
panic("gatewayplatform: nil provider")
}
platform := p.Platform()
if platform == "" {
panic("gatewayplatform: provider with empty platform")
}
if _, dup := m[platform]; dup {
panic(fmt.Sprintf("gatewayplatform: duplicate provider for platform %q", platform))
}
m[platform] = p
}
return &Registry{providers: m}
}
// Get 返回平台对应的 Provider;未注册(或 nil Registry)返回 nil。
// 调用点须保证所查平台已在装配期注册(Wire 注册全集,miss 属编程错误)。
func (r *Registry) Get(platform string) Provider {
if r == nil {
return nil
}
return r.providers[platform]
}
@@ -0,0 +1,112 @@
package gatewayplatform
import (
"context"
"sync"
"testing"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
// stubProvider 是注册表测试用的最小 Provider 实现。
type stubProvider struct {
platform string
}
func (s *stubProvider) Platform() string { return s.platform }
func (s *stubProvider) Forward(_ context.Context, _ *gin.Context, _ *service.Account, _ *ForwardRequest) (*service.ForwardResult, error) {
return nil, nil
}
func TestRegistry_GetReturnsRegisteredProvider(t *testing.T) {
anthropic := &stubProvider{platform: service.PlatformAnthropic}
antigravity := &stubProvider{platform: service.PlatformAntigravity}
gemini := &stubProvider{platform: service.PlatformGemini}
registry := NewRegistry(anthropic, antigravity, gemini)
require.Same(t, Provider(anthropic), registry.Get(service.PlatformAnthropic))
require.Same(t, Provider(antigravity), registry.Get(service.PlatformAntigravity))
require.Same(t, Provider(gemini), registry.Get(service.PlatformGemini))
}
func TestRegistry_GetUnregisteredPlatformReturnsNil(t *testing.T) {
registry := NewRegistry(&stubProvider{platform: service.PlatformAnthropic})
require.Nil(t, registry.Get(service.PlatformOpenAI), "未注册平台必须返回 nil")
require.Nil(t, registry.Get(""), "空平台必须返回 nil")
var nilRegistry *Registry
require.Nil(t, nilRegistry.Get(service.PlatformAnthropic), "nil Registry 必须返回 nil 而非 panic")
}
func TestNewRegistry_DuplicatePlatformPanics(t *testing.T) {
require.PanicsWithValue(t,
`gatewayplatform: duplicate provider for platform "anthropic"`,
func() {
NewRegistry(
&stubProvider{platform: service.PlatformAnthropic},
&stubProvider{platform: service.PlatformAnthropic},
)
})
}
func TestNewRegistry_NilProviderPanics(t *testing.T) {
require.PanicsWithValue(t, "gatewayplatform: nil provider", func() {
NewRegistry(&stubProvider{platform: service.PlatformAnthropic}, nil)
})
}
func TestNewRegistry_EmptyPlatformPanics(t *testing.T) {
require.PanicsWithValue(t, "gatewayplatform: provider with empty platform", func() {
NewRegistry(&stubProvider{platform: ""})
})
}
// TestRegistry_ConcurrentGet 验证构造完成后的注册表可被并发只读访问
// (配合 -race 检测数据竞争)。
func TestRegistry_ConcurrentGet(t *testing.T) {
registry := NewRegistry(
&stubProvider{platform: service.PlatformAnthropic},
&stubProvider{platform: service.PlatformAntigravity},
&stubProvider{platform: service.PlatformGemini},
)
platforms := []string{
service.PlatformAnthropic,
service.PlatformAntigravity,
service.PlatformGemini,
service.PlatformOpenAI, // miss 路径同样并发安全
}
var wg sync.WaitGroup
for i := 0; i < 32; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < 200; j++ {
platform := platforms[(i+j)%len(platforms)]
got := registry.Get(platform)
if platform == service.PlatformOpenAI {
require.Nil(t, got)
} else {
require.NotNil(t, got)
require.Equal(t, platform, got.Platform())
}
}
}(i)
}
wg.Wait()
}
// TestAdapters_PlatformIdentity 锁定三个 adapter 的注册键 = service.Platform* 常量
// (Registry 查找键与 :444/:794 调用点的查找表达式一致)。
func TestAdapters_PlatformIdentity(t *testing.T) {
require.Equal(t, service.PlatformAnthropic, NewAnthropicProvider(nil).Platform())
require.Equal(t, service.PlatformAntigravity, NewAntigravityProvider(nil).Platform())
require.Equal(t, service.PlatformGemini, NewGeminiProvider(nil).Platform())
}
+24 -9
View File
@@ -16,6 +16,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/domain"
"github.com/Wei-Shaw/sub2api/internal/gatewayhook"
"github.com/Wei-Shaw/sub2api/internal/gatewayplatform"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
@@ -42,6 +43,7 @@ type GatewayHandler struct {
gatewayService *service.GatewayService
geminiCompatService *service.GeminiMessagesCompatService
antigravityGatewayService *service.AntigravityGatewayService
platformRegistry *gatewayplatform.Registry
userService *service.UserService
billingCacheService *service.BillingCacheService
usageService *service.UsageService
@@ -62,6 +64,7 @@ func NewGatewayHandler(
gatewayService *service.GatewayService,
geminiCompatService *service.GeminiMessagesCompatService,
antigravityGatewayService *service.AntigravityGatewayService,
platformRegistry *gatewayplatform.Registry,
userService *service.UserService,
concurrencyService *service.ConcurrencyService,
billingCacheService *service.BillingCacheService,
@@ -97,6 +100,7 @@ func NewGatewayHandler(
gatewayService: gatewayService,
geminiCompatService: geminiCompatService,
antigravityGatewayService: antigravityGatewayService,
platformRegistry: platformRegistry,
userService: userService,
billingCacheService: billingCacheService,
usageService: usageService,
@@ -442,16 +446,20 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover
writerSizeBeforeForward := c.Writer.Size()
if account.Platform == service.PlatformAntigravity {
result, err = h.antigravityGatewayService.ForwardGemini(
// gemini provider → AntigravityGatewayService.ForwardGemini:
// model/stream 经 Parsed 透传(与原 reqModel/reqStream 同源),
// action 由 adapter 硬编码 "generateContent"。
result, err = h.platformRegistry.Get(service.PlatformGemini).Forward(
requestCtx,
c,
account,
reqModel,
"generateContent",
reqStream,
body,
hasBoundSession,
service.WithForwardGeminiSession(derefGroupID(apiKey.GroupID), sessionKey),
&gatewayplatform.ForwardRequest{
Parsed: parsedReq,
Body: body,
IsStickySession: hasBoundSession,
SessionGroupID: derefGroupID(apiKey.GroupID),
SessionKey: sessionKey,
},
)
} else {
result, err = h.geminiCompatService.Forward(requestCtx, c, account, body)
@@ -791,10 +799,17 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
}
// 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover
writerSizeBeforeForward := c.Writer.Size()
// 平台 → Provider 的映射逻辑保留在调用点:antigravity 平台的
// APIKey 账号走 anthropic provider(Claude 协议直连上游)。
forwardReq := &gatewayplatform.ForwardRequest{
Parsed: attemptParsedReq,
Body: attemptBody,
IsStickySession: hasBoundSession,
}
if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey {
result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, attemptBody, hasBoundSession)
result, err = h.platformRegistry.Get(service.PlatformAntigravity).Forward(requestCtx, c, account, forwardReq)
} else {
result, err = h.gatewayService.Forward(requestCtx, c, account, attemptParsedReq)
result, err = h.platformRegistry.Get(service.PlatformAnthropic).Forward(requestCtx, c, account, forwardReq)
}
// 兜底释放串行锁(正常情况已通过回调提前释放)
@@ -0,0 +1,20 @@
package handler
import (
"github.com/Wei-Shaw/sub2api/internal/gatewayplatform"
"github.com/Wei-Shaw/sub2api/internal/service"
)
// ProvideGatewayPlatformRegistry 装配 /v1/messages 平台 Forward 分发注册表
// (Phase-3 SEAM-DESIGN 裁决:anthropic / antigravity / gemini 三 adapter;
// OpenAI 走独立 OpenAIGatewayHandler 与独立计费管线,不进本注册表)。
func ProvideGatewayPlatformRegistry(
gatewayService *service.GatewayService,
antigravityGatewayService *service.AntigravityGatewayService,
) *gatewayplatform.Registry {
return gatewayplatform.NewRegistry(
gatewayplatform.NewAnthropicProvider(gatewayService),
gatewayplatform.NewAntigravityProvider(antigravityGatewayService),
gatewayplatform.NewGeminiProvider(antigravityGatewayService),
)
}
@@ -282,6 +282,7 @@ func p3CharNewHandler(t *testing.T, group *service.Group, accounts []*service.Ac
gwSvc,
nil, // geminiCompatService(:444 非 antigravity 分支误入会 panic → 用例显式失败)
agSvc,
ProvideGatewayPlatformRegistry(gwSvc, agSvc), // 与生产 Wire 装配同构的平台分发注册表
nil, // userService
concurrencySvc,
billingCacheSvc,
@@ -346,6 +346,7 @@ func schedInvNewHandler(t *testing.T, group *service.Group, accounts []*service.
gwSvc,
nil, // geminiCompatService
nil, // antigravityGatewayService
ProvideGatewayPlatformRegistry(gwSvc, nil), // anthropic 账号经 anthropic provider → gwSvc.Forward
nil, // userService
concurrencySvc,
billingCacheSvc,
@@ -406,7 +407,7 @@ func schedInvMessagesBody() []byte {
func TestSchedulingInvariant_FailoverSwitchLimit_DefaultValues(t *testing.T) {
t.Run("anthropic与gemini默认上限", func(t *testing.T) {
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
require.Equal(t, 10, h.maxAccountSwitches, "anthropic 默认换号上限必须为 10")
require.Equal(t, 3, h.maxAccountSwitchesGemini, "gemini 默认换号上限必须为 3")
})
@@ -420,7 +421,7 @@ func TestSchedulingInvariant_FailoverSwitchLimit_DefaultValues(t *testing.T) {
cfg := &config.Config{}
cfg.Gateway.MaxAccountSwitches = 5
cfg.Gateway.MaxAccountSwitchesGemini = 2
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, cfg, nil)
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, cfg, nil)
require.Equal(t, 5, h.maxAccountSwitches)
require.Equal(t, 2, h.maxAccountSwitchesGemini)
})
@@ -546,7 +547,7 @@ func TestSchedulingInvariant_FailoverSameAccountRetry_FullChain(t *testing.T) {
// (gemini 平台 Forward 的服务层内部 500 重试带秒级退避,完整 e2e 不可在
// 单测时间预算内执行,故此处固化 handler 循环契约层语义。)
func TestSchedulingInvariant_FailoverGemini_SwitchLimitLoopContract(t *testing.T) {
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
require.Equal(t, 3, h.maxAccountSwitchesGemini)
mock := &mockTempUnscheduler{}
+1
View File
@@ -150,6 +150,7 @@ var ProviderSet = wire.NewSet(
NewAnnouncementHandler,
NewChannelMonitorUserHandler,
ProvideGatewayHookChain,
ProvideGatewayPlatformRegistry,
NewGatewayHandler,
NewOpenAIGatewayHandler,
NewTotpHandler,
@@ -0,0 +1,24 @@
# 完成报告: [TASK-002] gatewayplatform 接缝(Provider/Registry/3 adapter + 分发替换)
- **完成状态**: Success
- **关联设计**: [SEAM-DESIGN.md v2 裁决记录](../../../.claude/plugin-refactor/phases/phase-3_platform/SEAM-DESIGN.md)
- **完成日期**: 2026-06-12
## 1. 任务完成简报
`internal/gatewayplatform` 落地:`Provider{Platform; Forward}`(v1 仅两方法)+ `ForwardRequest{Parsed, Body, IsStickySession, SessionGroupID, SessionKey}` + 构造期注册 Registry(重复/nil panic,运行期并发只读无锁)+ 3 个单语句直返 adapter(anthropic/antigravity/gemini,action="generateContent" 常量)。`:444`/`:794` 两处分发改 registry 查找,**`Type != APIKey` 条件保留在调用点**。OpenAI、v1beta 端点的 ForwardGemini、内核全部零触碰。
## 2. 等价性证据(要点)
- :444 逐字段对照:reqModel/reqStream 与 Parsed.Model/Stream 同源且无 mutation(gemini 分支不经 :794 的克隆循环);session 两字段经 ForwardRequest 重组同名 option——T2/T3 断言;
- :794 双分支共享 ForwardRequest(attemptBody=attemptParsedReq.Body.Bytes() 关系不变);session 维度留零值(现状不传、adapter 不消费——不虚构维度);
- gemini 平台非 antigravity 账号的 else 分支(geminiCompatService.Forward)保持原状(不在裁决 3 adapter 内);
- 错误透传:adapter 单语句 `return p.svc.Forward(...)`,T4 两用例(BetaBlockedError/PromptTooLong 的 errors.As 链)经新路径全绿。
## 3. 文件变更
新增 gatewayplatform 包 4 文件 + handler/gateway_platform.go(Wire provider);修改 gateway_handler.go(字段+两处替换)、handler/wire.go、wire_gen.go(wire@v0.7.0 生成);两个测试文件仅夹具加参(断言零改动)。
## 4. 验证(主控复跑确认)
T1-T4+action 契约全绿、gatewayplatform 包 -race 通过、`make test-invariants` 45 包、全量 unit 零失败、build/vet/gofmt 干净、bench compare exit 0(allocs 全部持平:266/329/93/134)。