feat: shadow eval samples /v1/messages and /v1/responses traffic - #36830
Conversation
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 What's done well: The dispatch table design ( Why not 5:
|
Greptile SummaryThe PR extends shadow evaluation sampling to Anthropic Messages and Responses API traffic while preserving chat-completion behavior
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/integrations/shadow_eval_logger.py | Adds surface-specific normalization, judgeability checks, guardrail-aware replay prevention, and shared dispatch for chat, Messages, and Responses traffic |
| litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py | Tightens the return annotation for the existing Anthropic translatable-parameter list |
| tests/test_litellm/integrations/test_shadow_eval_logger.py | Expands regression coverage across new surfaces, translations, tool calls, malformed payloads, and guardrail modes |
| basedpyright-code-budget.json | Ratchets down two basedpyright diagnostic budgets after the typing improvements |
Reviews (3): Last reviewed commit: "feat: shadow eval samples /v1/messages a..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
0746c89 to
a00dae4
Compare
|
All four findings addressed in one round: bridge system recovery via the wire body, messages alias documented, both responses call types tested, model_dump in the tool gate. @greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 5/5 All four findings are cleanly closed:
The only remaining micro-nit is that |
a00dae4 to
cc32851
Compare
|
Params finding fixed: each surface row translates params via its owner transformation, live-proven. @greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 The params concern is resolved — wire body sourcing for both surfaces is correct and live-proven, and the tests confirm the right params reach the shadow call. Here's the full picture: What's working well:
Remaining concerns (minor):
None of these block the feature — they're edge cases on the correctness margin. The core behavior is right, the params translation is now solid, and the test suite covers the important scenarios. |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit cc32851. Configure here.
7475ae1 to
6ef599d
Compare
|
Point 3 hardened in 6ef599d. Point 1: TypedDict is flat and 3.13 merges inherited annotations. Point 2: custom_tool_call is OpenAI's custom-tools output type. @greptileai rescore? |
6ef599d to
2611f41
Compare
|
bugbot run |
|
CI fully green on 2611f41, codecov patch covered. @greptileai can you reply here with a score and why that score? |
|
4/5. The implementation is well-engineered. The The one thing I'd flag: *((k, v) for k, v in wire_body.items() if k in ResponsesAPIOptionalRequestParams.__annotations__),Using Everything else is solid. The |
|
ResponsesAPIOptionalRequestParams is a TypedDict, not a BaseModel: model_fields doesn't exist on it, and TypedDicts merge inherited annotations. Keeping annotations |
2611f41 to
b2d998b
Compare
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
b2d998b to
e413a2f
Compare
|
@greptileai can you reply here with score and why |
|
bugbot run |
|
Overall, this is a well-engineered feature with excellent test coverage and thoughtful security design. Here's my assessment: Score: 4/5 What's done well:
One real concern — In k in ResponsesAPIOptionalRequestParams.__annotations__For a TypedDict, from typing import get_type_hints
# ...
*((k, v) for k, v in wire_body.items() if k in get_type_hints(ResponsesAPIOptionalRequestParams)),This matters because if a future parent TypedDict addition carries a generation param like Minor:
|
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e413a2f. Configure here.
|
Demoed on 3.13: TypedDict merges inherited annotations, a subclass sees top_logprobs in annotations, and the class is flat. Adapter init is pass. @greptileai score? |
TLDR
Problem this solves:
/v1/chat/completionstraffic;/v1/messagesand/v1/responsesrequests are silently skipped by the call_type allowlist, so a key whose clients speak those surfaces (Claude Code and Desktop, Responses API apps) evaluates as zero judged turnsanthropic_messagesandaresponsesHow it solves it:
A per-surface dispatch table (
_SURFACE_OPS) replaces the allowlist frozenset and the inline chat-shape coercion: membership in the table is the allowlist, and each row says how that surface's logged request becomes a chat-shaped request, messages and generation params both, and how its response yields the judgeable final textNormalization reuses litellm's own owners end to end, no new format logic:
LiteLLMAnthropicMessagesAdapter.translate_anthropic_to_openaifor Anthropic requests (block messages, top-levelsystem, and params: tools, tool_choice, thinking, stop_sequences, output_format translate, sampling params copy through),LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_requestfor Responses requests (inputplusinstructions,max_output_tokenstomax_tokens, Responses tools to chat tools,reasoningtoreasoning_effort,texttoresponse_format), andResponsesAPIResponse.output_textfor response textParam values come from the proxy's wire-body snapshot rather than the logged optional_params, because the logged params switch dialect per provider path: the openai-compatible
/v1/messagesbridge rides the Responses API and logs Responses-shaped params for an Anthropic request (live-probed), while the wire body is always what the client sent. Each surface filters the body to its own request schema, so surface-only keys likeprevious_response_idnever reach the shadow call, and one shared strip removesmodel,messages,stream,stream_options, andmetadatafrom every translated request before it forwardslitellm's logging layer already normalizes the response half per surface (Messages responses arrive as chat-shaped
ModelResponseon both provider paths, Responses requests keep theirinputinkwargs["messages"]), so each table row composes existing transformations onlyA uniform text-final gate skips turns whose real response carries tool calls (
tool_calls/function_callon chat-shaped responses,function_calloutput items on Responses), formalizing what chat sampling already did implicitly; request-side tool history stays allowed on every surface, matching chat behavior todayRebased onto reverse-direction jobs (feat(shadow_eval): add reverse-direction shadow eval jobs #36865): per-job gates (direction, turn budget, sampling) run first and the request normalizes once, only when at least one job sampled it. Dict-shaped Responses payloads validate into
ResponsesAPIResponse, so the derivedoutput_textproperty applies to both payload shapesRequests a pre_call guardrail rewrote are skipped on the two wire-body-sourced surfaces, detected through the standard_logging_guardrail_information entries spend logging already records: the wire-body snapshot is taken before the guardrail pre-call hook, so replaying it would resurrect content the guardrail stripped or masked. Chat sampling is unaffected since it sources the dispatched call
User Flow
/v1/messages(for example Claude Code pointed at the gateway) or/v1/responsesRelevant issues
Linear ticket
Pre-Submission checklist
Screenshots / Proof of Fix
Live proxy on this branch (port 4216, same rig and database as #36587's proof; stub upstream serving all three surfaces). Fresh job on the shadowed key, then one request per arm, each carrying surface-native params that would break or skew a chat call if forwarded raw:
A logging probe on the shadow calls confirms the translations (optional_params as the shadow acompletion saw them):
The same probe showed the bridge messages arm logs
model_parametersas{"instructions": ..., "max_output_tokens": 180, "tools": [Responses-shaped]}for an Anthropic-shaped client request, which is why params source from the wire body and not the logged snapshotType
🆕 New Feature
Caveats (if any)
/anthropic/v1/messagespassthrough route logs a different call_type with httpx payloads and stays excludedQA runbook
/v1/messages(both an openai-backed and an anthropic-backed model) and/v1/responses, including surface-native params (stop_sequences,top_k, anthropic tools,max_output_tokens, Responses tools,previous_response_id)/v1/chat/completionssampling is unchanged, streamed and non-streamed/v1/messagesand/v1/responses; expect them skipped without error rows (declared caveat)Final Attestation
Note
Cursor Bugbot is generating a summary for commit e413a2f. Configure here.