Skip to content

CompletionUsage validation crashes session when provider returns usage.completion_tokens=None mid-stream (OpenAI-compatible path) #6595

Description

Bug Description

When using the OpenAI-compatible LLM path (livekit.plugins.openai.llm.LLM, which inherits its streaming _run loop from livekit.agents.inference.llm.LLMStream), if the upstream provider sends a final streaming chunk where chunk.usage is a non-null object but one of its fields (e.g. completion_tokens) is null, the SDK crashes.

The code at livekit-agents/livekit/agents/inference/llm.py only guards on if chunk.usage is not None: before constructing:

usage=llm.CompletionUsage(
    completion_tokens=chunk.usage.completion_tokens,
    prompt_tokens=chunk.usage.prompt_tokens,
    total_tokens=chunk.usage.total_tokens,
    ...
)

CompletionUsage.completion_tokens is a required int (no Optional), so a null value raises a pydantic ValidationError, which gets wrapped and re-raised as APIConnectionError(retryable=False). Because it's marked non-retryable, this eventually surfaces as an unrecoverable llm_error that terminates the whole AgentSession — killing an in-progress voice call — even though the actual issue is a malformed/partial usage payload, not a real connection failure.

We're hitting this against an OpenAI-compatible proxy in front of a custom model; the proxy occasionally omits completion_tokens in the usage object on stream completion. This is analogous to #5404, which fixed a similar "usage present but incomplete on final chunk" issue for the Google/Gemini plugin — but the equivalent guard was never added to the shared OpenAI-compatible streaming path.

Expected Behavior

A null/missing field inside chunk.usage should not raise an unhandled, non-retryable exception that ends the session. At minimum, missing numeric usage fields should default to 0 (or the chunk should be treated as retryable) rather than crashing.

Reproduction Steps

  1. Configure livekit.plugins.openai.LLM against an OpenAI-compatible endpoint (e.g. a proxy in front of a non-OpenAI model).
  2. Have that endpoint return a streamed response whose final SSE chunk includes a usage object with completion_tokens: null (rather than omitting usage entirely).
  3. Observe the ValidationError for CompletionUsage, wrapped into APIConnectionError(retryable=False), terminating the session.

Operating System

Linux (GKE)

Models Used

OpenAI-compatible proxy in front of a custom-hosted model

Package Versions

livekit==1.1.13
livekit-agents==1.6.5
livekit-plugins-openai==1.6.5

Proposed Solution

if chunk.usage is not None:
    usage = chunk.usage
    usage_chunk = llm.ChatChunk(
        id=chunk.id,
        usage=llm.CompletionUsage(
            completion_tokens=usage.completion_tokens or 0,
            prompt_tokens=usage.prompt_tokens or 0,
            prompt_cached_tokens=cached_tokens or 0,
            total_tokens=usage.total_tokens or 0,
            service_tier=getattr(chunk, "service_tier", None),
        ),
    )
    self._event_ch.send_nowait(usage_chunk)

Additional Context

Traceback:

Traceback (most recent call last):
  File ".../livekit/agents/inference/llm.py", line 445, in _run
    usage=llm.CompletionUsage(
  File ".../pydantic/main.py", line 250, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 1 validation error for CompletionUsage
completion_tokens
  Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]

livekit.agents._exceptions.APIConnectionError: Connection error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions