Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/notewise/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
BOOL_SETTING_TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
BOOL_SETTING_FALSY_VALUES = frozenset({"0", "false", "no", "off"})
PYDANTIC_RESPONSE_USAGE_WARNING_PATTERN = (
r"(?s)^Pydantic serializer warnings:.*ResponseAPIUsage"
r"(?s)^Pydantic serializer warnings:.*(ResponseAPIUsage|ResponsesAPIResponse)"
)
GITHUB_REPOSITORY_OWNER = "whoisjayd"
GITHUB_REPOSITORY_NAME = "notewise"
Expand Down
4 changes: 4 additions & 0 deletions src/notewise/llm/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def suppress_litellm_noise() -> None:
verbose_logger.propagate = False
for handler in list(verbose_logger.handlers):
verbose_logger.removeHandler(handler)
if not verbose_logger.handlers:
# Avoid Python's `logging.lastResort` handler printing LiteLLM's
# non-blocking internal logging exceptions to stderr.
verbose_logger.addHandler(logging.NullHandler())
warnings.filterwarnings(
"ignore",
message=PYDANTIC_RESPONSE_USAGE_WARNING_PATTERN,
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/llm/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,29 @@ def test_suppress_litellm_noise_suppresses_response_usage_warning(self):
finally:
warnings.filters[:] = original_filters

def test_suppress_litellm_noise_suppresses_response_payload_warning(self):
"""LiteLLM's Responses response serializer warnings should stay off the TTY."""
original_filters = warnings.filters[:]
try:
suppress_litellm_noise()

with warnings.catch_warnings(record=True) as caught:
warnings.warn_explicit(
"Pydantic serializer warnings:\n"
" PydanticSerializationUnexpectedValue("
"Expected `ResponsesAPIResponse` - serialized value may not be "
"as expected [field_name='response', input_value={}, "
"input_type=dict])",
UserWarning,
filename="pydantic/main.py",
lineno=464,
module="pydantic.main",
)

assert caught == []
finally:
warnings.filters[:] = original_filters

def test_suppress_litellm_noise_sets_verbose_logger_level(self):
"""LiteLLM runtime should not attach terminal handlers."""
runtime = MagicMock()
Expand All @@ -653,6 +676,32 @@ def test_suppress_litellm_noise_sets_verbose_logger_level(self):
assert runtime.verbose_logger.propagate is False
runtime.verbose_logger.removeHandler.assert_called_once()

def test_suppress_litellm_noise_installs_null_handler(self):
"""LiteLLM's verbose logger should not fall back to logging.lastResort."""
logger_name = "LiteLLM-test-nullhandler"
litellm_logger = logging.getLogger(logger_name)
original_handlers = list(litellm_logger.handlers)
original_propagate = litellm_logger.propagate
original_level = litellm_logger.level
try:
litellm_logger.handlers.clear()
litellm_logger.propagate = False
litellm_logger.setLevel(logging.NOTSET)

runtime = SimpleNamespace(verbose_logger=litellm_logger)
with patch("notewise.llm.provider.litellm", runtime):
suppress_litellm_noise()

assert litellm_logger.propagate is False
assert any(
isinstance(handler, logging.NullHandler)
for handler in litellm_logger.handlers
)
finally:
litellm_logger.handlers[:] = original_handlers
litellm_logger.propagate = original_propagate
litellm_logger.setLevel(original_level)

def test_extract_usage_handles_invalid_values(self):
"""Non-numeric usage payloads should fail closed to zero values."""
provider = LLMProvider("gpt-4o")
Expand Down
Loading