Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ def _apply_option_translations(options: dict[str, Any]) -> None:
if old_key not in options or old_key == new_key:
continue
old_value = options.pop(old_key)
if old_key == "stop" and isinstance(old_value, str):
# ChatOptions allows a single stop string; stop_sequences takes a list.
old_value = [old_value]
options.setdefault(new_key, old_value)


Expand Down
13 changes: 13 additions & 0 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4192,3 +4192,16 @@ def get_test_image() -> Content:
assert len(response.text) > 0
# sample_image.jpg contains a photo of a house; the model should mention it.
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"


def test_option_translations_single_stop_string_becomes_list() -> None:
"""stop_sequences is an array in the Anthropic API, so a single stop string is wrapped."""
from agent_framework_anthropic._chat_client import _apply_option_translations

options: dict[str, Any] = {"stop": "END"}
_apply_option_translations(options)
assert options == {"stop_sequences": ["END"]}

options = {"stop": ["END", "STOP"]}
_apply_option_translations(options)
assert options == {"stop_sequences": ["END", "STOP"]}
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,8 @@ def _prepare_options(
if (top_p := options.get("top_p")) is not None:
run_options["inferenceConfig"]["topP"] = top_p
if (stop := options.get("stop")) is not None:
run_options["inferenceConfig"]["stopSequences"] = stop
# ChatOptions allows a single stop string; stopSequences takes a list.
run_options["inferenceConfig"]["stopSequences"] = [stop] if isinstance(stop, str) else stop

tool_config = self._prepare_tools(options.get("tools"))
if tool_mode := validate_tool_mode(options.get("tool_choice")):
Expand Down
10 changes: 10 additions & 0 deletions python/packages/bedrock/tests/test_bedrock_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,16 @@ def test_prepare_options_adds_instructions_and_sampling_settings() -> None:
}


def test_prepare_options_single_stop_string_becomes_list() -> None:
"""stopSequences only accepts a list, so a single stop string is wrapped."""
client = _make_client()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]

request = client._prepare_options(messages, {"stop": "DONE"})

assert request["inferenceConfig"]["stopSequences"] == ["DONE"]


def test_prepare_options_unsupported_tool_mode_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Unexpected tool modes should raise a clear error."""
from agent_framework_bedrock import _chat_client as chat_client_module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,9 @@ def _prepare_config(
for key, value in options.items():
if key in _OPTION_EXCLUDE_KEYS or value is None:
continue
if key == "stop" and isinstance(value, str):
# ChatOptions allows a single stop string; stop_sequences takes a list.
value = [value]
kwargs[_OPTION_TRANSLATIONS.get(key, key)] = value

response_format = options.get("response_format")
Expand Down
14 changes: 14 additions & 0 deletions python/packages/gemini/tests/test_gemini_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,20 @@ async def test_prepare_config_stop_sequences() -> None:
assert config.stop_sequences == ["END", "STOP"]


async def test_prepare_config_stop_single_string() -> None:
"""A single stop string is sent as a one-item stop_sequences list."""
client, mock = _make_gemini_client()
mock.aio.models.generate_content = AsyncMock(return_value=_make_response([_make_part(text="Hi")]))

await client.get_response(
messages=[Message(role="user", contents=[Content.from_text("Hi")])],
options={"stop": "END"},
)

config: types.GenerateContentConfig = mock.aio.models.generate_content.call_args.kwargs["config"]
assert config.stop_sequences == ["END"]


async def test_prepare_config_seed() -> None:
"""Forwards the seed option to GenerateContentConfig.seed for reproducible outputs."""
client, mock = _make_gemini_client()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An
if key in OLLAMA_MODEL_OPTIONS:
# Apply model option translations (e.g., max_tokens -> num_predict)
translated_key = OLLAMA_MODEL_OPTION_TRANSLATIONS.get(key, key)
if key == "stop" and isinstance(value, str):
# ChatOptions allows a single stop string; Ollama takes a list.
value = [value]
model_options[translated_key] = value
else:
# Apply top-level translations (e.g., response_format -> format)
Expand Down
10 changes: 10 additions & 0 deletions python/packages/ollama/tests/test_ollama_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,3 +828,13 @@ def test_mixed_policy_approval_roles_preserve_tool_result(self) -> None:

assert [message.role for message in prepared] == ["tool", "assistant"]
assert prepared[0].content == "safe result"


def test_prepare_options_single_stop_string_becomes_list(ollama_unit_test_env: dict[str, str]) -> None:
"""Ollama expects options.stop to be a list, so a single stop string is wrapped."""
client = OllamaChatClient()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]

request = client._prepare_options(messages, {"stop": "END"})

assert request["options"]["stop"] == ["END"]
Loading