Skip to content

Tolerate malformed tool calls in Python SDK chat parsing (fixes #864)#872

Open
justinchuby wants to merge 2 commits into
microsoft:mainfrom
justinchuby:fix/tolerant-tool-call-parsing
Open

Tolerate malformed tool calls in Python SDK chat parsing (fixes #864)#872
justinchuby wants to merge 2 commits into
microsoft:mainfrom
justinchuby:fix/tolerant-tool-call-parsing

Conversation

@justinchuby

Copy link
Copy Markdown
Contributor

Summary

Fixes #864.

When a loaded model emits a malformed tool_call (e.g. a second call with a missing function.name and arguments == "null" — exactly what smollm3-3b produced), the Python SDK crashed with a raw pydantic ValidationError instead of the FoundryLocalException its docstrings promise. A single bad tool call aborted the whole application with an internal traceback that gave no hint about the real cause (the model output).

The unguarded validation existed in both the non-streaming (ChatCompletion.model_validate_json) and streaming (ChatCompletionChunk.model_validate) paths.

Changes

Adds two small, pure helpers used by both paths:

  • _tool_call_is_valid(tool_call) — a function call must have a non-empty function.name; a custom call must have a custom object.
  • _drop_malformed_tool_calls(payload) — removes only the malformed tool-call entries in place (logging a warning per drop), so the rest of an otherwise-usable response still parses. Handles both message (non-streaming) and delta (streaming) shapes.

Validation is now wrapped so that, if parsing still fails, the raw payload is surfaced via a clear, catchable FoundryLocalException instead of an internal pydantic traceback (as the docstrings already promise).

Behavior for the reported case:

  • The valid first tool call is preserved; the malformed one is dropped with a warning → the response parses successfully.
  • If every tool call is malformed, tool_calls is removed and the message still parses.
  • Valid function and custom tool calls are never touched.

Applied to both sdk/python (published foundry-local-sdk, where the issue's traceback points) and sdk_v2 (2.0.0.dev0), each covering the non-streaming and streaming paths.

Tests

Adds sdk/python/test/openai/test_chat_client_tool_calls.py — 8 unit tests (no native binary / loaded model required) covering _tool_call_is_valid, dropping only malformed entries, the all-malformed case, valid responses left untouched, and the streaming delta shape. All pass locally.

The exact model-free repro from the issue now parses cleanly and returns the single valid get_weather tool call.

Small models routinely emit an invalid extra tool call (e.g. missing
`function.name` with `arguments == "null"`). `ChatClient.complete_chat`
and the streaming path validated the raw response with an unguarded
`ChatCompletion.model_validate_json` / `ChatCompletionChunk.model_validate`,
so a single bad tool call aborted the whole application with an opaque
pydantic `ValidationError` — even though the docstrings promise
`FoundryLocalException` on failure.

This adds two helpers, `_tool_call_is_valid` and
`_drop_malformed_tool_calls`, that drop only the malformed tool-call
entries (logging a warning for each) so the rest of an otherwise-usable
response still parses. Valid `function` and `custom` tool calls are
preserved. If validation still fails, the raw payload is now wrapped in a
clear, catchable `FoundryLocalException` instead of surfacing an internal
pydantic traceback.

Applied to both the published SDK (`sdk/python`) and `sdk_v2`, covering the
non-streaming and streaming paths, plus unit tests for the sanitizer.

Fixes microsoft#864

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 12, 2026 03:17
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

@justinchuby is attempting to deploy a commit to the MSFT-AIP Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI 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.

Pull request overview

Adds tolerant malformed tool-call handling across both Python SDK generations.

Changes:

  • Filters malformed function/custom tool calls.
  • Wraps response validation failures in FoundryLocalException.
  • Adds legacy SDK unit coverage.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
sdk/python/src/openai/chat_client.py Adds sanitization and wrapped parsing.
sdk/python/test/openai/test_chat_client_tool_calls.py Tests sanitization helpers.
sdk_v2/python/src/foundry_local_sdk/openai/chat_client.py Mirrors parsing changes in SDK v2.
Comment thread sdk/python/src/openai/chat_client.py Outdated
Comment on lines +36 to +39
if tool_call.get("type") == "custom" or "custom" in tool_call:
return isinstance(tool_call.get("custom"), dict)
function = tool_call.get("function")
return isinstance(function, dict) and bool(function.get("name"))
Comment on lines +34 to +37
if tool_call.get("type") == "custom" or "custom" in tool_call:
return isinstance(tool_call.get("custom"), dict)
function = tool_call.get("function")
return isinstance(function, dict) and bool(function.get("name"))
Comment thread sdk/python/src/openai/chat_client.py Outdated
Comment on lines +304 to +308
_drop_malformed_tool_calls(raw)
try:
chunk_queue.put(ChatCompletionChunk.model_validate(raw))
except ValidationError as exc:
raise FoundryLocalException(
Comment on lines +348 to +352
_drop_malformed_tool_calls(raw)
try:
yield ChatCompletionChunk.model_validate(raw)
except ValidationError as exc:
raise FoundryLocalException(
f"Foundry Local returned a non-JSON chat completion response: {response_json!r}"
) from exc

_drop_malformed_tool_calls(payload)
…d, v2 tests

- _tool_call_is_valid now dispatches on the explicit "type" discriminator
  instead of the presence of a "custom" key, so a valid function call carrying
  an extra "custom" key is no longer dropped; a missing/unknown discriminator
  is treated as malformed.
- Streaming chunk handlers (both SDKs) now wrap JSON decoding, normalization
  and model validation in one try, so invalid JSON / non-object payloads /
  malformed shapes surface as a catchable FoundryLocalException instead of
  JSONDecodeError / AttributeError.
- Add sdk_v2 unit tests mirroring the legacy suite plus discriminator cases,
  and extend the legacy suite with the same cases, so the two implementations
  cannot silently diverge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants