feat(standard-tests): validate tool call chunks during streaming (#34707)

As a LangChain user streaming a tool-calling model, I expect each
streamed chunk to expose structured `tool_call_chunk` content blocks so
I can render or process tool calls live, instead of waiting for the
final aggregated message.

This adds `tool_call_streaming` to `ModelProfile` and uses it in the
standard chat-model tool-calling tests. When a model profile opts in,
`test_tool_calling` and `test_tool_calling_async` now validate that at
least one streamed chunk includes a `tool_call_chunk` block via
`content_blocks`, while preserving the existing final-message
validation.

This keeps the contract profile-gated so providers can opt in once their
streaming chunk shape is verified. This PR opts in the providers
verified by smoke testing with straightforward profile coverage: OpenAI,
Anthropic, Fireworks, HuggingFace, OpenRouter, DeepSeek, and xAI. The
generated profile artifacts are refreshed so runtime profiles expose the
new capability flag.

Perplexity Responses also passed the smoke test, but its current profile
data is for the `sonar` family while the Responses smoke path used a
routed model string. That profile strategy is left as follow-up.
MistralAI currently streams `.tool_call_chunks`, but its content-block
translator exposes a complete `tool_call` block instead of
`tool_call_chunk`, so it also stays out of this flag until that
integration is fixed.
This commit is contained in:
Mason Daugherty
2026-06-10 22:29:02 -04:00
committed by GitHub
parent 7cc9d0c84d
commit 43880362d8
19 changed files with 547 additions and 37 deletions

View File

@@ -113,6 +113,12 @@ class ModelProfile(TypedDict, total=False):
tool_choice: bool
"""Whether the model supports [tool choice](https://docs.langchain.com/oss/python/langchain/models#forcing-tool-calls)."""
tool_call_streaming: bool
"""Whether the model returns properly structured `tool_call_chunks` when streaming.
Only meaningful when `tool_calling` is `True`.
"""
# --- Structured output ---
structured_output: bool
"""Whether the model supports native [structured output](https://docs.langchain.com/oss/python/langchain/models#structured-outputs)."""

View File

@@ -126,6 +126,7 @@ def _model_data_to_profile(model_data: dict[str, Any]) -> dict[str, Any]:
"reasoning_output": model_data.get("reasoning"),
"tool_calling": model_data.get("tool_call"),
"tool_choice": model_data.get("tool_choice"),
"tool_call_streaming": model_data.get("tool_call_streaming"),
"structured_output": model_data.get("structured_output"),
"attachment": model_data.get("attachment"),
"temperature": model_data.get("temperature"),

View File

@@ -41,6 +41,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-5-haiku-latest": {
"name": "Claude Haiku 3.5 (latest)",
@@ -66,6 +67,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-5-sonnet-20240620": {
"name": "Claude Sonnet 3.5",
@@ -92,6 +94,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-5-sonnet-20241022": {
"name": "Claude Sonnet 3.5 v2",
@@ -118,6 +121,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-7-sonnet-20250219": {
"name": "Claude Sonnet 3.7",
@@ -144,6 +148,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-haiku-20240307": {
"name": "Claude Haiku 3",
@@ -170,6 +175,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-opus-20240229": {
"name": "Claude Opus 3",
@@ -196,6 +202,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-3-sonnet-20240229": {
"name": "Claude Sonnet 3",
@@ -222,6 +229,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-fable-5": {
"name": "Claude Fable 5",
@@ -247,6 +255,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-haiku-4-5": {
"name": "Claude Haiku 4.5 (latest)",
@@ -272,6 +281,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-haiku-4-5-20251001": {
"name": "Claude Haiku 4.5",
@@ -297,6 +307,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-opus-4-0": {
"name": "Claude Opus 4 (latest)",
@@ -322,6 +333,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-opus-4-1": {
"name": "Claude Opus 4.1 (latest)",
@@ -347,6 +359,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-opus-4-1-20250805": {
"name": "Claude Opus 4.1",
@@ -372,6 +385,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-opus-4-20250514": {
"name": "Claude Opus 4",
@@ -397,6 +411,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-opus-4-5": {
"name": "Claude Opus 4.5 (latest)",
@@ -422,6 +437,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-opus-4-5-20251101": {
"name": "Claude Opus 4.5",
@@ -447,6 +463,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-opus-4-6": {
"name": "Claude Opus 4.6",
@@ -472,6 +489,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-opus-4-7": {
"name": "Claude Opus 4.7",
@@ -497,6 +515,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-opus-4-8": {
"name": "Claude Opus 4.8",
@@ -522,6 +541,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-sonnet-4-0": {
"name": "Claude Sonnet 4 (latest)",
@@ -547,6 +567,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-sonnet-4-20250514": {
"name": "Claude Sonnet 4",
@@ -572,6 +593,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-sonnet-4-5": {
"name": "Claude Sonnet 4.5 (latest)",
@@ -597,6 +619,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
"claude-sonnet-4-5-20250929": {
"name": "Claude Sonnet 4.5",
@@ -622,6 +645,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": False,
"tool_call_streaming": True,
},
"claude-sonnet-4-6": {
"name": "Claude Sonnet 4.6",
@@ -647,5 +671,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"structured_output": True,
"tool_call_streaming": True,
},
}

View File

@@ -6,6 +6,7 @@ pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
structured_output = false
tool_call_streaming = true
[overrides."claude-haiku-4-5"]
structured_output = true

View File

@@ -35,6 +35,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-reasoner": {
"name": "DeepSeek Reasoner",
@@ -55,6 +56,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-v4-flash": {
"name": "DeepSeek V4 Flash",
@@ -76,6 +78,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-v4-pro": {
"name": "DeepSeek V4 Pro",
@@ -97,5 +100,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
}

View File

@@ -0,0 +1,4 @@
provider = "deepseek"
[overrides]
tool_call_streaming = true

View File

@@ -36,6 +36,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/deepseek-v4-pro": {
"name": "DeepSeek V4 Pro",
@@ -57,6 +58,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/glm-5p1": {
"name": "GLM 5.1",
@@ -77,6 +79,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/gpt-oss-120b": {
"name": "GPT OSS 120B",
@@ -97,6 +100,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/gpt-oss-20b": {
"name": "GPT OSS 20B",
@@ -117,6 +121,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/kimi-k2p5": {
"name": "Kimi K2.5",
@@ -137,6 +142,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/kimi-k2p6": {
"name": "Kimi K2.6",
@@ -157,6 +163,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/minimax-m2p5": {
"name": "MiniMax-M2.5",
@@ -177,6 +184,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/minimax-m2p7": {
"name": "MiniMax-M2.7",
@@ -197,6 +205,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/models/qwen3p6-plus": {
"name": "Qwen 3.6 Plus",
@@ -217,6 +226,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/routers/glm-5p1-fast": {
"name": "GLM 5.1 Fast",
@@ -237,6 +247,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/routers/kimi-k2p6-fast": {
"name": "Kimi K2.6 Fast",
@@ -257,6 +268,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"accounts/fireworks/routers/kimi-k2p6-turbo": {
"name": "Kimi K2.6 Turbo",
@@ -277,5 +289,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
}

View File

@@ -0,0 +1,4 @@
provider = "fireworks-ai"
[overrides]
tool_call_streaming = true

View File

@@ -35,6 +35,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"MiniMaxAI/MiniMax-M2.5": {
"name": "MiniMax-M2.5",
@@ -55,6 +56,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"MiniMaxAI/MiniMax-M2.7": {
"name": "MiniMax-M2.7",
@@ -76,6 +78,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3-235B-A22B-Thinking-2507": {
"name": "Qwen3-235B-A22B-Thinking-2507",
@@ -96,6 +99,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
"name": "Qwen3-Coder-480B-A35B-Instruct",
@@ -116,6 +120,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Coder-Next": {
"name": "Qwen3-Coder-Next",
@@ -136,6 +141,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Embedding-4B": {
"name": "Qwen 3 Embedding 4B",
@@ -156,6 +162,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": False,
"attachment": False,
"temperature": False,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Embedding-8B": {
"name": "Qwen 3 Embedding 8B",
@@ -176,6 +183,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": False,
"attachment": False,
"temperature": False,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Next-80B-A3B-Instruct": {
"name": "Qwen3-Next-80B-A3B-Instruct",
@@ -196,6 +204,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3-Next-80B-A3B-Thinking": {
"name": "Qwen3-Next-80B-A3B-Thinking",
@@ -216,6 +225,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"Qwen/Qwen3.5-397B-A17B": {
"name": "Qwen3.5-397B-A17B",
@@ -236,6 +246,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"XiaomiMiMo/MiMo-V2-Flash": {
"name": "MiMo-V2-Flash",
@@ -256,6 +267,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-ai/DeepSeek-R1-0528": {
"name": "DeepSeek-R1-0528",
@@ -276,6 +288,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-ai/DeepSeek-V3.2": {
"name": "DeepSeek-V3.2",
@@ -296,6 +309,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"deepseek-ai/DeepSeek-V4-Pro": {
"name": "DeepSeek V4 Pro",
@@ -317,6 +331,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"moonshotai/Kimi-K2-Instruct": {
"name": "Kimi-K2-Instruct",
@@ -337,6 +352,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"moonshotai/Kimi-K2-Instruct-0905": {
"name": "Kimi-K2-Instruct-0905",
@@ -357,6 +373,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"moonshotai/Kimi-K2-Thinking": {
"name": "Kimi-K2-Thinking",
@@ -377,6 +394,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"moonshotai/Kimi-K2.5": {
"name": "Kimi-K2.5",
@@ -397,6 +415,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"moonshotai/Kimi-K2.6": {
"name": "Kimi-K2.6",
@@ -417,6 +436,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"zai-org/GLM-4.7": {
"name": "GLM-4.7",
@@ -437,6 +457,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"zai-org/GLM-4.7-Flash": {
"name": "GLM-4.7-Flash",
@@ -457,6 +478,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"zai-org/GLM-5": {
"name": "GLM-5",
@@ -477,6 +499,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
"zai-org/GLM-5.1": {
"name": "GLM-5.1",
@@ -497,5 +520,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": True,
"attachment": False,
"temperature": True,
"tool_call_streaming": True,
},
}

View File

@@ -0,0 +1,4 @@
provider = "huggingface"
[overrides]
tool_call_streaming = true

View File

@@ -40,6 +40,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-3.5-turbo": {
"name": "GPT-3.5-turbo",
@@ -66,6 +67,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": False,
"image_tool_message": False,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4": {
"name": "GPT-4",
@@ -92,6 +94,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4-turbo": {
"name": "GPT-4 Turbo",
@@ -118,6 +121,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4.1": {
"name": "GPT-4.1",
@@ -144,6 +148,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4.1-mini": {
"name": "GPT-4.1 mini",
@@ -170,6 +175,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4.1-nano": {
"name": "GPT-4.1 nano",
@@ -196,6 +202,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4o": {
"name": "GPT-4o",
@@ -222,6 +229,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4o-2024-05-13": {
"name": "GPT-4o (2024-05-13)",
@@ -248,6 +256,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4o-2024-08-06": {
"name": "GPT-4o (2024-08-06)",
@@ -274,6 +283,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4o-2024-11-20": {
"name": "GPT-4o (2024-11-20)",
@@ -300,6 +310,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-4o-mini": {
"name": "GPT-4o mini",
@@ -326,6 +337,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5": {
"name": "GPT-5",
@@ -352,6 +364,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5-chat-latest": {
"name": "GPT-5 Chat (latest)",
@@ -378,6 +391,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5-codex": {
"name": "GPT-5-Codex",
@@ -404,6 +418,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5-mini": {
"name": "GPT-5 Mini",
@@ -430,6 +445,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5-nano": {
"name": "GPT-5 Nano",
@@ -456,6 +472,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5-pro": {
"name": "GPT-5 Pro",
@@ -482,6 +499,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.1": {
"name": "GPT-5.1",
@@ -508,6 +526,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.1-chat-latest": {
"name": "GPT-5.1 Chat",
@@ -534,6 +553,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.1-codex": {
"name": "GPT-5.1 Codex",
@@ -560,6 +580,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.1-codex-max": {
"name": "GPT-5.1 Codex Max",
@@ -586,6 +607,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.1-codex-mini": {
"name": "GPT-5.1 Codex mini",
@@ -612,6 +634,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.2": {
"name": "GPT-5.2",
@@ -638,6 +661,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.2-chat-latest": {
"name": "GPT-5.2 Chat",
@@ -664,6 +688,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.2-codex": {
"name": "GPT-5.2 Codex",
@@ -690,6 +715,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.2-pro": {
"name": "GPT-5.2 Pro",
@@ -716,6 +742,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.3-chat-latest": {
"name": "GPT-5.3 Chat (latest)",
@@ -742,6 +769,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.3-codex": {
"name": "GPT-5.3 Codex",
@@ -768,6 +796,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.3-codex-spark": {
"name": "GPT-5.3 Codex Spark",
@@ -794,6 +823,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.4": {
"name": "GPT-5.4",
@@ -820,6 +850,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.4-mini": {
"name": "GPT-5.4 mini",
@@ -846,6 +877,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.4-nano": {
"name": "GPT-5.4 nano",
@@ -872,6 +904,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.4-pro": {
"name": "GPT-5.4 Pro",
@@ -898,6 +931,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.5": {
"name": "GPT-5.5",
@@ -924,6 +958,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-5.5-pro": {
"name": "GPT-5.5 Pro",
@@ -950,6 +985,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-image-1": {
"name": "gpt-image-1",
@@ -975,6 +1011,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-image-1-mini": {
"name": "gpt-image-1-mini",
@@ -1000,6 +1037,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"gpt-image-1.5": {
"name": "gpt-image-1.5",
@@ -1025,6 +1063,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o1": {
"name": "o1",
@@ -1051,6 +1090,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o1-pro": {
"name": "o1-pro",
@@ -1077,6 +1117,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o3": {
"name": "o3",
@@ -1103,6 +1144,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o3-deep-research": {
"name": "o3-deep-research",
@@ -1128,6 +1170,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o3-mini": {
"name": "o3-mini",
@@ -1154,6 +1197,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o3-pro": {
"name": "o3-pro",
@@ -1180,6 +1224,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o4-mini": {
"name": "o4-mini",
@@ -1206,6 +1251,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"o4-mini-deep-research": {
"name": "o4-mini-deep-research",
@@ -1231,6 +1277,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"text-embedding-3-large": {
"name": "text-embedding-3-large",
@@ -1256,6 +1303,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"text-embedding-3-small": {
"name": "text-embedding-3-small",
@@ -1281,6 +1329,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
"text-embedding-ada-002": {
"name": "text-embedding-ada-002",
@@ -1306,5 +1355,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"pdf_tool_message": True,
"image_tool_message": True,
"tool_choice": True,
"tool_call_streaming": True,
},
}

View File

@@ -6,6 +6,7 @@ pdf_inputs = true
pdf_tool_message = true
image_tool_message = true
tool_choice = true
tool_call_streaming = true
[overrides."gpt-3.5-turbo"]
image_url_inputs = false

View File

@@ -0,0 +1,4 @@
provider = "openrouter"
[overrides]
tool_call_streaming = true

View File

@@ -42,9 +42,9 @@ _PROFILES: dict[str, dict[str, Any]] = {
"last_updated": "2025-09-01",
"open_weights": False,
"max_input_tokens": 128000,
"max_output_tokens": 8192,
"max_output_tokens": 32768,
"text_inputs": True,
"image_inputs": True,
"image_inputs": False,
"audio_inputs": False,
"video_inputs": False,
"text_outputs": True,

View File

@@ -1,13 +1 @@
provider = "perplexity"
[overrides."sonar-deep-research"]
max_input_tokens = 128000
max_output_tokens = 8192
image_inputs = true
audio_inputs = false
video_inputs = false
image_outputs = false
audio_outputs = false
video_outputs = false
reasoning_output = true
tool_calling = false

View File

@@ -37,6 +37,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"grok-4.20-0309-reasoning": {
"name": "Grok 4.20 (Reasoning)",
@@ -59,6 +60,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"grok-4.20-multi-agent-0309": {
"name": "Grok 4.20 Multi-Agent",
@@ -81,6 +83,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"grok-4.3": {
"name": "Grok 4.3",
@@ -103,6 +106,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"grok-build-0.1": {
"name": "Grok Build 0.1",
@@ -125,6 +129,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"structured_output": True,
"attachment": True,
"temperature": True,
"tool_call_streaming": True,
},
"grok-imagine-image": {
"name": "Grok Imagine Image",
@@ -146,6 +151,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": False,
"attachment": True,
"temperature": False,
"tool_call_streaming": True,
},
"grok-imagine-image-quality": {
"name": "Grok Imagine Image Quality",
@@ -167,6 +173,7 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": False,
"attachment": True,
"temperature": False,
"tool_call_streaming": True,
},
"grok-imagine-video": {
"name": "Grok Imagine Video",
@@ -188,5 +195,6 @@ _PROFILES: dict[str, dict[str, Any]] = {
"tool_calling": False,
"attachment": True,
"temperature": False,
"tool_call_streaming": True,
},
}

View File

@@ -0,0 +1,4 @@
provider = "xai"
[overrides]
tool_call_streaming = true

View File

@@ -134,6 +134,22 @@ def _validate_tool_call_message(message: BaseMessage) -> None:
assert content_tool_call["id"] is not None
def _validate_tool_call_chunk(chunk: AIMessageChunk) -> bool:
"""Check whether a streaming chunk contains valid `tool_call_chunk` blocks.
Returns:
`True` if at least one `tool_call_chunk` block was found.
"""
found = False
for block in chunk.content_blocks:
if block.get("type") == "tool_call_chunk":
found = True
assert "name" in block, "tool_call_chunk block missing 'name' field"
assert "args" in block, "tool_call_chunk block missing 'args' field"
assert "id" in block, "tool_call_chunk block missing 'id' field"
return found
def _validate_tool_call_message_no_args(message: BaseMessage) -> None:
assert isinstance(message, AIMessage)
assert len(message.tool_calls) == 1
@@ -1666,6 +1682,10 @@ class ChatModelIntegrationTests(ChatModelTests):
Otherwise, in the case that only one tool is bound, ensure that
`tool_choice` supports the string `'any'` to force calling that tool.
If `tool_call_streaming = true` is set in the model's profile
augmentations, individual chunks are also validated to contain
`tool_call_chunk` blocks in `content_blocks`.
"""
if not self.has_tool_calling:
pytest.skip("Test requires tool calling.")
@@ -1680,13 +1700,28 @@ class ChatModelIntegrationTests(ChatModelTests):
result = model_with_tools.invoke(query)
_validate_tool_call_message(result)
tool_call_streaming = (
model.profile.get("tool_call_streaming", False) if model.profile else False
)
# Test stream
full: BaseMessage | None = None
found_tool_call_chunk = False
for chunk in model_with_tools.stream(query):
if tool_call_streaming and isinstance(chunk, AIMessageChunk):
found_tool_call_chunk |= _validate_tool_call_chunk(chunk)
full = chunk if full is None else full + chunk # type: ignore[assignment]
assert isinstance(full, AIMessage)
_validate_tool_call_message(full)
if tool_call_streaming:
assert found_tool_call_chunk, (
"Expected to find 'tool_call_chunk' blocks in content_blocks of at "
"least one chunk during streaming, but none were found. If this "
"model does not support streaming tool calls, set "
"tool_call_streaming=false in the model's profile augmentations."
)
async def test_tool_calling_async(self, model: BaseChatModel) -> None:
"""Test that the model generates tool calls.
@@ -1728,6 +1763,8 @@ class ChatModelIntegrationTests(ChatModelTests):
Otherwise, in the case that only one tool is bound, ensure that
`tool_choice` supports the string `'any'` to force calling that tool.
See `test_tool_calling` for `tool_call_streaming` profile configuration.
"""
if not self.has_tool_calling:
pytest.skip("Test requires tool calling.")
@@ -1742,13 +1779,28 @@ class ChatModelIntegrationTests(ChatModelTests):
result = await model_with_tools.ainvoke(query)
_validate_tool_call_message(result)
tool_call_streaming = (
model.profile.get("tool_call_streaming", False) if model.profile else False
)
# Test astream
full: BaseMessage | None = None
found_tool_call_chunk = False
async for chunk in model_with_tools.astream(query):
if tool_call_streaming and isinstance(chunk, AIMessageChunk):
found_tool_call_chunk |= _validate_tool_call_chunk(chunk)
full = chunk if full is None else full + chunk # type: ignore[assignment]
assert isinstance(full, AIMessage)
_validate_tool_call_message(full)
if tool_call_streaming:
assert found_tool_call_chunk, (
"Expected to find 'tool_call_chunk' blocks in content_blocks of at "
"least one chunk during streaming, but none were found. If this "
"model does not support streaming tool calls, set "
"tool_call_streaming=false in the model's profile augmentations."
)
def test_bind_runnables_as_tools(self, model: BaseChatModel) -> None:
"""Test bind runnables as tools.