From 29b7c90b29aef99a8be7af644ee03f23c8336fcf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 27 Apr 2026 04:47:17 -0400 Subject: [PATCH] chore: embeddings.py warns on fallback for non-default models (T107) --- chat/services/embeddings.py | 13 ++++++++++++- tests/test_embeddings.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/chat/services/embeddings.py b/chat/services/embeddings.py index ece6eae..44002ea 100644 --- a/chat/services/embeddings.py +++ b/chat/services/embeddings.py @@ -10,6 +10,7 @@ EmbeddingResult shape stays the same, only the generator changes. from __future__ import annotations import hashlib +import logging import math import struct @@ -18,6 +19,8 @@ from pydantic import BaseModel from chat.llm.client import LLMClient +_log = logging.getLogger(__name__) + DEFAULT_EMBEDDING_DIM = 384 DEFAULT_EMBEDDING_MODEL = "pseudo-sha256-384" FALLBACK_EMBEDDING_MODEL = "fallback" @@ -93,7 +96,15 @@ async def generate_embedding( return EmbeddingResult(vector=_pseudo_embed(text, dim), model=model, dim=dim) # Future: real embedding via client.embed(...). Phase 4.5 work. - # For Phase 4, any non-default model falls through to fallback. + # For Phase 4, any non-default model falls through to fallback — + # warn so misconfigured callers (e.g., a real-model swap that isn't + # wired up yet) don't silently degrade to a zero vector. + _log.warning( + "generate_embedding: non-default model %r returned fallback " + "(model client.embed() not yet implemented in Phase 4.5+); " + "downstream search will degrade silently. Configure a supported model.", + model, + ) return EmbeddingResult( vector=[0.0] * dim, model=FALLBACK_EMBEDDING_MODEL, dim=dim ) diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index b458681..4d1dc4b 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -20,6 +20,7 @@ The pseudo path doesn't touch the LLMClient, so we pass an empty from __future__ import annotations +import logging import math import pytest @@ -89,3 +90,33 @@ async def test_generate_embedding_unit_normalized(): result = await generate_embedding(_client(), text="some non-empty text") norm_sq = sum(x * x for x in result.vector) assert math.isclose(norm_sq, 1.0, abs_tol=1e-6) + + +@pytest.mark.asyncio +async def test_generate_embedding_non_default_model_logs_warning(caplog): + """T107: non-default model falls through to fallback and must warn. + + A Phase 4.5+ caller pointing at a real model that isn't yet wired + up would otherwise silently degrade (zero vector → useless cosine). + The warning surfaces the misconfiguration in logs. + """ + caplog.set_level(logging.WARNING, logger="chat.services.embeddings") + result = await generate_embedding(_client(), text="hello", model="real-model") + + # Behavior unchanged: still returns the fallback sentinel. + assert result.model == FALLBACK_EMBEDDING_MODEL == "fallback" + assert all(x == 0.0 for x in result.vector) + + # Warning fired and names the offending model. + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert any("non-default model" in r.getMessage() for r in warnings) + assert any("real-model" in r.getMessage() for r in warnings) + + +@pytest.mark.asyncio +async def test_generate_embedding_default_model_does_not_warn(caplog): + """T107: the silent default path must stay silent.""" + caplog.set_level(logging.WARNING, logger="chat.services.embeddings") + await generate_embedding(_client(), text="hello") + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert warnings == []