Skip to content

fix(chatgpt): keep text and accept string input for the Codex backend - #38086

Open
alekc wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
alekc:fix/chatgpt-preserve-text-and-string-input
Open

fix(chatgpt): keep text and accept string input for the Codex backend#38086
alekc wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
alekc:fix/chatgpt-preserve-text-and-string-input

Conversation

@alekc

@alekc alekc commented Aug 24, 2026

Copy link
Copy Markdown

TLDR

Problem this solves:

  • chatgpt/* answers prose when you ask for a strict JSON schema
  • No error and no warning, so it looks like a model failure
  • chatgpt/* rejects a plain string input outright

How it solves it:

  • Keep text in the outgoing request so the schema survives
  • Wrap a string input in a list before sending

User Flow

Before: a developer asking a ChatGPT subscription model for structured output silently gets prose, so their parser fails

  1. They send POST https://litellm-domain/v1/responses with a chatgpt/* model and a strict json_schema in text.format, asking for a city and country
  2. A 200 comes back, but the body is the sentence Paris, France rather than an object
  3. Their JSON parse fails, and nothing in the response or logs says the schema was never applied, so they go looking at the model and its prompt
  4. They try the simplest possible call instead, POST https://litellm-domain/v1/responses with "input": "say hi" as a plain string
  5. That returns 400 with {"detail":"Input must be a list"}, even though the same string works against OpenAI

After: the same two requests behave the way the API contract says

  1. They send the same POST https://litellm-domain/v1/responses with the strict json_schema
  2. A 200 comes back with {"city": "Paris", "country": "France"}, which their parser accepts
  3. They send the same POST https://litellm-domain/v1/responses with "input": "say hi" as a plain string
  4. A 200 comes back with a completed response, matching OpenAI's behaviour for a string input

Relevant issues

Fixes #38085
Refs #24356, refs #27851

Linear ticket

Pre-Submission checklist

  • 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
  • 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

Screenshots / Proof of Fix

Shared setup for both cases, run against the live Codex backend with no mocks. Model chatgpt/gpt-5.6-sol, prompt Paris is the capital of France. Return the city and country., schema {"city": string, "country": string} with strict: true and additionalProperties: false.

One thing a reviewer should know about how this was captured. Two other known defects on this provider, the forced streaming flag in #34094 / #34095 and the drained stream in #25429 / #31332, block the same call from the response side. Both arms below have equivalents of those two applied, identically, so the only difference between Before and After is this PR's request-side change. Without that, neither arm returns anything to compare.

Before (f005afa)

case 1, strict json_schema

  1. Send the request above with text.format set to the strict schema
  2. Observed: 'Paris, France', prose rather than JSON, no error and no warning

case 2, string input

  1. Send input as the plain string say hi
  2. Observed: litellm.BadRequestError: ChatgptException - {"detail":"Input must be a list"}

After (3ee1ccf)

case 1, strict json_schema

  1. Send the identical request
  2. Observed: {"city": "Paris", "country": "France"}, parses against the schema

case 2, string input

  1. Send the identical request
  2. Observed: accepted, status=completed

Unit tests, and confirmation that they fail without the production change:

  1. uv run pytest tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py -q gives 24 passed
  2. Reverting only litellm/llms/chatgpt/responses/transformation.py and rerunning gives 4 failed, 20 passed, the 4 being the new cases across both parametrised models

Type

🐛 Bug Fix

Caveats (if any)

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

Two request-side defects stop chatgpt/* serving structured output or a string
input.

The allowlist at the end of transform_responses_api_request omits "text". The
chat-to-responses bridge translates response_format into text.format, so the
allowlist discards strict schemas silently and the backend answers with prose
instead of JSON. The backend does honour text.format when it receives it.

Separately the backend rejects a bare string input with
{"detail": "Input must be a list"}, while the Responses API itself accepts
either shape, so the string needs wrapping before it is sent.

Both are request-shaping only, so neither depends on the streaming work in
BerriAI#31332 or BerriAI#34095, though those two block the same call and have to be in place
to observe this one end to end.

The "text" case was reported in BerriAI#24356 and closed by the stale bot without a
fix.

Signed-off-by: Alexander Chernov <alexander@chernov.it>
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR updates the ChatGPT Responses adapter to preserve structured-output configuration and normalize plain string inputs for the Codex backend

  • Wraps string input in a user-message list before parent request transformation
  • Allows the normalized text field through the provider request allowlist
  • Adds regression coverage for structured output and string and list inputs

Confidence Score: 4/5

The PR appears safe to merge after addressing the non-blocking source-convention violation

The request-shaping changes have focused regression and live-backend evidence, with only parameter-rebinding and unnecessary-comment cleanup remaining

Files Needing Attention: litellm/llms/chatgpt/responses/transformation.py

Important Files Changed

Filename Overview
litellm/llms/chatgpt/responses/transformation.py Corrects request shaping, but the new implementation violates repository conventions on parameter rebinding and routine explanatory comments
tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py Adds isolated regression tests covering text preservation, string normalization, and unchanged list behavior without network calls

Reviews (1): Last reviewed commit: "fix(chatgpt): keep text and accept strin..." | Re-trigger Greptile

Comment on lines +69 to +72
# The Responses API accepts a string or a list, but this backend
# rejects a string with {"detail": "Input must be a list"}.
if isinstance(input, str):
input = [{"role": "user", "content": input}]

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.

P2 Avoid comments and parameter rebinding

These routine comments and the input reassignment violate repository source conventions, adding maintenance overhead and potentially failing lint-budget checks

Context Used: CLAUDE.md (source)

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!

The LIT002 budget counts mutable-collection construction, and wrapping the
string input builds a list and a dict, which put the total two over its
ceiling. Both constructions are the wire payload this backend requires, so the
list cannot be replaced with an immutable equivalent; annotate the line with a
reason instead, matching the convention used elsewhere in the tree.

LIT005 freezes reasonless suppressions at zero, so the reason is required
rather than decorative.

Signed-off-by: Alexander Chernov <alexander@chernov.it>
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…rameter

Greptile flagged the parameter rebinding and the explanatory comments against
the repo conventions in CLAUDE.md, which bans rebinding a function parameter
(LIT011) and keeps comments to suppressions, TODOs and genuinely complex logic

Bind a Final local for the coerced input rather than reassigning `input`, and
drop both prose comments; the rationale lives in the commit and the PR body
rather than duplicated at the call site. The mutable-ok suppression stays,
since it is the allowed kind and the list is still constructed

Behaviour is unchanged: 24 tests pass, and the same 4 fail with only the
provider file reverted to base

Signed-off-by: Alexander Chernov <alexander@chernov.it>
@alekc

alekc commented Aug 24, 2026

Copy link
Copy Markdown
Author

code-quality fails on test-unit.yml timing invariants that reproduce on an unmodified base branch; this PR touches no workflow files

@codspeed-hq

codspeed-hq Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing alekc:fix/chatgpt-preserve-text-and-string-input (3ee1ccf) with litellm_internal_staging (d447be1)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (f005afa) during the generation of this report, so d447be1 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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]: chatgpt/* drops response_format and rejects a string input, because allowed_keys omits "text"

1 participant