Skip to content

Conversation

@omChauhanDev
Copy link

@omChauhanDev omChauhanDev commented Nov 4, 2025

Ultravox RealtimeModel: preserve JSON types for tool parameters (schema-based coercion)

Summary

This PR fixes a bug where Ultravox RealtimeModel emitted tool parameters as strings, breaking MCP and other type-strict tools. We now coerce parameters to the correct JSON types based on each tool’s schema before emitting FunctionCall, including proper handling of union types with null.

  • Fixes: #3713
  • Affects: livekit-plugins-ultravox
  • Impact: Restores compatibility with MCP servers and any type-validated tool schemas

Problem

Ultravox sometimes sends tool parameters as strings (e.g., "10", "true", "[\"low_stock\"]", "null"). We previously forwarded them as-is via json.dumps(event.parameters), causing downstream validation errors:

  • Arrays were strings
  • Numbers were strings
  • Booleans were strings
  • Optional unions with null (e.g., ["integer","null"]) received "null" instead of null

This made Ultravox RealtimeModel incompatible with MCP and other type-strict integrations.


What’s changed

  • Add schema-based coercion to convert stringified values to the correct JSON types:

    • Arrays: parse JSON arrays, recursively coerce items
    • Numbers/integers: coerce from strings (supports 1e3, -inf, etc.)
    • Booleans: coerce "true"/"false", "1"/"0", "yes"/"no", etc.
    • Objects: parse JSON objects, recursively coerce fields
    • Null unions: when schema allows null, map "null"/"None"null (also for anyOf/oneOf/allOf)
    • Fallback: best-effort coercion when schema is missing
  • Wire coercion into Ultravox tool invocation path:

    • Before: json.dumps(event.parameters)
    • After: json.dumps(coerced_params) where coerced_params honors the tool schema

Files changed

  • livekit-plugins/livekit-plugins-ultravox/livekit/plugins/ultravox/utils.py
    • New: coerce_parameters_to_schema(tools_ctx, tool_name, params) utility
    • Includes recursive coercion, union handling, and best-effort fallback
  • livekit-plugins/livekit-plugins-ultravox/livekit/plugins/ultravox/realtime/realtime_model.py
    • Use coerce_parameters_to_schema(...) in _handle_tool_invocation_event(...)

Before vs After

  • Before (tool args forwarded as-is)
    • alert_type: "[\"low_stock\"]"
    • limit: "10"
    • include_valuation: "true"
    • category_id: "null"
  • After (schema-correct types)
    • alert_type: ["low_stock"]
    • limit: 10
    • include_valuation: true
    • category_id: null

This aligns Ultravox behavior with OpenAI Realtime for tool calling.


Screenshots

  • Before: MCP validation error (string types)
image - After: MCP validation passes (correct JSON types) image

Testing

  • Manual:
    • Start the strict MCP server (as in the shared repro)
    • Run Ultravox RealtimeModel with the sample tool call
    • Verify MCP logs show proper types; no “Expected number/array/boolean, received string” errors

Backward compatibility

  • Safe: If Ultravox starts sending correctly typed parameters upstream, coercion is effectively a no-op.
  • The change only affects tool invocation arguments; no changes to other Ultravox message handling.

Release notes

  • Ultravox: Fix tool parameter typing. Parameters now preserve JSON types per schema (arrays, numbers, booleans, null), restoring compatibility with MCP and type-strict tools.

@omChauhanDev omChauhanDev changed the title fix: coerce parameters to tool schema fix: coerce parameters to tool schema Fixes: #3713 Nov 5, 2025
@omChauhanDev omChauhanDev changed the title fix: coerce parameters to tool schema Fixes: #3713 fix: coerce parameters to tool schema Nov 5, 2025
@theomonnom
Copy link
Member

Could Pydantic do that automatically?

@ShafiqDevs
Copy link

Any reviewers working on this? I'm so excited to test this out

@omChauhanDev
Copy link
Author

omChauhanDev commented Nov 7, 2025

@theomonnom
Pydantic could, but it'd require create_model() to dynamically build models from JSON schemas, then .model_validate() then .model_dump() for each call, avoided it since we're on the hot path of every tool invocation so thought that the direct approach would be some what lighter.

But if you prefer via Pydantic, I'm happy to rework it.

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] Ultravox RealtimeModel converts all tool parameters to strings instead of preserving JSON types

3 participants