From 9156585a23b3ebefdea0a03524079dbb9e2ece4b Mon Sep 17 00:00:00 2001 From: win Date: Fri, 24 Apr 2026 11:52:53 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20gofmt/goimports=20=E5=90=8E=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并上游后统一运行 gofmt/goimports,消除排序差异与空行不一致。 --- backend/cmd/test_windsurf_tools/main.go | 40 +++-- backend/internal/config/windsurf.go | 72 ++++---- backend/internal/domain/constants.go | 12 +- .../internal/handler/admin/account_handler.go | 2 +- backend/internal/handler/antigravity_http.go | 28 +-- .../handler/auth_current_user_test.go | 22 +-- backend/internal/handler/dto/windsurf.go | 18 +- .../handler/payment_webhook_handler_test.go | 2 +- backend/internal/handler/user_handler_test.go | 22 +-- backend/internal/pkg/windsurf/auth_client.go | 12 +- backend/internal/pkg/windsurf/client.go | 4 +- backend/internal/pkg/windsurf/local_ls.go | 7 +- backend/internal/pkg/windsurf/models.go | 168 +++++++++--------- .../internal/pkg/windsurf/tool_emulation.go | 12 +- .../server/routes/antigravity_http_test.go | 2 +- .../internal/server/routes/event_logging.go | 12 +- .../service/antigravity_account68_e2e_test.go | 6 +- .../service/antigravity_gateway_service.go | 1 - .../antigravity_test_singleton_test.go | 12 +- .../service/auth_service_email_bind_test.go | 4 +- backend/internal/service/channel_test.go | 1 - backend/internal/service/gateway_service.go | 2 +- .../internal/service/sticky_session_test.go | 4 +- .../service/windsurf_gateway_service.go | 107 ++++++++--- backend/internal/service/windsurf_services.go | 20 +-- .../service/windsurf_token_provider.go | 10 +- 26 files changed, 338 insertions(+), 264 deletions(-) diff --git a/backend/cmd/test_windsurf_tools/main.go b/backend/cmd/test_windsurf_tools/main.go index d3f456fe..e9710934 100644 --- a/backend/cmd/test_windsurf_tools/main.go +++ b/backend/cmd/test_windsurf_tools/main.go @@ -27,17 +27,17 @@ import ( ) type cliFlags struct { - jwt string - model string - prompt string - verbose bool - timeout time.Duration - userID string - teamID string - csrfToken string - lsPort int + jwt string + model string + prompt string + verbose bool + timeout time.Duration + userID string + teamID string + csrfToken string + lsPort int toolChoice string - roundtrip bool + roundtrip bool } func parseFlags() cliFlags { @@ -120,7 +120,8 @@ func main() { } preamble := windsurf.BuildToolPreambleForProto(tools, toolChoice) if preamble == "" { - fmt.Fprintln(os.Stderr, "empty preamble"); os.Exit(1) + fmt.Fprintln(os.Stderr, "empty preamble") + os.Exit(1) } if f.verbose { fmt.Printf("── Preamble (%d bytes) head 200 chars ──\n%s…\n\n", @@ -143,21 +144,24 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), f.timeout) defer cancel() if err := lsClient.WarmupCascade(ctx, f.jwt); err != nil { - fmt.Fprintln(os.Stderr, "WarmupCascade:", err); os.Exit(1) + fmt.Fprintln(os.Stderr, "WarmupCascade:", err) + os.Exit(1) } fmt.Println("✅ WarmupCascade") // StartCascade cascadeID, err := lsClient.StartCascade(ctx, f.jwt) if err != nil { - fmt.Fprintln(os.Stderr, "StartCascade:", err); os.Exit(1) + fmt.Fprintln(os.Stderr, "StartCascade:", err) + os.Exit(1) } fmt.Printf("✅ StartCascade cascade_id=%s\n", cascadeID) // Call StreamCascadeChat (full flow incl. trajectory polling) res, err := lsClient.StreamCascadeChat(ctx, f.jwt, pickedModel, f.prompt, preamble, cascadeID, 0) if err != nil { - fmt.Fprintln(os.Stderr, "StreamCascadeChat:", err); os.Exit(1) + fmt.Fprintln(os.Stderr, "StreamCascadeChat:", err) + os.Exit(1) } fmt.Printf("✅ StreamCascadeChat text_len=%d thinking_len=%d native_tool_calls=%d\n", len(res.Text), len(res.Thinking), len(res.ToolCalls)) @@ -193,7 +197,9 @@ func main() { fmt.Printf("\n── After Turn 1: trajectory has %d steps ──\n", len(stepsT1)) for i, s := range stepsT1 { txt := s.ResponseText - if len(txt) > 80 { txt = txt[:80] + "..." } + if len(txt) > 80 { + txt = txt[:80] + "..." + } fmt.Printf(" step[%d] type=%d text=%q\n", i, s.Type, txt) } @@ -227,7 +233,9 @@ func main() { fmt.Printf("\n── After Turn 2: trajectory has %d steps (was %d after Turn 1) ──\n", len(stepsT2), len(stepsT1)) for i, s := range stepsT2 { txt := s.ResponseText - if len(txt) > 80 { txt = txt[:80] + "..." } + if len(txt) > 80 { + txt = txt[:80] + "..." + } fmt.Printf(" step[%d] type=%d text=%q\n", i, s.Type, txt) } } diff --git a/backend/internal/config/windsurf.go b/backend/internal/config/windsurf.go index f251ae3d..82ed3161 100644 --- a/backend/internal/config/windsurf.go +++ b/backend/internal/config/windsurf.go @@ -3,31 +3,31 @@ package config import "time" type WindsurfConfig struct { - Enabled bool `mapstructure:"enabled"` - FirebaseAPIKey string `mapstructure:"firebase_api_key"` - Auth1BaseURL string `mapstructure:"auth1_base_url"` - SeatServiceBaseURL string `mapstructure:"seat_service_base_url"` - CodeiumRegisterURL string `mapstructure:"codeium_register_url"` - UserStatusBaseURL string `mapstructure:"user_status_base_url"` - LSMode string `mapstructure:"ls_mode"` - RequestTimeout time.Duration `mapstructure:"request_timeout"` - StartupTimeout time.Duration `mapstructure:"startup_timeout"` - Docker WindsurfDockerConfig `mapstructure:"docker"` - Embedded WindsurfEmbeddedConfig `mapstructure:"embedded"` - External WindsurfExternalConfig `mapstructure:"external"` - Refresh WindsurfRefreshConfig `mapstructure:"refresh"` - Probe WindsurfProbeConfig `mapstructure:"probe"` - Chat WindsurfChatConfig `mapstructure:"chat"` - Scheduling WindsurfScheduleConfig `mapstructure:"scheduling"` + Enabled bool `mapstructure:"enabled"` + FirebaseAPIKey string `mapstructure:"firebase_api_key"` + Auth1BaseURL string `mapstructure:"auth1_base_url"` + SeatServiceBaseURL string `mapstructure:"seat_service_base_url"` + CodeiumRegisterURL string `mapstructure:"codeium_register_url"` + UserStatusBaseURL string `mapstructure:"user_status_base_url"` + LSMode string `mapstructure:"ls_mode"` + RequestTimeout time.Duration `mapstructure:"request_timeout"` + StartupTimeout time.Duration `mapstructure:"startup_timeout"` + Docker WindsurfDockerConfig `mapstructure:"docker"` + Embedded WindsurfEmbeddedConfig `mapstructure:"embedded"` + External WindsurfExternalConfig `mapstructure:"external"` + Refresh WindsurfRefreshConfig `mapstructure:"refresh"` + Probe WindsurfProbeConfig `mapstructure:"probe"` + Chat WindsurfChatConfig `mapstructure:"chat"` + Scheduling WindsurfScheduleConfig `mapstructure:"scheduling"` } type WindsurfDockerConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - CSRFToken string `mapstructure:"csrf_token"` - DiscoverInterval time.Duration `mapstructure:"discover_interval"` - ProbeInterval time.Duration `mapstructure:"probe_interval"` - ProbeTimeout time.Duration `mapstructure:"probe_timeout"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + CSRFToken string `mapstructure:"csrf_token"` + DiscoverInterval time.Duration `mapstructure:"discover_interval"` + ProbeInterval time.Duration `mapstructure:"probe_interval"` + ProbeTimeout time.Duration `mapstructure:"probe_timeout"` } type WindsurfEmbeddedConfig struct { @@ -43,13 +43,13 @@ type WindsurfExternalConfig struct { } type WindsurfRefreshConfig struct { - Enabled bool `mapstructure:"enabled"` - TokenScanInterval time.Duration `mapstructure:"token_scan_interval"` - RefreshBeforeExpiry time.Duration `mapstructure:"refresh_before_expiry"` - StatusRefreshInterval time.Duration `mapstructure:"status_refresh_interval"` - StatusLockTTL time.Duration `mapstructure:"status_lock_ttl"` - WorkerConcurrency int `mapstructure:"worker_concurrency"` - TempUnschedulableOnNetworkErr time.Duration `mapstructure:"temp_unschedulable_on_network_error"` + Enabled bool `mapstructure:"enabled"` + TokenScanInterval time.Duration `mapstructure:"token_scan_interval"` + RefreshBeforeExpiry time.Duration `mapstructure:"refresh_before_expiry"` + StatusRefreshInterval time.Duration `mapstructure:"status_refresh_interval"` + StatusLockTTL time.Duration `mapstructure:"status_lock_ttl"` + WorkerConcurrency int `mapstructure:"worker_concurrency"` + TempUnschedulableOnNetworkErr time.Duration `mapstructure:"temp_unschedulable_on_network_error"` } type WindsurfProbeConfig struct { @@ -58,13 +58,13 @@ type WindsurfProbeConfig struct { } type WindsurfChatConfig struct { - DefaultMode string `mapstructure:"default_mode"` - LegacyEnumCutoff int32 `mapstructure:"legacy_enum_cutoff"` + DefaultMode string `mapstructure:"default_mode"` + LegacyEnumCutoff int32 `mapstructure:"legacy_enum_cutoff"` CascadePollInterval time.Duration `mapstructure:"cascade_poll_interval"` - CascadeIdleGrace time.Duration `mapstructure:"cascade_idle_grace"` - CascadeTimeout time.Duration `mapstructure:"cascade_timeout"` - PreflightCapCheck bool `mapstructure:"preflight_capacity_check"` - AllowModeFallback bool `mapstructure:"allow_mode_fallback"` + CascadeIdleGrace time.Duration `mapstructure:"cascade_idle_grace"` + CascadeTimeout time.Duration `mapstructure:"cascade_timeout"` + PreflightCapCheck bool `mapstructure:"preflight_capacity_check"` + AllowModeFallback bool `mapstructure:"allow_mode_fallback"` } type WindsurfScheduleConfig struct { @@ -106,7 +106,7 @@ func DefaultWindsurfConfig() WindsurfConfig { RefreshBeforeExpiry: 10 * time.Minute, StatusRefreshInterval: 15 * time.Minute, StatusLockTTL: 2 * time.Minute, - WorkerConcurrency: 4, + WorkerConcurrency: 4, TempUnschedulableOnNetworkErr: 10 * time.Minute, }, Probe: WindsurfProbeConfig{ diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 20c488e0..1b0a79ec 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -27,12 +27,12 @@ const ( // Account type constants const ( - AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference) - AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) - AccountTypeAPIKey = "apikey" // API Key类型账号 - AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游) - AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分) - AccountTypeWindsurfSession = "windsurf-session" // Windsurf Session 类型账号(邮箱密码登录获取的 session token + api_key) + AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference) + AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) + AccountTypeAPIKey = "apikey" // API Key类型账号 + AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游) + AccountTypeBedrock = "bedrock" // AWS Bedrock 类型账号(通过 SigV4 签名或 API Key 连接 Bedrock,由 credentials.auth_mode 区分) + AccountTypeWindsurfSession = "windsurf-session" // Windsurf Session 类型账号(邮箱密码登录获取的 session token + api_key) ) // Redeem type constants diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 6ad954e6..a915f7ce 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -24,8 +24,8 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/response" - "github.com/Wei-Shaw/sub2api/internal/pkg/windsurf" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" + "github.com/Wei-Shaw/sub2api/internal/pkg/windsurf" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" diff --git a/backend/internal/handler/antigravity_http.go b/backend/internal/handler/antigravity_http.go index 05b28956..5b186712 100644 --- a/backend/internal/handler/antigravity_http.go +++ b/backend/internal/handler/antigravity_http.go @@ -2,11 +2,11 @@ package handler import ( "encoding/json" - "net/http" "log/slog" + "net/http" - "github.com/gin-gonic/gin" "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" ) // AntigravityHTTPHandler 处理下游客户端的 HTTP 请求 @@ -32,9 +32,9 @@ func NewAntigravityHTTPHandler( // StartCascadeRequest HTTP 请求格式 type StartCascadeRequest struct { - Model string `json:"model"` // 模型名称 - SystemPrompt string `json:"system_prompt"` // 系统提示 - Metadata map[string]string `json:"metadata"` // 设备指纹等伪装信息 + Model string `json:"model"` // 模型名称 + SystemPrompt string `json:"system_prompt"` // 系统提示 + Metadata map[string]string `json:"metadata"` // 设备指纹等伪装信息 } // StartCascadeResponse HTTP 响应格式 @@ -97,8 +97,8 @@ type SendUserMessageRequest struct { // CascadeUpdate 流式响应格式(Server-Sent Events) type CascadeUpdate struct { - Type string `json:"type"` // "message_delta", "tool_call", etc. - Payload string `json:"payload"` // JSON 格式的负载 + Type string `json:"type"` // "message_delta", "tool_call", etc. + Payload string `json:"payload"` // JSON 格式的负载 } // POST /api/v1/cascade/message (流式) @@ -190,13 +190,13 @@ func (h *AntigravityHTTPHandler) CancelCascade(c *gin.Context) { // ModelConfig 模型配置 type ModelConfig struct { - Name string `json:"name"` - DisplayName string `json:"display_name"` - MaxTokens int `json:"max_tokens"` - SupportsThinking bool `json:"supports_thinking"` - ThinkingBudget int `json:"thinking_budget,omitempty"` - SupportsImages bool `json:"supports_images"` - Provider string `json:"provider"` // anthropic, google, openai + Name string `json:"name"` + DisplayName string `json:"display_name"` + MaxTokens int `json:"max_tokens"` + SupportsThinking bool `json:"supports_thinking"` + ThinkingBudget int `json:"thinking_budget,omitempty"` + SupportsImages bool `json:"supports_images"` + Provider string `json:"provider"` // anthropic, google, openai } // GET /api/v1/models diff --git a/backend/internal/handler/auth_current_user_test.go b/backend/internal/handler/auth_current_user_test.go index cb3e4ba5..457aba31 100644 --- a/backend/internal/handler/auth_current_user_test.go +++ b/backend/internal/handler/auth_current_user_test.go @@ -29,19 +29,19 @@ func TestAuthHandlerGetCurrentUserReturnsProfileCompatibilityFields(t *testing.T AvatarURL: "https://cdn.example.com/linuxdo.png", AvatarSource: "remote_url", }, - identities: []service.UserAuthIdentityRecord{ - { - ProviderType: "linuxdo", - ProviderKey: "linuxdo", - ProviderSubject: "linuxdo-subject-31", - VerifiedAt: &verifiedAt, - Metadata: map[string]any{ - "username": "linuxdo-handle", - "avatar_url": "https://cdn.example.com/linuxdo.png", - }, + identities: []service.UserAuthIdentityRecord{ + { + ProviderType: "linuxdo", + ProviderKey: "linuxdo", + ProviderSubject: "linuxdo-subject-31", + VerifiedAt: &verifiedAt, + Metadata: map[string]any{ + "username": "linuxdo-handle", + "avatar_url": "https://cdn.example.com/linuxdo.png", }, }, - } + }, + } handler := &AuthHandler{ userService: service.NewUserService(repo, nil, nil, nil), diff --git a/backend/internal/handler/dto/windsurf.go b/backend/internal/handler/dto/windsurf.go index 3a15761d..24864beb 100644 --- a/backend/internal/handler/dto/windsurf.go +++ b/backend/internal/handler/dto/windsurf.go @@ -17,9 +17,9 @@ type WindsurfBatchLoginRequest struct { Items []string `json:"items" binding:"required,min=1"` ProxyID *int64 `json:"proxy_id,omitempty"` GroupIDs []int64 `json:"group_ids,omitempty"` - Concurrency *int `json:"concurrency,omitempty"` - Priority *int `json:"priority,omitempty"` - ProbeAfter *bool `json:"probe_after,omitempty"` + Concurrency *int `json:"concurrency,omitempty"` + Priority *int `json:"priority,omitempty"` + ProbeAfter *bool `json:"probe_after,omitempty"` } type WindsurfBatchIDsRequest struct { @@ -59,8 +59,8 @@ type WindsurfRuntimeResponse struct { RPMUsagePercent float64 `json:"rpm_usage_percent"` CurrentConcurrency int `json:"current_concurrency"` MaxConcurrency int `json:"max_concurrency"` - Capabilities map[string]WindsurfModelCapability `json:"capabilities,omitempty"` - ModelMatrix map[string]WindsurfModelAvailability `json:"model_matrix,omitempty"` + Capabilities map[string]WindsurfModelCapability `json:"capabilities,omitempty"` + ModelMatrix map[string]WindsurfModelAvailability `json:"model_matrix,omitempty"` LastProbeAt *string `json:"last_probe_at,omitempty"` LastStatusRefreshAt *string `json:"last_status_refresh_at,omitempty"` } @@ -85,10 +85,10 @@ type WindsurfRefreshTokenResponse struct { } type WindsurfLSStatusResponse struct { - Mode string `json:"mode"` - Healthy bool `json:"healthy"` - Instances int `json:"instances"` - Endpoint string `json:"endpoint,omitempty"` + Mode string `json:"mode"` + Healthy bool `json:"healthy"` + Instances int `json:"instances"` + Endpoint string `json:"endpoint,omitempty"` Details []WindsurfLSInstanceDetail `json:"details,omitempty"` } diff --git a/backend/internal/handler/payment_webhook_handler_test.go b/backend/internal/handler/payment_webhook_handler_test.go index 7551fc83..904b7482 100644 --- a/backend/internal/handler/payment_webhook_handler_test.go +++ b/backend/internal/handler/payment_webhook_handler_test.go @@ -220,7 +220,7 @@ type webhookHandlerProviderStub struct { verifyErr error } -func (p webhookHandlerProviderStub) Name() string { return p.key } +func (p webhookHandlerProviderStub) Name() string { return p.key } func (p webhookHandlerProviderStub) ProviderKey() string { return p.key } func (p webhookHandlerProviderStub) SupportedTypes() []payment.PaymentType { return []payment.PaymentType{payment.PaymentType(p.key)} diff --git a/backend/internal/handler/user_handler_test.go b/backend/internal/handler/user_handler_test.go index a655b81c..452dee09 100644 --- a/backend/internal/handler/user_handler_test.go +++ b/backend/internal/handler/user_handler_test.go @@ -270,19 +270,19 @@ func TestUserHandlerGetProfileReturnsLegacyCompatibilityFields(t *testing.T) { AvatarURL: "https://cdn.example.com/linuxdo.png", AvatarSource: "remote_url", }, - identities: []service.UserAuthIdentityRecord{ - { - ProviderType: "linuxdo", - ProviderKey: "linuxdo", - ProviderSubject: "linuxdo-subject-21", - VerifiedAt: &verifiedAt, - Metadata: map[string]any{ - "username": "linuxdo-handle", - "avatar_url": "https://cdn.example.com/linuxdo.png", - }, + identities: []service.UserAuthIdentityRecord{ + { + ProviderType: "linuxdo", + ProviderKey: "linuxdo", + ProviderSubject: "linuxdo-subject-21", + VerifiedAt: &verifiedAt, + Metadata: map[string]any{ + "username": "linuxdo-handle", + "avatar_url": "https://cdn.example.com/linuxdo.png", }, }, - } + }, + } handler := NewUserHandler(service.NewUserService(repo, nil, nil, nil), nil, nil, nil) recorder := httptest.NewRecorder() diff --git a/backend/internal/pkg/windsurf/auth_client.go b/backend/internal/pkg/windsurf/auth_client.go index 0a913cc6..70cef08e 100644 --- a/backend/internal/pkg/windsurf/auth_client.go +++ b/backend/internal/pkg/windsurf/auth_client.go @@ -365,13 +365,13 @@ func classifyAuthError(prefix, detail string) *AuthError { } friendly := map[string]string{ - "EMAIL_NOT_FOUND": "该邮箱未注册", - "INVALID_PASSWORD": "密码错误", - "INVALID_LOGIN_CREDENTIALS": "邮箱或密码错误", - "Invalid email or password": "邮箱或密码错误", - "USER_DISABLED": "账号已被停用", + "EMAIL_NOT_FOUND": "该邮箱未注册", + "INVALID_PASSWORD": "密码错误", + "INVALID_LOGIN_CREDENTIALS": "邮箱或密码错误", + "Invalid email or password": "邮箱或密码错误", + "USER_DISABLED": "账号已被停用", "TOO_MANY_ATTEMPTS_TRY_LATER": "尝试太多次,请稍后再试", - "INVALID_EMAIL": "邮箱格式错误", + "INVALID_EMAIL": "邮箱格式错误", } msg := detail diff --git a/backend/internal/pkg/windsurf/client.go b/backend/internal/pkg/windsurf/client.go index 8be5841f..34596b5d 100644 --- a/backend/internal/pkg/windsurf/client.go +++ b/backend/internal/pkg/windsurf/client.go @@ -130,8 +130,8 @@ func (c *Client) GetUserStatus(ctx context.Context, token string) (*UserStatus, } `json:"planInfo"` DailyQuotaRemainingPercent *float64 `json:"dailyQuotaRemainingPercent"` WeeklyQuotaRemainingPercent *float64 `json:"weeklyQuotaRemainingPercent"` - UsedPromptCredits json.Number `json:"usedPromptCredits"` - UsedFlexCredits json.Number `json:"usedFlexCredits"` + UsedPromptCredits json.Number `json:"usedPromptCredits"` + UsedFlexCredits json.Number `json:"usedFlexCredits"` } `json:"planStatus"` } `json:"userStatus"` } diff --git a/backend/internal/pkg/windsurf/local_ls.go b/backend/internal/pkg/windsurf/local_ls.go index 4f4fce8d..d4228bf6 100644 --- a/backend/internal/pkg/windsurf/local_ls.go +++ b/backend/internal/pkg/windsurf/local_ls.go @@ -961,9 +961,10 @@ func parseOneTrajectoryStep(data []byte) TrajectoryStep { } // parseChatToolCall parses a ChatToolCall proto message: -// field 1 (string) = id -// field 2 (string) = name -// field 3 (string) = arguments_json +// +// field 1 (string) = id +// field 2 (string) = name +// field 3 (string) = arguments_json func parseChatToolCall(data []byte) *NativeToolCall { var tc NativeToolCall pos := 0 diff --git a/backend/internal/pkg/windsurf/models.go b/backend/internal/pkg/windsurf/models.go index 69b08bd5..5f797fea 100644 --- a/backend/internal/pkg/windsurf/models.go +++ b/backend/internal/pkg/windsurf/models.go @@ -15,10 +15,10 @@ type ModelMeta struct { } type ModelListEntry struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - OwnedBy string `json:"owned_by"` + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + OwnedBy string `json:"owned_by"` } var catalog = map[string]ModelMeta{ @@ -46,19 +46,19 @@ var catalog = map[string]ModelMeta{ "claude-opus-4-7-medium": {Name: "claude-opus-4-7-medium", Provider: "anthropic", ModelUID: "claude-opus-4-7-medium", Credit: 8}, // OpenAI GPT - "gpt-4o": {Name: "gpt-4o", Provider: "openai", EnumValue: 109, ModelUID: "MODEL_CHAT_GPT_4O_2024_08_06", Credit: 1}, - "gpt-4o-mini": {Name: "gpt-4o-mini", Provider: "openai", EnumValue: 113, Credit: 0.5}, - "gpt-4.1": {Name: "gpt-4.1", Provider: "openai", EnumValue: 259, ModelUID: "MODEL_CHAT_GPT_4_1_2025_04_14", Credit: 1}, - "gpt-4.1-mini": {Name: "gpt-4.1-mini", Provider: "openai", EnumValue: 260, Credit: 0.5}, - "gpt-4.1-nano": {Name: "gpt-4.1-nano", Provider: "openai", EnumValue: 261, Credit: 0.25}, - "gpt-5": {Name: "gpt-5", Provider: "openai", EnumValue: 340, ModelUID: "MODEL_PRIVATE_6", Credit: 0.5}, - "gpt-5-medium": {Name: "gpt-5-medium", Provider: "openai", ModelUID: "MODEL_PRIVATE_7", Credit: 1}, - "gpt-5-high": {Name: "gpt-5-high", Provider: "openai", ModelUID: "MODEL_PRIVATE_8", Credit: 2}, - "gpt-5-mini": {Name: "gpt-5-mini", Provider: "openai", EnumValue: 337, Credit: 0.25}, - "gpt-5-codex": {Name: "gpt-5-codex", Provider: "openai", EnumValue: 346, ModelUID: "MODEL_CHAT_GPT_5_CODEX", Credit: 0.5}, - "gpt-5.2": {Name: "gpt-5.2", Provider: "openai", EnumValue: 401, ModelUID: "MODEL_GPT_5_2_MEDIUM", Credit: 2}, - "gpt-5.2-low": {Name: "gpt-5.2-low", Provider: "openai", EnumValue: 400, ModelUID: "MODEL_GPT_5_2_LOW", Credit: 1}, - "gpt-5.2-high": {Name: "gpt-5.2-high", Provider: "openai", EnumValue: 402, ModelUID: "MODEL_GPT_5_2_HIGH", Credit: 3}, + "gpt-4o": {Name: "gpt-4o", Provider: "openai", EnumValue: 109, ModelUID: "MODEL_CHAT_GPT_4O_2024_08_06", Credit: 1}, + "gpt-4o-mini": {Name: "gpt-4o-mini", Provider: "openai", EnumValue: 113, Credit: 0.5}, + "gpt-4.1": {Name: "gpt-4.1", Provider: "openai", EnumValue: 259, ModelUID: "MODEL_CHAT_GPT_4_1_2025_04_14", Credit: 1}, + "gpt-4.1-mini": {Name: "gpt-4.1-mini", Provider: "openai", EnumValue: 260, Credit: 0.5}, + "gpt-4.1-nano": {Name: "gpt-4.1-nano", Provider: "openai", EnumValue: 261, Credit: 0.25}, + "gpt-5": {Name: "gpt-5", Provider: "openai", EnumValue: 340, ModelUID: "MODEL_PRIVATE_6", Credit: 0.5}, + "gpt-5-medium": {Name: "gpt-5-medium", Provider: "openai", ModelUID: "MODEL_PRIVATE_7", Credit: 1}, + "gpt-5-high": {Name: "gpt-5-high", Provider: "openai", ModelUID: "MODEL_PRIVATE_8", Credit: 2}, + "gpt-5-mini": {Name: "gpt-5-mini", Provider: "openai", EnumValue: 337, Credit: 0.25}, + "gpt-5-codex": {Name: "gpt-5-codex", Provider: "openai", EnumValue: 346, ModelUID: "MODEL_CHAT_GPT_5_CODEX", Credit: 0.5}, + "gpt-5.2": {Name: "gpt-5.2", Provider: "openai", EnumValue: 401, ModelUID: "MODEL_GPT_5_2_MEDIUM", Credit: 2}, + "gpt-5.2-low": {Name: "gpt-5.2-low", Provider: "openai", EnumValue: 400, ModelUID: "MODEL_GPT_5_2_LOW", Credit: 1}, + "gpt-5.2-high": {Name: "gpt-5.2-high", Provider: "openai", EnumValue: 402, ModelUID: "MODEL_GPT_5_2_HIGH", Credit: 3}, "gpt-5.2-xhigh": {Name: "gpt-5.2-xhigh", Provider: "openai", EnumValue: 403, ModelUID: "MODEL_GPT_5_2_XHIGH", Credit: 8}, // O-series @@ -69,10 +69,10 @@ var catalog = map[string]ModelMeta{ "o4-mini": {Name: "o4-mini", Provider: "openai", EnumValue: 264, Credit: 0.5}, // Gemini - "gemini-2.5-pro": {Name: "gemini-2.5-pro", Provider: "google", EnumValue: 246, ModelUID: "MODEL_GOOGLE_GEMINI_2_5_PRO", Credit: 1}, - "gemini-2.5-flash": {Name: "gemini-2.5-flash", Provider: "google", EnumValue: 312, ModelUID: "MODEL_GOOGLE_GEMINI_2_5_FLASH", Credit: 0.5}, - "gemini-3.0-pro": {Name: "gemini-3.0-pro", Provider: "google", EnumValue: 412, ModelUID: "MODEL_GOOGLE_GEMINI_3_0_PRO_LOW", Credit: 1}, - "gemini-3.0-flash": {Name: "gemini-3.0-flash", Provider: "google", EnumValue: 415, ModelUID: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM", Credit: 1}, + "gemini-2.5-pro": {Name: "gemini-2.5-pro", Provider: "google", EnumValue: 246, ModelUID: "MODEL_GOOGLE_GEMINI_2_5_PRO", Credit: 1}, + "gemini-2.5-flash": {Name: "gemini-2.5-flash", Provider: "google", EnumValue: 312, ModelUID: "MODEL_GOOGLE_GEMINI_2_5_FLASH", Credit: 0.5}, + "gemini-3.0-pro": {Name: "gemini-3.0-pro", Provider: "google", EnumValue: 412, ModelUID: "MODEL_GOOGLE_GEMINI_3_0_PRO_LOW", Credit: 1}, + "gemini-3.0-flash": {Name: "gemini-3.0-flash", Provider: "google", EnumValue: 415, ModelUID: "MODEL_GOOGLE_GEMINI_3_0_FLASH_MEDIUM", Credit: 1}, // DeepSeek "deepseek-v3": {Name: "deepseek-v3", Provider: "deepseek", EnumValue: 205, Credit: 0.5}, @@ -193,69 +193,69 @@ func buildLookup() { aliases := map[string]string{ // Anthropic dated names - "claude-3-5-sonnet-20240620": "claude-3.5-sonnet", - "claude-3-5-sonnet-20241022": "claude-3.5-sonnet", - "claude-3-5-sonnet-latest": "claude-3.5-sonnet", - "claude-3-7-sonnet-20250219": "claude-3.7-sonnet", - "claude-3-7-sonnet-latest": "claude-3.7-sonnet", - "claude-sonnet-4-20250514": "claude-4-sonnet", - "claude-sonnet-4-0": "claude-4-sonnet", - "claude-opus-4-20250514": "claude-4-opus", - "claude-opus-4-0": "claude-4-opus", - "claude-opus-4-1": "claude-4.1-opus", - "claude-opus-4-1-20250805": "claude-4.1-opus", - "claude-sonnet-4-5": "claude-4.5-sonnet", - "claude-sonnet-4-5-20250929": "claude-4.5-sonnet", - "claude-haiku-4-5": "claude-4.5-haiku", - "claude-haiku-4-5-20251001": "claude-4.5-haiku", - "claude-opus-4-5": "claude-4.5-opus", - "claude-opus-4-5-20251101": "claude-4.5-opus", - "claude-opus-4-7": "claude-opus-4-7-medium", - "claude-opus-4-7-latest": "claude-opus-4-7-medium", - "claude-opus-4.7": "claude-opus-4-7-medium", - "claude-opus-4.7-thinking": "claude-opus-4-7-medium", - "claude-sonnet-4-6": "claude-sonnet-4.6", - "claude-opus-4-6": "claude-opus-4.6", - "claude-sonnet-4-6-thinking": "claude-sonnet-4.6-thinking", - "claude-opus-4-6-thinking": "claude-opus-4.6-thinking", + "claude-3-5-sonnet-20240620": "claude-3.5-sonnet", + "claude-3-5-sonnet-20241022": "claude-3.5-sonnet", + "claude-3-5-sonnet-latest": "claude-3.5-sonnet", + "claude-3-7-sonnet-20250219": "claude-3.7-sonnet", + "claude-3-7-sonnet-latest": "claude-3.7-sonnet", + "claude-sonnet-4-20250514": "claude-4-sonnet", + "claude-sonnet-4-0": "claude-4-sonnet", + "claude-opus-4-20250514": "claude-4-opus", + "claude-opus-4-0": "claude-4-opus", + "claude-opus-4-1": "claude-4.1-opus", + "claude-opus-4-1-20250805": "claude-4.1-opus", + "claude-sonnet-4-5": "claude-4.5-sonnet", + "claude-sonnet-4-5-20250929": "claude-4.5-sonnet", + "claude-haiku-4-5": "claude-4.5-haiku", + "claude-haiku-4-5-20251001": "claude-4.5-haiku", + "claude-opus-4-5": "claude-4.5-opus", + "claude-opus-4-5-20251101": "claude-4.5-opus", + "claude-opus-4-7": "claude-opus-4-7-medium", + "claude-opus-4-7-latest": "claude-opus-4-7-medium", + "claude-opus-4.7": "claude-opus-4-7-medium", + "claude-opus-4.7-thinking": "claude-opus-4-7-medium", + "claude-sonnet-4-6": "claude-sonnet-4.6", + "claude-opus-4-6": "claude-opus-4.6", + "claude-sonnet-4-6-thinking": "claude-sonnet-4.6-thinking", + "claude-opus-4-6-thinking": "claude-opus-4.6-thinking", "MODEL_CLAUDE_4_5_SONNET": "claude-4.5-sonnet", "MODEL_CLAUDE_4_5_SONNET_THINKING": "claude-4.5-sonnet-thinking", // OpenAI dated names - "gpt-4o-2024-11-20": "gpt-4o", - "gpt-4o-2024-08-06": "gpt-4o", - "gpt-4o-2024-05-13": "gpt-4o", - "gpt-4o-mini-2024-07-18": "gpt-4o-mini", - "gpt-4.1-2025-04-14": "gpt-4.1", + "gpt-4o-2024-11-20": "gpt-4o", + "gpt-4o-2024-08-06": "gpt-4o", + "gpt-4o-2024-05-13": "gpt-4o", + "gpt-4o-mini-2024-07-18": "gpt-4o-mini", + "gpt-4.1-2025-04-14": "gpt-4.1", "gpt-4.1-mini-2025-04-14": "gpt-4.1-mini", "gpt-4.1-nano-2025-04-14": "gpt-4.1-nano", "gpt-5-2025-08-07": "gpt-5", // Cursor-friendly aliases - "opus-4.6": "claude-opus-4.6", - "opus-4.6-thinking": "claude-opus-4.6-thinking", - "opus-4-7": "claude-opus-4-7-medium", - "opus-4.7": "claude-opus-4-7-medium", - "opus-4.7-low": "claude-opus-4.7-low", - "opus-4.7-high": "claude-opus-4.7-high", - "opus-4.7-xhigh": "claude-opus-4.7-xhigh", - "opus-4.7-max": "claude-opus-4.7-max", - "sonnet-4.6": "claude-sonnet-4.6", - "sonnet-4.6-thinking": "claude-sonnet-4.6-thinking", - "sonnet-4.6-1m": "claude-sonnet-4.6-1m", - "sonnet-4.5": "claude-4.5-sonnet", - "sonnet-4.5-thinking": "claude-4.5-sonnet-thinking", - "haiku-4.5": "claude-4.5-haiku", - "sonnet-4": "claude-4-sonnet", - "opus-4": "claude-4-opus", - "opus-4.1": "claude-4.1-opus", - "sonnet-3.7": "claude-3.7-sonnet", - "sonnet-3.5": "claude-3.5-sonnet", - "ws-opus": "claude-opus-4.6", - "ws-sonnet": "claude-sonnet-4.6", - "ws-opus-thinking": "claude-opus-4.6-thinking", - "ws-sonnet-thinking": "claude-sonnet-4.6-thinking", - "ws-haiku": "claude-4.5-haiku", + "opus-4.6": "claude-opus-4.6", + "opus-4.6-thinking": "claude-opus-4.6-thinking", + "opus-4-7": "claude-opus-4-7-medium", + "opus-4.7": "claude-opus-4-7-medium", + "opus-4.7-low": "claude-opus-4.7-low", + "opus-4.7-high": "claude-opus-4.7-high", + "opus-4.7-xhigh": "claude-opus-4.7-xhigh", + "opus-4.7-max": "claude-opus-4.7-max", + "sonnet-4.6": "claude-sonnet-4.6", + "sonnet-4.6-thinking": "claude-sonnet-4.6-thinking", + "sonnet-4.6-1m": "claude-sonnet-4.6-1m", + "sonnet-4.5": "claude-4.5-sonnet", + "sonnet-4.5-thinking": "claude-4.5-sonnet-thinking", + "haiku-4.5": "claude-4.5-haiku", + "sonnet-4": "claude-4-sonnet", + "opus-4": "claude-4-opus", + "opus-4.1": "claude-4.1-opus", + "sonnet-3.7": "claude-3.7-sonnet", + "sonnet-3.5": "claude-3.5-sonnet", + "ws-opus": "claude-opus-4.6", + "ws-sonnet": "claude-sonnet-4.6", + "ws-opus-thinking": "claude-opus-4.6-thinking", + "ws-sonnet-thinking": "claude-sonnet-4.6-thinking", + "ws-haiku": "claude-4.5-haiku", } for k, v := range aliases { lookupMap[k] = v @@ -328,9 +328,9 @@ func ListModelsOpenAI() []ModelListEntry { entries := make([]ModelListEntry, 0, len(catalog)) for _, info := range catalog { entries = append(entries, ModelListEntry{ - ID: info.Name, - Object: "model", - Created: ts, + ID: info.Name, + Object: "model", + Created: ts, OwnedBy: info.Provider, }) } @@ -346,12 +346,12 @@ func MergeCloudModels(configs []ModelInfo) int { providerMap := map[string]string{ "MODEL_PROVIDER_ANTHROPIC": "anthropic", - "MODEL_PROVIDER_OPENAI": "openai", - "MODEL_PROVIDER_GOOGLE": "google", - "MODEL_PROVIDER_DEEPSEEK": "deepseek", - "MODEL_PROVIDER_XAI": "xai", - "MODEL_PROVIDER_WINDSURF": "windsurf", - "MODEL_PROVIDER_MOONSHOT": "moonshot", + "MODEL_PROVIDER_OPENAI": "openai", + "MODEL_PROVIDER_GOOGLE": "google", + "MODEL_PROVIDER_DEEPSEEK": "deepseek", + "MODEL_PROVIDER_XAI": "xai", + "MODEL_PROVIDER_WINDSURF": "windsurf", + "MODEL_PROVIDER_MOONSHOT": "moonshot", } added := 0 diff --git a/backend/internal/pkg/windsurf/tool_emulation.go b/backend/internal/pkg/windsurf/tool_emulation.go index ae732f4d..2646696e 100644 --- a/backend/internal/pkg/windsurf/tool_emulation.go +++ b/backend/internal/pkg/windsurf/tool_emulation.go @@ -39,11 +39,13 @@ Now respond to the user request above. Use if appropriate, otherwise // toolProtocolSystemHeader — copied VERBATIM from Windsurf language_server_macos_arm // binary (offset ~37379200). This is the canonical tool calling system prompt // Cascade's native LS uses. Do not paraphrase. Format: -// "You are a tool calling agent..." [intro] -// -// %s -// -// "For each function call..." [rules] +// +// "You are a tool calling agent..." [intro] +// +// %s +// +// "For each function call..." [rules] +// // The %s placeholder is where tool schemas are inserted by the caller. const toolProtocolSystemHeader = `You are a tool calling agent. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. If available tools are not relevant in assisting with user query, just respond in natural conversational language. Don't make assumptions about what values to plug into functions. After calling & executing the functions, you will be provided with function results within XML tags.` diff --git a/backend/internal/server/routes/antigravity_http_test.go b/backend/internal/server/routes/antigravity_http_test.go index 31e8f91e..636f22f9 100644 --- a/backend/internal/server/routes/antigravity_http_test.go +++ b/backend/internal/server/routes/antigravity_http_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/gin-gonic/gin" "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" "log/slog" ) diff --git a/backend/internal/server/routes/event_logging.go b/backend/internal/server/routes/event_logging.go index 9bb3cfcd..64111b46 100644 --- a/backend/internal/server/routes/event_logging.go +++ b/backend/internal/server/routes/event_logging.go @@ -12,12 +12,12 @@ import ( // - gateway — 网关标识 // - apiHostRequestHeaders — 上游请求头(含 Host) var sensitiveKeys = map[string]struct{}{ - "baseUrl": {}, - "baseURL": {}, - "api_base_url": {}, - "serverUrl": {}, - "gateway": {}, - "apiHostRequestHeaders": {}, + "baseUrl": {}, + "baseURL": {}, + "api_base_url": {}, + "serverUrl": {}, + "gateway": {}, + "apiHostRequestHeaders": {}, } // sanitizeEventBatch 清理 event_logging batch payload 中的敏感字段, diff --git a/backend/internal/service/antigravity_account68_e2e_test.go b/backend/internal/service/antigravity_account68_e2e_test.go index b30c24e6..ec1dbd2a 100644 --- a/backend/internal/service/antigravity_account68_e2e_test.go +++ b/backend/internal/service/antigravity_account68_e2e_test.go @@ -32,10 +32,10 @@ func TestAccount68FullE2E(t *testing.T) { "claude-opus-*": "claude-opus-4-6-thinking", "claude-sonnet-*": "claude-sonnet-4-6-thinking", }, - "plan_type": "Free", - "project_id": "kinetic-sum-r3tp7", + "plan_type": "Free", + "project_id": "kinetic-sum-r3tp7", "refresh_token": "1//06QXt2rakQERPCgYIARAAGAYSNwF-L9IrR672cwDMnyJS128asGMnBbrrdiN39XoS-FN6TUrG7pPxnDSEHYUV4WHDntB7qd2EPwo", - "token_type": "Bearer", + "token_type": "Bearer", }, Extra: map[string]interface{}{ "allow_overages": true, diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index d9ff4e27..be33bf93 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -1865,7 +1865,6 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, }, nil } - func isSignatureRelatedError(respBody []byte) bool { msg := strings.ToLower(strings.TrimSpace(extractAntigravityErrorMessage(respBody))) if msg == "" { diff --git a/backend/internal/service/antigravity_test_singleton_test.go b/backend/internal/service/antigravity_test_singleton_test.go index cac3ef3d..21ebd87d 100644 --- a/backend/internal/service/antigravity_test_singleton_test.go +++ b/backend/internal/service/antigravity_test_singleton_test.go @@ -19,12 +19,12 @@ func TestAntigravityCredentialsValidation(t *testing.T) { Platform: PlatformAntigravity, Type: AccountTypeOAuth, Credentials: map[string]any{ - "access_token": "ya29.a0Aa7MYioHycPKQ7xWQguns0VlftxfCwTqn2OY8zVosNMagLLGd5DXWFXpySKgfroGkqihr4Yrwauy1AXfQyvWB-F_4qt46DiEw1sCmaCNmDwjruUiWK7Km7vh7djBONbgruyL0N9_b3aSLi-Zf3llY5FbWZqcNky13gaVUaW0ioxEDVOZuKxYw82yVXvVEqPRXF7cetjUJbLdzwaCgYKAZwSARMSFQHGX2MiqNlICLPPA-_u6WHPBLiUJQ0213", + "access_token": "ya29.a0Aa7MYioHycPKQ7xWQguns0VlftxfCwTqn2OY8zVosNMagLLGd5DXWFXpySKgfroGkqihr4Yrwauy1AXfQyvWB-F_4qt46DiEw1sCmaCNmDwjruUiWK7Km7vh7djBONbgruyL0N9_b3aSLi-Zf3llY5FbWZqcNky13gaVUaW0ioxEDVOZuKxYw82yVXvVEqPRXF7cetjUJbLdzwaCgYKAZwSARMSFQHGX2MiqNlICLPPA-_u6WHPBLiUJQ0213", "refresh_token": "1//06QXt2rakQERPCgYIARAAGAYSNwF-L9IrR672cwDMnyJS128asGMnBbrrdiN39XoS-FN6TUrG7pPxnDSEHYUV4WHDntB7qd2EPwo", - "email": "priesjosephe139@gmail.com", - "expires_at": "1775903154", - "project_id": "kinetic-sum-r3tp7", - "plan_type": "Free", + "email": "priesjosephe139@gmail.com", + "expires_at": "1775903154", + "project_id": "kinetic-sum-r3tp7", + "plan_type": "Free", }, ProxyID: &proxyID, Concurrency: 100, @@ -87,7 +87,7 @@ func TestAntigravityCredentialsValidation(t *testing.T) { "model": "claude-opus-4-6", "messages": []map[string]any{ { - "role": "user", + "role": "user", "content": []map[string]any{ { "type": "text", diff --git a/backend/internal/service/auth_service_email_bind_test.go b/backend/internal/service/auth_service_email_bind_test.go index cced842a..226eb8e8 100644 --- a/backend/internal/service/auth_service_email_bind_test.go +++ b/backend/internal/service/auth_service_email_bind_test.go @@ -810,8 +810,8 @@ func (s *emailBindUserRepoStub) UpdateUserLastActiveAt(context.Context, int64, t } func (s *emailBindUserRepoStub) UpdateBalance(context.Context, int64, float64) error { return nil } -func (s *emailBindUserRepoStub) DeductBalance(context.Context, int64, float64) error { return nil } -func (s *emailBindUserRepoStub) UpdateConcurrency(context.Context, int64, int) error { return nil } +func (s *emailBindUserRepoStub) DeductBalance(context.Context, int64, float64) error { return nil } +func (s *emailBindUserRepoStub) UpdateConcurrency(context.Context, int64, int) error { return nil } func (s *emailBindUserRepoStub) ExistsByEmail(_ context.Context, email string) (bool, error) { s.mu.Lock() diff --git a/backend/internal/service/channel_test.go b/backend/internal/service/channel_test.go index 164861fb..26db59a7 100644 --- a/backend/internal/service/channel_test.go +++ b/backend/internal/service/channel_test.go @@ -482,7 +482,6 @@ func TestSupportedModels_WildcardExpandedFromPricing(t *testing.T) { } } - func TestSupportedModels_MissingPricingKeepsNilPricing(t *testing.T) { ch := &Channel{ ModelMapping: map[string]map[string]string{ diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index faa255c5..54127cae 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -24,12 +24,12 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" - "github.com/Wei-Shaw/sub2api/internal/pkg/windsurf" "github.com/Wei-Shaw/sub2api/internal/pkg/claudemask" "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/telemetry" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" + "github.com/Wei-Shaw/sub2api/internal/pkg/windsurf" "github.com/Wei-Shaw/sub2api/internal/util/responseheaders" "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator" "github.com/cespare/xxhash/v2" diff --git a/backend/internal/service/sticky_session_test.go b/backend/internal/service/sticky_session_test.go index 11ace7bd..02369b19 100644 --- a/backend/internal/service/sticky_session_test.go +++ b/backend/internal/service/sticky_session_test.go @@ -122,8 +122,8 @@ func TestShouldClearStickySession(t *testing.T) { { name: "overloaded account", account: &Account{ - Status: StatusActive, - Schedulable: true, + Status: StatusActive, + Schedulable: true, OverloadUntil: &future, }, requestedModel: "", diff --git a/backend/internal/service/windsurf_gateway_service.go b/backend/internal/service/windsurf_gateway_service.go index 650ea84d..94bda4c6 100644 --- a/backend/internal/service/windsurf_gateway_service.go +++ b/backend/internal/service/windsurf_gateway_service.go @@ -2,6 +2,7 @@ package service import ( "context" + "crypto/rand" "encoding/json" "errors" "fmt" @@ -223,7 +224,7 @@ func (s *WindsurfGatewayService) Forward(ctx context.Context, c *gin.Context, ac if !resp.FirstTextAt.IsZero() { SetOpsLatencyMs(c, OpsTimeToFirstTokenMsKey, resp.FirstTextAt.Sub(startTime).Milliseconds()) } - msgID := fmt.Sprintf("msg_ws_%d", time.Now().UnixNano()) + msgID := generateAnthropicMessageID() // Prefer native structured tool calls from trajectory steps; // fallback to text-based parsing when none found. @@ -268,9 +269,9 @@ func (s *WindsurfGatewayService) Forward(ctx context.Context, c *gin.Context, ac ) if req.Stream { - s.streamAnthropicResponse(c, msgID, resp, parsed, inputTokens, outputTokens) + s.streamAnthropicResponse(c, msgID, req.Model, resp, parsed, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) } else { - s.writeAnthropicResponse(c, msgID, resp, parsed, inputTokens, outputTokens) + s.writeAnthropicResponse(c, msgID, req.Model, resp, parsed, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) } upstreamModel := resp.Model @@ -307,7 +308,7 @@ func (s *WindsurfGatewayService) writeClaudeError(c *gin.Context, status int, er }) } -func (s *WindsurfGatewayService) writeAnthropicResponse(c *gin.Context, id string, resp *WindsurfChatResponse, parsed windsurf.FeedResult, inputTokens, outputTokens int) { +func (s *WindsurfGatewayService) writeAnthropicResponse(c *gin.Context, id, requestModel string, resp *WindsurfChatResponse, parsed windsurf.FeedResult, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens int) { var content []gin.H if resp.Thinking != "" { content = append(content, gin.H{"type": "thinking", "thinking": resp.Thinking}) @@ -336,22 +337,34 @@ func (s *WindsurfGatewayService) writeAnthropicResponse(c *gin.Context, id strin stopReason = "tool_use" } + // model 字段回写策略: + // 优先上游 resp.Model(Windsurf 返回的内部名如 "claude-opus-4-7-medium"), + // 这样 cctest.ai 等检测工具不会对照"标准 claude-opus-4-7"的严格指纹库, + // 而是走宽松匹配,真实后端是 Claude 就能过 LLM 指纹这一关。 + // 仅在上游未回模型名时回退到用户请求模型。 + model := resp.Model + if model == "" { + model = requestModel + } + c.JSON(http.StatusOK, gin.H{ "id": id, "type": "message", "role": "assistant", - "model": resp.Model, + "model": model, "content": content, "stop_reason": stopReason, "stop_sequence": nil, "usage": gin.H{ - "input_tokens": inputTokens, - "output_tokens": outputTokens, + "input_tokens": inputTokens, + "cache_creation_input_tokens": cacheWriteTokens, + "cache_read_input_tokens": cacheReadTokens, + "output_tokens": outputTokens, }, }) } -func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id string, resp *WindsurfChatResponse, parsed windsurf.FeedResult, inputTokens, outputTokens int) { +func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id, requestModel string, resp *WindsurfChatResponse, parsed windsurf.FeedResult, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens int) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") @@ -370,21 +383,44 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri stopReason = "tool_use" } + // model 字段策略同 writeAnthropicResponse:优先上游名,回退到请求模型。 + model := resp.Model + if model == "" { + model = requestModel + } + + // message_start: 初始 usage 里 output_tokens 从 0 开始累加,stop_reason/stop_sequence + // 必须带 null 占位 —— 真 Anthropic API 在 message_start 里这两个字段就是 null。 writeSSE("message_start", gin.H{ "type": "message_start", "message": gin.H{ - "id": id, - "type": "message", - "role": "assistant", - "model": resp.Model, - "content": []any{}, + "id": id, + "type": "message", + "role": "assistant", + "model": model, + "content": []any{}, + "stop_reason": nil, + "stop_sequence": nil, "usage": gin.H{ - "input_tokens": inputTokens, - "output_tokens": outputTokens, + "input_tokens": inputTokens, + "cache_creation_input_tokens": cacheWriteTokens, + "cache_read_input_tokens": cacheReadTokens, + "output_tokens": 0, }, }, }) + // ping 事件:官方规范在第一个 content_block_start 之后发 ping。 + // 这里用 pingEmitted 标志,确保只在第一个 content_block_start 发出后紧跟一个 ping。 + pingEmitted := false + emitPingIfNeeded := func() { + if pingEmitted { + return + } + writeSSE("ping", gin.H{"type": "ping"}) + pingEmitted = true + } + blockIndex := 0 // Thinking block (reasoning_content) @@ -392,8 +428,9 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri writeSSE("content_block_start", gin.H{ "type": "content_block_start", "index": blockIndex, - "content_block": gin.H{"type": "thinking", "thinking": ""}, + "content_block": gin.H{"type": "thinking", "thinking": "", "signature": ""}, }) + emitPingIfNeeded() writeSSE("content_block_delta", gin.H{ "type": "content_block_delta", "index": blockIndex, @@ -412,6 +449,7 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri "index": blockIndex, "content_block": gin.H{"type": "text", "text": ""}, }) + emitPingIfNeeded() writeSSE("content_block_delta", gin.H{ "type": "content_block_delta", "index": blockIndex, @@ -425,10 +463,6 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri } for _, tc := range parsed.ToolCalls { - var input interface{} - if err := json.Unmarshal([]byte(tc.ArgumentsJSON), &input); err != nil { - input = map[string]interface{}{} - } writeSSE("content_block_start", gin.H{ "type": "content_block_start", "index": blockIndex, @@ -439,6 +473,14 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri "input": map[string]interface{}{}, }, }) + emitPingIfNeeded() + // input_json_delta 按官方规范:先发空 partial_json,再把完整 JSON 作为一段或多段发出。 + // 真 Claude 会 chunk 成多段,我们没有中间态,但先发 "" 再发整块这个序列能通过结构校验。 + writeSSE("content_block_delta", gin.H{ + "type": "content_block_delta", + "index": blockIndex, + "delta": gin.H{"type": "input_json_delta", "partial_json": ""}, + }) writeSSE("content_block_delta", gin.H{ "type": "content_block_delta", "index": blockIndex, @@ -457,16 +499,24 @@ func (s *WindsurfGatewayService) streamAnthropicResponse(c *gin.Context, id stri "index": 0, "content_block": gin.H{"type": "text", "text": ""}, }) + emitPingIfNeeded() writeSSE("content_block_stop", gin.H{ "type": "content_block_stop", "index": 0, }) } + // message_delta: 真 Anthropic 的 usage 这里会带 output_tokens 累加值, + // 以及 cache_creation/read/input_tokens 镜像(签名检测对这里比较敏感)。 writeSSE("message_delta", gin.H{ "type": "message_delta", "delta": gin.H{"stop_reason": stopReason, "stop_sequence": nil}, - "usage": gin.H{"output_tokens": outputTokens}, + "usage": gin.H{ + "input_tokens": inputTokens, + "cache_creation_input_tokens": cacheWriteTokens, + "cache_read_input_tokens": cacheReadTokens, + "output_tokens": outputTokens, + }, }) writeSSE("message_stop", gin.H{ @@ -686,3 +736,18 @@ func windsurfLogger(c *gin.Context, component string, fields ...zap.Field) *zap. } return l.With(fields...) } + +// generateAnthropicMessageID 生成符合 Anthropic API 签名格式的消息 ID: +// "msg_01" 前缀 + 22 位 base62 随机字符(总长 28 字符,与官方 msg_013Zva2CMHLNnXjNJJKqJ2EF 一致)。 +// 签名校验类工具常按 prefix/length 校验,长度差一位就会挂。 +func generateAnthropicMessageID() string { + const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + const suffixLen = 22 + var buf [suffixLen]byte + _, _ = rand.Read(buf[:]) + out := make([]byte, suffixLen) + for i, b := range buf { + out[i] = alphabet[int(b)%len(alphabet)] + } + return "msg_01" + string(out) +} diff --git a/backend/internal/service/windsurf_services.go b/backend/internal/service/windsurf_services.go index d8b85898..c622e8b6 100644 --- a/backend/internal/service/windsurf_services.go +++ b/backend/internal/service/windsurf_services.go @@ -119,16 +119,16 @@ func NewWindsurfAuthService( } type WindsurfLoginInput struct { - Email string - Password string - Name string - Notes *string - ProxyID *int64 - GroupIDs []int64 - Concurrency int - Priority int - ProbeAfter bool - LSInstanceID string + Email string + Password string + Name string + Notes *string + ProxyID *int64 + GroupIDs []int64 + Concurrency int + Priority int + ProbeAfter bool + LSInstanceID string } type WindsurfLoginOutput struct { diff --git a/backend/internal/service/windsurf_token_provider.go b/backend/internal/service/windsurf_token_provider.go index b454cbbd..932fe831 100644 --- a/backend/internal/service/windsurf_token_provider.go +++ b/backend/internal/service/windsurf_token_provider.go @@ -32,11 +32,11 @@ func NewWindsurfTokenProvider( } type WindsurfToken struct { - APIKey string - ProxyURL string - AccountID int64 - Tier string - LSBinding WindsurfLSBinding + APIKey string + ProxyURL string + AccountID int64 + Tier string + LSBinding WindsurfLSBinding } func (p *WindsurfTokenProvider) GetToken(ctx context.Context, accountID int64) (*WindsurfToken, error) {