This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 88fab2b39 [api][python][integrations] Share the native output model
helper across chat model connections (#1277)
88fab2b39 is described below
commit 88fab2b395da7d7150c1c4969ccbf0d07c1995ab
Author: Weiqing Yang <[email protected]>
AuthorDate: Mon Oct 19 19:32:42 2026 -0700
[api][python][integrations] Share the native output model helper across
chat model connections (#1277)
Generated-by: GitHub Copilot CLI 1.0.98 (Claude Opus 5.5)
---
python/flink_agents/api/agents/types.py | 25 ++++++++++++++++
.../chat_models/anthropic/anthropic_chat_model.py | 27 +++---------------
.../chat_models/azure/azure_openai_chat_model.py | 28 ++++--------------
.../chat_models/dashscope_chat_model.py | 31 ++++++--------------
.../integrations/chat_models/ollama_chat_model.py | 32 ++++++---------------
.../chat_models/openai/openai_chat_model.py | 27 +++---------------
.../chat_models/vllm/vllm_chat_model.py | 5 ++--
.../chat_models/watsonx/watsonx_chat_model.py | 33 ++++++----------------
8 files changed, 66 insertions(+), 142 deletions(-)
diff --git a/python/flink_agents/api/agents/types.py
b/python/flink_agents/api/agents/types.py
index cc1528356..1f8e9a29c 100644
--- a/python/flink_agents/api/agents/types.py
+++ b/python/flink_agents/api/agents/types.py
@@ -68,6 +68,31 @@ class OutputSchema(BaseModel):
return self
+def native_output_model(output_schema: OutputSchema | None) -> type[BaseModel]
| None:
+ """The Pydantic model an output schema would be sent natively as, or
``None``.
+
+ Kept apart from ``render_output_schema`` because a connection makes two
checks
+ before rendering: whether native structured output can apply, and whether
the
+ schema conflicts with a caller-supplied response format. Neither check
renders
+ the schema. Rendering raises on a schema it cannot express, so the check
would
+ report a render failure instead of its own answer.
+
+ Args:
+ output_schema: The output schema requested for the response, if any.
+
+ Returns:
+ The ``BaseModel`` subclass the schema holds. ``None`` covers both no
schema at
+ all and a ``RowTypeInfo``, which has no native translation and keeps
the
+ prompt-engineering fallback.
+ """
+ model = (
+ output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
+ )
+ if not (isinstance(model, type) and issubclass(model, BaseModel)):
+ return None
+ return model
+
+
def render_output_schema(
model: type[BaseModel], render: Callable[[type[BaseModel]], dict[str, Any]]
) -> dict[str, Any]:
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
index 7264d5a3c..417dfc414 100644
---
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
+++
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
@@ -24,12 +24,13 @@ from typing import Any, Dict, List, Mapping, Sequence
from anthropic import Anthropic, transform_schema
from anthropic._types import NOT_GIVEN
from anthropic.types import MessageParam, TextBlockParam, ToolParam
-from pydantic import BaseModel, Field, PrivateAttr
+from pydantic import Field, PrivateAttr
from typing_extensions import override
from flink_agents.api.agents.types import (
OutputSchema,
contains_open_object,
+ native_output_model,
render_output_schema,
)
from flink_agents.api.chat_message import (
@@ -371,26 +372,6 @@ def _supports_sampling_params(effective_model: str | None)
-> bool:
return effective_model not in _SAMPLING_UNSUPPORTED_MODELS
-def _native_output_model(output_schema: Any) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation and keeps the prompt-engineering fallback.
-
- Separate from the render below because the feasibility check has to know
whether a
- schema would be sent without rendering it, and rendering raises on a
schema it
- cannot express.
- """
- if output_schema is None:
- return None
- model = (
- output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
- )
- if not (isinstance(model, type) and issubclass(model, BaseModel)):
- return None
- return model
-
-
def _native_output_config(output_schema: Any) -> Dict[str, Any] | None:
"""Build the Anthropic ``output_config`` for a native structured-output
request.
@@ -408,7 +389,7 @@ def _native_output_config(output_schema: Any) -> Dict[str,
Any] | None:
but declares no fields is sent as it is, leaving the provider to accept or
refuse
the document it receives.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return {
@@ -560,7 +541,7 @@ class AnthropicChatModelConnection(BaseChatModelConnection):
admits no undeclared keys, and the caller supplied no
``output_config`` of
its own.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
return (
model is not None
and (model_kwargs is None or "output_config" not in model_kwargs)
diff --git
a/python/flink_agents/integrations/chat_models/azure/azure_openai_chat_model.py
b/python/flink_agents/integrations/chat_models/azure/azure_openai_chat_model.py
index 7820f8135..e9776cbe6 100644
---
a/python/flink_agents/integrations/chat_models/azure/azure_openai_chat_model.py
+++
b/python/flink_agents/integrations/chat_models/azure/azure_openai_chat_model.py
@@ -26,12 +26,13 @@ from openai import NOT_GIVEN, AzureOpenAI
# has existed at this path since the structured-output support in openai
1.66.3 (the
# pinned minimum). A future openai bump that moves it will fail loudly on
import here.
from openai.lib._pydantic import to_strict_json_schema
-from pydantic import BaseModel, Field, PrivateAttr
+from pydantic import Field, PrivateAttr
from typing_extensions import override
from flink_agents.api.agents.types import (
OutputSchema,
contains_open_object,
+ native_output_model,
render_output_schema,
)
from flink_agents.api.chat_message import ChatMessage
@@ -99,25 +100,6 @@ _MIN_STRUCTURED_OUTPUT_API_VERSION = "2024-08-01"
_API_VERSION_DATE_PREFIX = re.compile(r"^\d{4}-\d{2}-\d{2}", re.ASCII)
-def _native_output_model(output_schema: Any) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation and keeps the prompt-engineering fallback.
-
- Separate from the render below because the caller-conflict check needs to
know
- whether a schema will be sent, and under what name, before anything is
rendered.
- """
- if output_schema is None:
- return None
- model = (
- output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
- )
- if not (isinstance(model, type) and issubclass(model, BaseModel)):
- return None
- return model
-
-
def _native_response_format(output_schema: Any) -> Dict[str, Any] | None:
"""Build the ``response_format`` for a native structured-output request.
@@ -131,7 +113,7 @@ def _native_response_format(output_schema: Any) ->
Dict[str, Any] | None:
but declares no fields is sent as it is, leaving the provider to accept or
refuse
the document it receives.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return {
@@ -344,7 +326,7 @@ class
AzureOpenAIChatModelConnection(BaseChatModelConnection):
admits no undeclared keys, and the configured api-version reaches
the
structured-output floor.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
return (
model is not None
and self._api_version_supports_structured_output()
@@ -421,7 +403,7 @@ class
AzureOpenAIChatModelConnection(BaseChatModelConnection):
# INFEASIBLE. Model capability is not asked: a caller that hands this
# connection a schema has already chosen to send it.
if self._can_apply_native_structured_output(output_schema, tools,
raw_kwargs):
- native_model = _native_output_model(output_schema)
+ native_model = native_output_model(output_schema)
# Tested before the schema is rendered. A caller who supplies both
a schema
# and a response_format has a conflict to resolve whatever the
schema turns
# out to render to, and reporting a render failure instead would
describe
diff --git
a/python/flink_agents/integrations/chat_models/dashscope_chat_model.py
b/python/flink_agents/integrations/chat_models/dashscope_chat_model.py
index d5404b309..d5b404ac6 100644
--- a/python/flink_agents/integrations/chat_models/dashscope_chat_model.py
+++ b/python/flink_agents/integrations/chat_models/dashscope_chat_model.py
@@ -21,10 +21,14 @@ import uuid
from typing import Any, Dict, List, Mapping, Sequence
from dashscope import Generation
-from pydantic import BaseModel, Field
+from pydantic import Field
from typing_extensions import override
-from flink_agents.api.agents.types import OutputSchema, render_output_schema
+from flink_agents.api.agents.types import (
+ OutputSchema,
+ native_output_model,
+ render_output_schema,
+)
from flink_agents.api.chat_message import (
ChatMessage,
MessageRole,
@@ -68,23 +72,6 @@ _NATIVE_STRUCTURED_OUTPUT_MODELS = frozenset(
)
-def _native_output_model(
- output_schema: OutputSchema | None,
-) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation and keeps the prompt-engineering fallback.
-
- Separate from the render below because the caller-conflict check needs to
know
- whether a schema will be sent, and under what name, before anything is
rendered.
- """
- schema = getattr(output_schema, "output_schema", None)
- if not (isinstance(schema, type) and issubclass(schema, BaseModel)):
- return None
- return schema
-
-
def _native_response_format(
output_schema: OutputSchema | None,
) -> Dict[str, Any] | None:
@@ -100,7 +87,7 @@ def _native_response_format(
but declares no fields is sent as it is, leaving the provider to accept or
refuse
the document it receives.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return {
@@ -273,7 +260,7 @@ class DashScopeChatModelConnection(BaseChatModelConnection):
bool
``True`` if ``output_schema`` wraps a ``BaseModel`` subclass.
"""
- return _native_output_model(output_schema) is not None
+ return native_output_model(output_schema) is not None
def chat(
self,
@@ -339,7 +326,7 @@ class DashScopeChatModelConnection(BaseChatModelConnection):
# schema turns out to render to, and reporting a render failure
instead
# would describe the wrong problem. The name is read off the model
class,
# so this needs no rendered document.
- native_model = _native_output_model(output_schema)
+ native_model = native_output_model(output_schema)
if "response_format" in kwargs:
msg = (
f"The {native_model.__name__} output schema is sent as "
diff --git a/python/flink_agents/integrations/chat_models/ollama_chat_model.py
b/python/flink_agents/integrations/chat_models/ollama_chat_model.py
index b350e7c48..3b157f7a4 100644
--- a/python/flink_agents/integrations/chat_models/ollama_chat_model.py
+++ b/python/flink_agents/integrations/chat_models/ollama_chat_model.py
@@ -21,10 +21,14 @@ import uuid
from typing import Any, Dict, List, Literal, Mapping, Sequence
from ollama import Client, Image, Message
-from pydantic import BaseModel, Field
+from pydantic import Field
from typing_extensions import override
-from flink_agents.api.agents.types import OutputSchema, render_output_schema
+from flink_agents.api.agents.types import (
+ OutputSchema,
+ native_output_model,
+ render_output_schema,
+)
from flink_agents.api.chat_message import (
Base64Source,
ChatMessage,
@@ -49,26 +53,6 @@ DEFAULT_CONTEXT_WINDOW = 2048
DEFAULT_REQUEST_TIMEOUT = 30.0
-def _native_output_model(output_schema: Any) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation and keeps the prompt-engineering fallback.
-
- Separate from the render below because the feasibility check has to know
whether a
- schema would be sent without rendering it, and rendering raises on a
schema it
- cannot express.
- """
- if output_schema is None:
- return None
- model = (
- output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
- )
- if not (isinstance(model, type) and issubclass(model, BaseModel)):
- return None
- return model
-
-
def _native_format(output_schema: Any) -> Dict[str, Any] | None:
"""Build the Ollama ``format`` payload for a native structured-output
request.
@@ -77,7 +61,7 @@ def _native_format(output_schema: Any) -> Dict[str, Any] |
None:
prompt-engineering fallback. A schema that cannot be rendered raises
through
``render_output_schema``, naming the model, like the other connections.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return render_output_schema(model, lambda m: m.model_json_schema())
@@ -199,7 +183,7 @@ class OllamaChatModelConnection(BaseChatModelConnection):
bool
``True`` if ``output_schema`` wraps a ``BaseModel`` subclass.
"""
- return _native_output_model(output_schema) is not None
+ return native_output_model(output_schema) is not None
def chat(
self,
diff --git
a/python/flink_agents/integrations/chat_models/openai/openai_chat_model.py
b/python/flink_agents/integrations/chat_models/openai/openai_chat_model.py
index e7df77466..aa7214cb3 100644
--- a/python/flink_agents/integrations/chat_models/openai/openai_chat_model.py
+++ b/python/flink_agents/integrations/chat_models/openai/openai_chat_model.py
@@ -26,12 +26,13 @@ from openai import NOT_GIVEN, OpenAI
# has existed at this path since the structured-output support in openai
1.66.3.
# A future openai bump that moves it will fail loudly on import here.
from openai.lib._pydantic import to_strict_json_schema
-from pydantic import BaseModel, Field, PrivateAttr
+from pydantic import Field, PrivateAttr
from typing_extensions import override
from flink_agents.api.agents.types import (
OutputSchema,
contains_open_object,
+ native_output_model,
render_output_schema,
)
from flink_agents.api.chat_message import ChatMessage
@@ -89,26 +90,6 @@ _NATIVE_STRUCTURED_OUTPUT_MODELS = frozenset(
)
-def _native_output_model(output_schema: Any) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation and keeps the prompt-engineering fallback.
-
- Separate from the render below because the feasibility check has to know
whether a
- schema would be sent without rendering it, and rendering raises on a
schema it
- cannot express.
- """
- if output_schema is None:
- return None
- model = (
- output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
- )
- if not (isinstance(model, type) and issubclass(model, BaseModel)):
- return None
- return model
-
-
def _native_response_format(output_schema: Any) -> Dict[str, Any] | None:
"""Build the OpenAI ``response_format`` for a native structured-output
request.
@@ -122,7 +103,7 @@ def _native_response_format(output_schema: Any) ->
Dict[str, Any] | None:
but declares no fields is sent as it is, leaving the provider to accept or
refuse
the document it receives.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return {
@@ -317,7 +298,7 @@ class OpenAIChatModelConnection(BaseChatModelConnection):
``True`` if ``output_schema`` wraps a ``BaseModel`` subclass whose
schema
admits no undeclared keys.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
return model is not None and not contains_open_object(model)
def chat(
diff --git
a/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
b/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
index a427801d1..db5bc464e 100644
--- a/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
+++ b/python/flink_agents/integrations/chat_models/vllm/vllm_chat_model.py
@@ -19,12 +19,11 @@ from typing import Any, List, Mapping
from typing_extensions import override
-from flink_agents.api.agents.types import OutputSchema
+from flink_agents.api.agents.types import OutputSchema, native_output_model
from flink_agents.api.tools.tool import Tool
from flink_agents.integrations.chat_models.openai.openai_chat_model import (
OpenAIChatModelConnection,
OpenAIChatModelSetup,
- _native_output_model,
)
DEFAULT_VLLM_API_BASE_URL = "http://localhost:8000/v1"
@@ -104,7 +103,7 @@ class VLLMChatModelConnection(OpenAIChatModelConnection):
rules, and its guided decoding honours ``additionalProperties``, so a
map
keeps its value type. Only a ``BaseModel`` subclass has a native
translation.
"""
- return _native_output_model(output_schema) is not None
+ return native_output_model(output_schema) is not None
class VLLMChatModelSetup(OpenAIChatModelSetup):
diff --git
a/python/flink_agents/integrations/chat_models/watsonx/watsonx_chat_model.py
b/python/flink_agents/integrations/chat_models/watsonx/watsonx_chat_model.py
index f4cbfb379..1a97cebd7 100644
--- a/python/flink_agents/integrations/chat_models/watsonx/watsonx_chat_model.py
+++ b/python/flink_agents/integrations/chat_models/watsonx/watsonx_chat_model.py
@@ -28,10 +28,14 @@ import httpx
from ibm_watsonx_ai import APIClient, Credentials
from ibm_watsonx_ai.foundation_models import ModelInference
from ibm_watsonx_ai.wml_client_error import ApiRequestFailure
-from pydantic import BaseModel, Field, PrivateAttr
+from pydantic import Field, PrivateAttr
from typing_extensions import override
-from flink_agents.api.agents.types import OutputSchema, render_output_schema
+from flink_agents.api.agents.types import (
+ OutputSchema,
+ native_output_model,
+ render_output_schema,
+)
from flink_agents.api.chat_message import (
ChatMessage,
MessageRole,
@@ -167,25 +171,6 @@ def _convert_to_watsonx_tool_call(tool_call:
ToolCallBlock) -> Dict[str, Any]:
}
-def _native_output_model(
- output_schema: OutputSchema | None,
-) -> type[BaseModel] | None:
- """The model a schema translates natively to, or ``None`` where none
applies.
-
- ``None`` covers both no schema at all and a ``RowTypeInfo``, which has no
native
- translation here and keeps the prompt-engineering fallback.
-
- Separate from the render below because the caller-conflict check needs to
know
- whether a schema will be sent, and under what name, before anything is
rendered.
- """
- model = (
- output_schema.output_schema if isinstance(output_schema, OutputSchema)
else None
- )
- if not (isinstance(model, type) and issubclass(model, BaseModel)):
- return None
- return model
-
-
def _native_response_format(
output_schema: OutputSchema | None,
) -> Dict[str, Any] | None:
@@ -197,7 +182,7 @@ def _native_response_format(
Rendering goes through the shared helper, which raises ``TypeError``
naming the
schema class and the remedy when the model has no JSON Schema.
"""
- model = _native_output_model(output_schema)
+ model = native_output_model(output_schema)
if model is None:
return None
return {
@@ -451,7 +436,7 @@ class WatsonxChatModelConnection(BaseChatModelConnection):
bool
``True`` if ``output_schema`` wraps a ``BaseModel`` subclass.
"""
- return _native_output_model(output_schema) is not None
+ return native_output_model(output_schema) is not None
def chat(
self,
@@ -512,7 +497,7 @@ class WatsonxChatModelConnection(BaseChatModelConnection):
# Feasibility is asked rather than restated, so a caller asking the
same
# question gets the answer this branch acts on.
if self._can_apply_native_structured_output(output_schema, tools,
raw_kwargs):
- native_model = _native_output_model(output_schema)
+ native_model = native_output_model(output_schema)
# A caller reaches the same request field through either channel,
and both
# have already merged into request_params. Only the branch that
sends a
# derived schema may reject the caller's value; every path that
skips it