Skip to content

fix(bedrock): handle concatenated JSON in tool call arguments - #20742

Merged
6 commits merged into
BerriAI:litellm_oss_staging_02_09_2026from
skylarkoo7:fix-20543-bedrock-concatenated-json-tool-calls
Feb 10, 2026
Merged

fix(bedrock): handle concatenated JSON in tool call arguments#20742
6 commits merged into
BerriAI:litellm_oss_staging_02_09_2026from
skylarkoo7:fix-20543-bedrock-concatenated-json-tool-calls

Conversation

@skylarkoo7

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes #20543

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

Problem

When using Bedrock Claude Sonnet 4.5 with tools enabled, the model sometimes returns multiple tool call argument objects concatenated in a single arguments string:

'{"command":["curl","-i","http://localhost:9009","-m","10"]}{"command":["curl","-i","http://localhost:9009/robots.txt","-m","5"]}{"command":["curl","-i","http://localhost:9009/sitemap.xml","-m","5"]}'

json.loads() fails with JSONDecodeError: Extra data, which crashes the entire request in _convert_to_bedrock_tool_call_invoke() and surfaces as:

litellm.APIConnectionError: Unable to convert openai tool calls=[...] to bedrock tool calls. Received error=Extra data: line 1 column 71 (char 70)

A previous fix attempt (PR #19198) was reverted (PR #19243) because it broke existing tests.

Solution

1. New helper: split_concatenated_json_objects() (common_utils.py)

Uses json.JSONDecoder.raw_decode() to walk a string containing one or more concatenated JSON objects and extract each one individually. This is a robust, incremental parser that handles:

  • Single valid JSON objects (normal case)
  • Multiple concatenated objects with or without whitespace
  • Non-dict JSON values (replaced with {} per Bedrock's toolUse.input requirement)

2. Updated _convert_to_bedrock_tool_call_invoke() (factory.py)

  • Normal JSON arguments: parsed via json.loads() as before (happy path untouched)
  • On JSONDecodeError: falls back to split_concatenated_json_objects() to split concatenated arguments into separate BedrockToolUseBlock entries
    • First block keeps the original tool ID
    • Subsequent blocks get suffixed IDs ({original_id}_1, {original_id}_2, ...)
    • cache_control is attached after the last split block
  • Also fixes: duplicate json.loads() calls and shadowed id builtin

Tests Added (12 total)

test_litellm_core_utils_prompt_templates_common_utils.py (6 tests):

test_litellm_core_utils_prompt_templates_factory.py (6 tests):

  • test_bedrock_tool_call_invoke_normal_single_tool - normal happy path
  • test_bedrock_tool_call_invoke_empty_arguments - empty args → {}
  • test_bedrock_tool_call_invoke_concatenated_json - core fix verification: 3 concatenated objects → 3 separate toolUse blocks with correct IDs
  • test_bedrock_tool_call_invoke_concatenated_json_with_cache_control - cache_control with split
  • test_bedrock_tool_call_invoke_non_dict_arguments - '""'{}
  • test_bedrock_tool_call_invoke_multiple_normal_tools - parallel tool calls

emerzon and others added 6 commits February 8, 2026 08:48
…-delta-index-mapping

fix(responses): preserve tool call argument deltas when streaming id is omitted
add missing indexes on VerificationToken table
When using Bedrock Claude Sonnet 4.5 with tools enabled, the model
sometimes returns multiple tool call arguments as concatenated JSON
objects in a single arguments string, e.g.
  '{"command":["curl",...]}{"command":["curl",...]}{"command":["curl",...]}'

json.loads() fails on this with "Extra data", crashing the entire
request in _convert_to_bedrock_tool_call_invoke.

This commit:
- Adds split_concatenated_json_objects() helper in common_utils.py
  that uses json.JSONDecoder.raw_decode() to walk a string and extract
  each JSON object individually.
- Updates _convert_to_bedrock_tool_call_invoke() to catch JSONDecodeError
  and attempt splitting concatenated objects into separate Bedrock
  toolUse blocks (first block keeps original ID, subsequent blocks get
  suffixed IDs).
- Fixes duplicate json.loads calls and a shadowed 'id' builtin.
- Adds 12 unit tests covering normal, empty, concatenated, and edge cases.

Fixes BerriAI#20543
@vercel

vercel Bot commented Feb 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 9, 2026 10:07am

Request Review

@CLAassistant

CLAassistant commented Feb 9, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR fixes a critical bug where Bedrock Claude Sonnet 4.5 returns multiple tool call argument objects concatenated in a single string (e.g., '{"cmd":"a"}{"cmd":"b"}{"cmd":"c"}'), causing JSONDecodeError: Extra data and request failures.

Key Changes:

  • Added split_concatenated_json_objects() helper in common_utils.py that uses JSONDecoder.raw_decode() to incrementally parse concatenated JSON objects
  • Updated _convert_to_bedrock_tool_call_invoke() in factory.py to catch JSONDecodeError and fall back to splitting logic, creating separate BedrockToolUseBlock entries with suffixed IDs (id, id_1, id_2, etc.)
  • Fixed duplicate json.loads() calls and shadowed id builtin (renamed to tool_id)
  • Properly handles cache_control by attaching cachePoint after the last split block
  • Added 12 comprehensive tests covering normal cases, concatenated JSON, edge cases, and cache_control behavior

Issues Found:

  • Minor style violation: inline import at factory.py:3290 should be moved to module-level imports per CLAUDE.md guidelines

The fix is well-designed, handles edge cases properly (empty strings, non-dict values, whitespace), and maintains backward compatibility with existing tests. The approach of creating multiple tool blocks with suffixed IDs is a sensible workaround for what appears to be a Bedrock API quirk.

Confidence Score: 4/5

  • Safe to merge with one minor style issue that should be addressed
  • Score reflects solid implementation with comprehensive tests and proper error handling. Deducted one point for the inline import style violation (factory.py:3290) which violates CLAUDE.md guidelines - the import should be at module level since json is already imported at the top of the file. Otherwise, the logic is sound, edge cases are covered, and the fix directly addresses issue [Bug]: Bedrock Claude Sonnet 4.5 returns concatenated JSON in tool call arguments causing "Extra data" error #20543 without breaking existing functionality.
  • factory.py:3290-3292 requires moving inline import to module level

Important Files Changed

Filename Overview
litellm/litellm_core_utils/prompt_templates/common_utils.py added split_concatenated_json_objects helper using JSONDecoder.raw_decode() to parse concatenated JSON objects
litellm/litellm_core_utils/prompt_templates/factory.py updated _convert_to_bedrock_tool_call_invoke to handle concatenated JSON by creating multiple toolUse blocks with suffixed IDs; includes style issue with inline import

Sequence Diagram

sequenceDiagram
    participant Caller
    participant CTBTI as _convert_to_bedrock_tool_call_invoke
    participant JSON as json.loads()
    participant Split as split_concatenated_json_objects
    participant Decoder as JSONDecoder.raw_decode()
    
    Caller->>CTBTI: tool_calls with arguments
    
    alt arguments is empty/whitespace
        CTBTI->>CTBTI: set arguments_dict = {}
    else arguments has content
        CTBTI->>JSON: parse arguments
        
        alt valid single JSON object
            JSON-->>CTBTI: return dict
            CTBTI->>CTBTI: create single BedrockToolUseBlock
        else JSONDecodeError (concatenated JSON)
            JSON--xCTBTI: raise JSONDecodeError
            CTBTI->>Split: split_concatenated_json_objects(arguments)
            
            loop for each JSON object in string
                Split->>Decoder: raw_decode(raw, idx)
                Decoder-->>Split: return (obj, end_idx)
                Split->>Split: append obj (or {} if non-dict)
            end
            
            Split-->>CTBTI: return list of parsed dicts
            
            alt parsed_objects is not empty
                loop for each obj in parsed_objects
                    CTBTI->>CTBTI: create BedrockToolUseBlock with suffixed ID
                end
                opt cache_control present
                    CTBTI->>CTBTI: append cachePoint after last block
                end
            else parsed_objects is empty
                CTBTI->>CTBTI: set arguments_dict = {}
            end
        end
    end
    
    CTBTI-->>Caller: return list of BedrockContentBlock
Loading

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +3290 to +3292
from litellm.litellm_core_utils.prompt_templates.common_utils import (
split_concatenated_json_objects,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline import violates style guide (CLAUDE.md:93) - json already imported at top of file

Suggested change
from litellm.litellm_core_utils.prompt_templates.common_utils import (
split_concatenated_json_objects,
)
# split_concatenated_json_objects imported at module level (line ~1-100)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ghost
ghost changed the base branch from main to litellm_oss_staging_02_09_2026 February 10, 2026 04:04
@ghost
ghost merged this pull request into BerriAI:litellm_oss_staging_02_09_2026 Feb 10, 2026
7 of 8 checks passed
Sameerlite added a commit that referenced this pull request Feb 10, 2026
* fix(responses): preserve streamed tool deltas when id is omitted

* fix(responses): guard ambiguous tool-call index reuse

* add missing indexes on VerificationToken table

* fix(bedrock): handle concatenated JSON in tool call arguments

When using Bedrock Claude Sonnet 4.5 with tools enabled, the model
sometimes returns multiple tool call arguments as concatenated JSON
objects in a single arguments string, e.g.
  '{"command":["curl",...]}{"command":["curl",...]}{"command":["curl",...]}'

json.loads() fails on this with "Extra data", crashing the entire
request in _convert_to_bedrock_tool_call_invoke.

This commit:
- Adds split_concatenated_json_objects() helper in common_utils.py
  that uses json.JSONDecoder.raw_decode() to walk a string and extract
  each JSON object individually.
- Updates _convert_to_bedrock_tool_call_invoke() to catch JSONDecodeError
  and attempt splitting concatenated objects into separate Bedrock
  toolUse blocks (first block keeps original ID, subsequent blocks get
  suffixed IDs).
- Fixes duplicate json.loads calls and a shadowed 'id' builtin.
- Adds 12 unit tests covering normal, empty, concatenated, and edge cases.

Fixes #20543

---------

Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: Carlo Alberto Ferraris <cafxx@mercari.com>
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…I#20742)

* fix(responses): preserve streamed tool deltas when id is omitted

* fix(responses): guard ambiguous tool-call index reuse

* add missing indexes on VerificationToken table

* fix(bedrock): handle concatenated JSON in tool call arguments

When using Bedrock Claude Sonnet 4.5 with tools enabled, the model
sometimes returns multiple tool call arguments as concatenated JSON
objects in a single arguments string, e.g.
  '{"command":["curl",...]}{"command":["curl",...]}{"command":["curl",...]}'

json.loads() fails on this with "Extra data", crashing the entire
request in _convert_to_bedrock_tool_call_invoke.

This commit:
- Adds split_concatenated_json_objects() helper in common_utils.py
  that uses json.JSONDecoder.raw_decode() to walk a string and extract
  each JSON object individually.
- Updates _convert_to_bedrock_tool_call_invoke() to catch JSONDecodeError
  and attempt splitting concatenated objects into separate Bedrock
  toolUse blocks (first block keeps original ID, subsequent blocks get
  suffixed IDs).
- Fixes duplicate json.loads calls and a shadowed 'id' builtin.
- Adds 12 unit tests covering normal, empty, concatenated, and edge cases.

Fixes BerriAI#20543

---------

Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: Carlo Alberto Ferraris <cafxx@mercari.com>
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bedrock Claude Sonnet 4.5 returns concatenated JSON in tool call arguments causing "Extra data" error

5 participants