agent-runtime-core 0.8.0__py3-none-any.whl → 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_runtime_core/__init__.py +65 -3
- agent_runtime_core/agentic_loop.py +285 -20
- agent_runtime_core/config.py +8 -0
- agent_runtime_core/contexts.py +72 -4
- agent_runtime_core/interfaces.py +29 -11
- agent_runtime_core/llm/anthropic.py +161 -7
- agent_runtime_core/llm/models_config.py +50 -6
- agent_runtime_core/llm/openai.py +51 -2
- agent_runtime_core/multi_agent.py +1419 -17
- agent_runtime_core/persistence/__init__.py +8 -0
- agent_runtime_core/persistence/base.py +318 -1
- agent_runtime_core/persistence/file.py +226 -2
- agent_runtime_core/privacy.py +250 -0
- agent_runtime_core/tool_calling_agent.py +3 -1
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.1.dist-info}/METADATA +2 -1
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.1.dist-info}/RECORD +18 -17
- agent_runtime_core-0.9.1.dist-info/licenses/LICENSE +83 -0
- agent_runtime_core-0.8.0.dist-info/licenses/LICENSE +0 -21
- {agent_runtime_core-0.8.0.dist-info → agent_runtime_core-0.9.1.dist-info}/WHEEL +0 -0
agent_runtime_core/contexts.py
CHANGED
|
@@ -24,11 +24,15 @@ import json
|
|
|
24
24
|
import os
|
|
25
25
|
from datetime import datetime
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any, Callable, Optional
|
|
27
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING
|
|
28
28
|
from uuid import UUID, uuid4
|
|
29
29
|
|
|
30
30
|
from agent_runtime_core.interfaces import EventType, Message, ToolRegistry
|
|
31
31
|
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from agent_runtime_core.multi_agent import SystemContext
|
|
34
|
+
from agent_runtime_core.privacy import PrivacyConfig, UserContext
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class InMemoryRunContext:
|
|
34
38
|
"""
|
|
@@ -65,10 +69,13 @@ class InMemoryRunContext:
|
|
|
65
69
|
metadata: Optional[dict] = None,
|
|
66
70
|
tool_registry: Optional[ToolRegistry] = None,
|
|
67
71
|
on_event: Optional[Callable[[str, dict], None]] = None,
|
|
72
|
+
system_context: Optional["SystemContext"] = None,
|
|
73
|
+
user_context: Optional["UserContext"] = None,
|
|
74
|
+
privacy_config: Optional["PrivacyConfig"] = None,
|
|
68
75
|
):
|
|
69
76
|
"""
|
|
70
77
|
Initialize an in-memory run context.
|
|
71
|
-
|
|
78
|
+
|
|
72
79
|
Args:
|
|
73
80
|
run_id: Unique identifier for this run (auto-generated if not provided)
|
|
74
81
|
conversation_id: Associated conversation ID (optional)
|
|
@@ -77,6 +84,9 @@ class InMemoryRunContext:
|
|
|
77
84
|
metadata: Run metadata
|
|
78
85
|
tool_registry: Registry of available tools
|
|
79
86
|
on_event: Optional callback for events (for testing/debugging)
|
|
87
|
+
system_context: Optional SystemContext for multi-agent systems with shared knowledge
|
|
88
|
+
user_context: Optional UserContext for user isolation and privacy
|
|
89
|
+
privacy_config: Optional PrivacyConfig for privacy settings (defaults to max privacy)
|
|
80
90
|
"""
|
|
81
91
|
self._run_id = run_id or uuid4()
|
|
82
92
|
self._conversation_id = conversation_id
|
|
@@ -88,7 +98,18 @@ class InMemoryRunContext:
|
|
|
88
98
|
self._state: Optional[dict] = None
|
|
89
99
|
self._events: list[dict] = []
|
|
90
100
|
self._on_event = on_event
|
|
91
|
-
|
|
101
|
+
self._system_context = system_context
|
|
102
|
+
|
|
103
|
+
# Import here to avoid circular imports
|
|
104
|
+
from agent_runtime_core.privacy import (
|
|
105
|
+
DEFAULT_PRIVACY_CONFIG,
|
|
106
|
+
ANONYMOUS_USER,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Default to secure settings: anonymous user + strict privacy
|
|
110
|
+
self._user_context = user_context if user_context is not None else ANONYMOUS_USER
|
|
111
|
+
self._privacy_config = privacy_config if privacy_config is not None else DEFAULT_PRIVACY_CONFIG
|
|
112
|
+
|
|
92
113
|
@property
|
|
93
114
|
def run_id(self) -> UUID:
|
|
94
115
|
"""Unique identifier for this run."""
|
|
@@ -118,7 +139,22 @@ class InMemoryRunContext:
|
|
|
118
139
|
def tool_registry(self) -> ToolRegistry:
|
|
119
140
|
"""Registry of available tools for this agent."""
|
|
120
141
|
return self._tool_registry
|
|
121
|
-
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def system_context(self) -> Optional["SystemContext"]:
|
|
145
|
+
"""System context for multi-agent systems with shared knowledge."""
|
|
146
|
+
return self._system_context
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def user_context(self) -> "UserContext":
|
|
150
|
+
"""User context for privacy and data isolation. Defaults to ANONYMOUS_USER."""
|
|
151
|
+
return self._user_context
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def privacy_config(self) -> "PrivacyConfig":
|
|
155
|
+
"""Privacy configuration for this run. Defaults to DEFAULT_PRIVACY_CONFIG (strict)."""
|
|
156
|
+
return self._privacy_config
|
|
157
|
+
|
|
122
158
|
async def emit(self, event_type: EventType | str, payload: dict) -> None:
|
|
123
159
|
"""Emit an event (stored in memory)."""
|
|
124
160
|
event_type_str = event_type.value if hasattr(event_type, 'value') else str(event_type)
|
|
@@ -195,6 +231,9 @@ class FileRunContext:
|
|
|
195
231
|
metadata: Optional[dict] = None,
|
|
196
232
|
tool_registry: Optional[ToolRegistry] = None,
|
|
197
233
|
on_event: Optional[Callable[[str, dict], None]] = None,
|
|
234
|
+
system_context: Optional["SystemContext"] = None,
|
|
235
|
+
user_context: Optional["UserContext"] = None,
|
|
236
|
+
privacy_config: Optional["PrivacyConfig"] = None,
|
|
198
237
|
):
|
|
199
238
|
"""
|
|
200
239
|
Initialize a file-based run context.
|
|
@@ -208,6 +247,9 @@ class FileRunContext:
|
|
|
208
247
|
metadata: Run metadata
|
|
209
248
|
tool_registry: Registry of available tools
|
|
210
249
|
on_event: Optional callback for events
|
|
250
|
+
system_context: Optional SystemContext for multi-agent systems with shared knowledge
|
|
251
|
+
user_context: Optional UserContext for user isolation and privacy
|
|
252
|
+
privacy_config: Optional PrivacyConfig for privacy settings (defaults to max privacy)
|
|
211
253
|
"""
|
|
212
254
|
self._run_id = run_id or uuid4()
|
|
213
255
|
self._checkpoint_dir = Path(checkpoint_dir)
|
|
@@ -219,6 +261,17 @@ class FileRunContext:
|
|
|
219
261
|
self._cancelled = False
|
|
220
262
|
self._on_event = on_event
|
|
221
263
|
self._state_cache: Optional[dict] = None
|
|
264
|
+
self._system_context = system_context
|
|
265
|
+
|
|
266
|
+
# Import here to avoid circular imports
|
|
267
|
+
from agent_runtime_core.privacy import (
|
|
268
|
+
DEFAULT_PRIVACY_CONFIG,
|
|
269
|
+
ANONYMOUS_USER,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Default to secure settings: anonymous user + strict privacy
|
|
273
|
+
self._user_context = user_context if user_context is not None else ANONYMOUS_USER
|
|
274
|
+
self._privacy_config = privacy_config if privacy_config is not None else DEFAULT_PRIVACY_CONFIG
|
|
222
275
|
|
|
223
276
|
# Ensure checkpoint directory exists
|
|
224
277
|
self._checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -253,6 +306,21 @@ class FileRunContext:
|
|
|
253
306
|
"""Registry of available tools for this agent."""
|
|
254
307
|
return self._tool_registry
|
|
255
308
|
|
|
309
|
+
@property
|
|
310
|
+
def system_context(self) -> Optional["SystemContext"]:
|
|
311
|
+
"""System context for multi-agent systems with shared knowledge."""
|
|
312
|
+
return self._system_context
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def user_context(self) -> "UserContext":
|
|
316
|
+
"""User context for privacy and data isolation. Defaults to ANONYMOUS_USER."""
|
|
317
|
+
return self._user_context
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def privacy_config(self) -> "PrivacyConfig":
|
|
321
|
+
"""Privacy configuration for this run. Defaults to DEFAULT_PRIVACY_CONFIG (strict)."""
|
|
322
|
+
return self._privacy_config
|
|
323
|
+
|
|
256
324
|
def _checkpoint_path(self) -> Path:
|
|
257
325
|
"""Get the path to the checkpoint file for this run."""
|
|
258
326
|
return self._checkpoint_dir / f"{self._run_id}.json"
|
agent_runtime_core/interfaces.py
CHANGED
|
@@ -323,13 +323,21 @@ class ToolRegistry:
|
|
|
323
323
|
"""
|
|
324
324
|
return self.to_openai_format()
|
|
325
325
|
|
|
326
|
-
async def execute(
|
|
326
|
+
async def execute(
|
|
327
|
+
self,
|
|
328
|
+
name: str,
|
|
329
|
+
arguments: dict,
|
|
330
|
+
ctx: Optional["RunContext"] = None,
|
|
331
|
+
**kwargs
|
|
332
|
+
) -> Any:
|
|
327
333
|
"""
|
|
328
334
|
Execute a tool by name.
|
|
329
335
|
|
|
330
336
|
Args:
|
|
331
337
|
name: Tool name
|
|
332
338
|
arguments: Tool arguments
|
|
339
|
+
ctx: Optional RunContext, passed to handlers with requires_context=True
|
|
340
|
+
**kwargs: Additional arguments to pass to the handler
|
|
333
341
|
|
|
334
342
|
Returns:
|
|
335
343
|
Tool result
|
|
@@ -340,7 +348,15 @@ class ToolRegistry:
|
|
|
340
348
|
tool = self._tools.get(name)
|
|
341
349
|
if not tool:
|
|
342
350
|
raise KeyError(f"Tool not found: {name}")
|
|
343
|
-
|
|
351
|
+
|
|
352
|
+
# Check if the tool requires context
|
|
353
|
+
requires_context = tool.metadata.get('requires_context', False) if tool.metadata else False
|
|
354
|
+
|
|
355
|
+
if requires_context and ctx is not None:
|
|
356
|
+
# Pass ctx to the handler for tools that need it (e.g., sub-agent tools)
|
|
357
|
+
return await tool.handler(**arguments, ctx=ctx, **kwargs)
|
|
358
|
+
else:
|
|
359
|
+
return await tool.handler(**arguments, **kwargs)
|
|
344
360
|
|
|
345
361
|
async def execute_with_events(
|
|
346
362
|
self,
|
|
@@ -350,19 +366,19 @@ class ToolRegistry:
|
|
|
350
366
|
) -> Any:
|
|
351
367
|
"""
|
|
352
368
|
Execute a tool and automatically emit events.
|
|
353
|
-
|
|
369
|
+
|
|
354
370
|
This is a convenience method that wraps execute() and handles
|
|
355
371
|
event emission automatically. Use this in your agent loop to
|
|
356
372
|
reduce boilerplate.
|
|
357
|
-
|
|
373
|
+
|
|
358
374
|
Args:
|
|
359
375
|
tool_call: Tool call object with name, arguments, and id
|
|
360
376
|
ctx: Run context for emitting events
|
|
361
377
|
**kwargs: Additional arguments to pass to the tool
|
|
362
|
-
|
|
378
|
+
|
|
363
379
|
Returns:
|
|
364
380
|
Tool result
|
|
365
|
-
|
|
381
|
+
|
|
366
382
|
Example:
|
|
367
383
|
for tool_call in response.tool_calls:
|
|
368
384
|
result = await tools.execute_with_events(tool_call, ctx)
|
|
@@ -373,17 +389,17 @@ class ToolRegistry:
|
|
|
373
389
|
"tool_args": tool_call.arguments,
|
|
374
390
|
"tool_call_id": tool_call.id,
|
|
375
391
|
})
|
|
376
|
-
|
|
377
|
-
# Execute the tool
|
|
378
|
-
result = await self.execute(tool_call.name, tool_call.arguments, **kwargs)
|
|
379
|
-
|
|
392
|
+
|
|
393
|
+
# Execute the tool, passing ctx for tools that require it
|
|
394
|
+
result = await self.execute(tool_call.name, tool_call.arguments, ctx=ctx, **kwargs)
|
|
395
|
+
|
|
380
396
|
# Emit tool result event
|
|
381
397
|
await ctx.emit(EventType.TOOL_RESULT, {
|
|
382
398
|
"tool_name": tool_call.name,
|
|
383
399
|
"tool_call_id": tool_call.id,
|
|
384
400
|
"result": result,
|
|
385
401
|
})
|
|
386
|
-
|
|
402
|
+
|
|
387
403
|
return result
|
|
388
404
|
|
|
389
405
|
|
|
@@ -501,6 +517,7 @@ class LLMResponse:
|
|
|
501
517
|
model: str = ""
|
|
502
518
|
finish_reason: str = ""
|
|
503
519
|
raw_response: Optional[Any] = None
|
|
520
|
+
thinking: Optional[str] = None # Extended thinking content (Anthropic)
|
|
504
521
|
|
|
505
522
|
@property
|
|
506
523
|
def tool_calls(self) -> Optional[list["LLMToolCall"]]:
|
|
@@ -528,6 +545,7 @@ class LLMStreamChunk:
|
|
|
528
545
|
tool_calls: Optional[list] = None
|
|
529
546
|
finish_reason: Optional[str] = None
|
|
530
547
|
usage: Optional[dict] = None
|
|
548
|
+
thinking: Optional[str] = None # Extended thinking content (Anthropic)
|
|
531
549
|
|
|
532
550
|
|
|
533
551
|
class TraceSink(ABC):
|
|
@@ -90,6 +90,83 @@ class AnthropicClient(LLMClient):
|
|
|
90
90
|
|
|
91
91
|
return os.environ.get("ANTHROPIC_API_KEY")
|
|
92
92
|
|
|
93
|
+
def _validate_tool_call_pairs(self, messages: list[Message]) -> list[Message]:
|
|
94
|
+
"""
|
|
95
|
+
Validate and repair tool_use/tool_result pairing in message history.
|
|
96
|
+
|
|
97
|
+
Anthropic requires that every tool_use block has a corresponding tool_result
|
|
98
|
+
block immediately after. This can be violated if a run fails mid-way through
|
|
99
|
+
tool execution (e.g., timeout, crash, API error during parallel tool calls).
|
|
100
|
+
|
|
101
|
+
This method removes orphaned tool_use blocks (assistant messages with tool_calls
|
|
102
|
+
that don't have corresponding tool results).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
messages: List of messages in framework-neutral format
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Cleaned list of messages with orphaned tool_use blocks removed
|
|
109
|
+
"""
|
|
110
|
+
if not messages:
|
|
111
|
+
return messages
|
|
112
|
+
|
|
113
|
+
# First pass: collect all tool_call_ids that have results
|
|
114
|
+
tool_result_ids = set()
|
|
115
|
+
for msg in messages:
|
|
116
|
+
if msg.get("role") == "tool" and msg.get("tool_call_id"):
|
|
117
|
+
tool_result_ids.add(msg["tool_call_id"])
|
|
118
|
+
|
|
119
|
+
# Second pass: check each assistant message with tool_calls
|
|
120
|
+
cleaned_messages = []
|
|
121
|
+
orphaned_count = 0
|
|
122
|
+
|
|
123
|
+
for msg in messages:
|
|
124
|
+
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
125
|
+
# Check which tool_calls have results
|
|
126
|
+
tool_calls = msg.get("tool_calls", [])
|
|
127
|
+
valid_tool_calls = []
|
|
128
|
+
orphaned_ids = []
|
|
129
|
+
|
|
130
|
+
for tc in tool_calls:
|
|
131
|
+
# Handle both formats: {"id": ...} and {"function": {...}, "id": ...}
|
|
132
|
+
tc_id = tc.get("id")
|
|
133
|
+
if tc_id in tool_result_ids:
|
|
134
|
+
valid_tool_calls.append(tc)
|
|
135
|
+
else:
|
|
136
|
+
orphaned_ids.append(tc_id)
|
|
137
|
+
|
|
138
|
+
if orphaned_ids:
|
|
139
|
+
orphaned_count += len(orphaned_ids)
|
|
140
|
+
print(
|
|
141
|
+
f"[anthropic] Removing {len(orphaned_ids)} orphaned tool_use blocks "
|
|
142
|
+
f"without results: {orphaned_ids[:3]}{'...' if len(orphaned_ids) > 3 else ''}",
|
|
143
|
+
flush=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if valid_tool_calls:
|
|
147
|
+
# Keep the message but only with valid tool_calls
|
|
148
|
+
cleaned_msg = msg.copy()
|
|
149
|
+
cleaned_msg["tool_calls"] = valid_tool_calls
|
|
150
|
+
cleaned_messages.append(cleaned_msg)
|
|
151
|
+
elif msg.get("content"):
|
|
152
|
+
# No valid tool_calls but has text content - keep as regular message
|
|
153
|
+
cleaned_msg = {
|
|
154
|
+
"role": "assistant",
|
|
155
|
+
"content": msg["content"],
|
|
156
|
+
}
|
|
157
|
+
cleaned_messages.append(cleaned_msg)
|
|
158
|
+
# else: skip the message entirely (no valid tool_calls, no content)
|
|
159
|
+
else:
|
|
160
|
+
cleaned_messages.append(msg)
|
|
161
|
+
|
|
162
|
+
if orphaned_count > 0:
|
|
163
|
+
print(
|
|
164
|
+
f"[anthropic] Cleaned {orphaned_count} orphaned tool_use blocks from message history",
|
|
165
|
+
flush=True,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return cleaned_messages
|
|
169
|
+
|
|
93
170
|
async def generate(
|
|
94
171
|
self,
|
|
95
172
|
messages: list[Message],
|
|
@@ -99,11 +176,32 @@ class AnthropicClient(LLMClient):
|
|
|
99
176
|
tools: Optional[list[dict]] = None,
|
|
100
177
|
temperature: Optional[float] = None,
|
|
101
178
|
max_tokens: Optional[int] = None,
|
|
179
|
+
thinking: bool = False,
|
|
180
|
+
thinking_budget: Optional[int] = None,
|
|
102
181
|
**kwargs,
|
|
103
182
|
) -> LLMResponse:
|
|
104
|
-
"""
|
|
183
|
+
"""
|
|
184
|
+
Generate a completion from Anthropic.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
messages: List of messages in framework-neutral format
|
|
188
|
+
model: Model ID to use (defaults to self.default_model)
|
|
189
|
+
stream: Whether to stream the response (not used here, use stream() method)
|
|
190
|
+
tools: List of tools in OpenAI format
|
|
191
|
+
temperature: Sampling temperature (0.0 to 1.0)
|
|
192
|
+
max_tokens: Maximum tokens to generate
|
|
193
|
+
thinking: Enable extended thinking mode for deeper reasoning
|
|
194
|
+
thinking_budget: Max tokens for thinking (default: 10000, max: 128000)
|
|
195
|
+
**kwargs: Additional parameters passed to the API
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
LLMResponse with the generated message
|
|
199
|
+
"""
|
|
105
200
|
model = model or self.default_model
|
|
106
201
|
|
|
202
|
+
# Validate and repair message history before processing
|
|
203
|
+
messages = self._validate_tool_call_pairs(messages)
|
|
204
|
+
|
|
107
205
|
# Extract system message and convert other messages
|
|
108
206
|
system_message = None
|
|
109
207
|
converted_messages = []
|
|
@@ -126,15 +224,28 @@ class AnthropicClient(LLMClient):
|
|
|
126
224
|
request_kwargs["system"] = system_message
|
|
127
225
|
if tools:
|
|
128
226
|
request_kwargs["tools"] = self._convert_tools(tools)
|
|
129
|
-
|
|
227
|
+
|
|
228
|
+
# Handle extended thinking mode
|
|
229
|
+
if thinking:
|
|
230
|
+
# Extended thinking requires specific configuration
|
|
231
|
+
# Temperature must be 1.0 when using thinking
|
|
232
|
+
request_kwargs["thinking"] = {
|
|
233
|
+
"type": "enabled",
|
|
234
|
+
"budget_tokens": thinking_budget or 10000,
|
|
235
|
+
}
|
|
236
|
+
# Temperature must be exactly 1.0 for extended thinking
|
|
237
|
+
request_kwargs["temperature"] = 1.0
|
|
238
|
+
elif temperature is not None:
|
|
130
239
|
request_kwargs["temperature"] = temperature
|
|
131
240
|
|
|
132
241
|
request_kwargs.update(kwargs)
|
|
133
242
|
|
|
134
243
|
response = await self._client.messages.create(**request_kwargs)
|
|
135
244
|
|
|
245
|
+
message, thinking_content = self._convert_response(response)
|
|
246
|
+
|
|
136
247
|
return LLMResponse(
|
|
137
|
-
message=
|
|
248
|
+
message=message,
|
|
138
249
|
usage={
|
|
139
250
|
"prompt_tokens": response.usage.input_tokens,
|
|
140
251
|
"completion_tokens": response.usage.output_tokens,
|
|
@@ -143,6 +254,7 @@ class AnthropicClient(LLMClient):
|
|
|
143
254
|
model=response.model,
|
|
144
255
|
finish_reason=response.stop_reason or "",
|
|
145
256
|
raw_response=response,
|
|
257
|
+
thinking=thinking_content,
|
|
146
258
|
)
|
|
147
259
|
|
|
148
260
|
async def stream(
|
|
@@ -151,11 +263,31 @@ class AnthropicClient(LLMClient):
|
|
|
151
263
|
*,
|
|
152
264
|
model: Optional[str] = None,
|
|
153
265
|
tools: Optional[list[dict]] = None,
|
|
266
|
+
thinking: bool = False,
|
|
267
|
+
thinking_budget: Optional[int] = None,
|
|
268
|
+
temperature: Optional[float] = None,
|
|
154
269
|
**kwargs,
|
|
155
270
|
) -> AsyncIterator[LLMStreamChunk]:
|
|
156
|
-
"""
|
|
271
|
+
"""
|
|
272
|
+
Stream a completion from Anthropic.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
messages: List of messages in framework-neutral format
|
|
276
|
+
model: Model ID to use
|
|
277
|
+
tools: List of tools in OpenAI format
|
|
278
|
+
thinking: Enable extended thinking mode
|
|
279
|
+
thinking_budget: Max tokens for thinking (default: 10000)
|
|
280
|
+
temperature: Sampling temperature (ignored if thinking=True)
|
|
281
|
+
**kwargs: Additional parameters
|
|
282
|
+
|
|
283
|
+
Yields:
|
|
284
|
+
LLMStreamChunk with delta content and thinking content
|
|
285
|
+
"""
|
|
157
286
|
model = model or self.default_model
|
|
158
287
|
|
|
288
|
+
# Validate and repair message history before processing
|
|
289
|
+
messages = self._validate_tool_call_pairs(messages)
|
|
290
|
+
|
|
159
291
|
# Extract system message and convert other messages
|
|
160
292
|
system_message = None
|
|
161
293
|
converted_messages = []
|
|
@@ -179,6 +311,16 @@ class AnthropicClient(LLMClient):
|
|
|
179
311
|
if tools:
|
|
180
312
|
request_kwargs["tools"] = self._convert_tools(tools)
|
|
181
313
|
|
|
314
|
+
# Handle extended thinking mode
|
|
315
|
+
if thinking:
|
|
316
|
+
request_kwargs["thinking"] = {
|
|
317
|
+
"type": "enabled",
|
|
318
|
+
"budget_tokens": thinking_budget or 10000,
|
|
319
|
+
}
|
|
320
|
+
request_kwargs["temperature"] = 1.0
|
|
321
|
+
elif temperature is not None:
|
|
322
|
+
request_kwargs["temperature"] = temperature
|
|
323
|
+
|
|
182
324
|
request_kwargs.update(kwargs)
|
|
183
325
|
|
|
184
326
|
async with self._client.messages.stream(**request_kwargs) as stream:
|
|
@@ -186,6 +328,9 @@ class AnthropicClient(LLMClient):
|
|
|
186
328
|
if event.type == "content_block_delta":
|
|
187
329
|
if hasattr(event.delta, "text"):
|
|
188
330
|
yield LLMStreamChunk(delta=event.delta.text)
|
|
331
|
+
elif hasattr(event.delta, "thinking"):
|
|
332
|
+
# Extended thinking content
|
|
333
|
+
yield LLMStreamChunk(delta="", thinking=event.delta.thinking)
|
|
189
334
|
elif event.type == "message_stop":
|
|
190
335
|
yield LLMStreamChunk(finish_reason="stop")
|
|
191
336
|
|
|
@@ -327,14 +472,23 @@ class AnthropicClient(LLMClient):
|
|
|
327
472
|
})
|
|
328
473
|
return result
|
|
329
474
|
|
|
330
|
-
def _convert_response(self, response) -> Message:
|
|
331
|
-
"""
|
|
475
|
+
def _convert_response(self, response) -> tuple[Message, Optional[str]]:
|
|
476
|
+
"""
|
|
477
|
+
Convert Anthropic response to our format.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Tuple of (message, thinking_content)
|
|
481
|
+
"""
|
|
332
482
|
content = ""
|
|
333
483
|
tool_calls = []
|
|
484
|
+
thinking_content = None
|
|
334
485
|
|
|
335
486
|
for block in response.content:
|
|
336
487
|
if block.type == "text":
|
|
337
488
|
content += block.text
|
|
489
|
+
elif block.type == "thinking":
|
|
490
|
+
# Extended thinking block
|
|
491
|
+
thinking_content = block.thinking
|
|
338
492
|
elif block.type == "tool_use":
|
|
339
493
|
# Convert input to JSON string (not Python str() which gives wrong format)
|
|
340
494
|
arguments = json.dumps(block.input) if isinstance(block.input, dict) else str(block.input)
|
|
@@ -355,4 +509,4 @@ class AnthropicClient(LLMClient):
|
|
|
355
509
|
if tool_calls:
|
|
356
510
|
result["tool_calls"] = tool_calls
|
|
357
511
|
|
|
358
|
-
return result
|
|
512
|
+
return result, thinking_content
|
|
@@ -22,19 +22,48 @@ class ModelInfo:
|
|
|
22
22
|
supports_tools: bool = True
|
|
23
23
|
supports_vision: bool = False
|
|
24
24
|
supports_streaming: bool = True
|
|
25
|
+
supports_thinking: bool = False # Extended thinking (Anthropic) or reasoning (OpenAI)
|
|
25
26
|
description: str = ""
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
# Registry of supported models
|
|
29
30
|
SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
30
|
-
# OpenAI Models
|
|
31
|
+
# OpenAI Models - GPT-5 (latest)
|
|
32
|
+
"gpt-5": ModelInfo(
|
|
33
|
+
id="gpt-5",
|
|
34
|
+
name="GPT-5",
|
|
35
|
+
provider="openai",
|
|
36
|
+
context_window=256000,
|
|
37
|
+
supports_vision=True,
|
|
38
|
+
supports_thinking=True,
|
|
39
|
+
description="Most capable OpenAI model with reasoning",
|
|
40
|
+
),
|
|
41
|
+
"gpt-5-mini": ModelInfo(
|
|
42
|
+
id="gpt-5-mini",
|
|
43
|
+
name="GPT-5 Mini",
|
|
44
|
+
provider="openai",
|
|
45
|
+
context_window=256000,
|
|
46
|
+
supports_vision=True,
|
|
47
|
+
supports_thinking=True,
|
|
48
|
+
description="Fast GPT-5 variant with reasoning",
|
|
49
|
+
),
|
|
50
|
+
"gpt-5-turbo": ModelInfo(
|
|
51
|
+
id="gpt-5-turbo",
|
|
52
|
+
name="GPT-5 Turbo",
|
|
53
|
+
provider="openai",
|
|
54
|
+
context_window=256000,
|
|
55
|
+
supports_vision=True,
|
|
56
|
+
supports_thinking=True,
|
|
57
|
+
description="Balanced GPT-5 variant for production",
|
|
58
|
+
),
|
|
59
|
+
# OpenAI Models - GPT-4o
|
|
31
60
|
"gpt-4o": ModelInfo(
|
|
32
61
|
id="gpt-4o",
|
|
33
62
|
name="GPT-4o",
|
|
34
63
|
provider="openai",
|
|
35
64
|
context_window=128000,
|
|
36
65
|
supports_vision=True,
|
|
37
|
-
description="Most capable
|
|
66
|
+
description="Most capable GPT-4 model, multimodal",
|
|
38
67
|
),
|
|
39
68
|
"gpt-4o-mini": ModelInfo(
|
|
40
69
|
id="gpt-4o-mini",
|
|
@@ -52,12 +81,14 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
52
81
|
supports_vision=True,
|
|
53
82
|
description="Previous generation flagship",
|
|
54
83
|
),
|
|
84
|
+
# OpenAI Models - o-series (reasoning)
|
|
55
85
|
"o1": ModelInfo(
|
|
56
86
|
id="o1",
|
|
57
87
|
name="o1",
|
|
58
88
|
provider="openai",
|
|
59
89
|
context_window=200000,
|
|
60
90
|
supports_tools=False,
|
|
91
|
+
supports_thinking=True,
|
|
61
92
|
description="Advanced reasoning model",
|
|
62
93
|
),
|
|
63
94
|
"o1-mini": ModelInfo(
|
|
@@ -66,6 +97,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
66
97
|
provider="openai",
|
|
67
98
|
context_window=128000,
|
|
68
99
|
supports_tools=False,
|
|
100
|
+
supports_thinking=True,
|
|
69
101
|
description="Fast reasoning model",
|
|
70
102
|
),
|
|
71
103
|
"o3-mini": ModelInfo(
|
|
@@ -74,6 +106,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
74
106
|
provider="openai",
|
|
75
107
|
context_window=200000,
|
|
76
108
|
supports_tools=True,
|
|
109
|
+
supports_thinking=True,
|
|
77
110
|
description="Latest reasoning model with tool use",
|
|
78
111
|
),
|
|
79
112
|
|
|
@@ -84,6 +117,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
84
117
|
provider="anthropic",
|
|
85
118
|
context_window=200000,
|
|
86
119
|
supports_vision=True,
|
|
120
|
+
supports_thinking=True,
|
|
87
121
|
description="Best balance of speed and capability for agents and coding",
|
|
88
122
|
),
|
|
89
123
|
"claude-opus-4-5-20251101": ModelInfo(
|
|
@@ -92,6 +126,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
92
126
|
provider="anthropic",
|
|
93
127
|
context_window=200000,
|
|
94
128
|
supports_vision=True,
|
|
129
|
+
supports_thinking=True,
|
|
95
130
|
description="Premium model - maximum intelligence with practical performance",
|
|
96
131
|
),
|
|
97
132
|
"claude-haiku-4-5-20251001": ModelInfo(
|
|
@@ -100,6 +135,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
100
135
|
provider="anthropic",
|
|
101
136
|
context_window=200000,
|
|
102
137
|
supports_vision=True,
|
|
138
|
+
supports_thinking=True,
|
|
103
139
|
description="Fastest model with near-frontier intelligence",
|
|
104
140
|
),
|
|
105
141
|
# Anthropic Models - Claude 4 (previous generation)
|
|
@@ -109,6 +145,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
109
145
|
provider="anthropic",
|
|
110
146
|
context_window=200000,
|
|
111
147
|
supports_vision=True,
|
|
148
|
+
supports_thinking=True,
|
|
112
149
|
description="Previous generation Sonnet",
|
|
113
150
|
),
|
|
114
151
|
"claude-opus-4-20250514": ModelInfo(
|
|
@@ -117,6 +154,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
117
154
|
provider="anthropic",
|
|
118
155
|
context_window=200000,
|
|
119
156
|
supports_vision=True,
|
|
157
|
+
supports_thinking=True,
|
|
120
158
|
description="Previous generation Opus",
|
|
121
159
|
),
|
|
122
160
|
# Anthropic Models - Claude 3.5 (legacy)
|
|
@@ -126,6 +164,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
126
164
|
provider="anthropic",
|
|
127
165
|
context_window=200000,
|
|
128
166
|
supports_vision=True,
|
|
167
|
+
supports_thinking=True,
|
|
129
168
|
description="Legacy model, still excellent",
|
|
130
169
|
),
|
|
131
170
|
"claude-3-5-haiku-20241022": ModelInfo(
|
|
@@ -134,6 +173,7 @@ SUPPORTED_MODELS: dict[str, ModelInfo] = {
|
|
|
134
173
|
provider="anthropic",
|
|
135
174
|
context_window=200000,
|
|
136
175
|
supports_vision=True,
|
|
176
|
+
supports_thinking=True,
|
|
137
177
|
description="Legacy fast model",
|
|
138
178
|
),
|
|
139
179
|
}
|
|
@@ -150,19 +190,20 @@ def get_model_info(model_id: str) -> Optional[ModelInfo]:
|
|
|
150
190
|
def get_provider_for_model(model_id: str) -> Optional[str]:
|
|
151
191
|
"""
|
|
152
192
|
Detect the provider for a model ID.
|
|
153
|
-
|
|
193
|
+
|
|
154
194
|
Returns "openai", "anthropic", or None if unknown.
|
|
155
195
|
"""
|
|
156
196
|
# Check registry first
|
|
157
197
|
if model_id in SUPPORTED_MODELS:
|
|
158
198
|
return SUPPORTED_MODELS[model_id].provider
|
|
159
|
-
|
|
199
|
+
|
|
160
200
|
# Fallback heuristics for unlisted models
|
|
161
|
-
if model_id.startswith("gpt-") or model_id.startswith("o1") or
|
|
201
|
+
if (model_id.startswith("gpt-") or model_id.startswith("o1") or
|
|
202
|
+
model_id.startswith("o3") or model_id.startswith("gpt5")):
|
|
162
203
|
return "openai"
|
|
163
204
|
if model_id.startswith("claude"):
|
|
164
205
|
return "anthropic"
|
|
165
|
-
|
|
206
|
+
|
|
166
207
|
return None
|
|
167
208
|
|
|
168
209
|
|
|
@@ -174,6 +215,9 @@ def list_models_for_ui() -> list[dict]:
|
|
|
174
215
|
"name": m.name,
|
|
175
216
|
"provider": m.provider,
|
|
176
217
|
"description": m.description,
|
|
218
|
+
"supports_thinking": m.supports_thinking,
|
|
219
|
+
"supports_tools": m.supports_tools,
|
|
220
|
+
"supports_vision": m.supports_vision,
|
|
177
221
|
}
|
|
178
222
|
for m in SUPPORTED_MODELS.values()
|
|
179
223
|
]
|