mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(proxy): keep startup warnings when suppress_startup_output is on and apply it from CONFIG_FILE_PATH and before other settings
Warnings that name something the operator must fix now print through print_startup_warning, which skips the suppress gate. The config key is read with the CLI's json_logs handling for --config and CONFIG_FILE_PATH, applied before the rest of litellm_settings in load_config, and can only turn the setting on. The Prometheus config panel obeys the setting, and startup prints use the plain log format's color rule.
This commit is contained in:
parent
19b928bf5e
commit
bd16c04094
11 changed files with 230 additions and 70 deletions
|
|
@ -10,10 +10,12 @@ import sys
|
|||
from collections.abc import Iterator, Mapping
|
||||
from datetime import datetime
|
||||
from logging import Formatter
|
||||
from types import FrameType
|
||||
from typing import Final, TextIO
|
||||
from urllib.parse import unquote
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
|
|
@ -622,9 +624,14 @@ def _color_enabled(stream: TextIO | None) -> bool:
|
|||
return not os.environ.get("NO_COLOR") and _stream_is_tty(stream)
|
||||
|
||||
|
||||
def _terminal_colors(stdout: TextIO | None, stderr: TextIO | None) -> bool:
|
||||
"""True when both streams are an interactive color terminal, the one rule for log lines and startup prints."""
|
||||
return _color_enabled(stdout) and _color_enabled(stderr)
|
||||
|
||||
|
||||
def _plain_log_format(stdout: TextIO | None, stderr: TextIO | None) -> str:
|
||||
"""The plain-text log format, colorized only when both streams are an interactive terminal."""
|
||||
return _COLOR_LOG_FORMAT if _color_enabled(stdout) and _color_enabled(stderr) else _PLAIN_LOG_FORMAT
|
||||
return _COLOR_LOG_FORMAT if _terminal_colors(stdout, stderr) else _PLAIN_LOG_FORMAT
|
||||
|
||||
|
||||
class LevelRoutingStreamHandler(logging.StreamHandler):
|
||||
|
|
@ -662,6 +669,16 @@ def resolve_log_level(log_level: str) -> int:
|
|||
|
||||
json_logs: Final = _parse_env_flag(os.getenv("JSON_LOGS"))
|
||||
suppress_startup_output: Final = _parse_env_flag(os.getenv("LITELLM_SUPPRESS_STARTUP_OUTPUT"))
|
||||
|
||||
|
||||
class StartupOutputSettings(TypedDict, total=False):
|
||||
json_logs: ReadOnly[object]
|
||||
suppress_startup_output: ReadOnly[object]
|
||||
|
||||
|
||||
STARTUP_OUTPUT_SETTINGS: Final = TypeAdapter(StartupOutputSettings)
|
||||
|
||||
|
||||
# Create a handler for the logger (you may need to adapt this based on your needs)
|
||||
log_level: Final = os.getenv("LITELLM_LOG", "DEBUG")
|
||||
numeric_level: Final[int] = resolve_log_level(log_level)
|
||||
|
|
@ -910,8 +927,9 @@ def print_startup_message(message: str) -> None:
|
|||
"""Write a boot-time message meant for the operator's terminal.
|
||||
|
||||
LITELLM_SUPPRESS_STARTUP_OUTPUT or litellm_settings.suppress_startup_output turns every one of them
|
||||
off. Plain mode prints it with its spacing, keeping the ANSI colors only when stdout is a color
|
||||
terminal by the same check as the plain log format, so a container log never stores escape codes.
|
||||
off; a warning goes through print_startup_warning instead. Plain mode prints it with its spacing,
|
||||
keeping the ANSI colors only on a color terminal by the same check as the plain log format, so a
|
||||
container log never stores escape codes.
|
||||
With JSON logs on, every line of stdout is read by a log pipeline, so the message goes out as one
|
||||
INFO record of the proxy logger instead, on stdout like the print whatever handlers the logging
|
||||
config gave that logger, with the ANSI color codes and surrounding blank lines removed, located at
|
||||
|
|
@ -919,15 +937,25 @@ def print_startup_message(message: str) -> None:
|
|||
"""
|
||||
if litellm.suppress_startup_output is True:
|
||||
return
|
||||
frame: Final = inspect.currentframe()
|
||||
_write_startup_message(message, frame.f_back if frame is not None else None)
|
||||
|
||||
|
||||
def print_startup_warning(message: str) -> None:
|
||||
"""Write a boot-time warning exactly as print_startup_message writes a message, even with
|
||||
suppress_startup_output on, since it names something the operator has to fix."""
|
||||
frame: Final = inspect.currentframe()
|
||||
_write_startup_message(message, frame.f_back if frame is not None else None)
|
||||
|
||||
|
||||
def _write_startup_message(message: str, caller: FrameType | None) -> None:
|
||||
if not litellm.json_logs:
|
||||
printed: Final = message if _color_enabled(sys.stdout) else _ANSI_ESCAPE.sub("", message)
|
||||
printed: Final = message if _terminal_colors(sys.stdout, sys.stderr) else _ANSI_ESCAPE.sub("", message)
|
||||
print(printed, flush=True) # noqa: T201 # plain mode: this is the operator-terminal print it replaces
|
||||
return
|
||||
text: Final = _ANSI_ESCAPE.sub("", message).strip("\n")
|
||||
if not text:
|
||||
return
|
||||
frame: Final = inspect.currentframe()
|
||||
caller: Final = frame.f_back if frame is not None else None
|
||||
record: Final = verbose_proxy_logger.makeRecord(
|
||||
verbose_proxy_logger.name,
|
||||
logging.INFO,
|
||||
|
|
|
|||
|
|
@ -1085,8 +1085,8 @@ class PrometheusLogger(CustomLogger):
|
|||
# Build label filters from valid configurations
|
||||
label_filters: Final = self._build_label_filters(parsed_configs)
|
||||
|
||||
# Pretty print the processed configuration
|
||||
self._pretty_print_prometheus_config(label_filters)
|
||||
if litellm.suppress_startup_output is not True:
|
||||
self._pretty_print_prometheus_config(label_filters)
|
||||
return label_filters
|
||||
|
||||
def _parse_exclude_config(self) -> tuple[frozenset[str], frozenset[str]]:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import os
|
|||
import subprocess
|
||||
from typing import Final
|
||||
|
||||
from litellm._logging import print_startup_message, verbose_logger
|
||||
from litellm._logging import print_startup_message, print_startup_warning, verbose_logger
|
||||
|
||||
|
||||
def extract_sql_commands(diff_output: str) -> list[str]:
|
||||
|
|
@ -63,7 +63,7 @@ def check_prisma_schema_diff_helper(db_url: str) -> tuple[bool, list[str]]:
|
|||
run_prisma,
|
||||
)
|
||||
except ImportError as e:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
f"Skipping the migration diff: litellm-proxy-extras has no Prisma runner. Error: {e}",
|
||||
)
|
||||
return False, []
|
||||
|
|
@ -96,14 +96,14 @@ def check_prisma_schema_diff_helper(db_url: str) -> tuple[bool, list[str]]:
|
|||
else:
|
||||
return False, []
|
||||
except subprocess.TimeoutExpired:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
f"Timed out after {timeout}s generating the migration diff. "
|
||||
f"Raise {PRISMA_COMMAND_TIMEOUT_ENV_VAR} if this database needs longer.",
|
||||
)
|
||||
return False, []
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_message: Final = f"Failed to generate migration diff. Error: {e.stderr}"
|
||||
print_startup_message(error_message)
|
||||
print_startup_warning(error_message)
|
||||
return False, []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import sys
|
|||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional
|
||||
|
||||
from litellm._logging import print_startup_message, verbose_proxy_logger
|
||||
from litellm._logging import print_startup_message, print_startup_warning, verbose_proxy_logger
|
||||
from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
from litellm.proxy.policy_engine.policy_validator import PolicyValidator
|
||||
|
|
@ -87,7 +87,7 @@ def _print_policies_on_startup(
|
|||
print_startup_message("\n".join(lines))
|
||||
else:
|
||||
print_startup_message("\n".join(lines))
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
f"\n{_yellow_color_code}Warning: No policy_attachments configured. Policies will not be applied to any requests.{_reset_color_code}\n",
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ import click
|
|||
import httpx
|
||||
from click.core import ParameterSource
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import ConfigDict, TypeAdapter
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm._logging import print_startup_message
|
||||
from litellm._logging import StartupOutputSettings, print_startup_message, print_startup_warning
|
||||
from litellm.constants import DEFAULT_NUM_WORKERS_LITELLM_PROXY
|
||||
from litellm.proxy.db.pgbouncer import (
|
||||
PgBouncerError,
|
||||
|
|
@ -205,7 +206,7 @@ def deprecated_v2_flag_passed_on_cli() -> bool:
|
|||
|
||||
|
||||
def _print_enforce_prisma_migration_check_notice() -> None:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM Proxy: --enforce_prisma_migration_check is "
|
||||
"deprecated and has no effect, because the proxy always exits "
|
||||
"when database setup fails at startup. You can safely remove it.\033[0m",
|
||||
|
|
@ -215,10 +216,9 @@ def _print_enforce_prisma_migration_check_notice() -> None:
|
|||
def _print_or_hold_enforce_prisma_migration_check_notice(
|
||||
*, reads_config_file: bool, returns_before_config: bool
|
||||
) -> Callable[[], None] | None:
|
||||
"""Print the notice now, or, while a config file can still switch json_logs or suppress_startup_output
|
||||
on, return a printer to call once it is read; a boot that fails before that prints the held notice on
|
||||
the way out."""
|
||||
if not reads_config_file or returns_before_config:
|
||||
"""Print the notice now, or, while a config file can still switch json_logs on, return a printer
|
||||
to call once it is read; a boot that fails before that prints the held notice on the way out."""
|
||||
if not reads_config_file or litellm.json_logs or returns_before_config:
|
||||
_print_enforce_prisma_migration_check_notice()
|
||||
return None
|
||||
print_once: Final = functools.cache(_print_enforce_prisma_migration_check_notice)
|
||||
|
|
@ -228,6 +228,28 @@ def _print_or_hold_enforce_prisma_migration_check_notice(
|
|||
return print_once
|
||||
|
||||
|
||||
class _ConfigStartupOutputSettings(TypedDict, total=False):
|
||||
litellm_settings: ReadOnly[StartupOutputSettings | None]
|
||||
|
||||
|
||||
_CONFIG_STARTUP_OUTPUT_SETTINGS: Final = TypeAdapter(_ConfigStartupOutputSettings)
|
||||
|
||||
|
||||
def _apply_config_logging_settings(litellm_settings: StartupOutputSettings | None) -> None:
|
||||
"""Turn on the config's json_logs and suppress_startup_output before any worker starts. Workers that
|
||||
uvicorn or granian spawn import litellm afresh and read both from the env at import, so they log JSON
|
||||
and stay quiet from their first line instead of from when load_config reaches the key."""
|
||||
if litellm_settings is None:
|
||||
return
|
||||
if litellm_settings.get("json_logs") is True:
|
||||
litellm.json_logs = True
|
||||
os.environ["JSON_LOGS"] = "true"
|
||||
litellm.turn_on_json()
|
||||
if litellm_settings.get("suppress_startup_output") is True:
|
||||
litellm.suppress_startup_output = True
|
||||
os.environ["LITELLM_SUPPRESS_STARTUP_OUTPUT"] = "true"
|
||||
|
||||
|
||||
class ProxyInitializationHelpers:
|
||||
@staticmethod
|
||||
def echo_litellm_version():
|
||||
|
|
@ -357,7 +379,7 @@ class ProxyInitializationHelpers:
|
|||
if "timeout_worker_healthcheck" in inspect.signature(uvicorn.Config.__init__).parameters:
|
||||
uvicorn_args["timeout_worker_healthcheck"] = timeout_worker_healthcheck
|
||||
else:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
f"\033[1;33mLiteLLM Proxy: --timeout_worker_healthcheck "
|
||||
f"requires uvicorn>=0.37.0, but installed uvicorn=={uvicorn.__version__}. "
|
||||
f"Ignoring the flag.\033[0m",
|
||||
|
|
@ -380,7 +402,7 @@ class ProxyInitializationHelpers:
|
|||
import uvicorn
|
||||
|
||||
if max_requests_before_restart is None:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM Proxy: --max_requests_before_restart_jitter "
|
||||
"has no effect without --max_requests_before_restart\033[0m\n",
|
||||
)
|
||||
|
|
@ -388,7 +410,7 @@ class ProxyInitializationHelpers:
|
|||
if "limit_max_requests_jitter" in inspect.signature(uvicorn.Config.__init__).parameters:
|
||||
uvicorn_args["limit_max_requests_jitter"] = jitter
|
||||
else:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
f"\033[1;33mLiteLLM Proxy: --max_requests_before_restart_jitter "
|
||||
f"requires uvicorn>=0.41.0, but installed uvicorn=={uvicorn.__version__}. "
|
||||
f"Ignoring the flag.\033[0m",
|
||||
|
|
@ -555,12 +577,12 @@ class ProxyInitializationHelpers:
|
|||
|
||||
print_startup_message(f"\033[1;32mLiteLLM Proxy: Starting server on {host}:{port} using Granian\033[0m\n")
|
||||
if max_requests_before_restart is not None:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM: --max_requests_before_restart is not supported by Granian "
|
||||
"(Granian uses workers_lifetime in seconds, not a per-request limit).\033[0m\n",
|
||||
)
|
||||
if ciphers is not None:
|
||||
print_startup_message("\033[1;33mLiteLLM: --ciphers is not applied when using --run_granian.\033[0m\n")
|
||||
print_startup_warning("\033[1;33mLiteLLM: --ciphers is not applied when using --run_granian.\033[0m\n")
|
||||
|
||||
kwargs: Final[dict[str, object]] = {
|
||||
"target": "litellm.proxy.proxy_server:app",
|
||||
|
|
@ -682,7 +704,7 @@ class ProxyInitializationHelpers:
|
|||
gunicorn_options["max_requests"] = max_requests_before_restart
|
||||
if max_requests_before_restart_jitter is not None:
|
||||
if max_requests_before_restart is None:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM Proxy: --max_requests_before_restart_jitter "
|
||||
"has no effect without --max_requests_before_restart\033[0m\n",
|
||||
)
|
||||
|
|
@ -723,7 +745,7 @@ class ProxyInitializationHelpers:
|
|||
with open(os.devnull, "w") as devnull:
|
||||
subprocess.Popen(command, stdout=devnull, stderr=devnull)
|
||||
except Exception as e:
|
||||
print_startup_message(f"""
|
||||
print_startup_warning(f"""
|
||||
LiteLLM Warning: proxy started with `ollama` model\n`ollama serve` failed with Exception{e}. \nEnsure you run `ollama serve`
|
||||
""")
|
||||
|
||||
|
|
@ -1202,9 +1224,11 @@ def run_server(
|
|||
if validate_config is True:
|
||||
ProxyInitializationHelpers.run_config_validation(config)
|
||||
return
|
||||
env_config_file_path: Final = (os.environ.get("CONFIG_FILE_PATH") or None) if config is None else None
|
||||
held_prisma_notice: Final = (
|
||||
_print_or_hold_enforce_prisma_migration_check_notice(
|
||||
reads_config_file=config is not None, returns_before_config=health is True or test is True
|
||||
reads_config_file=config is not None or env_config_file_path is not None,
|
||||
returns_before_config=health is True or test is True,
|
||||
)
|
||||
if enforce_prisma_migration_check
|
||||
else None
|
||||
|
|
@ -1316,29 +1340,9 @@ def run_server(
|
|||
|
||||
### LITELLM SETTINGS ###
|
||||
litellm_settings = _config.get("litellm_settings", None)
|
||||
if (
|
||||
litellm_settings is not None
|
||||
and "json_logs" in litellm_settings
|
||||
and litellm_settings["json_logs"] is True
|
||||
):
|
||||
import litellm
|
||||
|
||||
litellm.json_logs = True
|
||||
# Workers that uvicorn or granian spawn import litellm afresh and read this at import,
|
||||
# so they log JSON from their first record instead of plain text until load_config
|
||||
# reaches the json_logs key.
|
||||
os.environ["JSON_LOGS"] = "true"
|
||||
|
||||
litellm.turn_on_json()
|
||||
if (
|
||||
litellm_settings is not None
|
||||
and "suppress_startup_output" in litellm_settings
|
||||
and litellm_settings["suppress_startup_output"] is True
|
||||
):
|
||||
import litellm
|
||||
|
||||
litellm.suppress_startup_output = True
|
||||
os.environ["LITELLM_SUPPRESS_STARTUP_OUTPUT"] = "true"
|
||||
_apply_config_logging_settings(
|
||||
_CONFIG_STARTUP_OUTPUT_SETTINGS.validate_python(_config).get("litellm_settings")
|
||||
)
|
||||
if held_prisma_notice is not None:
|
||||
held_prisma_notice()
|
||||
### GENERAL SETTINGS ###
|
||||
|
|
@ -1402,6 +1406,16 @@ def run_server(
|
|||
os.chdir(original_dir)
|
||||
if database_url is not None and isinstance(database_url, str):
|
||||
os.environ["DATABASE_URL"] = database_url
|
||||
elif env_config_file_path is not None:
|
||||
import asyncio
|
||||
|
||||
_apply_config_logging_settings(
|
||||
_CONFIG_STARTUP_OUTPUT_SETTINGS.validate_python(
|
||||
asyncio.run(ProxyConfig().get_config(config_file_path=env_config_file_path))
|
||||
).get("litellm_settings")
|
||||
)
|
||||
if held_prisma_notice is not None:
|
||||
held_prisma_notice()
|
||||
|
||||
# Handle database URL construction when no config file is used
|
||||
if config is None and os.getenv("DATABASE_URL") is None:
|
||||
|
|
@ -1539,7 +1553,7 @@ def run_server(
|
|||
env_value=os.getenv("USE_V2_MIGRATION_RESOLVER"),
|
||||
)
|
||||
if deprecated_v2_flag_passed_on_cli() and use_v2_resolver:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM Proxy: --use_v2_migration_resolver is "
|
||||
"deprecated and has no effect, because the v2 migration "
|
||||
"resolver is now the default. You can safely remove it. To "
|
||||
|
|
@ -1547,7 +1561,7 @@ def run_server(
|
|||
"--use_legacy_migration_resolver.\033[0m",
|
||||
)
|
||||
if not use_v2_resolver:
|
||||
print_startup_message(
|
||||
print_startup_warning(
|
||||
"\033[1;33mLiteLLM Proxy: Using the legacy (v1) migration "
|
||||
"resolver. It performs the diff-and-force recovery that can "
|
||||
"cause schema thrashing during rolling deploys where two "
|
||||
|
|
@ -1661,7 +1675,7 @@ def run_server(
|
|||
run_gunicorn=run_gunicorn is True,
|
||||
)
|
||||
if single_worker_warning is not None:
|
||||
print_startup_message(f"\033[1;33m{single_worker_warning}\033[0m\n")
|
||||
print_startup_warning(f"\033[1;33m{single_worker_warning}\033[0m\n")
|
||||
if run_gunicorn is False and run_hypercorn is False and run_granian is False:
|
||||
if limit_concurrency is not None:
|
||||
uvicorn_args["limit_concurrency"] = limit_concurrency
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ import litellm._redis
|
|||
from litellm import Router
|
||||
from litellm._internal_context import service_target, with_service_target
|
||||
from litellm._logging import (
|
||||
STARTUP_OUTPUT_SETTINGS,
|
||||
print_startup_message,
|
||||
redact_string,
|
||||
verbose_proxy_logger,
|
||||
|
|
@ -6549,6 +6550,8 @@ class ProxyConfig:
|
|||
from litellm.types.integrations.prometheus import validate_caller_identity_settings
|
||||
|
||||
validate_caller_identity_settings(litellm_settings)
|
||||
if STARTUP_OUTPUT_SETTINGS.validate_python(litellm_settings).get("suppress_startup_output") is True:
|
||||
litellm.suppress_startup_output = True
|
||||
|
||||
# ANSI escape code for blue text
|
||||
blue_color_code: Final = "\033[94m"
|
||||
|
|
@ -6870,6 +6873,8 @@ class ProxyConfig:
|
|||
setattr(litellm, key, value)
|
||||
elif key == "drop_params":
|
||||
litellm.drop_params = drop_params_flag(value, "litellm_settings.drop_params", verbose_proxy_logger)
|
||||
elif key == "suppress_startup_output":
|
||||
continue
|
||||
else:
|
||||
verbose_proxy_logger.debug(
|
||||
"%s setting litellm.%s=%s%s",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,26 @@ def test_future_metrics_with_end_user_are_filtered():
|
|||
litellm.enable_end_user_cost_tracking_prometheus_only = original_setting
|
||||
|
||||
|
||||
@pytest.mark.parametrize("suppressed", [False, True])
|
||||
def test_prometheus_config_panel_obeys_suppress_startup_output(
|
||||
monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str], suppressed: bool
|
||||
):
|
||||
"""The processed-config panel is a startup print like the banner, so suppress_startup_output turns it off
|
||||
while the label filters still load."""
|
||||
clear_prometheus_registry()
|
||||
monkeypatch.setattr(litellm, "suppress_startup_output", suppressed)
|
||||
monkeypatch.setattr(
|
||||
litellm,
|
||||
"prometheus_metrics_config",
|
||||
[{"group": "service_metrics", "metrics": ["litellm_deployment_total_requests"], "include_labels": ["team"]}],
|
||||
)
|
||||
|
||||
label_filters = PrometheusLogger()._parse_prometheus_config()
|
||||
|
||||
assert label_filters["litellm_deployment_total_requests"] == ["team"]
|
||||
assert ("Prometheus Configuration Processed" in capfd.readouterr().out) is not suppressed
|
||||
|
||||
|
||||
def test_prometheus_config_parsing():
|
||||
"""Test that prometheus metrics configuration is parsed correctly"""
|
||||
# Clear registry before test
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ NO_ATTACHMENTS_WARNING: Final = (
|
|||
)
|
||||
|
||||
|
||||
class _Stdout(StringIO):
|
||||
class _Stream(StringIO):
|
||||
def __init__(self, terminal: bool) -> None:
|
||||
super().__init__()
|
||||
self._terminal = terminal
|
||||
|
|
@ -75,8 +75,9 @@ def test_policy_table_prints_unchanged_in_plain_mode(
|
|||
without color codes on a piped stdout."""
|
||||
monkeypatch.setattr(litellm, "json_logs", False)
|
||||
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||
stdout: Final = _Stdout(terminal)
|
||||
stdout: Final = _Stream(terminal)
|
||||
monkeypatch.setattr(sys, "stdout", stdout)
|
||||
monkeypatch.setattr(sys, "stderr", _Stream(terminal))
|
||||
|
||||
_print_policies_on_startup(POLICIES, attachments)
|
||||
|
||||
|
|
|
|||
|
|
@ -2117,6 +2117,52 @@ async def test_ProxyConfig_lists_search_tools_once_at_startup_not_on_every_repar
|
|||
assert out.count("Proxy initialized with Search Tools:") == 1, out
|
||||
assert out.count("web-search (tavily)") == 1, out
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("already_on", "config_value"),
|
||||
[
|
||||
pytest.param(True, False, id="env-var-on-and-config-false"),
|
||||
pytest.param(False, True, id="config-key-below-cache"),
|
||||
],
|
||||
)
|
||||
async def test_ProxyConfig_load_config_keeps_startup_output_off_once_either_setting_turns_it_on(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capfd: pytest.CaptureFixture[str],
|
||||
already_on: bool,
|
||||
config_value: bool,
|
||||
):
|
||||
"""Only `true` turns suppress_startup_output on, from the env var or the config key, and a config `false`
|
||||
never turns the env var back off. The key applies before any other litellm_settings key, so a `cache: true`
|
||||
above it prints nothing either."""
|
||||
config_file: Final = tmp_path / "suppress.yaml"
|
||||
config_file.write_text(
|
||||
"model_list: []\n"
|
||||
"litellm_settings:\n"
|
||||
" cache: true\n"
|
||||
" cache_params:\n"
|
||||
" type: local\n"
|
||||
f" suppress_startup_output: {str(config_value).lower()}\n"
|
||||
"search_tools:\n"
|
||||
" - search_tool_name: web-search\n"
|
||||
" litellm_params:\n"
|
||||
" search_provider: tavily\n"
|
||||
)
|
||||
monkeypatch.setattr(litellm, "json_logs", False)
|
||||
monkeypatch.setattr(litellm, "suppress_startup_output", already_on)
|
||||
monkeypatch.setattr(litellm, "cache", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
|
||||
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
|
||||
|
||||
await ProxyConfig().load_config(router=None, config_file_path=str(config_file))
|
||||
|
||||
out: Final = capfd.readouterr().out
|
||||
assert litellm.suppress_startup_output is True
|
||||
assert "Setting Cache on Proxy" not in out, out
|
||||
assert "Proxy initialized with Search Tools:" not in out, out
|
||||
|
||||
|
||||
|
||||
def test_ProxyConfig_merge_config_and_db_search_tools_returns_superset():
|
||||
config_tools = [
|
||||
|
|
|
|||
|
|
@ -3964,7 +3964,12 @@ class TestJsonLogsConfigReachesSpawnedWorkers:
|
|||
each worker prints before its load_config."""
|
||||
|
||||
@staticmethod
|
||||
def _env_after_boot(config_path: Path, litellm_settings: dict[str, object], name: str = "JSON_LOGS") -> str | None:
|
||||
def _env_after_boot(
|
||||
config_path: Path,
|
||||
litellm_settings: dict[str, object],
|
||||
name: str = "JSON_LOGS",
|
||||
from_config_file_path_env: bool = False,
|
||||
) -> str | None:
|
||||
config_path.write_text(yaml.dump({"model_list": [], "litellm_settings": litellm_settings}))
|
||||
mock_proxy_config: Final = MagicMock()
|
||||
mock_proxy_config.return_value.get_config = AsyncMock(return_value=yaml.safe_load(config_path.read_text()))
|
||||
|
|
@ -3977,10 +3982,12 @@ class TestJsonLogsConfigReachesSpawnedWorkers:
|
|||
clean_env: Final = {
|
||||
k: v
|
||||
for k, v in os.environ.items()
|
||||
if k not in ("DATABASE_URL", "DIRECT_URL", "JSON_LOGS", "LITELLM_SUPPRESS_STARTUP_OUTPUT")
|
||||
if k not in ("DATABASE_URL", "DIRECT_URL", "JSON_LOGS", "LITELLM_SUPPRESS_STARTUP_OUTPUT", "CONFIG_FILE_PATH")
|
||||
}
|
||||
config_env: Final = {"CONFIG_FILE_PATH": str(config_path)} if from_config_file_path_env else {}
|
||||
config_args: Final = [] if from_config_file_path_env else ["--config", str(config_path)]
|
||||
with (
|
||||
patch.dict(os.environ, clean_env, clear=True),
|
||||
patch.dict(os.environ, {**clean_env, **config_env}, clear=True),
|
||||
patch.dict(
|
||||
"sys.modules",
|
||||
{"proxy_server": mock_proxy_module, "litellm.proxy.proxy_server": mock_proxy_module},
|
||||
|
|
@ -3988,7 +3995,7 @@ class TestJsonLogsConfigReachesSpawnedWorkers:
|
|||
patch("atexit.register"),
|
||||
patch("litellm.turn_on_json"), # test-quality-ok: it swaps every logger handler in this pytest worker
|
||||
):
|
||||
run_server.main(["--config", str(config_path), "--local", "--skip_server_startup"], standalone_mode=False)
|
||||
run_server.main([*config_args, "--local", "--skip_server_startup"], standalone_mode=False)
|
||||
return os.environ.get(name)
|
||||
|
||||
def test_json_logs_true_exports_json_logs_to_spawned_workers(self, tmp_path: Path, monkeypatch) -> None:
|
||||
|
|
@ -4007,12 +4014,21 @@ class TestJsonLogsConfigReachesSpawnedWorkers:
|
|||
settings: Final = {"suppress_startup_output": setting}
|
||||
assert self._env_after_boot(tmp_path / "config.yaml", settings, "LITELLM_SUPPRESS_STARTUP_OUTPUT") == exported
|
||||
|
||||
@pytest.mark.parametrize("name", ["JSON_LOGS", "LITELLM_SUPPRESS_STARTUP_OUTPUT"])
|
||||
def test_a_config_named_by_CONFIG_FILE_PATH_reaches_spawned_workers_too(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, name: str
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "json_logs", False)
|
||||
monkeypatch.setattr(litellm, "suppress_startup_output", False)
|
||||
settings: Final[dict[str, object]] = {"json_logs": True, "suppress_startup_output": True}
|
||||
assert self._env_after_boot(tmp_path / "config.yaml", settings, name, from_config_file_path_env=True) == "true"
|
||||
|
||||
|
||||
class TestEnforcePrismaMigrationCheckNoticeAroundTheConfigRead:
|
||||
"""With a config file, the `--enforce_prisma_migration_check` deprecation notice waits until the config
|
||||
is read, so a `json_logs: true` set only there makes it a JSON record and a `suppress_startup_output: true`
|
||||
set only there silences it. Otherwise it is printed once, and a boot whose config read fails still prints
|
||||
it before the error, as it did before the wait."""
|
||||
is read, so a `json_logs: true` set only there makes it a JSON record, while `suppress_startup_output: true`
|
||||
leaves it, since it is a warning. Otherwise it is printed once, and a boot whose config read fails still
|
||||
prints it before the error, as it did before the wait."""
|
||||
|
||||
NOTICE: Final = "--enforce_prisma_migration_check is deprecated and has no effect"
|
||||
|
||||
|
|
@ -4070,7 +4086,7 @@ class TestEnforcePrismaMigrationCheckNoticeAroundTheConfigRead:
|
|||
assert self.NOTICE in json.loads(notice_lines[0])["message"]
|
||||
|
||||
@pytest.mark.parametrize("json_logs", [False, True])
|
||||
def test_a_config_only_suppress_startup_output_silences_the_notice(
|
||||
def test_a_config_only_suppress_startup_output_keeps_the_notice(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], json_logs: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "json_logs", json_logs)
|
||||
|
|
@ -4079,7 +4095,7 @@ class TestEnforcePrismaMigrationCheckNoticeAroundTheConfigRead:
|
|||
|
||||
self._boot(tmp_path / "config.yaml", AsyncMock(return_value=config))
|
||||
|
||||
assert self.NOTICE not in capsys.readouterr().out
|
||||
assert capsys.readouterr().out.count(self.NOTICE) == 1
|
||||
|
||||
|
||||
def test_granian_flag_notices_are_json_records_in_json_mode(monkeypatch, capfd):
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from litellm._logging import (
|
|||
_plain_log_format,
|
||||
_stdout_truncation_marker,
|
||||
print_startup_message,
|
||||
print_startup_warning,
|
||||
turn_on_json,
|
||||
format_base64_size,
|
||||
session_id_var,
|
||||
|
|
@ -1863,14 +1864,15 @@ class _CapturedStream(StringIO):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stdout_tty, no_color, want_color",
|
||||
[(True, None, True), (False, None, False), (True, "1", False)],
|
||||
"stdout_tty, stderr_tty, no_color, want_color",
|
||||
[(True, True, None, True), (False, True, None, False), (True, False, None, False), (True, True, "1", False)],
|
||||
)
|
||||
def test_print_startup_message_keeps_colors_only_on_a_color_terminal(
|
||||
monkeypatch: pytest.MonkeyPatch, stdout_tty: bool, no_color: str | None, want_color: bool
|
||||
monkeypatch: pytest.MonkeyPatch, stdout_tty: bool, stderr_tty: bool, no_color: str | None, want_color: bool
|
||||
):
|
||||
"""Operators without JSON logs keep the colored, spaced output on a terminal. A container runtime reads
|
||||
stdout through a pipe and stores the escape codes as text, so there, and under NO_COLOR, the colors go."""
|
||||
stdout through a pipe and stores the escape codes as text, so there, and under NO_COLOR, the colors go.
|
||||
The rule is the plain log format's, so a `2>file` run drops the colors from prints and log lines alike."""
|
||||
monkeypatch.setattr(litellm, "json_logs", False)
|
||||
if no_color is None:
|
||||
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||
|
|
@ -1878,6 +1880,8 @@ def test_print_startup_message_keeps_colors_only_on_a_color_terminal(
|
|||
monkeypatch.setenv("NO_COLOR", no_color)
|
||||
stdout: Final = _CapturedStream(stdout_tty)
|
||||
monkeypatch.setattr(sys, "stdout", stdout)
|
||||
monkeypatch.setattr(sys, "stderr", _CapturedStream(stderr_tty))
|
||||
assert (_plain_log_format(sys.stdout, sys.stderr) == _COLOR_LOG_FORMAT) is want_color
|
||||
|
||||
print_startup_message("\n\033[1;32mLiteLLM: Proxy initialized with Config, Set models:\033[0m\n")
|
||||
|
||||
|
|
@ -1905,6 +1909,32 @@ def test_print_startup_message_writes_nothing_when_suppressed(
|
|||
assert capfd.readouterr() == ("", "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json_logs", [False, True])
|
||||
def test_print_startup_warning_still_prints_when_suppressed(
|
||||
monkeypatch: pytest.MonkeyPatch, capfd: pytest.CaptureFixture[str], json_logs: bool
|
||||
):
|
||||
"""A warning names something the operator has to fix, like an ignored flag or a failed migration diff,
|
||||
so suppress_startup_output leaves it in place, at the caller's location in JSON mode."""
|
||||
monkeypatch.setattr(litellm, "json_logs", json_logs)
|
||||
monkeypatch.setattr(litellm, "suppress_startup_output", True)
|
||||
monkeypatch.setattr(sys, "excepthook", sys.excepthook)
|
||||
if json_logs:
|
||||
turn_on_json()
|
||||
|
||||
print_startup_warning("\033[1;33mLiteLLM: --ciphers is not applied when using --run_granian.\033[0m\n")
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert err == ""
|
||||
lines: Final = [raw for raw in out.splitlines() if raw.strip()]
|
||||
assert len(lines) == 1, lines
|
||||
if json_logs:
|
||||
record: Final = json.loads(lines[0])
|
||||
assert record["message"] == "LiteLLM: --ciphers is not applied when using --run_granian."
|
||||
assert str(record["logger"]).startswith("test_logging.py:"), record["logger"]
|
||||
else:
|
||||
assert lines[0] == "LiteLLM: --ciphers is not applied when using --run_granian."
|
||||
|
||||
|
||||
def test_print_startup_message_is_one_json_record_in_json_mode(monkeypatch, capfd):
|
||||
"""With JSON logs on, a multi-line colored startup message is one INFO record with the colors and the
|
||||
surrounding blank lines removed, so a log pipeline parses it like every other line. The logger sits
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue