Skip to content

fix(xai): reject FileSearch plus collections_search or file_search - #6998

Merged
longcw merged 3 commits into
livekit:mainfrom
rayanoai:xai-file-search-reserved-name
Aug 28, 2026
Merged

fix(xai): reject FileSearch plus collections_search or file_search#6998
longcw merged 3 commits into
livekit:mainfrom
rayanoai:xai-file-search-reserved-name

Conversation

@mehrdroid

Copy link
Copy Markdown
Contributor

Summary

  • Fail fast in xAI realtime session.update when FileSearch is mixed with a client function named collections_search or file_search (including names nested in a Toolset).
  • Reported to support@x.ai (no public ticket). xAI docs already alias native collections_search to OpenAI-compat file_search. Mixing FileSearch with a client function of either name makes grok-voice-latest return server_error / internal_error / param=null on the first response.create; hello still works; the socket stays up.

Test plan

  • Unit tests for both reserved names: plain @function_tool, name= override, and raw_schema
  • Nested Toolset: FileSearch on top + reserved name inside the set
  • FileSearch alone, reserved name without FileSearch, and other function names still pass
  • uv run pytest tests/test_realtime/test_xai_realtime_model.py --unit

Made with Cursor

@mehrdroid
mehrdroid requested a review from a team as a code owner August 26, 2026 21:46
@CLAassistant

CLAassistant commented Aug 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ Devin Review: No Issues Found

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

Open in Devin Review

@mehrdroid
mehrdroid force-pushed the xai-file-search-reserved-name branch from 5b426c1 to a4203f6 Compare August 26, 2026 21:49
_FILE_SEARCH_RESERVED_FUNCTION_NAMES = frozenset({"collections_search", "file_search"})


def _raise_if_file_search_reserved_name_conflict(tools: Sequence[Tool | Toolset]) -> None:

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.

the input is already a flat tool list, flat = ToolContext(tools).flatten() is redudent?

    def _create_tools_update_event(self, tools: list[llm.Tool]) -> dict[str, Any]:
        _raise_if_file_search_reserved_name_conflict(tools)

@mehrdroid mehrdroid Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tools list at _create_tools_update_event is already flat, so ToolContext.flatten() was redundant. Dropped it. The helper still uses get_fnc_tool_names so a reserved function nested in a Toolset is caught.

return result


_FILE_SEARCH_RESERVED_FUNCTION_NAMES = frozenset({"collections_search", "file_search"})

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.

could you include the reserved function names for other tools, e.g. https://docs.x.ai/developers/tools/x-search, https://docs.x.ai/developers/tools/collections-search, https://docs.x.ai/developers/tools/web-search

Measured against wss://api.x.ai/v1/realtime (grok-voice-latest), one session per row:
session.update with the tool list the plugin builds, then response.create.

xAI tool client function name first response.create reserved?
WebSearch web_search server_error / internal_error yes
WebSearch browse_page server_error / internal_error yes
WebSearch view_image completed no
WebSearch harmless_control completed no
XSearch x_keyword_search server_error / internal_error yes
XSearch x_semantic_search server_error / internal_error yes
XSearch x_user_search server_error / internal_error yes
XSearch x_thread_fetch server_error / internal_error yes
XSearch x_search completed no
XSearch harmless_control completed no
FileSearch collections_search server_error / internal_error yes
FileSearch file_search completed no
FileSearch view_document completed no
FileSearch harmless_control completed no

A name is reserved only by the tool thlly:

tools enabled client function name result
none collections_search completed
none web_search completed
none browse_page completed
WebSearch collections_search completed
FileSearch web_search completed

So the reserved set is seven names, keyed to the tool that is actually enabled:

xAI tool reserved function names
WebSearch web_search, browse_page
XSearch x_keyword_search, x_semantic_search, x_user_search, x_thread_fetch
FileSearch collections_search

@mehrdroid mehrdroid Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to the names you measured, keyed to the tool that’s actually enabled:

WebSearch: web_search, browse_page
XSearch: x_keyword_search, x_semantic_search, x_user_search, x_thread_fetch
FileSearch: collections_search
Also reserving file_search on FileSearch as the OpenAI-compatible alias, even though your session completed.

Mixing an xAI provider tool with a client function of a name that tool
already uses makes grok-voice-latest return server_error on the first
response.create. Fail before session.update; reserved names are keyed
to the tool that is actually enabled.
@mehrdroid
mehrdroid force-pushed the xai-file-search-reserved-name branch from a4203f6 to 7fa3c27 Compare August 27, 2026 05:30
devin-ai-integration[bot]

This comment was marked as resolved.

A client function of that name plus FileSearch still 500s grok-voice-latest
on the first response.create.
Comment on lines +89 to +107
def _raise_if_xai_tool_reserved_name_conflict(tools: Sequence[Tool | Toolset]) -> None:
reserved: set[str] = set()
owners: dict[str, str] = {}
for tool in tools:
for tool_cls, names in _XAI_TOOL_RESERVED_FUNCTION_NAMES.items():
if isinstance(tool, tool_cls):
reserved |= names
for name in names:
owners[name] = tool_cls.__name__
if not reserved:
return
for name in get_fnc_tool_names(tools):
if name in reserved:
raise ValueError(
f"xAI {owners[name]} already uses the function name {name!r}. "
"Rename or remove the function; mixing the provider tool with a "
"client function of a reserved name makes grok-voice-latest "
"return server_error/internal_error on the first response.create."
)

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.

A ClassVar on each class removes the table, the union, and the owners dict:

class XAITool(ProviderTool):
    """Base class for xAI server-side provider tools."""

    # function names the server answers to once this tool is enabled. Measured against
    # grok-voice-latest: a client function of the same name makes the first
    # response.create return server_error.
    _reserved_function_names: ClassVar[frozenset[str]] = frozenset()

    @abstractmethod
    def to_dict(self) -> dict[str, Any]: ...
class WebSearch(XAITool):
    _reserved_function_names: ClassVar[frozenset[str]] = frozenset({"web_search", "browse_page"})


class XSearch(XAITool):
    _reserved_function_names: ClassVar[frozenset[str]] = frozenset(
        {"x_keyword_search", "x_semantic_search", "x_user_search", "x_thread_fetch"}
    )


class FileSearch(XAITool):
    _reserved_function_names: ClassVar[frozenset[str]] = frozenset(
        {"collections_search", "file_search"}
    )

then _raise_if_xai_tool_reserved_name_conflict could be rewrite as

Suggested change
def _raise_if_xai_tool_reserved_name_conflict(tools: Sequence[Tool | Toolset]) -> None:
reserved: set[str] = set()
owners: dict[str, str] = {}
for tool in tools:
for tool_cls, names in _XAI_TOOL_RESERVED_FUNCTION_NAMES.items():
if isinstance(tool, tool_cls):
reserved |= names
for name in names:
owners[name] = tool_cls.__name__
if not reserved:
return
for name in get_fnc_tool_names(tools):
if name in reserved:
raise ValueError(
f"xAI {owners[name]} already uses the function name {name!r}. "
"Rename or remove the function; mixing the provider tool with a "
"client function of a reserved name makes grok-voice-latest "
"return server_error/internal_error on the first response.create."
)
def _raise_if_xai_tool_reserved_name_conflict(tools: Sequence[Tool]) -> None:
"""Reject a client function whose name an enabled xAI provider tool already answers to."""
fnc_names = {
tool.info.name for tool in tools if isinstance(tool, (FunctionTool, RawFunctionTool))
}
for tool in tools:
if not isinstance(tool, XAITool):
continue
if conflicts := sorted(tool._reserved_function_names & fnc_names):
names = ", ".join(repr(name) for name in conflicts)
raise ValueError(
f"xAI {type(tool).__name__} already uses the function name(s) {names}. "
"Rename or remove them; a client function that shadows a provider tool's "
"own name makes grok-voice-latest return server_error/internal_error on "
"the first response.create."
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — reserved names live on each tool as a ClassVar, and the helper intersects those with client function names on the already-flat list.

A ClassVar on WebSearch, XSearch, and FileSearch replaces the side table so
the session.update check intersects enabled tools with client function names.
@longcw
longcw merged commit ef114e7 into livekit:main Aug 28, 2026
17 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