refactor(types): name the LLMContext/OpenAI boundary with explicit cast helpers

LLMContext's NotGiven, LLMContextToolChoice, and LLMStandardMessage are
currently aliased to their OpenAI equivalents, so passing values
between the two sides type-checks implicitly. That works today but
obscures the fact that these are meant to be conceptually distinct —
if LLMContext ever diverges from OpenAI's types, every implicit
crossing would silently break.

Introduce two module-private cast helpers in open_ai_adapter.py:

- _openai_from_llm_context_tool_choice(tool_choice)
- _openai_from_llm_standard_message(message)

Both are typed no-ops today (implemented with typing.cast) but each
carries a docstring explaining why the cast is present, and every
boundary crossing now routes through a named function. Future readers
(and future greps) can find the crossings; a later divergence becomes
a mechanical find-and-update rather than hunting through adapter code.

No behavior change, no pyright error delta.
This commit is contained in:
Paul Kompfner
2026-04-24 10:10:03 -04:00
parent d0495eeef6
commit c113cacd59

View File

@@ -6,7 +6,7 @@
"""OpenAI LLM adapter for Pipecat."""
from typing import Any, TypedDict, TypeGuard, TypeVar
from typing import Any, TypedDict, TypeGuard, TypeVar, cast
from openai._types import NotGiven as OpenAINotGiven
from openai.types.chat import (
@@ -22,12 +22,39 @@ from pipecat.processors.aggregators.llm_context import (
LLMContextMessage,
LLMContextToolChoice,
LLMSpecificMessage,
LLMStandardMessage,
NotGiven,
)
_T = TypeVar("_T")
def _openai_from_llm_context_tool_choice(
tool_choice: LLMContextToolChoice | NotGiven,
) -> ChatCompletionToolChoiceOptionParam | OpenAINotGiven:
"""Reinterpret an LLMContext ``tool_choice`` as OpenAI's type.
The underlying types are currently aliased — ``LLMContextToolChoice`` is
``ChatCompletionToolChoiceOptionParam`` and LLMContext's ``NotGiven`` is
OpenAI's — so this is a typed no-op today. It's kept as a named boundary
so that if the LLMContext side ever diverges from OpenAI's types, every
crossing is visible and easy to update.
"""
return cast("ChatCompletionToolChoiceOptionParam | OpenAINotGiven", tool_choice)
def _openai_from_llm_standard_message(
message: LLMStandardMessage,
) -> ChatCompletionMessageParam:
"""Reinterpret an LLMContext standard message as OpenAI's type.
Same rationale as :func:`_openai_from_llm_context_tool_choice`: the
aliased types make this a no-op today, but the boundary is preserved
for future divergence.
"""
return cast("ChatCompletionMessageParam", message)
def is_given(value: _T | OpenAINotGiven) -> TypeGuard[_T]:
"""Check whether a value was explicitly provided.
@@ -117,7 +144,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
"messages": messages,
# NOTE; LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
"tools": self.from_standard_tools(context.tools),
"tool_choice": context.tool_choice,
"tool_choice": _openai_from_llm_context_tool_choice(context.tool_choice),
}
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> list[ChatCompletionToolParam]:
@@ -166,7 +193,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
result.append(message.message)
else:
# Standard message, pass through unchanged
result.append(message)
result.append(_openai_from_llm_standard_message(message))
if convert_developer_to_user:
for msg in result:
@@ -178,5 +205,4 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
def _from_standard_tool_choice(
self, tool_choice: LLMContextToolChoice | NotGiven
) -> ChatCompletionToolChoiceOptionParam | OpenAINotGiven:
# Just a pass-through: tool_choice is already the right type
return tool_choice
return _openai_from_llm_context_tool_choice(tool_choice)