fix(skills): embed the request as the pre-call hooks returned it, not the original text

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-05 20:24:39 +00:00
parent 702d3b548c
commit a12ccf76df
2 changed files with 54 additions and 6 deletions

View file

@ -47,6 +47,16 @@ class _EmbeddingData(BaseModel):
data: tuple[_EmbeddingItem, ...]
class _EmbeddingRequest(BaseModel):
"""The /embeddings-shaped request as the pre-call hooks (rate limits, budgets, guardrails) hand it back."""
model_config = ConfigDict(frozen=True, extra="ignore")
model: str
input: tuple[str, ...]
metadata: dict[str, object] # mutable-ok: the router mutates the metadata dict it is handed
def cosine_similarity(left: Vector, right: Vector) -> float:
dot: Final = sum(a * b for a, b in zip(left, right, strict=True))
norms: Final = math.sqrt(sum(a * a for a in left)) * math.sqrt(sum(b * b for b in right))
@ -68,15 +78,21 @@ def router_embedder(
"""Embeds through the router after the same key rate-limit, budget and guardrail pre-call hooks /embeddings runs."""
async def embed(texts: Sequence[str]) -> Sequence[Vector]:
batch: Final = list(texts) # mutable-ok: Router.aembedding accepts only str | list input
metadata: Final = embedding_spend_metadata(user_api_key_dict)
request: Final = { # mutable-ok: pre_call_hook mutates the request dict in place
"model": embedding_model,
"input": batch,
"metadata": metadata,
"input": list(texts), # mutable-ok: Router.aembedding accepts only str | list input
"metadata": embedding_spend_metadata(user_api_key_dict),
}
await proxy_logging_obj.pre_call_hook(user_api_key_dict=user_api_key_dict, data=request, call_type="aembedding")
response: Final = await router.aembedding(model=embedding_model, input=batch, metadata=metadata)
processed: Final = _EmbeddingRequest.model_validate(
await proxy_logging_obj.pre_call_hook(
user_api_key_dict=user_api_key_dict, data=request, call_type="aembedding"
)
)
response: Final = await router.aembedding(
model=processed.model,
input=list(processed.input), # mutable-ok: Router.aembedding accepts only str | list input
metadata=processed.metadata,
)
return tuple(item.embedding for item in _EmbeddingData.model_validate(response.model_dump()).data)
return embed

View file

@ -329,6 +329,38 @@ class TestSearchSkills:
assert checked["data"]["input"] == router.aembedding.await_args.kwargs["input"]
assert checked["data"]["metadata"]["user_api_key"] == "hashed-caller-key"
@pytest.mark.asyncio
async def test_the_embedding_model_sees_the_request_as_the_guardrails_rewrote_it(self) -> None:
router = MagicMock()
router.aembedding = AsyncMock(
side_effect=lambda model, input, metadata: litellm.EmbeddingResponse(
model=model,
data=[{"object": "embedding", "index": i, "embedding": [1.0, 0.0, 0.0]} for i in range(len(input))],
)
)
key_limits = MagicMock()
key_limits.pre_call_hook = AsyncMock(
side_effect=lambda user_api_key_dict, data, call_type: {
**data,
"input": ["[MASKED]" for _ in data["input"]],
"metadata": {**data["metadata"], "guardrail": "masked"},
}
)
await search_skills(
"language translation",
SKILLS,
1,
router=router,
embedding_model="text-embedding-3-small",
index=SkillSearchIndex(),
user_api_key_dict=CALLER,
proxy_logging_obj=key_limits,
)
sent = tuple(call.kwargs for call in router.aembedding.await_args_list)
assert sent
assert all(set(call["input"]) == {"[MASKED]"} for call in sent)
assert all(call["metadata"]["guardrail"] == "masked" for call in sent)
@pytest.mark.asyncio
async def test_a_key_over_its_limit_never_reaches_the_embedding_model(self) -> None:
router = _embedding_router()