Skip to content

Commit c25946e

Browse files
committed
fix(providers): drop loguru, fix FallbackBoundModel failover log placeholders
The `FallbackBoundModel._notify` WARNING used stdlib `%s` positional substitution but `_log` was loguru when installed, which does not perform that substitution — so the failover line rendered with literal `%s` placeholders instead of the resolved provider labels: cubepi.providers.fallback: failover triggered failed=%s → next=%s reason=%s attempt=%s/%s cubepi has never declared loguru as a dependency (see the architecture notes in `chain_providers` / `_log_tracing_warning`), and hosts that prefer loguru can intercept stdlib records. Remove the try-import-loguru branches in `fallback.py` and `base._log_listener_exception`, promote `logging` to a top-level import in `base.py`, drop the duplicate inline import in `chain_providers`. The existing `%s`-style log strings now render correctly through stdlib `logging`.
1 parent 2634e0d commit c25946e

3 files changed

Lines changed: 23 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8686
removed. Existing middlewares that return `None` after one injection are
8787
unaffected. This enables evaluation loops like `GoalMiddleware`.
8888

89+
### Changed
90+
91+
- **Internal logging now uses stdlib `logging` exclusively.** Previously
92+
`FallbackBoundModel` and the provider listener-exception path tried to
93+
import `loguru` first and fell back to stdlib. The loguru path was
94+
silently incorrect — loguru does not perform `%s` argument substitution,
95+
so failover warnings rendered literal `%s` placeholders instead of the
96+
resolved labels. cubepi has never declared loguru as a dependency; hosts
97+
that prefer loguru should intercept stdlib logging records into it. No
98+
public API change.
99+
89100
### Fixed
90101

102+
- **`FallbackBoundModel` failover log line now substitutes its placeholders.**
103+
Before the loguru removal above, the WARNING emitted on every failover
104+
read `failed=%s → next=%s reason=%s attempt=%s/%s` literally because
105+
the loguru-backed logger ignored the positional args. Now renders as
106+
`failed=anthropic/claude-opus-4-5 → next=openai/gpt-5 reason=… attempt=1/2`.
107+
91108
- **`Recorder.attach()` and `Meter.attach()` now subscribe to every provider in
92109
a `FallbackBoundModel` chain** (closes #167). Previously they only listened
93110
to `chain[0].provider`, so post-failover calls executed against `chain[1..]`

cubepi/providers/base.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import copy
55
import inspect
6+
import logging
67
from dataclasses import dataclass, field
78
from typing import (
89
Any,
@@ -555,18 +556,9 @@ async def _safe_run_coroutine(cb: Callable, coro: Any) -> None:
555556

556557

557558
def _log_listener_exception(cb: Callable, exc: BaseException) -> None:
558-
try:
559-
from loguru import logger
560-
561-
logger.opt(exception=exc).warning(
562-
"cubepi provider listener {} raised; swallowed", cb
563-
)
564-
except ImportError:
565-
import logging
566-
567-
logging.getLogger("cubepi.providers").warning(
568-
"cubepi provider listener %r raised; swallowed", cb, exc_info=exc
569-
)
559+
logging.getLogger("cubepi.providers").warning(
560+
"cubepi provider listener %r raised; swallowed", cb, exc_info=exc
561+
)
570562

571563

572564
def _detach(listeners: list, cb: Callable) -> None:
@@ -967,8 +959,6 @@ def chain_providers(model: object) -> list["BaseProvider"]:
967959
fallback chain so post-failover provider events land in the trace /
968960
metric stream.
969961
"""
970-
import logging
971-
972962
if model is None:
973963
return []
974964
chain = getattr(model, "chain", None)

cubepi/providers/fallback.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import asyncio
44
import inspect
5+
import logging
56
import time
67
from dataclasses import dataclass
78
from typing import Any, Awaitable, Callable
@@ -29,12 +30,7 @@
2930
chain_providers, # re-exported for back-compat; canonical home is base.py
3031
)
3132

32-
try:
33-
from loguru import logger as _log
34-
except ImportError: # pragma: no cover
35-
import logging as _logging
36-
37-
_log = _logging.getLogger("cubepi.providers.fallback")
33+
_log = logging.getLogger("cubepi.providers.fallback")
3834

3935

4036
DEFAULT_TRIGGER_ERRORS: frozenset[type[ProviderError]] = frozenset(

0 commit comments

Comments
 (0)