Repository: PrefectHQ/prefect

Issue:
openapi schema built incorrectly when `SecretStr` in flow parameters with `pydantic>=2`
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

openapi schema for flows does not represent `SecretStr` params with `format: password`, but rather as a generic `object`

### Reproduction

```python3
# pip install -U pydantic
from prefect import flow
from pydantic import SecretStr


@flow(log_prints=True)
def accepts_secret(secret: SecretStr) -> None:
    print(secret.get_secret_value())

if __name__ == "__main__":
    accepts_secret.serve(name="Accepts Secret")
```


### Error

_No response_

### Versions

```Text
» prefect version
Version:             2.14.13+34.gc082e43d0
API version:         0.8.4
Python version:      3.11.7
Git commit:          c082e43d
Built:               Thu, Jan 11, 2024 12:45 PM
OS/Arch:             darwin/arm64
Profile:             pong
Server type:         cloud
```


### Additional context

- since the UI sees the v2 case as `object` without a password format, secrets are exposed in the UI

schema with v2
```
   'parameter_openapi_schema': {
      'type': 'object', 'title': 'Parameters', 'required': ['secret'], 'properties': {'secret': {'title': 'secret', 'position': 0}}
   },
```

schema with v1
```
    'parameter_openapi_schema': {
        'type': 'object',
        'title': 'Parameters',
        'required': ['secret'],
        'properties': {'secret': {'type': 'string', 'title': 'secret', 'format': 'password', 'position': 0, 'writeOnly': True}}
    },
```

- getting `SecretStr` from the `v1` module does not work either

```
...
  File "/Users/nate/micromamba/envs/prefect-monorepo/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 1990, in _extract_get_pydantic_json_schema
    raise PydanticUserError(
pydantic.errors.PydanticUserError: The `__modify_schema__` method is not supported in Pydantic v2. Use `__get_pydantic_json_schema__` instead.
For further information visit https://errors.pydantic.dev/2.5/u/custom-json-schema
```


Discussion:

Resolving patch:
diff --git a/src/prefect/_internal/pydantic/v2_schema.py b/src/prefect/_internal/pydantic/v2_schema.py
--- a/src/prefect/_internal/pydantic/v2_schema.py
+++ b/src/prefect/_internal/pydantic/v2_schema.py
@@ -30,16 +30,26 @@ def is_v2_model(v) -> bool:
     return False
 
 
-def has_v2_model_as_param(signature: inspect.Signature) -> bool:
+def is_v2_type(v) -> bool:
+    if is_v2_model(v):
+        return True
+
+    try:
+        return v.__module__.startswith("pydantic.types")
+    except AttributeError:
+        return False
+
+
+def has_v2_type_as_param(signature: inspect.Signature) -> bool:
     parameters = signature.parameters.values()
     for p in parameters:
         # check if this parameter is a v2 model
-        if is_v2_model(p.annotation):
+        if is_v2_type(p.annotation):
             return True
 
         # check if this parameter is a collection of types
         for v in typing.get_args(p.annotation):
-            if is_v2_model(v):
+            if is_v2_type(v):
                 return True
     return False
 
diff --git a/src/prefect/flows.py b/src/prefect/flows.py
--- a/src/prefect/flows.py
+++ b/src/prefect/flows.py
@@ -48,11 +48,11 @@
 
 if HAS_PYDANTIC_V2:
     import pydantic.v1 as pydantic
-    from pydantic import BaseModel as V2BaseModel
     from pydantic import ValidationError as V2ValidationError
     from pydantic.v1 import BaseModel as V1BaseModel
     from pydantic.v1.decorator import ValidatedFunction as V1ValidatedFunction
 
+    from ._internal.pydantic.v2_schema import is_v2_type
     from ._internal.pydantic.v2_validated_func import V2ValidatedFunction
     from ._internal.pydantic.v2_validated_func import (
         V2ValidatedFunction as ValidatedFunction,
@@ -490,13 +490,13 @@ def validate_parameters(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
             has_v1_models = any(isinstance(o, V1BaseModel) for o in args) or any(
                 isinstance(o, V1BaseModel) for o in kwargs.values()
             )
-            has_v2_models = any(isinstance(o, V2BaseModel) for o in args) or any(
-                isinstance(o, V2BaseModel) for o in kwargs.values()
+            has_v2_types = any(is_v2_type(o) for o in args) or any(
+                is_v2_type(o) for o in kwargs.values()
             )
 
-            if has_v1_models and has_v2_models:
+            if has_v1_models and has_v2_types:
                 raise ParameterTypeError(
-                    "Cannot mix Pydantic v1 and v2 models as arguments to a flow."
+                    "Cannot mix Pydantic v1 and v2 types as arguments to a flow."
                 )
 
             if has_v1_models:
diff --git a/src/prefect/utilities/callables.py b/src/prefect/utilities/callables.py
--- a/src/prefect/utilities/callables.py
+++ b/src/prefect/utilities/callables.py
@@ -14,7 +14,7 @@
 
     from prefect._internal.pydantic.v2_schema import (
         create_v2_schema,
-        has_v2_model_as_param,
+        has_v2_type_as_param,
         process_v2_params,
     )
 else:
@@ -323,7 +323,7 @@ def parameter_schema(fn: Callable) -> ParameterSchema:
     class ModelConfig:
         arbitrary_types_allowed = True
 
-    if HAS_PYDANTIC_V2 and has_v2_model_as_param(signature):
+    if HAS_PYDANTIC_V2 and has_v2_type_as_param(signature):
         create_schema = create_v2_schema
         process_params = process_v2_params
     else:


Test patch:
diff --git a/tests/test_flows.py b/tests/test_flows.py
--- a/tests/test_flows.py
+++ b/tests/test_flows.py
@@ -1441,7 +1441,7 @@ def my_subflow(x):
 
         assert my_flow() == data
 
-    def test_subflow_parameters_can_be_pydantic_types_from_task_future(self):
+    def test_subflow_parameters_can_be_pydantic_models_from_task_future(self):
         @flow
         def my_flow():
             return my_subflow(identity.submit(ParameterTestModel(data=1)))
@@ -1456,7 +1456,7 @@ def my_subflow(x):
 
         assert my_flow() == ParameterTestModel(data=1)
 
-    def test_subflow_parameter_annotations_can_be_non_pydantic_classes(self):
+    def test_subflow_parameter_annotations_can_be_normal_classes(self):
         class Test:
             pass
 
diff --git a/tests/utilities/test_callables.py b/tests/utilities/test_callables.py
--- a/tests/utilities/test_callables.py
+++ b/tests/utilities/test_callables.py
@@ -13,6 +13,7 @@
 
 import pytest
 from packaging.version import Version
+from pydantic import SecretStr
 
 from prefect.exceptions import ParameterBindError
 from prefect.utilities import callables
@@ -447,6 +448,51 @@ def f(
             },
         }
 
+    def test_function_with_secretstr(self):
+        def f(x: SecretStr):
+            pass
+
+        schema = callables.parameter_schema(f)
+        assert schema.dict() == {
+            "title": "Parameters",
+            "type": "object",
+            "properties": {
+                "x": {
+                    "title": "x",
+                    "position": 0,
+                    "format": "password",
+                    "type": "string",
+                    "writeOnly": True,
+                },
+            },
+            "required": ["x"],
+        }
+
+    @pytest.mark.skipif(
+        not HAS_PYDANTIC_V2, reason="pydantic v1 module only present in pydantic v2"
+    )
+    def test_function_with_v1_secretstr_from_compat_module(self):
+        import pydantic.v1 as pydantic
+
+        def f(x: pydantic.SecretStr):
+            pass
+
+        schema = callables.parameter_schema(f)
+        assert schema.dict() == {
+            "title": "Parameters",
+            "type": "object",
+            "properties": {
+                "x": {
+                    "title": "x",
+                    "position": 0,
+                    "format": "password",
+                    "type": "string",
+                    "writeOnly": True,
+                },
+            },
+            "required": ["x"],
+        }
+
 
 class TestMethodToSchema:
     def test_methods_with_no_arguments(self):