massgen 0.1.0a3__py3-none-any.whl → 0.1.2__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.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (120) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +15 -2
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +83 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +286 -15
  10. massgen/backend/capabilities.py +6 -6
  11. massgen/backend/chat_completions.py +200 -103
  12. massgen/backend/claude.py +115 -18
  13. massgen/backend/claude_code.py +378 -14
  14. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  15. massgen/backend/gemini.py +1333 -1629
  16. massgen/backend/gemini_mcp_manager.py +545 -0
  17. massgen/backend/gemini_trackers.py +344 -0
  18. massgen/backend/gemini_utils.py +43 -0
  19. massgen/backend/grok.py +39 -6
  20. massgen/backend/response.py +147 -81
  21. massgen/cli.py +605 -110
  22. massgen/config_builder.py +376 -27
  23. massgen/configs/README.md +123 -80
  24. massgen/configs/basic/multi/three_agents_default.yaml +3 -3
  25. massgen/configs/basic/single/single_agent.yaml +1 -1
  26. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  29. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  30. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  31. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  34. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  35. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  36. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  39. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  40. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  41. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  42. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  46. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  47. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  52. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  56. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  57. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  60. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  61. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  62. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  65. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  66. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  67. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  68. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  69. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +7 -29
  70. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +5 -6
  71. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +4 -4
  72. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +4 -4
  73. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +2 -2
  74. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  75. massgen/formatter/_chat_completions_formatter.py +104 -0
  76. massgen/formatter/_claude_formatter.py +120 -0
  77. massgen/formatter/_gemini_formatter.py +448 -0
  78. massgen/formatter/_response_formatter.py +88 -0
  79. massgen/frontend/coordination_ui.py +4 -2
  80. massgen/logger_config.py +35 -3
  81. massgen/message_templates.py +56 -6
  82. massgen/orchestrator.py +512 -16
  83. massgen/stream_chunk/base.py +3 -0
  84. massgen/tests/custom_tools_example.py +392 -0
  85. massgen/tests/mcp_test_server.py +17 -7
  86. massgen/tests/test_config_builder.py +423 -0
  87. massgen/tests/test_custom_tools.py +401 -0
  88. massgen/tests/test_intelligent_planning_mode.py +643 -0
  89. massgen/tests/test_tools.py +127 -0
  90. massgen/token_manager/token_manager.py +13 -4
  91. massgen/tool/README.md +935 -0
  92. massgen/tool/__init__.py +39 -0
  93. massgen/tool/_async_helpers.py +70 -0
  94. massgen/tool/_basic/__init__.py +8 -0
  95. massgen/tool/_basic/_two_num_tool.py +24 -0
  96. massgen/tool/_code_executors/__init__.py +10 -0
  97. massgen/tool/_code_executors/_python_executor.py +74 -0
  98. massgen/tool/_code_executors/_shell_executor.py +61 -0
  99. massgen/tool/_exceptions.py +39 -0
  100. massgen/tool/_file_handlers/__init__.py +10 -0
  101. massgen/tool/_file_handlers/_file_operations.py +218 -0
  102. massgen/tool/_manager.py +634 -0
  103. massgen/tool/_registered_tool.py +88 -0
  104. massgen/tool/_result.py +66 -0
  105. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  106. massgen/tool/docs/builtin_tools.md +681 -0
  107. massgen/tool/docs/exceptions.md +794 -0
  108. massgen/tool/docs/execution_results.md +691 -0
  109. massgen/tool/docs/manager.md +887 -0
  110. massgen/tool/docs/workflow_toolkits.md +529 -0
  111. massgen/tool/workflow_toolkits/__init__.py +57 -0
  112. massgen/tool/workflow_toolkits/base.py +55 -0
  113. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  114. massgen/tool/workflow_toolkits/vote.py +167 -0
  115. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/METADATA +87 -129
  116. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/RECORD +120 -44
  117. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/WHEEL +0 -0
  118. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/entry_points.txt +0 -0
  119. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/licenses/LICENSE +0 -0
  120. {massgen-0.1.0a3.dist-info → massgen-0.1.2.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
17
17
  import httpx
18
18
 
19
19
  from ..logger_config import log_backend_activity, logger
20
+ from ..tool import ToolManager
20
21
  from .base import LLMBackend, StreamChunk
21
22
 
22
23
 
@@ -107,13 +108,22 @@ SUPPORTED_AUDIO_MIME_TYPES = {
107
108
  }
108
109
 
109
110
 
110
- class MCPBackend(LLMBackend):
111
+ class CustomToolAndMCPBackend(LLMBackend):
111
112
  """Base backend class with MCP (Model Context Protocol) support."""
112
113
 
113
114
  def __init__(self, api_key: Optional[str] = None, **kwargs):
114
115
  """Initialize backend with MCP support."""
115
116
  super().__init__(api_key, **kwargs)
116
117
 
118
+ # Custom tools support - initialize before api_params_handler
119
+ self.custom_tool_manager = ToolManager()
120
+ self._custom_tool_names: set[str] = set()
121
+
122
+ # Register custom tools if provided
123
+ custom_tools = kwargs.get("custom_tools", [])
124
+ if custom_tools:
125
+ self._register_custom_tools(custom_tools)
126
+
117
127
  # MCP integration (filesystem MCP server may have been injected by base class)
118
128
  self.mcp_servers = self.config.get("mcp_servers", [])
119
129
  self.allowed_tools = kwargs.pop("allowed_tools", None)
@@ -169,6 +179,261 @@ class MCPBackend(LLMBackend):
169
179
  async def _process_stream(self, stream, all_params, agent_id: Optional[str] = None) -> AsyncGenerator[StreamChunk, None]:
170
180
  """Process stream."""
171
181
 
182
+ # Custom tools support
183
+ def _register_custom_tools(self, custom_tools: List[Dict[str, Any]]) -> None:
184
+ """Register custom tools with the tool manager.
185
+
186
+ Supports flexible configuration:
187
+ - function: str | List[str]
188
+ - description: str (shared) | List[str] (1-to-1 mapping)
189
+ - preset_args: dict (shared) | List[dict] (1-to-1 mapping)
190
+
191
+ Examples:
192
+ # Single function
193
+ function: "my_func"
194
+ description: "My description"
195
+
196
+ # Multiple functions with shared description
197
+ function: ["func1", "func2"]
198
+ description: "Shared description"
199
+
200
+ # Multiple functions with individual descriptions
201
+ function: ["func1", "func2"]
202
+ description: ["Description 1", "Description 2"]
203
+
204
+ # Multiple functions with mixed (shared desc, individual args)
205
+ function: ["func1", "func2"]
206
+ description: "Shared description"
207
+ preset_args: [{"arg1": "val1"}, {"arg1": "val2"}]
208
+
209
+ Args:
210
+ custom_tools: List of custom tool configurations
211
+ """
212
+ # Collect unique categories and create them if needed
213
+ categories = set()
214
+ for tool_config in custom_tools:
215
+ if isinstance(tool_config, dict):
216
+ category = tool_config.get("category", "default")
217
+ if category != "default":
218
+ categories.add(category)
219
+
220
+ # Create categories that don't exist
221
+ for category in categories:
222
+ if category not in self.custom_tool_manager.tool_categories:
223
+ self.custom_tool_manager.setup_category(
224
+ category_name=category,
225
+ description=f"Custom {category} tools",
226
+ enabled=True,
227
+ )
228
+
229
+ # Register each custom tool
230
+ for tool_config in custom_tools:
231
+ try:
232
+ if isinstance(tool_config, dict):
233
+ # Extract base configuration
234
+ path = tool_config.get("path")
235
+ category = tool_config.get("category", "default")
236
+
237
+ # Normalize function field to list
238
+ func_field = tool_config.get("function")
239
+ if isinstance(func_field, str):
240
+ functions = [func_field]
241
+ elif isinstance(func_field, list):
242
+ functions = func_field
243
+ else:
244
+ logger.error(
245
+ f"Invalid function field type: {type(func_field)}. " f"Must be str or List[str].",
246
+ )
247
+ continue
248
+
249
+ if not functions:
250
+ logger.error("Empty function list in tool config")
251
+ continue
252
+
253
+ num_functions = len(functions)
254
+
255
+ # Process name field (can be str or List[str])
256
+ name_field = tool_config.get("name")
257
+ names = self._process_field_for_functions(
258
+ name_field,
259
+ num_functions,
260
+ "name",
261
+ )
262
+ if names is None:
263
+ continue # Validation error, skip this tool
264
+
265
+ # Process description field (can be str or List[str])
266
+ desc_field = tool_config.get("description")
267
+ descriptions = self._process_field_for_functions(
268
+ desc_field,
269
+ num_functions,
270
+ "description",
271
+ )
272
+ if descriptions is None:
273
+ continue # Validation error, skip this tool
274
+
275
+ # Process preset_args field (can be dict or List[dict])
276
+ preset_field = tool_config.get("preset_args")
277
+ preset_args_list = self._process_field_for_functions(
278
+ preset_field,
279
+ num_functions,
280
+ "preset_args",
281
+ )
282
+ if preset_args_list is None:
283
+ continue # Validation error, skip this tool
284
+
285
+ # Register each function with its corresponding values
286
+ for i, func in enumerate(functions):
287
+ # Load the function first if custom name is needed
288
+ if names[i] and names[i] != func:
289
+ # Need to load function and apply custom name
290
+ if path:
291
+ loaded_func = self.custom_tool_manager._load_function_from_path(path, func)
292
+ else:
293
+ loaded_func = self.custom_tool_manager._load_builtin_function(func)
294
+
295
+ if loaded_func is None:
296
+ logger.error(f"Could not load function '{func}' from path: {path}")
297
+ continue
298
+
299
+ # Apply custom name by modifying __name__ attribute
300
+ loaded_func.__name__ = names[i]
301
+
302
+ # Register with loaded function (no path needed)
303
+ self.custom_tool_manager.add_tool_function(
304
+ path=None,
305
+ func=loaded_func,
306
+ category=category,
307
+ preset_args=preset_args_list[i],
308
+ description=descriptions[i],
309
+ )
310
+ else:
311
+ # No custom name or same as function name, use normal registration
312
+ self.custom_tool_manager.add_tool_function(
313
+ path=path,
314
+ func=func,
315
+ category=category,
316
+ preset_args=preset_args_list[i],
317
+ description=descriptions[i],
318
+ )
319
+
320
+ # Use custom name for logging and tracking if provided
321
+ registered_name = names[i] if names[i] else func
322
+
323
+ # Track tool name for categorization
324
+ if registered_name.startswith("custom_tool__"):
325
+ self._custom_tool_names.add(registered_name)
326
+ else:
327
+ self._custom_tool_names.add(f"custom_tool__{registered_name}")
328
+
329
+ logger.info(
330
+ f"Registered custom tool: {registered_name} from {path} " f"(category: {category}, " f"desc: '{descriptions[i][:50] if descriptions[i] else 'None'}...')",
331
+ )
332
+
333
+ except Exception as e:
334
+ func_name = tool_config.get("function", "unknown")
335
+ logger.error(
336
+ f"Failed to register custom tool {func_name}: {e}",
337
+ exc_info=True,
338
+ )
339
+
340
+ def _process_field_for_functions(
341
+ self,
342
+ field_value: Any,
343
+ num_functions: int,
344
+ field_name: str,
345
+ ) -> Optional[List[Any]]:
346
+ """Process a config field that can be a single value or list.
347
+
348
+ Conversion rules:
349
+ - None → [None, None, ...] (repeated num_functions times)
350
+ - Single value (not list) → [value, value, ...] (shared)
351
+ - List with matching length → use as-is (1-to-1 mapping)
352
+ - List with wrong length → ERROR (return None)
353
+
354
+ Args:
355
+ field_value: The field value from config
356
+ num_functions: Number of functions being registered
357
+ field_name: Name of the field (for error messages)
358
+
359
+ Returns:
360
+ List of values (one per function), or None if validation fails
361
+
362
+ Examples:
363
+ _process_field_for_functions(None, 3, "desc")
364
+ → [None, None, None]
365
+
366
+ _process_field_for_functions("shared", 3, "desc")
367
+ → ["shared", "shared", "shared"]
368
+
369
+ _process_field_for_functions(["a", "b", "c"], 3, "desc")
370
+ → ["a", "b", "c"]
371
+
372
+ _process_field_for_functions(["a", "b"], 3, "desc")
373
+ → None (error logged)
374
+ """
375
+ # Case 1: None or missing field → use None for all functions
376
+ if field_value is None:
377
+ return [None] * num_functions
378
+
379
+ # Case 2: Single value (not a list) → share across all functions
380
+ if not isinstance(field_value, list):
381
+ return [field_value] * num_functions
382
+
383
+ # Case 3: List value → must match function count exactly
384
+ if len(field_value) == num_functions:
385
+ return field_value
386
+ else:
387
+ # Length mismatch → validation error
388
+ logger.error(
389
+ f"Configuration error: {field_name} is a list with "
390
+ f"{len(field_value)} items, but there are {num_functions} functions. "
391
+ f"Either use a single value (shared) or a list with exactly "
392
+ f"{num_functions} items (1-to-1 mapping).",
393
+ )
394
+ return None
395
+
396
+ async def _execute_custom_tool(self, call: Dict[str, Any]) -> str:
397
+ """Execute a custom tool and return the result.
398
+
399
+ Args:
400
+ call: Function call dictionary with name and arguments
401
+
402
+ Returns:
403
+ The execution result as a string
404
+ """
405
+ import json
406
+
407
+ tool_request = {
408
+ "name": call["name"],
409
+ "input": json.loads(call["arguments"]) if isinstance(call["arguments"], str) else call["arguments"],
410
+ }
411
+
412
+ result_text = ""
413
+ try:
414
+ async for result in self.custom_tool_manager.execute_tool(tool_request):
415
+ # Accumulate results
416
+ if hasattr(result, "output_blocks"):
417
+ for block in result.output_blocks:
418
+ if hasattr(block, "data"):
419
+ result_text += str(block.data)
420
+ elif hasattr(block, "content"):
421
+ result_text += str(block.content)
422
+ elif hasattr(result, "content"):
423
+ result_text += str(result.content)
424
+ else:
425
+ result_text += str(result)
426
+ except Exception as e:
427
+ logger.error(f"Error in custom tool execution: {e}")
428
+ result_text = f"Error: {str(e)}"
429
+
430
+ return result_text or "Tool executed successfully"
431
+
432
+ def _get_custom_tools_schemas(self) -> List[Dict[str, Any]]:
433
+ """Get OpenAI-formatted schemas for all registered custom tools."""
434
+ return self.custom_tool_manager.fetch_tool_schemas()
435
+
436
+ # MCP support methods
172
437
  async def _setup_mcp_tools(self) -> None:
173
438
  """Initialize MCP client for mcp_tools-based servers (stdio + streamable-http)."""
174
439
  if not self.mcp_servers or self._mcp_initialized:
@@ -268,10 +533,10 @@ class MCPBackend(LLMBackend):
268
533
  max_retries: int = 3,
269
534
  ) -> Tuple[str, Any]:
270
535
  """Execute MCP function with exponential backoff retry logic."""
271
- # Check if planning mode is enabled - block MCP tool execution during planning
272
- if self.is_planning_mode_enabled():
273
- logger.info(f"[MCP] Planning mode enabled - blocking MCP tool execution: {function_name}")
274
- error_str = "🚫 [MCP] Planning mode active - MCP tools blocked during coordination"
536
+ # Check if this specific MCP tool is blocked by planning mode
537
+ if self.is_mcp_tool_blocked(function_name):
538
+ logger.info(f"[MCP] Planning mode enabled - blocking MCP tool: {function_name}")
539
+ error_str = f"🚫 [MCP] Tool '{function_name}' blocked during coordination (planning mode active)"
275
540
  return error_str, {"error": error_str, "blocked_by": "planning_mode", "function_name": function_name}
276
541
 
277
542
  # Convert JSON string to dict for shared utility
@@ -783,14 +1048,16 @@ class MCPBackend(LLMBackend):
783
1048
  async for chunk in self.yield_mcp_status_chunks(use_mcp):
784
1049
  yield chunk
785
1050
 
786
- if use_mcp:
1051
+ use_custom_tools = bool(self._custom_tool_names)
1052
+
1053
+ if use_mcp or use_custom_tools:
787
1054
  # MCP MODE: Recursive function call detection and execution
788
1055
  logger.info("Using recursive MCP execution mode")
789
1056
 
790
1057
  current_messages = self._trim_message_history(messages.copy())
791
1058
 
792
1059
  # Start recursive MCP streaming
793
- async for chunk in self._stream_with_mcp_tools(current_messages, tools, client, **kwargs):
1060
+ async for chunk in self._stream_with_custom_and_mcp_tools(current_messages, tools, client, **kwargs):
794
1061
  yield chunk
795
1062
 
796
1063
  else:
@@ -798,7 +1065,7 @@ class MCPBackend(LLMBackend):
798
1065
  logger.info("Using no-MCP mode")
799
1066
 
800
1067
  # Start non-MCP streaming
801
- async for chunk in self._stream_without_mcp_tools(messages, tools, client, **kwargs):
1068
+ async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
802
1069
  yield chunk
803
1070
 
804
1071
  except Exception as e:
@@ -808,7 +1075,7 @@ class MCPBackend(LLMBackend):
808
1075
  await self._record_mcp_circuit_breaker_failure(e, agent_id)
809
1076
 
810
1077
  # Handle MCP exceptions with fallback
811
- async for chunk in self._stream_handle_mcp_exceptions(e, messages, tools, client, **kwargs):
1078
+ async for chunk in self._stream_handle_custom_and_mcp_exceptions(e, messages, tools, client, **kwargs):
812
1079
  yield chunk
813
1080
  else:
814
1081
  logger.error(f"Streaming error: {e}")
@@ -824,7 +1091,7 @@ class MCPBackend(LLMBackend):
824
1091
 
825
1092
  if isinstance(e, (MCPConnectionError, MCPTimeoutError, MCPServerError, MCPError)):
826
1093
  # Handle MCP exceptions with fallback
827
- async for chunk in self._stream_handle_mcp_exceptions(e, messages, tools, client, **kwargs):
1094
+ async for chunk in self._stream_handle_custom_and_mcp_exceptions(e, messages, tools, client, **kwargs):
828
1095
  yield chunk
829
1096
  else:
830
1097
  # Generic setup error: still notify if MCP was configured
@@ -837,7 +1104,7 @@ class MCPBackend(LLMBackend):
837
1104
  )
838
1105
 
839
1106
  # Proceed with non-MCP streaming
840
- async for chunk in self._stream_without_mcp_tools(messages, tools, client, **kwargs):
1107
+ async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
841
1108
  yield chunk
842
1109
  except Exception as inner_e:
843
1110
  logger.error(f"Streaming error during MCP setup fallback: {inner_e}")
@@ -845,7 +1112,7 @@ class MCPBackend(LLMBackend):
845
1112
  finally:
846
1113
  await self._cleanup_client(client)
847
1114
 
848
- async def _stream_without_mcp_tools(
1115
+ async def _stream_without_custom_and_mcp_tools(
849
1116
  self,
850
1117
  messages: List[Dict[str, Any]],
851
1118
  tools: List[Dict[str, Any]],
@@ -885,7 +1152,7 @@ class MCPBackend(LLMBackend):
885
1152
  async for chunk in self._process_stream(stream, all_params, agent_id):
886
1153
  yield chunk
887
1154
 
888
- async def _stream_handle_mcp_exceptions(
1155
+ async def _stream_handle_custom_and_mcp_exceptions(
889
1156
  self,
890
1157
  error: Exception,
891
1158
  messages: List[Dict[str, Any]],
@@ -921,7 +1188,7 @@ class MCPBackend(LLMBackend):
921
1188
  content=f"\nāš ļø {user_message} ({error}); continuing without MCP tools\n",
922
1189
  )
923
1190
 
924
- async for chunk in self._stream_without_mcp_tools(messages, tools, client, **kwargs):
1191
+ async for chunk in self._stream_without_custom_and_mcp_tools(messages, tools, client, **kwargs):
925
1192
  yield chunk
926
1193
 
927
1194
  def _track_mcp_function_names(self, tools: List[Dict[str, Any]]) -> None:
@@ -1012,7 +1279,7 @@ class MCPBackend(LLMBackend):
1012
1279
  self._mcp_functions.clear()
1013
1280
  self._mcp_function_names.clear()
1014
1281
 
1015
- async def __aenter__(self) -> "MCPBackend":
1282
+ async def __aenter__(self) -> "CustomToolAndMCPBackend":
1016
1283
  """Async context manager entry."""
1017
1284
  # Initialize MCP tools if configured
1018
1285
  if MCPResourceManager:
@@ -1087,6 +1354,10 @@ class MCPBackend(LLMBackend):
1087
1354
  """Check if a tool call is an MCP function."""
1088
1355
  return tool_name in self._mcp_functions
1089
1356
 
1357
+ def is_custom_tool_call(self, tool_name: str) -> bool:
1358
+ """Check if a tool call is a custom tool function."""
1359
+ return tool_name in self._custom_tool_names
1360
+
1090
1361
  def get_mcp_tools_formatted(self) -> List[Dict[str, Any]]:
1091
1362
  """Get MCP tools formatted for specific API format."""
1092
1363
  if not self._mcp_functions:
@@ -137,13 +137,14 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
137
137
  builtin_tools=["web_search", "code_execution"],
138
138
  filesystem_support="mcp",
139
139
  models=[
140
+ "claude-haiku-4-5-20251001",
140
141
  "claude-sonnet-4-5-20250929",
142
+ "claude-opus-4-1-20250805",
141
143
  "claude-sonnet-4-20250514",
142
- "claude-opus-4-20250514",
143
144
  "claude-3-5-sonnet-latest",
144
145
  "claude-3-5-haiku-latest",
145
146
  ],
146
- default_model="claude-sonnet-4-20250514",
147
+ default_model="claude-sonnet-4-5-20250929",
147
148
  env_var="ANTHROPIC_API_KEY",
148
149
  notes="Web search and code execution are built-in tools. Audio/video understanding support (v0.0.30+).",
149
150
  ),
@@ -175,8 +176,8 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
175
176
  filesystem_support="native",
176
177
  models=[
177
178
  "claude-sonnet-4-5-20250929",
179
+ "claude-opus-4-1-20250805",
178
180
  "claude-sonnet-4-20250514",
179
- "claude-opus-4-20250514",
180
181
  ],
181
182
  default_model="claude-sonnet-4-5-20250929",
182
183
  env_var="ANTHROPIC_API_KEY",
@@ -218,12 +219,11 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
218
219
  filesystem_support="mcp",
219
220
  models=[
220
221
  "grok-4",
222
+ "grok-4-fast",
221
223
  "grok-3",
222
224
  "grok-3-mini",
223
- "grok-beta",
224
- "grok-vision-beta",
225
225
  ],
226
- default_model="grok-beta",
226
+ default_model="grok-4",
227
227
  env_var="XAI_API_KEY",
228
228
  notes="Web search includes real-time data access.",
229
229
  ),