Repository navigation
refactor(decisions): dispatch /v1/decisions through provider configs and the shared HTTP handler - #45130
Conversation
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
bugbot run |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
bugbot run |
115200e to
a717565
Compare
e3fda06 to
d85c20e
Compare
…and the shared HTTP handler Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ror and drop explanatory docstrings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d85c20e to
5276719
Compare
…n every provider `_map_upstream_exception` now maps a status `_handle_error` synthesized for a non-HTTP failure to `APIConnectionError` before `exception_type` runs, so a Perplexity (openai-compatible) deployment that cannot be reached no longer answers `InternalServerError` where main answers `APIConnectionError`. Audit cells: the unreachable-deployment integration cell is parametrized over the five providers, OpenRouter connection failures are pinned on chat, stream, embeddings, decisions and the SDK, a deployment whose provider has no decisions config gets the 400 naming every supported provider, and a unit cell pins the mapper alone. The first docstring sentence of the five translation bases is dropped.
|
bugbot run |
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 bd81e17. Configure here.
…and the shared HTTP handler (BerriAI#45130) * refactor(decisions): dispatch /v1/decisions through provider configs and the shared HTTP handler Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(decisions): map OpenRouter connection failures to APIConnectionError and drop explanatory docstrings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(decisions): keep APIConnectionError for unreachable deployments on every provider `_map_upstream_exception` now maps a status `_handle_error` synthesized for a non-HTTP failure to `APIConnectionError` before `exception_type` runs, so a Perplexity (openai-compatible) deployment that cannot be reached no longer answers `InternalServerError` where main answers `APIConnectionError`. Audit cells: the unreachable-deployment integration cell is parametrized over the five providers, OpenRouter connection failures are pinned on chat, stream, embeddings, decisions and the SDK, a deployment whose provider has no decisions config gets the 400 naming every supported provider, and a unit cell pins the mapper alone. The first docstring sentence of the five translation bases is dropped. --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> (cherry picked from commit 0e09e99) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
TLDR
Problem this solves:
/v1/decisionspicks its provider from a hardcodedDECISIONS_ENDPOINTSdict inlitellm/decisions/main.pyTranslationTestCasepinning its exact path, headers and bodyHow it solves it:
BaseDecisionsConfigper provider: URL, headers, request and response transformsProviderConfigManager.get_provider_decisions_configresolves the config fromget_llm_providerBaseLLMHTTPHandler.decisionsand.adecisionsdo the HTTP, logging and error mapping onceTranslationTestCasebase per provider undertests/integration/translation/decisions/_map_openrouter_exceptionnow honorsstatus_code_is_synthesized, so an unreachable OpenRouter raisesAPIConnectionErrorinstead of a made-up 500APIError. That also fixes OpenRouter chat and embeddings on main, which go through the same_handle_errorpath_map_upstream_exceptioninlitellm/decisions/main.pymaps a connection failure (a status_handle_errorsynthesized and markedstatus_code_is_synthesized) toAPIConnectionErrorfor every decisions provider beforeexception_typeruns. At 5276719 a Perplexity deployment that could not be reached answeredlitellm.InternalServerErrorinstead of thelitellm.APIConnectionErrormain answers, because_map_openai_exception(the mapper for every openai-compatible provider) reads the synthesized 500 as the provider's own. The alternative, teachingexception_typeor_map_openai_exceptionto skip synthesized statuses, would change the error class of chat and embeddings connection failures for every openai-compatible provider on main, so it is not taken hereAPIConnectionErroron chat, streaming chat, embeddings and decisions and the SDK raises it, an unreachable decisions deployment of each of the five providers answersAPIConnectionErrorwhile its sibling keeps serving (tests/unit/decisions/test_main.py::test_unreachable_upstream_maps_to_a_connection_errorpins the mapper alone), and a deployment whose provider has no decisions config gets the 400 naming all five providersPR 2 of 4 for LIT-9282, stacked on #45129. The five providers (typesafe, perplexity, openrouter, cloudflare, strands_decider) keep the same provider URL,
Authorizationheader and wire body as before, whichtest_decisions_wire.pyand the new base cases pin. The base cases were written from each provider's own API docs and a real captured reply, not from whatmain.pysends today, and they pass at this tip. PR 3 switches the public body to the OpenAI Decisions shape using the types from #45129, PR 4 addsopenai/gpt-6-lunaReading order:
litellm/llms/base_llm/decisions/transformation.py,litellm/decisions/main.py, thedecisionsmethod pair inlitellm/llms/custom_httpx/llm_http_handler.py, then the five 15 to 40 line provider configs underlitellm/llms/<provider>/decisions/. The rest is registration rows and testsUser Flow
Before: every decisions call answers, and an OpenRouter deployment that cannot be reached reports a generic
APIErrorwhere every other unreachable deployment reportsAPIConnectionErrortypesafe/jev-1.13.0,openrouter/typesafe/jev-1.13,perplexity/pplx-decider-v1-27b,cloudflare/@cf/cloudflare/clefand their self-hostedstrands_decider/strands-decider-2B-hobson-v19tomodel_listwithmodel_info.mode: evaluation, plusopenrouter/openai/gpt-5-minifor chat, and start the proxy"model": "typesafe/jev-1.13.0", astatestring and aquestionsmap holding anoul, achoiceand ascorequestionanswerskeyed by question name andusagewithinput_tokens368 andoutput_tokens62"model": "openai/gpt-6-luna"and get 400Unknown Decisions provider 'openai'. Supported providers: perplexity, typesafe, openrouter, cloudflare, strands_deciderapi_basethat is down and send POST http://localhost:4000/v1/chat/completions with"model": "openrouter/openai/gpt-5-mini"; they get 500 withlitellm.APIError: APIError: OpenrouterException - Cannot connect to host ..., while the same test against a TypeSafe deployment that is down sayslitellm.APIConnectionError: TypesafeException - Cannot connect to host ...api_basethat is down and send POST http://localhost:4000/v1/decisions to each; every one answers 500litellm.APIConnectionError: <Provider>Exception - Cannot connect to host ...adecisionsrow per decisions call with the provider's own path inapi_base(/v1/systemone,/alpha/decisions,/v1/decisions,/ai/run/@cf/cloudflare/clef) and the cost-map spendAfter: the same calls answer the same, and the unreachable OpenRouter deployment reports
APIConnectionErrorlike every other unreachable deploymenttypesafe/jev-1.13.0,openrouter/typesafe/jev-1.13,perplexity/pplx-decider-v1-27b,cloudflare/@cf/cloudflare/clefand their self-hostedstrands_decider/strands-decider-2B-hobson-v19tomodel_listwithmodel_info.mode: evaluation, plusopenrouter/openai/gpt-5-minifor chat, and start the proxy"model": "typesafe/jev-1.13.0", astatestring and aquestionsmap holding anoul, achoiceand ascorequestionanswerskeyed by question name andusagewithinput_tokens368 andoutput_tokens62"model": "openai/gpt-6-luna"and get 400Unknown Decisions provider 'openai'. Supported providers: openrouter, perplexity, typesafe, strands_decider, cloudflare, the same five in a different orderapi_basethat is down and send POST http://localhost:4000/v1/chat/completions with"model": "openrouter/openai/gpt-5-mini"; they get 500 withlitellm.APIConnectionError: APIConnectionError: OpenrouterException - Cannot connect to host ..., the same class the TypeSafe deployment that is down reportsapi_basethat is down and send POST http://localhost:4000/v1/decisions to each; every one still answers 500litellm.APIConnectionError: <Provider>Exception - Cannot connect to host ...adecisionsrows with the sameapi_basepaths and the same spendLinear ticket
Resolves LIT-9282
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Two live proxies on the QA box, each
--num_workers 2with its own Postgres, one booted from the merge base and one from the tip, with a recorder in front of each real vendor. Real TypeSafe, OpenRouter, Perplexity, Cloudflare and OpenAI keys, curl as the end-user client.WIRE_<PROVIDER>is that leg's recorder, which logs each request and forwards it unchanged to the vendor; the StrandsDecider recorder has no public service behind it (it is a self-hosted decider), so its rows prove the outbound request onlyConfig, the same on both legs:
body_jev.json; thedec_*cells send it with.modelswapped to the deployment named in the cell:{ "model": "typesafe/jev-1.13.0", "state": "Ticket (billing): The export job hangs at 99% and never finishes", "questions": { "defect": {"type": "noul", "instructions": "Is this a defect?"}, "severity": {"type": "choice", "instructions": "How severe is it?", "criteria": {"low": "Cosmetic", "high": "Blocks the user"}}, "confidence": {"type": "score", "instructions": "How confident are you?", "criteria": ["Not sure", "Certain"]} } }Cells, run against each leg with
PROXYandKEY(the master key) set; every JSON reply is printed throughjq -cand cut at 700 characters:Before (79f62db, 2 workers, 127.0.0.1:58201)
After (bd81e17, 2 workers, 127.0.0.1:52979)
Outbound wire
Recorder logs normalized with
jq -Sc 'del(.ts, .upstream_ms)'(the recorder redactsauthorization). Before logged typesafe 6, openrouter 2, perplexity 2, cloudflare 2, strands_decider 4 requests and After typesafe 6, openrouter 2, perplexity 2, cloudflare 2, strands_decider 4 (the/healthprobes, thedec_*cells and the generated-key cells; the down deployments point at a closed port, so nothing reaches a recorder for them) and the normalized diff is empty for all five. One record per provider from the After leg, thedec_*body:{"body":"{\"model\":\"jev-1.13.0\",\"state\":\"Ticket (billing): The export job hangs at 99% and never finishes\",\"questions\":{\"defect\":{\"type\":\"noul\",\"instructions\":\"Is this a defect?\"},\"severity\":{\"type\":\"choice\",\"instructions\":\"How severe is it?\",\"criteria\":{\"low\":\"Cosmetic\",\"high\":\"Blocks the user\"}},\"confidence\":{\"type\":\"score\",\"instructions\":\"How confident are you?\",\"criteria\":[\"Not sure\",\"Certain\"]}}}","headers":{"accept":"*/*","authorization":"<redacted len=115>","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/v1/systemone","status":200} {"body":"{\"model\":\"typesafe/jev-1.13\",\"state\":\"Ticket (billing): The export job hangs at 99% and never finishes\",\"questions\":{\"defect\":{\"type\":\"noul\",\"instructions\":\"Is this a defect?\"},\"severity\":{\"type\":\"choice\",\"instructions\":\"How severe is it?\",\"criteria\":{\"low\":\"Cosmetic\",\"high\":\"Blocks the user\"}},\"confidence\":{\"type\":\"score\",\"instructions\":\"How confident are you?\",\"criteria\":[\"Not sure\",\"Certain\"]}}}","headers":{"accept":"*/*","authorization":"<redacted len=80>","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/alpha/decisions","status":200} {"body":"{\"model\":\"pplx-decider-v1-27b\",\"state\":\"Ticket (billing): The export job hangs at 99% and never finishes\",\"questions\":{\"defect\":{\"type\":\"noul\",\"instructions\":\"Is this a defect?\"},\"severity\":{\"type\":\"choice\",\"instructions\":\"How severe is it?\",\"criteria\":{\"low\":\"Cosmetic\",\"high\":\"Blocks the user\"}},\"confidence\":{\"type\":\"score\",\"instructions\":\"How confident are you?\",\"criteria\":[\"Not sure\",\"Certain\"]}}}","headers":{"accept":"*/*","authorization":"<redacted len=60>","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/v1/decisions","status":200} {"body":"{\"model\":\"clef\",\"state\":\"Ticket (billing): The export job hangs at 99% and never finishes\",\"questions\":{\"defect\":{\"type\":\"noul\",\"instructions\":\"Is this a defect?\"},\"severity\":{\"type\":\"choice\",\"instructions\":\"How severe is it?\",\"criteria\":{\"low\":\"Cosmetic\",\"high\":\"Blocks the user\"}},\"confidence\":{\"type\":\"score\",\"instructions\":\"How confident are you?\",\"criteria\":[\"Not sure\",\"Certain\"]}}}","headers":{"accept":"*/*","authorization":"<redacted len=60>","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/ai/run/@cf/cloudflare/clef","status":200} {"body":"{\"model\":\"strands-decider-2B-hobson-v19\",\"state\":\"Ticket (billing): The export job hangs at 99% and never finishes\",\"questions\":{\"defect\":{\"type\":\"noul\",\"instructions\":\"Is this a defect?\"},\"severity\":{\"type\":\"choice\",\"instructions\":\"How severe is it?\",\"criteria\":{\"low\":\"Cosmetic\",\"high\":\"Blocks the user\"}},\"confidence\":{\"type\":\"score\",\"instructions\":\"How confident are you?\",\"criteria\":[\"Not sure\",\"Certain\"]}}}","headers":{"accept":"*/*","content-type":"application/json","user-agent":"litellm/1.106.0"},"method":"POST","path":"/v1/systemone","status":null}Spend rows
/spend/logsrows for the decisions deployments and the OpenRouter chat deployments, recorder ports replaced byPORT; the Before and After files are byte for byte identical:[{"model":"@cf/cloudflare/clef","status":null,"spend":0.00003504,"total_tokens":146,"prompt_tokens":146,"completion_tokens":0,"api_base":"http://127.0.0.1:PORT/ai/run/@cf/cloudflare/clef","provider":"cloudflare"},{"model":"cloudflare/@cf/cloudflare/clef","status":null,"spend":0.00006887999999999999,"total_tokens":287,"prompt_tokens":287,"completion_tokens":0,"api_base":"http://127.0.0.1:PORT/ai/run/@cf/cloudflare/clef","provider":"cloudflare"},{"model":"cloudflare/@cf/cloudflare/clef","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:9/ai/run/@cf/cloudflare/clef","provider":"cloudflare"},{"model":"jev-1.13.0","status":null,"spend":0.000011592,"total_tokens":296,"prompt_tokens":276,"completion_tokens":20,"api_base":"http://127.0.0.1:PORT/v1/systemone","provider":"typesafe"},{"model":"openai/gpt-6-luna","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"","provider":""},{"model":"openrouter/openai/gpt-5-mini","status":null,"spend":0.000003,"total_tokens":12,"prompt_tokens":12,"completion_tokens":0,"api_base":"https://openrouter.ai/api/v1/chat/completions","provider":"openrouter"},{"model":"openrouter/openai/gpt-5-mini","status":"failure","spend":0.0,"total_tokens":13,"prompt_tokens":13,"completion_tokens":0,"api_base":"http://127.0.0.1:9/chat/completions","provider":"openrouter"},{"model":"openrouter/typesafe/jev-1.13","status":null,"spend":0.000015456,"total_tokens":430,"prompt_tokens":368,"completion_tokens":62,"api_base":"http://127.0.0.1:PORT/alpha/decisions","provider":"openrouter"},{"model":"openrouter/typesafe/jev-1.13","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"","provider":"openrouter"},{"model":"openrouter/typesafe/jev-1.13","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:9/alpha/decisions","provider":"openrouter"},{"model":"perplexity/pplx-decider-v1-27b","status":null,"spend":0.00001196,"total_tokens":302,"prompt_tokens":299,"completion_tokens":3,"api_base":"http://127.0.0.1:PORT/v1/decisions","provider":"perplexity"},{"model":"perplexity/pplx-decider-v1-27b","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:9/v1/decisions","provider":"perplexity"},{"model":"pplx-decider-v1-27b","status":null,"spend":0.00000356,"total_tokens":90,"prompt_tokens":89,"completion_tokens":1,"api_base":"http://127.0.0.1:PORT/v1/decisions","provider":"perplexity"},{"model":"strands_decider/strands-decider-2B-hobson-v19","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:PORT/v1/systemone","provider":"strands_decider"},{"model":"typesafe/jev-1.13","status":null,"spend":0.000011592,"total_tokens":296,"prompt_tokens":276,"completion_tokens":20,"api_base":"http://127.0.0.1:PORT/alpha/decisions","provider":"openrouter"},{"model":"typesafe/jev-1.13.0","status":null,"spend":0.000015456,"total_tokens":430,"prompt_tokens":368,"completion_tokens":62,"api_base":"http://127.0.0.1:PORT/v1/systemone","provider":"typesafe"},{"model":"typesafe/jev-1.13.0","status":null,"spend":0.000015456,"total_tokens":430,"prompt_tokens":368,"completion_tokens":62,"api_base":"http://127.0.0.1:PORT/v1/systemone","provider":"typesafe"},{"model":"typesafe/jev-1.13.0","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"","provider":""},{"model":"typesafe/jev-1.13.0","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:9/v1/systemone","provider":"typesafe"},{"model":"typesafe/jev-1.13.0","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"http://127.0.0.1:PORT/v1/systemone","provider":"typesafe"},{"model":"unknown-model","status":"failure","spend":0.0,"total_tokens":0,"prompt_tokens":0,"completion_tokens":0,"api_base":"","provider":""}]SDK client
litellm.decisionsfrom each leg's own checkout against the same recorders and a closed port (ab3/sdk_cell.py; the TypeSafe key is read from the box's.env, never printed):Observations
scoreon TypeSafe and OpenRouter; Perplexity and Cloudflare answers are identical to the digitlitellm.APIConnectionErroron both legs, through curl and the SDK; at 5276719, before the audit fix in the TLDR, the Perplexity one saidlitellm.InternalServerError. Before, the Perplexity and OpenRouter messages carried the class name twice (litellm.APIConnectionError: APIConnectionError: PerplexityException - ...) where TypeSafe and Cloudflare carried it once; After, all four readlitellm.APIConnectionError: <Provider>Exception - ...litellm.AuthenticationErroron both legsopenai/gpt-6-luna: 400Unknown Decisions provider 'openai'on both legs, the supported list in a different orderstate: the same 400s on both legsapi_base: 500 on both legs,litellm.APIError: APIError: OpenrouterExceptionBefore andlitellm.APIConnectionError: APIConnectionError: OpenrouterExceptionAfter (the review fix in the TLDR)/health: the same healthy set on both legs; unhealthyopenai/gpt-6-luna400, the four down decisions deployments and the down OpenRouter chat deployment 500 (error classes as above) and the StrandsDecider 500 (no service behind its recorder)/v1/messagesunchangedtypesafe/jev-1.13.0: decisions 200 with its model and 403 for another model, on both legsadecisionswith the provider path inapi_baseVerdict: PASS (Before = After at every cell except the three intended differences named above: the error class of an unreachable OpenRouter chat deployment, the order of the supported-provider list, and the duplicated class name dropped from the Perplexity and OpenRouter decisions connection errors)
/audit
Merge base 79f62db, head bd81e17, rig
lit9282audon the GCE box (three legs, two workers each, own Postgres, Redis and scripted upstream per leg), lanesbase,head-run1,head-run2,ctl-providers,ctl-managementandctl-cost; summariesround-lanes.jsonproviders/test_decisions_wire.py::test_sdk_sync_and_async_clients_send_the_same_requestproviders/test_decisions_wire.py::test_unreachable_upstream_fails_only_its_own_deployment[perplexity]providers/test_decisions_wire.py::test_unreachable_upstream_fails_only_its_own_deployment[typesafe]providers/test_decisions_wire.py::test_unreachable_upstream_fails_only_its_own_deployment[openrouter]providers/test_decisions_wire.py::test_unreachable_upstream_fails_only_its_own_deployment[strands_decider]providers/test_decisions_wire.py::test_unreachable_upstream_fails_only_its_own_deployment[cloudflare]providers/test_decisions_wire.py::test_an_unreachable_openrouter_deployment_reports_a_connection_error_on_chat_embeddings_and_decisionsproviders/test_decisions_wire.py::test_sdk_openrouter_connection_failures_raise_a_connection_errorproviders/test_decisions_wire.py::test_a_deployment_whose_provider_has_no_decisions_support_is_refused_naming_every_supported_providertranslation/decisions/basic/test_decisions_basic_cloudflare.py::test_decisions_basic_cloudflare[cloudflare/@cf/cloudflare/clef-basic]translation/decisions/basic/test_decisions_basic_openrouter.py::test_decisions_basic_openrouter[openrouter/typesafe/jev-1.13-basic]translation/decisions/basic/test_decisions_basic_perplexity.py::test_decisions_basic_perplexity[perplexity/pplx-decider-v1-27b-basic]translation/decisions/basic/test_decisions_basic_strands_decider.py::test_decisions_basic_strands_decider[strands_decider/strands-decider-2B-hobson-v19-basic]translation/decisions/basic/test_decisions_basic_typesafe.py::test_decisions_basic_typesafe[typesafe/jev-1.13.0-basic]providers/test_decisions_wire.py::test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map[perplexity]providers/test_decisions_wire.py::test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map[typesafe]providers/test_decisions_wire.py::test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map[openrouter]providers/test_decisions_wire.py::test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map[strands_decider]providers/test_decisions_wire.py::test_each_provider_gets_its_own_path_key_and_body_and_is_billed_from_the_cost_map[cloudflare]providers/test_decisions_wire.py::test_repeated_identical_requests_each_reach_the_upstream_and_are_each_billedproviders/test_decisions_wire.py::test_gateway_only_fields_stay_at_the_gateway_and_tags_reach_the_spend_logproviders/test_decisions_wire.py::test_invalid_bodies_are_refused_at_the_gateway_without_an_upstream_callproviders/test_decisions_wire.py::test_unknown_model_is_refused_like_chatproviders/test_decisions_wire.py::test_key_checks_match_chatproviders/test_decisions_wire.py::test_request_body_api_base_is_refused_like_chat_without_an_upstream_callproviders/test_decisions_wire.py::test_a_deployment_without_a_key_sends_the_provider_env_key_to_its_configured_api_baseproviders/test_decisions_wire.py::test_a_deployment_opted_into_client_api_base_sends_decisions_and_chat_to_the_body_api_baseproviders/test_decisions_wire.py::test_a_config_pass_through_at_v1_decisions_keeps_answering_and_the_native_api_serves_decisionsproviders/test_decisions_wire.py::test_upstream_errors_keep_their_status_and_log_an_unbilled_failure[401]providers/test_decisions_wire.py::test_upstream_errors_keep_their_status_and_log_an_unbilled_failure[429]providers/test_decisions_wire.py::test_upstream_errors_keep_their_status_and_log_an_unbilled_failure[500]providers/test_decisions_wire.py::test_upstream_success_without_answers_is_a_gateway_side_server_errorproviders/test_decisions_chaos.py::test_burst_over_both_routes_bills_each_call_once_with_its_own_statusproviders/test_decisions_chaos.py::test_worker_sigkill_mid_burst_leaves_the_sibling_serving_the_default_modelproviders/test_decisions_chaos.py::test_upstream_outage_fails_its_calls_and_recovery_on_the_same_port_restores_themmanagement/test_model_health_check.py::test_evaluation_mode_health_check_resolves_the_mode_from_the_cost_map_and_sends_the_default_probemanagement/test_model_health_check.py::test_evaluation_mode_health_check_sends_the_configured_state_and_questionsmanagement/test_model_health_check.py::test_evaluation_mode_health_check_of_the_self_hosted_strands_model_resolves_the_mode_from_the_cost_mapcost_calculation/test_cost_tracking.py::test_case_bills_expected_cost[perplexity/pplx-decider-v1-27b-decisions]| L1 | live: TypeSafe, OpenRouter, Perplexity, Cloudflare reached with real keys through the Before and After proxies above (
/live-pr-riskA/B at bd81e17 vs 79f62db, outbound wire diff empty, replies equal but for the sampled score, spend rows identical) | PASS || L2 | live: StrandsDecider has no public service; its outbound request (POST
/v1/systemone, noAuthorizationheader, Jev body) is in the After leg's recorder log and its reply shape is pinned by the A1 and A2 cells | PASS |Every cell ran at bd81e17; the two head runs collected and passed the same selections with no skips and no retries
Last updated: bd81e17 (the Before/After legs, the SDK cell and the audit all ran at this tip)
Type
🧹 Refactoring
Caveats (if any)
Low
litellm.APIConnectionErrorinstead of 500litellm.APIError; the status is unchanged and the new class is the one every other provider already reports, chosen in the review fix 7933ace named in the TLDRUnknown Decisions provider400 lists the five supported providers in registration order (openrouter, perplexity, typesafe, strands_decider, cloudflare) instead of the old dict order; status and set are unchangedlitellm.APIConnectionError: PerplexityException - ...instead oflitellm.APIConnectionError: APIConnectionError: PerplexityException - ..., the class name once like TypeSafe and Cloudflare already had; class and status are unchangedscoreandprobabilitiesdiffer between Before and After by a few hundredths because the model is sampled, not because of this PR;noul,choice,legendandusagematchAuthorizationheader, body) and not a reply; the scripted-upstream cells cover its reply shapelitellm.adecisions; that migration is tracked separately on LIT-9282Unknown Decisions provider400 as beforeFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/a7a7d712811643b1a6416531d7d99cfa
Open in Devin Desktop: https://app.devin.ai/desktop/session/a7a7d712811643b1a6416531d7d99cfa?variant=devin
Requested by: @kerry-berri
Note
Medium Risk
Touches request routing, API key resolution, and response parsing for
/v1/decisions; behavior is intended to be unchanged but errors and provider resolution paths shifted to shared infrastructure.Overview
Refactors
/v1/decisionsso it no longer uses a hardcodedDECISIONS_ENDPOINTSmap and inline httpx inlitellm/decisions/main.py. Calls now go throughget_llm_provider,ProviderConfigManager.get_provider_decisions_config, andBaseLLMHTTPHandler.decisions/adecisions, matching other LiteLLM provider APIs.Each supported provider (perplexity, typesafe, openrouter, cloudflare, strands_decider) gets a
BaseDecisionsConfigsubclass (replacingJevCompatibleDecisionsEndpointsingletons) for URL, auth/env resolution, request body, and response unwrapping. The base class centralizes default Jev/System One wire behavior; Cloudflare keeps custom URL andresultenvelope handling.DECISIONS_ENDPOINTSis removed from the public module exports.DecisionsResponse.set_hidden_paramsis added for metadata after transforms. Malformed upstream payloads without validanswersnow surface asInternalServerErrorduring response validation.Tests: per-provider
TranslationTestCasebases and integration tests pin expected paths/headers/bodies; unit tests cover provider rejection, emptycustom_llm_providerfalling back to model prefix, Router deployment API keys, and bad upstream responses. Proxy integration config adds mock entries for all five providers.Reviewed by Cursor Bugbot for commit ee4dd89. Bugbot is set up for automated code reviews on this repo. Configure here.