Skip to content

fix(parsing): handle empty function tool call arguments#3502

Open
sohumt123 wants to merge 1 commit into
openai:mainfrom
sohumt123:fix/empty-function-tool-arguments
Open

fix(parsing): handle empty function tool call arguments#3502
sohumt123 wants to merge 1 commit into
openai:mainfrom
sohumt123:fix/empty-function-tool-arguments

Conversation

@sohumt123

Copy link
Copy Markdown
  • I understand that this repository is auto-generated and my pull request may not be merged

This change is entirely within src/openai/lib/ and tests/lib/, which CONTRIBUTING.md states the generator will never modify ("The generator will never modify the contents of the src/openai/lib/ and examples/ directories."), so it should be safe from being overwritten on the next generation.

Changes being requested

parse_function_tool_arguments() raises when a strict function tool call has empty-string arguments.

Problem

The API returns empty-string arguments (rather than "{}") for strict function tools that take no arguments or whose parameters are all optional. Both copies of parse_function_tool_arguments() pass that empty string straight to the parser:

  • src/openai/lib/_parsing/_completions.pyjson.loads(function.arguments) for strict tools and model_parse_json(input_fn.model, function.arguments) for PydanticFunctionTool
  • src/openai/lib/_parsing/_responses.py — the same two paths with function_call.arguments

json.loads("") raises json.JSONDecodeError: Expecting value: line 1 column 1 (char 0), and model_parse_json(model, "") raises a pydantic ValidationError. This affects chat.completions.parse(), responses.parse(), and the streaming done events, so with chat.completions.stream() the exception leaks out of the stream iterator before get_final_completion() can be reached.

Interestingly, the SDK's own incremental streaming parser already guards this exact case on the delta path:

# src/openai/lib/streaming/chat/_completions.py
if (
    input_tool
    and input_tool.get("function", {}).get("strict")
    and tool_call_snapshot.function.arguments
):
    tool_call_snapshot.function.parsed_arguments = from_json(...)

but the shared parse_function_tool_arguments() helper used by the non-streaming and done/final paths does not, which is what led me to believe the empty-arguments case is real and this omission is an oversight rather than intentional.

Minimal repro on a clean checkout:

from openai.lib._parsing._completions import parse_function_tool_arguments
from openai.types.chat.chat_completion_message_function_tool_call import Function

parse_function_tool_arguments(
    input_tools=[{
        "type": "function",
        "function": {
            "name": "noop",
            "strict": True,
            "parameters": {"type": "object", "properties": {}, "additionalProperties": False},
        },
    }],
    function=Function(name="noop", arguments=""),
)
# json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Fix

In both parse_function_tool_arguments() implementations, normalize empty/whitespace-only arguments to "{}" before parsing:

args = function.arguments  # (function_call.arguments in _responses.py)
if not args or not args.strip():
    args = "{}"

The raw path then returns {} and the PydanticFunctionTool path parses "{}" into the model (valid for zero-param/all-optional strict schemas, which are the only ones for which the API emits empty-string arguments). Non-empty arguments are byte-for-byte unaffected, so there is no regression surface. The normalization is scoped strictly to empty/whitespace strings; if the API ever sent "" for a schema with required fields, the Pydantic path would now surface a clearer ValidationError (missing fields) rather than a JSON-syntax error.

Testing

Added offline, plain-assert unit tests (no network, no mock server, no snapshots):

  • tests/lib/chat/test_completions.py — direct helper (empty / whitespace -> {}, non-empty regression guard, PydanticFunctionTool no-args model -> instance) and an end-to-end parse_chat_completion case that asserts parsed_arguments == {}.
  • tests/lib/responses/test_responses.py — the responses variant of the helper.
  • tests/lib/chat/test_completions_streaming.py — feeds hand-built chunks through ChatCompletionStreamState; pre-fix handle_chunk raised JSONDecodeError, post-fix the tool_calls.function.arguments.done event fires with parsed_arguments == {} and get_final_completion() agrees.
python -m pytest tests/lib/ -q
267 passed

The nine empty-argument assertions fail on a clean checkout and pass with the fix; the full tests/lib/ suite passes, and ruff check / ruff format --check / pyright / mypy are clean on the changed files.

Additional context & links

None — this is a self-discovered bug; I could not find an existing issue or PR describing empty-string arguments to parse_function_tool_arguments.

The API returns empty-string arguments (rather than "{}") for strict
function tools that take no arguments or whose parameters are all
optional. `parse_function_tool_arguments` passed that empty string
straight to `json.loads`/`model_parse_json`, which raised
`json.JSONDecodeError` (or a pydantic `ValidationError`) on the
non-streaming parse and done/final streaming paths.

The SDK's own incremental streaming parser already guards this case on
the delta path, but the shared helpers used by `chat.completions.parse`,
`responses.parse`, and the streaming done events did not, so the
exception leaked out of the stream iterator before `get_final_completion`.

Normalize empty/whitespace-only arguments to "{}" in both the chat and
responses helpers before parsing. Non-empty arguments are unaffected.
@sohumt123 sohumt123 requested a review from a team as a code owner July 14, 2026 06:54
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.

1 participant