diff --git a/backend/internal/service/openai_agent_identity_compat_test.go b/backend/internal/service/openai_agent_identity_compat_test.go index bbfa82481d..ff58276310 100644 --- a/backend/internal/service/openai_agent_identity_compat_test.go +++ b/backend/internal/service/openai_agent_identity_compat_test.go @@ -189,6 +189,32 @@ func TestOpenAIWSAgentIdentityRecoveryRequiresTaskInvalidBody(t *testing.T) { })) } +func TestValidateOpenAIWSBearerTokenAllowsAgentIdentityWithoutStoredToken(t *testing.T) { + t.Run("Given Agent Identity When a WS path receives no bearer token Then dial-time assertion auth is allowed", func(t *testing.T) { + account := &Account{ + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Credentials: map[string]any{ + "auth_mode": OpenAIAuthModeAgentIdentity, + }, + } + + require.NoError(t, validateOpenAIWSBearerToken(account, "")) + }) + + t.Run("Given bearer credentials When a WS path receives no token Then the request is rejected", func(t *testing.T) { + accounts := []*Account{ + {Platform: PlatformOpenAI, Type: AccountTypeOAuth}, + {Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"auth_mode": OpenAIAuthModePersonalAccessToken}}, + {Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, + } + + for _, account := range accounts { + require.EqualError(t, validateOpenAIWSBearerToken(account, ""), "token is empty") + } + }) +} + func TestOpenAIWSConnPoolHeadersFactoryRunsAtDialAndStalePrewarmIsDiscarded(t *testing.T) { cfg := &config.Config{} cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1 diff --git a/backend/internal/service/openai_ws_forwarder_ingress.go b/backend/internal/service/openai_ws_forwarder_ingress.go index 169919b8b9..f7a54fb2b5 100644 --- a/backend/internal/service/openai_ws_forwarder_ingress.go +++ b/backend/internal/service/openai_ws_forwarder_ingress.go @@ -39,8 +39,8 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient( if account == nil { return errors.New("account is nil") } - if strings.TrimSpace(token) == "" { - return errors.New("token is empty") + if err := validateOpenAIWSBearerToken(account, token); err != nil { + return err } // 预取一次 OpenAI Fast Policy settings,绑定到 ctx,让该 WS session diff --git a/backend/internal/service/openai_ws_forwarder_payload.go b/backend/internal/service/openai_ws_forwarder_payload.go index 0aa4adc313..67fdddc9f7 100644 --- a/backend/internal/service/openai_ws_forwarder_payload.go +++ b/backend/internal/service/openai_ws_forwarder_payload.go @@ -15,6 +15,16 @@ import ( "github.com/tidwall/sjson" ) +func validateOpenAIWSBearerToken(account *Account, token string) error { + if account == nil { + return errors.New("account is nil") + } + if strings.TrimSpace(token) == "" && !account.IsOpenAIAgentIdentity() { + return errors.New("token is empty") + } + return nil +} + func (s *OpenAIGatewayService) buildOpenAIResponsesWSURL(account *Account) (string, error) { if account == nil { return "", errors.New("account is nil") diff --git a/backend/internal/service/openai_ws_v2_passthrough_adapter.go b/backend/internal/service/openai_ws_v2_passthrough_adapter.go index 75df7191ac..72fbbd0f35 100644 --- a/backend/internal/service/openai_ws_v2_passthrough_adapter.go +++ b/backend/internal/service/openai_ws_v2_passthrough_adapter.go @@ -243,8 +243,8 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough( if account == nil { return errors.New("account is nil") } - if strings.TrimSpace(token) == "" { - return errors.New("token is empty") + if err := validateOpenAIWSBearerToken(account, token); err != nil { + return err } requestModel := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "model").String()) requestPreviousResponseID := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "previous_response_id").String())