diff --git a/CHANGELOG.md b/CHANGELOG.md index 46905c5..51ba465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.2] - 2026-02-05 + +### Fixed + +- **tool_choice format incompatibility** - Removed `extra_body` tool_choice hack for Claude backend; PydanticAI handles tool_choice natively for Anthropic, preventing infinite tool call loops +- **CI trigger** - Changed workflow trigger from `release:published` to `push:tags:v[0-9]*` + ## [2.0.1] - 2026-02-05 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index ba6488d..71ad7d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tatlock" -version = "2.0.1" +version = "2.0.2" description = "OpenAI-compatible API with Ollama backend" requires-python = ">=3.12" dependencies = [] diff --git a/src/agents/tatlock.py b/src/agents/tatlock.py index 0d6d3db..b65b9a9 100644 --- a/src/agents/tatlock.py +++ b/src/agents/tatlock.py @@ -494,13 +494,13 @@ class TatlockAgent(AgentInterface): ) # Run with scoped tools and tracker - # Force tool_choice: required to make LLM actually call tools - from pydantic_ai.settings import ModelSettings + # Force tool_choice to make LLM actually call tools + from src.anthropic.model_selector import get_tool_choice_settings result = await scoped_agent.run( enriched_message, message_history=pydantic_history if pydantic_history else None, deps=tool_tracker, - model_settings=ModelSettings(extra_body={"tool_choice": "required"}) + model_settings=get_tool_choice_settings(), ) logger.info( @@ -624,7 +624,6 @@ class TatlockAgent(AgentInterface): - tool_outputs: Dict mapping tool names to their outputs - raw_output: The agent's raw text output """ - from pydantic_ai.settings import ModelSettings from pydantic_ai.messages import ( ModelRequest, ModelResponse, @@ -684,11 +683,12 @@ class TatlockAgent(AgentInterface): ) # Run with scoped tools and tracker + from src.anthropic.model_selector import get_tool_choice_settings result = await scoped_agent.run( enriched_message, message_history=pydantic_history if pydantic_history else None, deps=tool_tracker, - model_settings=ModelSettings(extra_body={"tool_choice": "required"}) + model_settings=get_tool_choice_settings(), ) # Extract tool calls and results from the agent's messages diff --git a/src/anthropic/__init__.py b/src/anthropic/__init__.py index e101c66..7c46682 100644 --- a/src/anthropic/__init__.py +++ b/src/anthropic/__init__.py @@ -7,11 +7,13 @@ Provides model selection with automatic fallback between Claude and Ollama. from src.anthropic.model_selector import ( check_claude_health, get_model, + get_tool_choice_settings, is_claude_available, ) __all__ = [ "check_claude_health", "get_model", + "get_tool_choice_settings", "is_claude_available", ] diff --git a/src/anthropic/model_selector.py b/src/anthropic/model_selector.py index 1465547..04c7461 100644 --- a/src/anthropic/model_selector.py +++ b/src/anthropic/model_selector.py @@ -132,6 +132,23 @@ def get_model(prefer_cloud: bool | None = None) -> Union[AnthropicModel, OpenAIC ) +def get_tool_choice_settings() -> 'ModelSettings': + """ + Get model_settings for forcing tool calls on the first request. + + For Claude: PydanticAI handles tool_choice natively, so no extra_body needed. + For Ollama: Pass tool_choice="required" via extra_body to force tool calling. + """ + from pydantic_ai.settings import ModelSettings + + if is_claude_available() and config.PREFER_CLOUD_BACKEND: + # PydanticAI's Anthropic model handles tool_choice internally + return ModelSettings() + else: + # Ollama needs explicit tool_choice via extra_body + return ModelSettings(extra_body={"tool_choice": "required"}) + + def get_model_info() -> dict: """ Get information about the current model configuration.