Skip to content

Commit 1d140ff

Browse files
committed
fix: include handoffs when deciding to inject JSON
prompt The JSON prompt injection was only triggered when tools list was non-empty, but handoffs are converted to function tools and added separately. This meant that agents using only handoffs with output_schema would not get the prompt injection even when enable_structured_output_with_tools=True, causing Gemini to error with 'Function calling with response mime type application/json is unsupported.' Changes: - Combine tools and handoffs before checking if JSON prompt should be injected - Add test case for handoffs-only scenario - Update inline comment to clarify why handoffs must be included This ensures the opt-in flag works correctly for multi-agent scenarios where an agent might use handoffs without regular tools.
1 parent cfcd777 commit 1d140ff

File tree

10 files changed

+3945
-3930
lines changed

10 files changed

+3945
-3930
lines changed

src/agents/extensions/models/litellm_model.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,12 @@ async def _fetch_response(
292292

293293
# Check if we need to inject JSON output prompt for models that don't support
294294
# tools + structured output simultaneously (like Gemini)
295+
# Note: handoffs are converted to function tools, so we need to include them in the check
296+
tools_and_handoffs = list(tools) if tools else []
297+
if handoffs:
298+
tools_and_handoffs.extend(handoffs)
295299
inject_json_prompt = should_inject_json_prompt(
296-
output_schema, tools, self.enable_structured_output_with_tools
300+
output_schema, tools_and_handoffs, self.enable_structured_output_with_tools
297301
)
298302
if inject_json_prompt and output_schema:
299303
json_prompt = get_json_output_prompt(output_schema)

src/agents/models/interface.py

Lines changed: 125 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,125 @@
1-
from __future__ import annotations
2-
3-
import abc
4-
import enum
5-
from collections.abc import AsyncIterator
6-
from typing import TYPE_CHECKING
7-
8-
from openai.types.responses.response_prompt_param import ResponsePromptParam
9-
10-
from ..agent_output import AgentOutputSchemaBase
11-
from ..handoffs import Handoff
12-
from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent
13-
from ..tool import Tool
14-
15-
if TYPE_CHECKING:
16-
from ..model_settings import ModelSettings
17-
18-
19-
class ModelTracing(enum.Enum):
20-
DISABLED = 0
21-
"""Tracing is disabled entirely."""
22-
23-
ENABLED = 1
24-
"""Tracing is enabled, and all data is included."""
25-
26-
ENABLED_WITHOUT_DATA = 2
27-
"""Tracing is enabled, but inputs/outputs are not included."""
28-
29-
def is_disabled(self) -> bool:
30-
return self == ModelTracing.DISABLED
31-
32-
def include_data(self) -> bool:
33-
return self == ModelTracing.ENABLED
34-
35-
36-
class Model(abc.ABC):
37-
"""The base interface for calling an LLM."""
38-
39-
@abc.abstractmethod
40-
async def get_response(
41-
self,
42-
system_instructions: str | None,
43-
input: str | list[TResponseInputItem],
44-
model_settings: ModelSettings,
45-
tools: list[Tool],
46-
output_schema: AgentOutputSchemaBase | None,
47-
handoffs: list[Handoff],
48-
tracing: ModelTracing,
49-
*,
50-
previous_response_id: str | None,
51-
conversation_id: str | None,
52-
prompt: ResponsePromptParam | None,
53-
) -> ModelResponse:
54-
"""Get a response from the model.
55-
56-
Args:
57-
system_instructions: The system instructions to use.
58-
input: The input items to the model, in OpenAI Responses format.
59-
model_settings: The model settings to use.
60-
tools: The tools available to the model.
61-
output_schema: The output schema to use.
62-
handoffs: The handoffs available to the model.
63-
tracing: Tracing configuration.
64-
previous_response_id: the ID of the previous response. Generally not used by the model,
65-
except for the OpenAI Responses API.
66-
conversation_id: The ID of the stored conversation, if any.
67-
prompt: The prompt config to use for the model.
68-
69-
Returns:
70-
The full model response.
71-
"""
72-
pass
73-
74-
@abc.abstractmethod
75-
def stream_response(
76-
self,
77-
system_instructions: str | None,
78-
input: str | list[TResponseInputItem],
79-
model_settings: ModelSettings,
80-
tools: list[Tool],
81-
output_schema: AgentOutputSchemaBase | None,
82-
handoffs: list[Handoff],
83-
tracing: ModelTracing,
84-
*,
85-
previous_response_id: str | None,
86-
conversation_id: str | None,
87-
prompt: ResponsePromptParam | None,
88-
) -> AsyncIterator[TResponseStreamEvent]:
89-
"""Stream a response from the model.
90-
91-
Args:
92-
system_instructions: The system instructions to use.
93-
input: The input items to the model, in OpenAI Responses format.
94-
model_settings: The model settings to use.
95-
tools: The tools available to the model.
96-
output_schema: The output schema to use.
97-
handoffs: The handoffs available to the model.
98-
tracing: Tracing configuration.
99-
previous_response_id: the ID of the previous response. Generally not used by the model,
100-
except for the OpenAI Responses API.
101-
conversation_id: The ID of the stored conversation, if any.
102-
prompt: The prompt config to use for the model.
103-
104-
Returns:
105-
An iterator of response stream events, in OpenAI Responses format.
106-
"""
107-
pass
108-
109-
110-
class ModelProvider(abc.ABC):
111-
"""The base interface for a model provider.
112-
113-
Model provider is responsible for looking up Models by name.
114-
"""
115-
116-
@abc.abstractmethod
117-
def get_model(self, model_name: str | None) -> Model:
118-
"""Get a model by name.
119-
120-
Args:
121-
model_name: The name of the model to get.
122-
123-
Returns:
124-
The model.
125-
"""
1+
from __future__ import annotations
2+
3+
import abc
4+
import enum
5+
from collections.abc import AsyncIterator
6+
from typing import TYPE_CHECKING
7+
8+
from openai.types.responses.response_prompt_param import ResponsePromptParam
9+
10+
from ..agent_output import AgentOutputSchemaBase
11+
from ..handoffs import Handoff
12+
from ..items import ModelResponse, TResponseInputItem, TResponseStreamEvent
13+
from ..tool import Tool
14+
15+
if TYPE_CHECKING:
16+
from ..model_settings import ModelSettings
17+
18+
19+
class ModelTracing(enum.Enum):
20+
DISABLED = 0
21+
"""Tracing is disabled entirely."""
22+
23+
ENABLED = 1
24+
"""Tracing is enabled, and all data is included."""
25+
26+
ENABLED_WITHOUT_DATA = 2
27+
"""Tracing is enabled, but inputs/outputs are not included."""
28+
29+
def is_disabled(self) -> bool:
30+
return self == ModelTracing.DISABLED
31+
32+
def include_data(self) -> bool:
33+
return self == ModelTracing.ENABLED
34+
35+
36+
class Model(abc.ABC):
37+
"""The base interface for calling an LLM."""
38+
39+
@abc.abstractmethod
40+
async def get_response(
41+
self,
42+
system_instructions: str | None,
43+
input: str | list[TResponseInputItem],
44+
model_settings: ModelSettings,
45+
tools: list[Tool],
46+
output_schema: AgentOutputSchemaBase | None,
47+
handoffs: list[Handoff],
48+
tracing: ModelTracing,
49+
*,
50+
previous_response_id: str | None,
51+
conversation_id: str | None,
52+
prompt: ResponsePromptParam | None,
53+
) -> ModelResponse:
54+
"""Get a response from the model.
55+
56+
Args:
57+
system_instructions: The system instructions to use.
58+
input: The input items to the model, in OpenAI Responses format.
59+
model_settings: The model settings to use.
60+
tools: The tools available to the model.
61+
output_schema: The output schema to use.
62+
handoffs: The handoffs available to the model.
63+
tracing: Tracing configuration.
64+
previous_response_id: the ID of the previous response. Generally not used by the model,
65+
except for the OpenAI Responses API.
66+
conversation_id: The ID of the stored conversation, if any.
67+
prompt: The prompt config to use for the model.
68+
69+
Returns:
70+
The full model response.
71+
"""
72+
pass
73+
74+
@abc.abstractmethod
75+
def stream_response(
76+
self,
77+
system_instructions: str | None,
78+
input: str | list[TResponseInputItem],
79+
model_settings: ModelSettings,
80+
tools: list[Tool],
81+
output_schema: AgentOutputSchemaBase | None,
82+
handoffs: list[Handoff],
83+
tracing: ModelTracing,
84+
*,
85+
previous_response_id: str | None,
86+
conversation_id: str | None,
87+
prompt: ResponsePromptParam | None,
88+
) -> AsyncIterator[TResponseStreamEvent]:
89+
"""Stream a response from the model.
90+
91+
Args:
92+
system_instructions: The system instructions to use.
93+
input: The input items to the model, in OpenAI Responses format.
94+
model_settings: The model settings to use.
95+
tools: The tools available to the model.
96+
output_schema: The output schema to use.
97+
handoffs: The handoffs available to the model.
98+
tracing: Tracing configuration.
99+
previous_response_id: the ID of the previous response. Generally not used by the model,
100+
except for the OpenAI Responses API.
101+
conversation_id: The ID of the stored conversation, if any.
102+
prompt: The prompt config to use for the model.
103+
104+
Returns:
105+
An iterator of response stream events, in OpenAI Responses format.
106+
"""
107+
pass
108+
109+
110+
class ModelProvider(abc.ABC):
111+
"""The base interface for a model provider.
112+
113+
Model provider is responsible for looking up Models by name.
114+
"""
115+
116+
@abc.abstractmethod
117+
def get_model(self, model_name: str | None) -> Model:
118+
"""Get a model by name.
119+
120+
Args:
121+
model_name: The name of the model to get.
122+
123+
Returns:
124+
The model.
125+
"""

0 commit comments

Comments
 (0)