Repository: bentoml/BentoML
Pull request #3896 (2023-05-27 23:39:35)

feat(tracking): enable telemetry to track serve from Server API
This PR add a telemetry field into our serve metrics to enable checking if users
are running serve from the new Server API or from the CLI. Since we are copying
over the environment to the subprocess, it will still respect
BENTOML_DO_NOT_TRACK

This is usefull for tracking OpenLLM telemetry.

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>



Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3aa13cc3..c17285e5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -73,7 +73,7 @@ jobs:
         run: |
           npm install -g npm@^7 pyright
       - name: Setup bufbuild/buf
-        uses: bufbuild/buf-setup-action@v1.19.0
+        uses: bufbuild/buf-setup-action@v1.20.0
         with:
           github_token: ${{ github.token }}
       - name: Cache pip dependencies
diff --git a/src/bentoml/_internal/frameworks/transformers.py b/src/bentoml/_internal/frameworks/transformers.py
index 4b8ce31c..2f73d11f 100644
--- a/src/bentoml/_internal/frameworks/transformers.py
+++ b/src/bentoml/_internal/frameworks/transformers.py
@@ -60,7 +60,7 @@ if t.TYPE_CHECKING:
 
         @classmethod
         def from_pretrained(
-            cls, pretrained_model_name_or_path: str, **kwargs: t.Any
+            cls, pretrained_model_name_or_path: str, *args: t.Any, **kwargs: t.Any
         ) -> PreTrainedProtocol:
             ...
 
@@ -356,11 +356,13 @@ def load_model(
 
 
 @t.overload
-def load_model(bento_model: str | Tag | Model, **kwargs: t.Any) -> PreTrainedProtocol:
+def load_model(
+    bento_model: str | Tag | Model, *args: t.Any, **kwargs: t.Any
+) -> TransformersPreTrained:
     ...
 
 
-def load_model(bento_model: str | Tag | Model, **kwargs: t.Any) -> t.Any:
+def load_model(bento_model: str | Tag | Model, *args: t.Any, **kwargs: t.Any) -> t.Any:
     """
     Load the Transformers model from BentoML local modelstore with given name.
 
@@ -368,6 +370,8 @@ def load_model(bento_model: str | Tag | Model, **kwargs: t.Any) -> t.Any:
         bento_model: Either the tag of the model to get from the store,
                      or a BentoML :class:`~bentoml.Model` instance to load the
                      model from.
+        args: Additional model args to be passed into the model if the object is a Transformers PreTrained protocol.
+              This shouldn't be used when the bento_model is a pipeline.
         kwargs: Additional keyword arguments to pass into the pipeline.
 
     Returns:
@@ -463,8 +467,11 @@ def load_model(bento_model: str | Tag | Model, **kwargs: t.Any) -> t.Any:
         if "_pretrained_class" in bento_model.info.metadata:
             with open(bento_model.path_of(PRETRAINED_PROTOCOL_NAME), "rb") as f:
                 protocol: PreTrainedProtocol = cloudpickle.load(f)
-            return protocol.from_pretrained(bento_model.path, **kwargs)
+            return protocol.from_pretrained(bento_model.path, *args, **kwargs)
         else:
+            assert (
+                len(args) == 0
+            ), "Positional args are not supported for pipeline. Make sure to only use kwargs instead."
             with open(bento_model.path_of(PIPELINE_PICKLE_NAME), "rb") as f:
                 pipeline_class: type[transformers.Pipeline] = cloudpickle.load(f)
 
diff --git a/src/bentoml/_internal/utils/analytics/schemas.py b/src/bentoml/_internal/utils/analytics/schemas.py
index 2bc7fb9e..8ad5e4f7 100644
--- a/src/bentoml/_internal/utils/analytics/schemas.py
+++ b/src/bentoml/_internal/utils/analytics/schemas.py
@@ -190,6 +190,7 @@ class ServeInitEvent(EventMeta):
     serve_id: str
     production: bool
     serve_from_bento: bool
+    serve_from_server_api: bool
 
     bento_creation_timestamp: t.Optional[datetime]
     serve_kind: str = attr.field(validator=attr.validators.in_(SERVE_KIND))
@@ -209,6 +210,7 @@ class ServeUpdateEvent(EventMeta):
     triggered_at: datetime
     duration_in_seconds: int
     serve_kind: str = attr.field(validator=attr.validators.in_(SERVE_KIND))
+    serve_from_server_api: bool
     component: str = attr.field(
         validator=attr.validators.and_(
             attr.validators.instance_of(str), attr.validators.in_(COMPONENT_KIND)
diff --git a/src/bentoml/_internal/utils/analytics/usage_stats.py b/src/bentoml/_internal/utils/analytics/usage_stats.py
index ada306bf..b82b382b 100644
--- a/src/bentoml/_internal/utils/analytics/usage_stats.py
+++ b/src/bentoml/_internal/utils/analytics/usage_stats.py
@@ -40,11 +40,17 @@ if TYPE_CHECKING:
 logger = logging.getLogger(__name__)
 
 BENTOML_DO_NOT_TRACK = "BENTOML_DO_NOT_TRACK"
+BENTOML_SERVE_FROM_SERVER_API = "__BENTOML_SERVE_FROM_SERVER_API"
 USAGE_TRACKING_URL = "https://t.bentoml.com"
 SERVE_USAGE_TRACKING_INTERVAL_SECONDS = int(12 * 60 * 60)  # every 12 hours
 USAGE_REQUEST_TIMEOUT_SECONDS = 1
 
 
+@lru_cache(maxsize=None)
+def _bentoml_serve_from_server_api() -> bool:
+    return os.environ.get(BENTOML_SERVE_FROM_SERVER_API, str(False)).lower() == "true"
+
+
 @lru_cache(maxsize=1)
 def do_not_track() -> bool:  # pragma: no cover
     # Returns True if and only if the environment variable is defined and has value True.
@@ -126,6 +132,7 @@ def _track_serve_init(
     svc: Service,
     production: bool,
     serve_kind: str,
+    from_server_api: bool,
     serve_info: ServeInfo = Provide[BentoMLContainer.serve_info],
 ):
     if svc.bento is not None:
@@ -133,6 +140,7 @@ def _track_serve_init(
         event_properties = ServeInitEvent(
             serve_id=serve_info.serve_id,
             serve_from_bento=True,
+            serve_from_server_api=from_server_api,
             production=production,
             serve_kind=serve_kind,
             bento_creation_timestamp=bento.info.creation_time,
@@ -148,6 +156,7 @@ def _track_serve_init(
         event_properties = ServeInitEvent(
             serve_id=serve_info.serve_id,
             serve_from_bento=False,
+            serve_from_server_api=from_server_api,
             production=production,
             serve_kind=serve_kind,
             bento_creation_timestamp=None,
@@ -237,6 +246,7 @@ def track_serve(
     svc: Service,
     *,
     production: bool = False,
+    from_server_api: bool | None = None,
     serve_kind: str = "http",
     component: str = "standalone",
     metrics_client: PrometheusClient = Provide[BentoMLContainer.metrics_client],
@@ -246,7 +256,15 @@ def track_serve(
         yield
         return
 
-    _track_serve_init(svc=svc, production=production, serve_kind=serve_kind)
+    if from_server_api is None:
+        from_server_api = _bentoml_serve_from_server_api()
+
+    _track_serve_init(
+        svc=svc,
+        production=production,
+        serve_kind=serve_kind,
+        from_server_api=from_server_api,
+    )
 
     if _usage_event_debugging():
         tracking_interval = 5
@@ -267,6 +285,8 @@ def track_serve(
                 serve_kind=serve_kind,
                 # Current accept components are "standalone", "api_server" and "runner"
                 component=component,
+                # check if serve is running from server API or just normal CLI
+                serve_from_server_api=from_server_api,
                 triggered_at=now,
                 duration_in_seconds=int((now - last_tracked_timestamp).total_seconds()),
                 metrics=get_metrics_report(metrics_client, serve_kind=serve_kind),
diff --git a/src/bentoml/bentos.py b/src/bentoml/bentos.py
index 61a714c9..84f0762f 100644
--- a/src/bentoml/bentos.py
+++ b/src/bentoml/bentos.py
@@ -5,6 +5,7 @@ User facing python APIs for managing local bentos and build new bentos.
 from __future__ import annotations
 
 import os
+import re
 import typing as t
 import logging
 import tempfile
@@ -21,6 +22,7 @@ from ._internal.bento import Bento
 from ._internal.utils import resolve_user_filepath
 from ._internal.bento.build_config import BentoBuildConfig
 from ._internal.configuration.containers import BentoMLContainer
+from ._internal.utils.analytics.usage_stats import _usage_event_debugging
 
 if t.TYPE_CHECKING:
     from .server import Server
@@ -48,27 +50,27 @@ __all__ = [
 
 
 @inject
-def list(  # pylint: disable=redefined-builtin
-    tag: t.Optional[t.Union[Tag, str]] = None,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
-) -> "t.List[Bento]":
+def list(
+    tag: Tag | str | None = None,
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
+) -> t.List[Bento]:
     return _bento_store.list(tag)
 
 
 @inject
 def get(
-    tag: t.Union[Tag, str],
+    tag: Tag | str,
     *,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
 ) -> Bento:
     return _bento_store.get(tag)
 
 
 @inject
 def delete(
-    tag: t.Union[Tag, str],
+    tag: Tag | str,
     *,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
 ):
     _bento_store.delete(tag)
 
@@ -76,13 +78,13 @@ def delete(
 @inject
 def import_bento(
     path: str,
-    input_format: t.Optional[str] = None,
+    input_format: str | None = None,
     *,
-    protocol: t.Optional[str] = None,
-    user: t.Optional[str] = None,
-    passwd: t.Optional[str] = None,
+    protocol: str | None = None,
+    user: str | None = None,
+    passwd: str | None = None,
     params: t.Optional[t.Dict[str, str]] = None,
-    subpath: t.Optional[str] = None,
+    subpath: str | None = None,
     _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
 ) -> Bento:
     """
@@ -156,16 +158,16 @@ def import_bento(
 
 @inject
 def export_bento(
-    tag: t.Union[Tag, str],
+    tag: Tag | str,
     path: str,
-    output_format: t.Optional[str] = None,
+    output_format: str | None = None,
     *,
-    protocol: t.Optional[str] = None,
-    user: t.Optional[str] = None,
-    passwd: t.Optional[str] = None,
-    params: t.Optional[t.Dict[str, str]] = None,
-    subpath: t.Optional[str] = None,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
+    protocol: str | None = None,
+    user: str | None = None,
+    passwd: str | None = None,
+    params: dict[str, str] | None = None,
+    subpath: str | None = None,
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
 ) -> str:
     """
     Export a bento.
@@ -240,10 +242,10 @@ def export_bento(
 
 @inject
 def push(
-    tag: t.Union[Tag, str],
+    tag: Tag | str,
     *,
     force: bool = False,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
     _yatai_client: YataiClient = Provide[BentoMLContainer.yatai_client],
 ):
     """Push Bento to a yatai server."""
@@ -255,10 +257,10 @@ def push(
 
 @inject
 def pull(
-    tag: t.Union[Tag, str],
+    tag: Tag | str,
     *,
     force: bool = False,
-    _bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
+    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
     _yatai_client: YataiClient = Provide[BentoMLContainer.yatai_client],
 ):
     _yatai_client.pull_bento(tag, force=force, bento_store=_bento_store)
@@ -377,7 +379,16 @@ def build(
             logger.error("Failed to build BentoService bundle: %s", e)
             raise
 
-    return get(output.decode("utf-8").strip().split("\n")[-1])
+    if _usage_event_debugging():
+        # NOTE: This usually only concern BentoML devs.
+        pattern = r"^__tag__:[^:\n]+:[^:\n]+"
+        matched = re.search(pattern, output.decode("utf-8").strip(), re.MULTILINE)
+        assert matched is not None, f"Failed to find tag from output: {output}"
+        _, _, tag = matched.group(0).partition(":")
+    else:
+        # This branch is the current behaviour that doesn't concern BentoML users.
+        tag = output.decode("utf-8").strip().split("\n")[-1]
+    return get(tag, _bento_store=_bento_store)
 
 
 @inject
@@ -419,7 +430,16 @@ def build_bentofile(
         logger.error("Failed to build BentoService bundle: %s", e)
         raise
 
-    return get(output.decode("utf-8").strip().split("\n")[-1])
+    if _usage_event_debugging():
+        # NOTE: This usually only concern BentoML devs.
+        pattern = r"^__tag__:[^:\n]+:[^:\n]+"
+        matched = re.search(pattern, output.decode("utf-8").strip(), re.MULTILINE)
+        assert matched is not None, f"Failed to find tag from output: {output}"
+        _, _, tag = matched.group(0).partition(":")
+    else:
+        # This branch is the current behaviour that doesn't concern BentoML users.
+        tag = output.decode("utf-8").strip().split("\n")[-1]
+    return get(tag, _bento_store=_bento_store)
 
 
 def containerize(bento_tag: Tag | str, **kwargs: t.Any) -> bool:
diff --git a/src/bentoml/server.py b/src/bentoml/server.py
index d0285db0..7a37c44a 100644
--- a/src/bentoml/server.py
+++ b/src/bentoml/server.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import os
 import sys
 import typing as t
 import logging
@@ -17,6 +18,7 @@ from ._internal.tag import Tag
 from ._internal.bento import Bento
 from ._internal.service import Service
 from ._internal.configuration.containers import BentoMLContainer
+from ._internal.utils.analytics.usage_stats import BENTOML_SERVE_FROM_SERVER_API
 
 if TYPE_CHECKING:
     from types import TracebackType
@@ -145,6 +147,12 @@ class Server(ABC):
             stderr: The stderr to pass to the server. Default to ``None``.
             text: Whether to output as text or bytes for stdout and stderr. Default to ``None``.
         """
+        # NOTE: User shouldn't manually set this since this envvar will be managed by BentoML.
+        os.environ[BENTOML_SERVE_FROM_SERVER_API] = str(True)
+
+        if env is None:
+            env = {}
+        env.update(os.environ.copy())
 
         class _Manager:
             def __init__(__inner_self):
@@ -184,11 +192,50 @@ class Server(ABC):
 
         return _Manager()
 
-    @abstractmethod
     def get_client(self) -> Client | None:
+        if self.process is None:
+            # NOTE: if the process is None, we reset this envvar
+            del os.environ[BENTOML_SERVE_FROM_SERVER_API]
+            logger.warning(
+                "Attempted to get a client for a BentoML server that was not running! Try running 'bentoml.*Server.start()' first."
+            )
+            return
+        assert self.process is not None
+        out_code = self.process.poll()
+        if out_code == 0:
+            # NOTE: if the process is None, we reset this envvar
+            del os.environ[BENTOML_SERVE_FROM_SERVER_API]
+            logger.warning(
+                "Attempted to get a client from a BentoML server that has already exited! You can run '.start()' again to restart it."
+            )
+            return
+        elif out_code is not None:
+            # NOTE: if the process is None, we reset this envvar
+            del os.environ[BENTOML_SERVE_FROM_SERVER_API]
+            logs = "Attempted to get a client from a BentoML server that has already exited with an error!\nServer Output:\n"
+            if self.process.stdout is not None and not self.process.stdout.closed:
+                s = self.process.stdout.read()
+                logs += textwrap.indent(
+                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
+                )
+            if self.process.stderr is not None and not self.process.stderr.closed:
+                logs += "\nServer Error:\n"
+                s = self.process.stderr.read()
+                logs += textwrap.indent(
+                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
+                )
+            logger.warning(logs)
+            return
+        return self._get_client()
+
+    @abstractmethod
+    def _get_client(self) -> Client | None:
         pass
 
     def stop(self) -> None:
+        # NOTE: User shouldn't manually set this since this envvar will be managed by BentoML.
+        del os.environ[BENTOML_SERVE_FROM_SERVER_API]
+
         if self.process is None:
             logger.warning("Attempted to stop a BentoML server that was not running!")
             return
@@ -301,37 +348,9 @@ class HTTPServer(Server):
         logger.warning(
             "'Server.client()' is deprecated, use 'Server.get_client()' instead."
         )
-        return self.get_client()
-
-    def get_client(self) -> HTTPClient | None:
-        if self.process is None:
-            logger.warning(
-                "Attempted to get a client for a BentoML server that was not running! Try running 'bentoml.*Server.start()' first."
-            )
-            return
-        assert self.process is not None
-        out_code = self.process.poll()
-        if out_code == 0:
-            logger.warning(
-                "Attempted to get a client from a BentoML server that has already exited! You can run '.start()' again to restart it."
-            )
-            return
-        elif out_code is not None:
-            logs = "Attempted to get a client from a BentoML server that has already exited with an error!\nServer Output:\n"
-            if self.process.stdout is not None and not self.process.stdout.closed:
-                s = self.process.stdout.read()
-                logs += textwrap.indent(
-                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
-                )
-            if self.process.stderr is not None and not self.process.stderr.closed:
-                logs += "\nServer Error:\n"
-                s = self.process.stderr.read()
-                logs += textwrap.indent(
-                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
-                )
-            logger.warning(logs)
-            return
+        return t.cast("HTTPClient | None", self.get_client())
 
+    def _get_client(self) -> HTTPClient | None:
         if self._client is None:
             from .client import HTTPClient
 
@@ -401,35 +420,7 @@ class GrpcServer(Server):
         if grpc_protocol_version is not None:
             self.args.extend(["--protocol-version", str(grpc_protocol_version)])
 
-    def get_client(self) -> GrpcClient | None:
-        if self.process is None:
-            logger.warning(
-                "Attempted to get a client for a BentoML server that was not running! Try running 'bentoml.*Server.start()' first."
-            )
-            return
-        assert self.process is not None
-        out_code = self.process.poll()
-        if out_code == 0:
-            logger.warning(
-                "Attempted to get a client from a BentoML server that has already exited! You can run '.start()' again to restart it."
-            )
-            return
-        elif out_code is not None:
-            logs = "Attempted to get a client from a BentoML server that has already exited with an error!\nServer Output:\n"
-            if self.process.stdout is not None and not self.process.stdout.closed:
-                s = self.process.stdout.read()
-                logs += textwrap.indent(
-                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
-                )
-            if self.process.stderr is not None and not self.process.stderr.closed:
-                logs += "\nServer Error:\n"
-                s = self.process.stderr.read()
-                logs += textwrap.indent(
-                    s.decode("utf-8") if isinstance(s, bytes) else s, " " * 4
-                )
-            logger.warning(logs)
-            return
-
+    def _get_client(self) -> GrpcClient | None:
         if self._client is None:
             from .client import GrpcClient
 
diff --git a/src/bentoml_cli/bentos.py b/src/bentoml_cli/bentos.py
index d4fb76ca..c57afe08 100644
--- a/src/bentoml_cli/bentos.py
+++ b/src/bentoml_cli/bentos.py
@@ -64,6 +64,7 @@ def add_bento_management_commands(cli: Group):
     from bentoml._internal.configuration import get_quiet_mode
     from bentoml._internal.bento.build_config import BentoBuildConfig
     from bentoml._internal.configuration.containers import BentoMLContainer
+    from bentoml._internal.utils.analytics.usage_stats import _usage_event_debugging
 
     bento_store = BentoMLContainer.bento_store.get()
     yatai_client = BentoMLContainer.yatai_client.get()
@@ -311,7 +312,7 @@ def add_bento_management_commands(cli: Group):
         build_ctx: str,
         bentofile: str,
         version: str,
-        output: t.Literal["tag", "default"],
+        output: t.Literal["tag", "default", "yaml"],
         _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
     ):
         """Build a new Bento from current directory."""
@@ -334,7 +335,14 @@ def add_bento_management_commands(cli: Group):
         # NOTE: Don't remove the return statement here, since we will need this
         # for usage stats collection if users are opt-in.
         if output == "tag":
-            click.echo(bento.tag)
+            if _usage_event_debugging():
+                # NOTE: Since we are logging all of the trackintg id to stdout
+                # We will prefix the tag with __tag__ and we can use regex to correctly
+                # get the tag from 'bentoml.bentos.build|build_bentofile'
+                click.echo(f"__tag__:{bento.tag}")
+            else:
+                # Current behaviour
+                click.echo(bento.tag)
             return bento
 
         if not get_quiet_mode():
diff --git a/src/bentoml_cli/serve.py b/src/bentoml_cli/serve.py
index 416de059..077bbbf2 100644
--- a/src/bentoml_cli/serve.py
+++ b/src/bentoml_cli/serve.py
@@ -2,15 +2,50 @@ from __future__ import annotations
 
 import os
 import sys
+import typing as t
 import logging
 
 import click
 
+if t.TYPE_CHECKING:
+    P = t.ParamSpec("P")
+    F = t.Callable[P, t.Any]
+
+
 logger = logging.getLogger(__name__)
 
 DEFAULT_DEV_SERVER_HOST = "127.0.0.1"
 
 
+def deprecated_option(*param_decls: str, **attrs: t.Any):
+    """Marks a given options as deprecated, and omit a warning when it's used"""
+    deprecated = attrs.pop("deprecated", True)
+    new_behaviour = attrs.pop("current_behaviour", None)
+    assert new_behaviour is not None, "current_behaviour is required"
+
+    def show_deprecated_callback(
+        ctx: click.Context, param: click.Parameter, value: t.Any
+    ):
+        if value is not param.default and deprecated:
+            name = "'--%(name)s'" if attrs.get("is_flag", False) else "'%(name)s'"
+            DEPRECATION_WARNING = f"DeprecationWarning: The parameter {name} is deprecated and will be removed in the future. (Current behaviour: %(new_behaviour)s)"
+            click.secho(
+                DEPRECATION_WARNING
+                % {"name": param.name, "new_behaviour": new_behaviour},
+                fg="yellow",
+                err=True,
+            )
+
+    def decorator(f: F[t.Any]) -> t.Callable[[F[t.Any]], click.Command]:
+        msg = attrs.pop("help", "")
+        msg += " (Deprecated)" if msg else "(Deprecated)"
+        attrs.setdefault("help", msg)
+        attrs.setdefault("callback", show_deprecated_callback)
+        return click.option(*param_decls, **attrs)(f)
+
+    return decorator
+
+
 def add_serve_command(cli: click.Group) -> None:
     from bentoml.grpc.utils import LATEST_PROTOCOL_VERSION
     from bentoml._internal.log import configure_server_logging
@@ -27,13 +62,14 @@ def add_serve_command(cli: click.Group) -> None:
         default=False,
         show_default=True,
     )
-    @click.option(
+    @deprecated_option(
         "--production",
         type=click.BOOL,
-        help="Deprecated option, by default BentoServer will run in production mode",
+        help="Run BentoServer in production mode",
+        current_behaviour="This is enabled by default. To run in development mode, use '--development'.",
         is_flag=True,
         default=True,
-        show_default=True,
+        show_default=False,
     )
     @click.option(
         "-p",
@@ -135,13 +171,12 @@ def add_serve_command(cli: click.Group) -> None:
     def serve(  # type: ignore (unused warning)
         bento: str,
         development: bool,
-        production: bool,  # deprecated and ignored
         port: int,
         host: str,
         api_workers: int | None,
         backlog: int,
         reload: bool,
-        working_dir: str,
+        working_dir: str | None,
         ssl_certfile: str | None,
         ssl_keyfile: str | None,
         ssl_keyfile_password: str | None,
@@ -149,6 +184,7 @@ def add_serve_command(cli: click.Group) -> None:
         ssl_cert_reqs: int | None,
         ssl_ca_certs: str | None,
         ssl_ciphers: str | None,
+        **attrs: t.Any,
     ) -> None:
         """Start a HTTP BentoServer from a given 🍱
 
@@ -242,13 +278,14 @@ def add_serve_command(cli: click.Group) -> None:
         default=False,
         show_default=True,
     )
-    @click.option(
+    @deprecated_option(
         "--production",
         type=click.BOOL,
-        help="Deprecated option, by default BentoServer will run in production mode",
+        help="Run BentoServer in production mode",
+        current_behaviour="This is enabled by default. To run in development mode, use '--development'.",
         is_flag=True,
         default=True,
-        show_default=True,
+        show_default=False,
     )
     @click.option(
         "-p",
@@ -353,13 +390,12 @@ def add_serve_command(cli: click.Group) -> None:
     def serve_grpc(  # type: ignore (unused warning)
         bento: str,
         development: bool,
-        production: bool,  # deprecated and ignored
         port: int,
         host: str,
         api_workers: int | None,
         backlog: int,
         reload: bool,
-        working_dir: str,
+        working_dir: str | None,
         ssl_certfile: str | None,
         ssl_keyfile: str | None,
         ssl_ca_certs: str | None,
@@ -367,6 +403,7 @@ def add_serve_command(cli: click.Group) -> None:
         enable_channelz: bool,
         max_concurrent_streams: int | None,
         protocol_version: str,
+        **attrs: t.Any,
     ):
         """Start a gRPC BentoServer from a given 🍱
 


Test patch:
diff --git a/tests/unit/_internal/utils/test_analytics.py b/tests/unit/_internal/utils/test_analytics.py
index 180f07ff..e160292a 100644
--- a/tests/unit/_internal/utils/test_analytics.py
+++ b/tests/unit/_internal/utils/test_analytics.py
@@ -1,19 +1,15 @@
 # pylint: disable=unused-argument
 from __future__ import annotations
 
-import typing as t
 import logging
+import typing as t
 from typing import TYPE_CHECKING
-from unittest.mock import Mock
-from unittest.mock import patch
+from unittest.mock import Mock, patch
 
 import pytest
-from schema import Or
-from schema import And
-from schema import Schema
-from prometheus_client.parser import (
-    text_string_to_metric_families,  # type: ignore (no prometheus types)
-)
+from prometheus_client.parser import \
+    text_string_to_metric_families  # type: ignore (no prometheus types)
+from schema import And, Or, Schema
 
 import bentoml
 from bentoml._internal.utils import analytics
@@ -148,6 +144,7 @@ def test_track_serve_init(
         production=production,
         serve_info=analytics.usage_stats.get_serve_info(),
         serve_kind="http",
+        from_server_api=False,
     )
 
     assert mock_do_not_track.called
@@ -160,6 +157,7 @@ def test_track_serve_init(
             production=production,
             serve_info=analytics.usage_stats.get_serve_info(),
             serve_kind="http",
+            from_server_api=False,
         )
     assert "model_types" in caplog.text
 
@@ -184,6 +182,7 @@ def test_track_serve_init_no_bento(
             production=False,
             serve_info=analytics.usage_stats.get_serve_info(),
             serve_kind="http",
+            from_server_api=False,
         )
     assert "model_types" not in caplog.text