Skip to content

fix(embedding): observable in-process fallback + mode-tagged errors; dedup get_embedding - #312

Merged
guangyu-reflexio merged 1 commit into
mainfrom
fix/embedding-fallback-observability
Jul 8, 2026
Merged

fix(embedding): observable in-process fallback + mode-tagged errors; dedup get_embedding#312
guangyu-reflexio merged 1 commit into
mainfrom
fix/embedding-fallback-observability

Conversation

@guangyu-reflexio

@guangyu-reflexio guangyu-reflexio commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Two OSS-only quality items from the embedding-stability redesign (Phase 1, non-urgent follow-ups to the #311 lock fix):

  1. Fallback observability (1c). When a local/* model's daemon /health probe fails, mode resolution silently returns inprocess and loads a second in-process model copy — invisible to operators. Now emits a rate-limited (≤1/60s) WARNING naming the probe target, the failure reason, and the second-copy implication. Fires only on the model-driven fallback, never on an explicit REFLEXIO_EMBEDDING_PROVIDER=inprocess (intentional, returns earlier). Adds a mode=<resolved_mode> tag on embedding-error log/exception context so failures are attributable to the daemon vs in-process path. Exception types/messages/control-flow unchanged.

  2. Dedup get_embedding/get_embeddings. Extracted a shared private _embed_texts(..., batch=); the two public methods now delegate (single passes [text], returns [0]). Signatures preserved. Note: extracted-to-helper rather than a plain get_embeddings([t])[0] collapse because the single/batch error-message wording differs and is asserted verbatim by existing tests — the batch= flag selects the wording, so no message regression and no output change.

Out of scope (different semantics — not a safe dedup): unifying _truncate_and_renormalise / _pad / _resize_embedding.

Tests

New: fallback-warns-once-then-rate-limits, explicit-inprocess-does-not-warn, mismatched-daemon-warns-with-model-reason; get_embedding==get_embeddings[0] parity; in-process failure carries mode=. tests/server/llm/ = 515 passed (baseline 507); ruff + pyright clean. No enterprise files, no embedding output semantics changed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved embedding fallback handling when a local service is unreachable or serving a different model.
    • Reduced repeated fallback warnings so logs stay cleaner during ongoing outages.
    • Made single-text and batch embedding results behave consistently for local embeddings.
    • Added clearer embedding error messages with mode-aware logging for easier troubleshooting.

…bedding

Observability (design 1c):
- Emit a rate-limited (once/60s per process) WARNING when local/* mode
  resolution falls back to inprocess because the daemon /health probe failed
  (or the daemon serves a different model). The message names the probe
  target, the failure reason, and that a second in-process model copy is
  loaded. An explicit REFLEXIO_EMBEDDING_PROVIDER=inprocess is intentional and
  does not warn.
- Tag the embedding error path with the resolved mode: the service-provider
  unavailability warning and the _litellm_embedding raise sites now log
  extra={"mode": ...} so failures are attributable to daemon vs in-process
  vs cloud. Exception types and control flow are unchanged.

Dedup (Phase-3 item 1):
- get_embedding and get_embeddings now share one private _embed_texts dispatch
  body; get_embedding is a thin [text] -> [0] wrapper. The only per-caller
  divergence (batch vs single error-message wording) is preserved via a
  `batch` flag, since existing tests assert both wordings verbatim — a plain
  one-liner collapse would have changed those messages, so the shared body was
  extracted instead. Embedding output semantics (routing, truncation, index
  ordering) are byte-for-byte unchanged.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors embedding dispatch in _litellm_embedding.py into a shared _embed_texts helper with mode-tagged error logging, and adds rate-limited in-process fallback warnings with failure-reason tracking to embedding_service_provider.py, plus corresponding tests validating both behaviors.

Changes

Embedding dispatch and fallback

Layer / File(s) Summary
Shared embedding error helper
reflexio/server/llm/_litellm_embedding.py
Adds _embedding_error(message, mode) that logs a mode-tagged warning and returns a LiteLLMClientError.
Unified embed dispatcher
reflexio/server/llm/_litellm_embedding.py
get_embedding/get_embeddings become thin wrappers over new _embed_texts(...), which centralizes provider-mode gating, service/Nomic/local routing, truncation, litellm call construction, response reordering, and batch-aware error handling via _embedding_error.
Fallback failure-reason tracking
reflexio/server/llm/providers/embedding_service_provider.py
Adds module state for last probe failure reason and warning timestamp; _local_service_status() persists HTTP/JSON error reasons; new _warn_inprocess_fallback(model, reason) rate-limits fallback warnings.
Provider mode routing and mode-tagged calls
reflexio/server/llm/providers/embedding_service_provider.py
embedding_provider_mode() consults _local_service_status() and invokes _warn_inprocess_fallback on mismatch/unreachable daemon; get_service_embeddings()/_post_embedding_batch() propagate mode into unavailability warnings.
Fallback and dispatch parity tests
tests/server/llm/test_embedding_service_provider.py, tests/server/llm/test_litellm_client_unit.py
Adds tests for warning rate-limiting, explicit inprocess mode suppression, mismatch warnings, single/batch embedding parity, and mode-tagged error logging on failure.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant embedding_provider_mode
  participant LocalServiceStatus
  participant WarnFallback
  participant GetServiceEmbeddings

  Caller->>embedding_provider_mode: request local/* embedding
  embedding_provider_mode->>LocalServiceStatus: check /health, active_model
  alt reachable and model matches
    LocalServiceStatus-->>embedding_provider_mode: local_service
    embedding_provider_mode->>GetServiceEmbeddings: route via HTTP service
  else mismatch or unreachable
    LocalServiceStatus-->>embedding_provider_mode: failure reason
    embedding_provider_mode->>WarnFallback: warn(model, reason)
    WarnFallback-->>embedding_provider_mode: rate-limited warning emitted/suppressed
    embedding_provider_mode-->>Caller: inprocess mode
  end
Loading

Possibly related PRs

  • ReflexioAI/reflexio#129: Both PRs change fallback logic between local_service and inprocess in embedding_service_provider.py based on health/active_model probes.
  • ReflexioAI/reflexio#157: Both PRs modify local/* embedding routing/fallback and associated warning behavior in the same provider module.
  • ReflexioAI/reflexio#273: Both PRs change _litellm_embedding.py's dispatch/routing for get_embedding/get_embeddings and update matching embedding tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: observable in-process fallback/mode-tagged errors and deduping get_embedding.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/embedding-fallback-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
reflexio/server/llm/_litellm_embedding.py (2)

364-368: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use !s conversion flag instead of str() in f-strings.

Ruff RUF010 flags str(e) inside f-strings on lines 366, 389, and 422. The idiomatic form is {e!s}.

♻️ Proposed fix
                 raise _embedding_error(
                     f"Nomic{' batch' if batch else ''} embedding generation "
-                    f"failed: {str(e)}",
+                    f"failed: {e!s}",
                     mode,
                 ) from e
                 raise _embedding_error(
                     f"Local{' batch' if batch else ''} embedding generation "
-                    f"failed: {str(e)}",
+                    f"failed: {e!s}",
                     mode,
                 ) from e
             raise _embedding_error(
                 f"{'Batch embedding' if batch else 'Embedding'} generation "
-                f"failed: {str(e)}",
+                f"failed: {e!s}",
                 mode,
             ) from e

Also applies to: 387-391, 420-424

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reflexio/server/llm/_litellm_embedding.py` around lines 364 - 368, Replace
the `str(e)` calls inside the f-strings in `_litellm_embedding.py` with the `!s`
conversion flag, using the existing exception variable in
`_embedding_error`-related raise paths (including the Nomic batch branch and the
other matching embedding error branches). This keeps the message formatting
idiomatic and satisfies Ruff RUF010 without changing the surrounding error
handling or `raise ... from e` behavior.

Source: Linters/SAST tools


348-354: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid redundant embedding_provider_mode resolution.

Line 348 resolves mode = embedding_provider_mode(embedding_model), but line 351 calls should_use_embedding_service(embedding_model) which internally calls embedding_provider_mode again. For local/* models this triggers a second daemon health probe (_local_service_status()) and a second _warn_inprocess_fallback invocation (suppressed by rate-limiting, but still wasteful). Since mode is already in hand, check it directly.

♻️ Proposed fix
         embedding_model = model or self._resolve_default_embedding_model()
         mode = embedding_provider_mode(embedding_model)
         if mode == "off":
             raise EmbeddingUnavailableError("Embedding provider is disabled")
-        if should_use_embedding_service(embedding_model):
+        if mode in _SERVICE_MODES:
             return get_service_embeddings(
                 texts, model=embedding_model, dimensions=dimensions
             )

If _SERVICE_MODES is not currently imported, add it to the existing import from embedding_service_provider. Alternatively, a local set check or a thin is_service_mode(mode) helper in the provider module would avoid exposing a private constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reflexio/server/llm/_litellm_embedding.py` around lines 348 - 354, The
embedding selection logic in the function that resolves `embedding_model` is
doing duplicate provider resolution: `embedding_provider_mode(embedding_model)`
is already computed, but `should_use_embedding_service(embedding_model)`
rechecks it and can trigger extra local service probes. Update this branch to
use the existing `mode` value directly to decide whether to call
`get_service_embeddings`, and import or otherwise reference the service-mode
set/helper from `embedding_service_provider` if needed so the check stays
centralized without another `embedding_provider_mode` call.

Source: Linters/SAST tools

reflexio/server/llm/providers/embedding_service_provider.py (1)

204-235: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider adding extra={"mode": "inprocess"} to the fallback warning for log attribution consistency.

The _post_embedding_batch unavailability warning (lines 433-438) and the _embedding_error helper both attach extra={"mode": mode} to their log records, enabling downstream consumers to filter/group by resolved mode. The fallback warning here doesn't include this attribute, making it the only embedding-related WARNING without mode attribution. Adding it would let operators uniformly query all embedding warnings by mode.

♻️ Optional: add mode extra to fallback warning
     _LOGGER.warning(
         "Embedding daemon probe at %s failed for model %r; falling back to an "
         "in-process embedding model copy (a second model is loaded into this "
         "worker process). Probe failure: %s. Further fallback warnings are "
         "suppressed for %.0fs.",
         _local_service_url(),
         model,
         reason or "unknown",
         _INPROCESS_FALLBACK_WARN_INTERVAL_SECONDS,
+        extra={"mode": "inprocess"},
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@reflexio/server/llm/providers/embedding_service_provider.py` around lines 204
- 235, The fallback warning in _warn_inprocess_fallback is missing the same log
attribution used elsewhere for embedding warnings. Update the _LOGGER.warning
call to include extra metadata with mode set to inprocess so downstream
consumers can consistently filter and group logs by resolved mode, matching the
behavior of _post_embedding_batch and _embedding_error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@reflexio/server/llm/_litellm_embedding.py`:
- Around line 364-368: Replace the `str(e)` calls inside the f-strings in
`_litellm_embedding.py` with the `!s` conversion flag, using the existing
exception variable in `_embedding_error`-related raise paths (including the
Nomic batch branch and the other matching embedding error branches). This keeps
the message formatting idiomatic and satisfies Ruff RUF010 without changing the
surrounding error handling or `raise ... from e` behavior.
- Around line 348-354: The embedding selection logic in the function that
resolves `embedding_model` is doing duplicate provider resolution:
`embedding_provider_mode(embedding_model)` is already computed, but
`should_use_embedding_service(embedding_model)` rechecks it and can trigger
extra local service probes. Update this branch to use the existing `mode` value
directly to decide whether to call `get_service_embeddings`, and import or
otherwise reference the service-mode set/helper from
`embedding_service_provider` if needed so the check stays centralized without
another `embedding_provider_mode` call.

In `@reflexio/server/llm/providers/embedding_service_provider.py`:
- Around line 204-235: The fallback warning in _warn_inprocess_fallback is
missing the same log attribution used elsewhere for embedding warnings. Update
the _LOGGER.warning call to include extra metadata with mode set to inprocess so
downstream consumers can consistently filter and group logs by resolved mode,
matching the behavior of _post_embedding_batch and _embedding_error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 623d8698-39b1-4e90-b329-afbc9abd018d

📥 Commits

Reviewing files that changed from the base of the PR and between f9a95ea and 52ec65c.

📒 Files selected for processing (4)
  • reflexio/server/llm/_litellm_embedding.py
  • reflexio/server/llm/providers/embedding_service_provider.py
  • tests/server/llm/test_embedding_service_provider.py
  • tests/server/llm/test_litellm_client_unit.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant