Skip to content

fix(responses): decode JSON-string tool schemas before sending to the provider - #39844

Merged
tin-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_responses_tool_params_string
Sep 5, 2026
Merged

fix(responses): decode JSON-string tool schemas before sending to the provider#39844
tin-berri merged 3 commits into
litellm_internal_stagingfrom
litellm_responses_tool_params_string

Conversation

@tin-berri

@tin-berri tin-berri commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • A tool schema sent as a JSON string reached the provider verbatim
  • Provider 400 named the routed model, not the offending tool
  • Surfaced through the 1M Context auto-router preset on an Azure GPT tier

How it solves it:

  • Decode a string parameters that parses to an object before the wire
  • Reject anything else with a litellm 400 naming tools[i].parameters
  • One owner for the request and compact-request tool sanitization

User Flow

Before: a developer whose coding agent defines a tool with a JSON-encoded schema gets an opaque provider error blaming the model tier

  1. They send POST https://litellm-domain/v1/messages to their 1M Context auto-router with tools[0].input_schema set to the string "{\"type\":\"object\",...}"
  2. The request classifies SIMPLE and lands on the gpt-5.6-luna tier
  3. They get back HTTP 400: AzureException BadRequestError - Invalid type for 'tools[0].parameters': expected an object, but got a string instead. Received Model Group=gpt-5.6-luna
  4. They read that as a broken preset and file it against the router

After: the same request succeeds, and a genuinely malformed schema gets an error that points at the tool

  1. They send the same POST https://litellm-domain/v1/messages with the same string input_schema
  2. The request classifies SIMPLE and lands on the gpt-5.6-luna tier
  3. They get back HTTP 200 with a tool_use block for the tool they defined
  4. If the string does not decode to an object, they get HTTP 400 litellm.BadRequestError: Invalid type for 'tools[0].parameters': expected an object, but got str instead from the gateway itself, before any provider call

Relevant issues

Linear ticket

Resolves LIT-6991

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Local proxy on :4733 from this worktree, real Postgres, deployments gpt-5.6-luna / gpt-5.6-terra / gpt-5.6-sol / claude-opus-5 registered against a live gateway as upstream, plus my-1m-router, an auto-router with the 1M Context preset's tiers. Every run below is a real paid model call. The customer's Azure resource is not reachable from this box (every deployment 404s), so the upstream here is the same model family behind OpenAI's Responses validator, which emits the byte-identical Invalid type for 'tools[0].parameters' error. The only variable between Before and After is the source tree

Shared payload: a get_weather tool whose schema is the JSON string "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}", and a control request with the same schema as an object. Both arms were captured with the same repro.sh and chatbridge.sh, differing only in the checked-out source

Before (59d42d3)

/v1/responses, string parameters

  1. curl -X POST http://127.0.0.1:4733/v1/responses -d '{"model":"gpt-5.6-luna","input":"weather in Paris","tools":[{"type":"function","name":"get_weather","parameters":"<string schema>"}]}'
  2. HTTP 400: Invalid type for 'tools[0].parameters': expected an object, but got a string instead.

/v1/messages, string input_schema

  1. curl -X POST http://127.0.0.1:4733/v1/messages -d '{"model":"gpt-5.6-luna","max_tokens":64,"messages":[{"role":"user","content":"weather in Paris"}],"tools":[{"name":"get_weather","description":"w","input_schema":"<string schema>"}]}'
  2. HTTP 400: Invalid type for 'tools[0].parameters': expected an object, but got a string instead.

/v1/messages through the 1M Context auto-router

  1. Same body as above with "model":"my-1m-router" and content "hi" so it classifies SIMPLE
  2. HTTP 400: Invalid type for 'tools[0].parameters': expected an object, but got a string instead. This is the customer's exact report

/v1/chat/completions bridged to Responses, nested string parameters

  1. curl -X POST http://127.0.0.1:4733/v1/chat/completions -d '{"model":"luna-responses-mode","messages":[{"role":"user","content":"weather in Paris"}],"tools":[{"type":"function","function":{"name":"get_weather","parameters":"<string schema>"}}]}' (deployment declares model_info.mode: responses)
  2. HTTP 400: Invalid type for 'tools[0].parameters': expected an object, but got a string instead. Note the error names the flat tools[0].parameters even though the request nested it under function, which is how the customer's report ended up with that shape

Controls, object schema

  1. Same /v1/responses and /v1/messages requests with the schema as an object
  2. HTTP 200 on both, /v1/messages returns a tool_use block

After (6140d88)

/v1/responses, string parameters

  1. Identical curl
  2. HTTP 200, id: resp_84m6-zTsfRUQYDA..., model: gpt-5.6-luna, output: [function_call]

/v1/messages, string input_schema

  1. Identical curl
  2. HTTP 200, model: gpt-5.6-luna, output: [tool_use]

/v1/messages through the 1M Context auto-router

  1. Identical curl
  2. HTTP 200, model: my-1m-router, output: [text]

/v1/chat/completions bridged to Responses, nested string parameters

  1. Identical curl
  2. HTTP 200, id: chatcmpl-a7d710d8-35..., finish_reason: tool_calls, tool call get_weather

Controls, object schema

  1. Identical curls
  2. HTTP 200 on both, unchanged from Before

Type

🐛 Bug Fix

Caveats (if any)

Low

  • Nested Codex namespace tools arrays are not walked; only top-level entries decode
  • The Anthropic Messages to chat/completions hop (adapters/transformation.py) copies input_schema the same way and its extra-key merge raises AttributeError on a string schema. Same class, different wire shape, not in this report, left as a follow-up
  • A non-object schema that used to fail at the provider now fails inside litellm with the same status and wording, but the type name is Python's (str, list) rather than the provider's (a string, an array)
  • parameters: null and an omitted schema are forwarded untouched, since the API accepts both; a live probe returned 200 for null, null with strict: false, and omitted

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Low Risk
Scoped to Responses API outbound request shaping for tools, with clear validation and unit tests; no auth or persistence changes.

Overview
Fixes Responses API requests where function tools carry parameters as a JSON string (common when agents pass stringified input_schema), which previously caused opaque provider 400s blaming the model.

OpenAI Responses transformation now runs tool prep through a shared _prepared_input_and_tools path for both normal and /compact requests. Before flattening schema combinators, it parses string parameters into objects via safe_json_loads; object, null, and omitted schemas are unchanged. Invalid values raise litellm.BadRequestError naming tools[i].parameters before any upstream call. Tool typing is widened to Sequence for the sanitization helpers.

Tests cover decode/reject/unchanged cases on standard and compact transforms, plus Azure after chat-shaped tool un-nesting. type-discipline-budget.json limits are nudged down slightly.

Reviewed by Cursor Bugbot for commit 6140d88. Bugbot is set up for automated code reviews on this repo. Configure here.

@tin-berri
tin-berri requested a review from a team September 5, 2026 01:31
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai can you reply here with a score and why that score?

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Confidence score: 4/5

The fix correctly centralizes tool preparation for both Responses and compact requests, decodes JSON strings only when they produce an object, and raises a targeted litellm.BadRequestError for malformed or non-object schemas. The Azure path flattens nested chat-style tools before invoking the shared sanitizer, and the added tests cover successful decoding, compact requests, invalid values, unchanged object/omitted parameters, and Azure behavior.

I’m not giving 5/5 because the required CI check is still pending, and the PR documents two intentionally unhandled paths: nested Codex namespace tools and the Anthropic Messages-to-chat adapter. Those are reasonable follow-ups if they are later included in scope; for the reported top-level Responses/Azure regression, this is a solid 4/5.

@codspeed-hq

codspeed-hq Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_responses_tool_params_string (6140d88) with litellm_internal_staging (7573632)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR normalizes JSON-encoded Responses API tool schemas into objects before provider dispatch and returns an indexed client error for invalid schemas. The shared preparation path now applies the same behavior to standard and compact requests, with coverage for OpenAI and Azure request shapes, invalid values, null schemas, and omitted schemas

Confidence Score: 5/5

The PR appears safe to merge, with no outstanding correctness or repository-rule findings

The schema conversion accepts objects, decodes only JSON strings containing objects, preserves provider-supported null and absent schemas, and rejects other values before dispatch. Both previous rule findings were manually resolved, and the earlier null-schema concern was withdrawn after the provider compatibility evidence

Important Files Changed

Filename Overview
litellm/llms/openai/responses/transformation.py Adds centralized tool-schema decoding and validation shared by standard and compact Responses requests
tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py Adds focused regression coverage and completes the typing and null-schema follow-ups from the previous review
type-discipline-budget.json Lowers the relevant type-discipline limits to match the typed implementation

Reviews (2): Last reviewed commit: "test(responses): pin null tool schemas a..." | Re-trigger Greptile

Comment thread litellm/llms/openai/responses/transformation.py
Comment thread litellm/llms/openai/responses/transformation.py Outdated
Comment thread tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py Outdated

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@tin-berri

Copy link
Copy Markdown
Contributor Author

Pushed 66ac753. Declined the null finding with a live probe and a pinning test; fixed both P2s. @greptileai can you reply here with a score and why that score?

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Score: 5/5 (high confidence).

Commit 66ac753343 addresses the remaining review points:

  • The null finding is correctly declined: the live probe establishes that explicit parameters: null is accepted, and the regression test pins object, null, omitted, and built-in tool shapes.
  • Both P2s are fixed: the comments were reduced to necessary rationale, and the parametrized invalid-input test now uses object while the string-only test remains typed as str.
  • Sanitization is centralized for both normal and compact requests, decodes only JSON strings that produce objects, preserves valid existing shapes, and raises an indexed BadRequestError for invalid schemas.
  • Coverage includes the compact path and Azure’s chat-shaped tool normalization, with the Buildkite e2e check passing.

No remaining blocking concerns from this review. Confidence score: 5/5.

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai review

@tin-berri
tin-berri enabled auto-merge (squash) September 5, 2026 01:41
@tin-berri

Copy link
Copy Markdown
Contributor Author

The 5 integrations reds are pre-existing, not from this PR. test_datadog_llm_obs.py::test_a_redacted_span_carries_every_declared_guardrail_field fails identically at the merge base 639b3f4: guardrail_cost_by_unit is declared on the type but absent from the redacted record, from 4914914. This PR touches only the OpenAI Responses transform and its test.

@tin-berri

Copy link
Copy Markdown
Contributor Author

Merged current staging (ab0fc27). The integrations red was staging's own, fixed upstream by d4fd658 after this branch's base; re-ran the live evidence on the merged tree, all legs still 200. @greptileai can you reply here with a score and why that score?

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Confidence score: 5/5.

The merged staging tree preserves the fix, and the updated live evidence confirms all reported paths still return 200, including direct Responses requests, Messages input-schema conversion, the 1M Context auto-router/Azure route, the chat-to-Responses bridge, and the object-schema controls.

The implementation remains correctly scoped: standard and compact requests share one preparation path, JSON strings are decoded only when they produce an object, valid object/null/omitted schemas are preserved, and malformed or non-object values fail early with an indexed litellm.BadRequestError. The regression coverage exercises these cases plus Azure’s nested tool shape. The integrations failures were confirmed as staging-only and fixed upstream, so I have no remaining correctness concerns.

@cursor cursor 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.

Stale Bugbot comment from a previous run.

@tin-berri
tin-berri enabled auto-merge (squash) September 5, 2026 03:53
… provider

A caller that hands a tool schema over already JSON-encoded reached the
Responses API with a string `parameters`, and the provider rejected the
request with a 400 naming the routed model instead of the offending tool.
Decode it at the one place every Responses request converges, and refuse
anything that is neither an object nor a string encoding one.

Collapses the duplicated input/tool sanitization block shared by the
request and compact-request builders into a single owner, so the decode
cannot be wired into one path and not the other.
…metrized cases

The Responses API serves `parameters: null` and an omitted schema alike, so
neither may raise. Pin both against a future tightening, annotate the
parametrized inputs, and trim the docstrings back to what the code does not
already say.
Lower the type-discipline limits for violations removed by the response transform fix.
@tin-berri
tin-berri force-pushed the litellm_responses_tool_params_string branch from ab0fc27 to 6140d88 Compare September 5, 2026 05:05
@tin-berri

Copy link
Copy Markdown
Contributor Author

Rebased onto current litellm_internal_staging at 7573632; final head is 6140d88. @greptileai can you reply here with a score and why that score?

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Confidence score: 5/5.

The rebased head 6140d881f8 preserves the fix cleanly. Tool preparation is centralized in litellm/llms/openai/responses/transformation.py, so both standard and compact Responses requests decode JSON strings consistently. Existing object, null, omitted, and built-in tool schemas remain unchanged, while malformed or non-object values fail early with an indexed litellm.BadRequestError. Azure’s nested chat-shaped tools are flattened before the shared sanitizer runs.

The focused regression tests cover successful decoding, compact requests, invalid values, accepted edge cases, and Azure behavior. The current Buildkite E2E check is passing, and I found no remaining correctness concerns.

@cursor cursor 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.

✅ 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 6140d88. Configure here.

@tin-berri
tin-berri merged commit 78ad88f into litellm_internal_staging Sep 5, 2026
183 of 184 checks passed
@tin-berri
tin-berri deleted the litellm_responses_tool_params_string branch September 5, 2026 05:14
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.

2 participants