Name the rejected tool when a provider rejects the request - #733
Conversation
Map litellm BadRequestError to LLMBadRequestError carrying the implicated tool names, and stop classify_llm_error re-generalizing it. Error frames from tools mode now also carry error_type.
SummaryAdds an VerdictLooks good. EvidenceNo evidence score was produced for this change. The description documents 13 targeted tests covering the named-tool, unnamed-tool, no-tools, and context-window-precedence cases, plus an end-to-end reproduction with a malformed MCP schema, which is proportionate to the size of the change. Notes
Posted automatically by AGENT-REVIEW-BOT-3 |
The first cut of this treated any BadRequestError as tool-caused whenever tools were in the request, and blamed the entire selection when the provider named no tool. Most 400s are not about tools -- an out-of-range parameter, a malformed message history -- so that traded one misleading message for another, and sent users off to disable tools that were never at fault. Attribution now requires evidence: the tool the provider named, matched as a whole token so "calc" is not blamed for text containing "calculate"; or, when the text points at the tool payload without naming a tool, every tool in the request so the user can bisect. Anything else reports a plain rejection and names no tool. Also: - Add LLMBadRequestError to the passthrough tuples in call_with_rag() and call_with_rag_and_tools(). Those fall back to a simpler call when RAG breaks; a rejection is not a RAG failure, and without the passthrough the same refused request went to the provider twice with RAG blamed in the logs. - Handle LLMBadRequestError in the main.py WebSocket chat handler so the non-streaming path reports error_type "bad_request" too, instead of falling through to the generic DomainError branch as "domain".
|
Pushed the review fixes to this branch (
Backend suite: 1842 passed, 17 skipped, 0 failures. The red checks here are the fork-PR ceiling, not the change — |
Fixes #728. Fixes #729.
A malformed tool definition makes the provider reject the whole request, so no tool ever runs and the turn fails. The provider names the offending function, but that detail was replaced by a generic message before it could reach the user:
The message points at the model, so the natural response is to switch models, which does not help. Users recover by deselecting tools one at a time.
Cause
Two layers each generalized, and the first destroyed what the second needed.
_raise_llm_domain_error(litellm_caller.py) had noBadRequestErrorbranch, so it fell through to a fixedLLMServiceErrorstring.classify_llm_error(error_handler.py) then ran its keyword matching againststr(error), which by then was that fixed string rather than the provider's message. Nothing matched, so it produced a second generic message.Because the detail was gone before classification ran, fixing
classify_llm_erroralone could not recover the tool name.Changes
LLMBadRequestErrordomain error carryingtool_names._raise_llm_domain_errormapslitellm.BadRequestErrorto it, and takes an optionaltools_schemaso it can report which tools were in the request. Placed after the context-window check, sincelitellm.ContextWindowExceededErrorsubclassesBadRequestError.classify_llm_errorshort-circuits onLLMBadRequestErrorand passes its message through.error_type_for()maps a domain error class to theerror_typevocabulary already used inmain.py; the tools-mode error frame now includes it (Streaming tools error frame omits error_type #729).Existing classification behavior for rate limit, timeout, auth, context window and generic errors is unchanged.
Test plan
atlas/tests/test_tool_error_attribution.py— 13 tests, written before the fix and confirmed failing for the expected reasons firstContextWindowExceededErrorstill classifies as context window, not bad requestvault_api_key_lookup) is not rerouted to the auth brancherror_type: "bad_request"ruff checkclean on all touched filesReproduced end to end before and after with an MCP server advertising
inputSchema: {"type": "string"}and a local OpenAI-compatible stub applying the documentedtype: "object"validation.Not included
main.pyroutesLLMBadRequestErrorthrough itsDomainErrorhandler, so that path reportserror_type: "domain"while carrying the same message. Left alone to keep this change scoped; worth aligning separately.The underlying cause — MCP tool schemas are never validated at discovery — is #727 and needs its own fix. This change makes the failure legible, not impossible.