Compare commits

...
4 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 e15def607d fix: remove extra_body tool_choice hack for Claude backend
Build and Push / build (push) Successful in 1m57s
Build and Push / release (push) Successful in 3s
PydanticAI handles tool_choice natively for Anthropic. The extra_body
hack caused an infinite tool call loop where Claude kept calling the
same tool because tool_choice was forced to "any".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:51:42 +01:00
jpmschweitzerandClaude Opus 4.5 6dd1c2e2a9 fix: trigger CI on version tag push instead of release event
Changed workflow trigger from release:published to push:tags:v[0-9]*
so that pushing a version tag triggers the build pipeline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 09:52:43 +01:00
jpmschweitzerandClaude Opus 4.5 3617218359 chore: release v2.0.1
Build and Push / release (release) Failing after 3s
Build and Push / build (release) Successful in 1m21s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 09:49:46 +01:00
jpmschweitzerandClaude Opus 4.5 c7a4012831 fix: use AnthropicProvider to pass api_key to PydanticAI model
AnthropicModel doesn't accept api_key directly; it must be passed
through an AnthropicProvider instance.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 09:47:02 +01:00
6 changed files with 43 additions and 9 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
name: Build and Push name: Build and Push
on: on:
release: push:
types: [published] tags:
- 'v[0-9]*'
jobs: jobs:
release: release:
+13
View File
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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
- **Expert agent registration failure** - `AnthropicModel` does not accept `api_key` directly; now passes it via `AnthropicProvider`
## [2.0.0] - 2026-02-05 ## [2.0.0] - 2026-02-05
### Added ### Added
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tatlock" name = "tatlock"
version = "2.0.0" version = "2.0.2"
description = "OpenAI-compatible API with Ollama backend" description = "OpenAI-compatible API with Ollama backend"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [] dependencies = []
+5 -5
View File
@@ -494,13 +494,13 @@ class TatlockAgent(AgentInterface):
) )
# Run with scoped tools and tracker # Run with scoped tools and tracker
# Force tool_choice: required to make LLM actually call tools # Force tool_choice to make LLM actually call tools
from pydantic_ai.settings import ModelSettings from src.anthropic.model_selector import get_tool_choice_settings
result = await scoped_agent.run( result = await scoped_agent.run(
enriched_message, enriched_message,
message_history=pydantic_history if pydantic_history else None, message_history=pydantic_history if pydantic_history else None,
deps=tool_tracker, deps=tool_tracker,
model_settings=ModelSettings(extra_body={"tool_choice": "required"}) model_settings=get_tool_choice_settings(),
) )
logger.info( logger.info(
@@ -624,7 +624,6 @@ class TatlockAgent(AgentInterface):
- tool_outputs: Dict mapping tool names to their outputs - tool_outputs: Dict mapping tool names to their outputs
- raw_output: The agent's raw text output - raw_output: The agent's raw text output
""" """
from pydantic_ai.settings import ModelSettings
from pydantic_ai.messages import ( from pydantic_ai.messages import (
ModelRequest, ModelRequest,
ModelResponse, ModelResponse,
@@ -684,11 +683,12 @@ class TatlockAgent(AgentInterface):
) )
# Run with scoped tools and tracker # Run with scoped tools and tracker
from src.anthropic.model_selector import get_tool_choice_settings
result = await scoped_agent.run( result = await scoped_agent.run(
enriched_message, enriched_message,
message_history=pydantic_history if pydantic_history else None, message_history=pydantic_history if pydantic_history else None,
deps=tool_tracker, 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 # Extract tool calls and results from the agent's messages
+2
View File
@@ -7,11 +7,13 @@ Provides model selection with automatic fallback between Claude and Ollama.
from src.anthropic.model_selector import ( from src.anthropic.model_selector import (
check_claude_health, check_claude_health,
get_model, get_model,
get_tool_choice_settings,
is_claude_available, is_claude_available,
) )
__all__ = [ __all__ = [
"check_claude_health", "check_claude_health",
"get_model", "get_model",
"get_tool_choice_settings",
"is_claude_available", "is_claude_available",
] ]
+19 -1
View File
@@ -9,6 +9,7 @@ from typing import Union
from pydantic_ai.models.anthropic import AnthropicModel from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.anthropic import AnthropicProvider
from src.core.config import config from src.core.config import config
from src.core.logging_config import get_logger from src.core.logging_config import get_logger
@@ -113,7 +114,7 @@ def get_model(prefer_cloud: bool | None = None) -> Union[AnthropicModel, OpenAIC
) )
return AnthropicModel( return AnthropicModel(
model_name=config.ANTHROPIC_MODEL, model_name=config.ANTHROPIC_MODEL,
api_key=config.ANTHROPIC_API_KEY, provider=AnthropicProvider(api_key=config.ANTHROPIC_API_KEY),
) )
# Fall back to Ollama # Fall back to Ollama
@@ -131,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: def get_model_info() -> dict:
""" """
Get information about the current model configuration. Get information about the current model configuration.