Skip to content

fix(openai): avoid mutating raw tool schemas for Responses#6305

Merged
longcw merged 1 commit into
livekit:mainfrom
VectorPeak:codex/openai-responses-raw-schema-copy
Jul 3, 2026
Merged

fix(openai): avoid mutating raw tool schemas for Responses#6305
longcw merged 1 commit into
livekit:mainfrom
VectorPeak:codex/openai-responses-raw-schema-copy

Conversation

@VectorPeak

Copy link
Copy Markdown
Contributor

This PR prevents the OpenAI Responses provider-format conversion from mutating a raw tool schema object that may be reused by later provider conversions.

What Problem This Solves

to_responses_fnc_ctx() currently handles RawFunctionTool by assigning the stored schema directly:

schema = tool.info.raw_schema
schema["type"] = "function"
schemas.append(schema)

schema is the same dict object as tool.info.raw_schema, so adding the Responses-specific "type": "function" field mutates the tool's stored raw schema in place.

This is order-dependent. If the same raw tool is first converted for openai.responses and then reused for regular OpenAI Chat Completions, the later Chat Completions conversion sees a raw schema that has already been polluted with a provider-format field.

Responses and Chat Completions use different tool payload shells:

# Responses-style function tool
{
    "type": "function",
    "name": "...",
    "parameters": {...},
}

# Chat Completions-style tool
{
    "type": "function",
    "function": {
        "name": "...",
        "parameters": {...},
    },
}

The bug is not that Responses needs "type": "function"; the bug is that the formatter writes that field onto the persistent raw schema instead of a temporary payload copy.

Changes

Copy the raw schema before adding the Responses-specific type field:

schema = {**tool.info.raw_schema, "type": "function"}

This keeps the generated Responses payload unchanged while preserving RawFunctionTool.info.raw_schema for later conversions, caller inspection, and reuse.

This PR only changes raw function tool handling in the OpenAI Responses provider-format path. It does not change normal FunctionTool schema generation, provider tools, chat message conversion, or OpenAI Chat Completions formatting.

Evidence

A focused regression test now covers a single raw function tool reused across provider formats:

ctx = ToolContext([raw_tool_1])
raw_schema = raw_tool_1.info.raw_schema.copy()

responses_tools = ctx.parse_function_tools("openai.responses")

assert responses_tools[0]["type"] == "function"
assert raw_tool_1.info.raw_schema == raw_schema
assert "type" not in raw_tool_1.info.raw_schema

chat_tools = ctx.parse_function_tools("openai")
assert chat_tools[0] == {
    "type": "function",
    "function": raw_tool_1.info.raw_schema,
}
assert "type" not in chat_tools[0]["function"]

Before this fix, the Responses conversion added "type" to raw_tool_1.info.raw_schema. After this fix, the Responses payload still includes "type": "function", but the raw schema object remains unchanged.

Possible call chain / impact

User-defined @function_tool(raw_schema=...)
  -> ToolContext([...])
  -> OpenAI Responses request
  -> ToolContext.parse_function_tools("openai.responses")
  -> openai.to_responses_fnc_ctx()
  -> schema = tool.info.raw_schema
  -> schema["type"] = "function"
  -> same RawFunctionTool reused later
  -> ToolContext.parse_function_tools("openai")
  -> openai.to_fnc_ctx()
  -> {"type": "function", "function": polluted_raw_schema}

The affected surface is raw tool schema conversion for OpenAI Responses.

Sibling surfaces checked:

  • openai.to_fnc_ctx() wraps raw schemas for Chat Completions but does not write provider-specific keys into the stored raw schema.
  • FunctionTool conversion builds fresh schemas through helper functions and is not affected.
  • Provider tools are appended through to_dict() and are outside this raw-schema mutation path.

Validation

uv run --no-sync pytest tests/test_tools.py::TestToolContext::test_openai_responses_raw_schema_does_not_mutate_tool_schema -q
uv run --no-sync ruff check livekit-agents/livekit/agents/llm/_provider_format/openai.py tests/test_tools.py
uv run --no-sync ruff format --check livekit-agents/livekit/agents/llm/_provider_format/openai.py tests/test_tools.py
git diff --check

All commands passed locally. A broader uv run --no-sync pytest --unit tests/test_tools.py -k responses_raw_schema -q attempt was blocked during unrelated unit-test collection because this local Windows environment does not have several provider test dependencies installed, such as livekit.plugins, langchain_core, and google.genai.

@CLAassistant

CLAassistant commented Jul 3, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@VectorPeak VectorPeak marked this pull request as ready for review July 3, 2026 08:22
@VectorPeak VectorPeak requested a review from a team as a code owner July 3, 2026 08:22

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

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

looks good to me!

@longcw longcw merged commit 95538f2 into livekit:main Jul 3, 2026
18 checks passed
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.

3 participants