Compare commits

...
5 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 31e7884d8f fix: remove Steward analysis from user-visible reasoning
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 1m21s
The Steward's internal routing analysis (DELEGATE, COMPLEXITY, etc.)
was being exposed in <think> blocks. This is implementation detail,
not useful reasoning for the user.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 12:19:05 +01:00
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
8 changed files with 49 additions and 39 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:
+19
View File
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.0.3] - 2026-02-05
### Fixed
- **Steward analysis leaking into responses** - Removed internal routing analysis (`DELEGATE: tatlock_core...`) from user-visible reasoning in both streaming and non-streaming paths
## [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.3"
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.
-10
View File
@@ -651,16 +651,6 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
# Build response output items # Build response output items
output_items = [] output_items = []
# Add Steward reasoning as a reasoning output item
output_items.append(ReasoningOutputItem(
id=f"reasoning_{generate_id()}",
summary=[
"🎩 Steward's Analysis:",
enriched.steward_reasoning,
],
status="completed"
))
# Add Tatlock's message # Add Tatlock's message
output_items.append(MessageOutputItem( output_items.append(MessageOutputItem(
id=f"msg_{generate_id()}", id=f"msg_{generate_id()}",
-20
View File
@@ -166,26 +166,6 @@ class StreamingCoordinator:
conversation_id=conversation_id, conversation_id=conversation_id,
) )
# Stream Steward's analysis as reasoning summary
steward_lines = enriched.steward_reasoning.split('\n')
for line in steward_lines:
if line.strip():
yield ReasoningSummaryDelta(delta=line + "\n")
await asyncio.sleep(0.05)
yield ReasoningSummaryDone()
# Add Steward reasoning to output items
reasoning_item = ReasoningOutputItem(
id=f"reasoning_{generate_id()}",
summary=[
"🎩 Steward's Analysis:",
enriched.steward_reasoning,
],
status="completed"
)
output_items.append(reasoning_item)
# Initialize tool tracker # Initialize tool tracker
tracker = ToolCallTracker( tracker = ToolCallTracker(
recommended_capabilities=enriched.recommendation.recommended_capabilities, recommended_capabilities=enriched.recommendation.recommended_capabilities,