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
@@ -28,11 +28,9 @@ agents:
28
28
  DISCORD_TOKEN: "${DISCORD_TOKEN}"
29
29
  security:
30
30
  level: "high"
31
- system_message: |
32
- Available Discord Tools:
33
- - Discord server interaction via MCP integration
34
- - Message reading, sending, and management
35
- - Channel and server information access
31
+ exclude_tools:
32
+ - mcp__discord__discord_send_webhook_message
33
+ - mcp__discord__discord_edit_webhook_message
36
34
 
37
35
  - id: "openai_discord_agent"
38
36
  backend:
@@ -50,11 +48,6 @@ agents:
50
48
  exclude_tools:
51
49
  - mcp__discord__discord_send_webhook_message
52
50
  - mcp__discord__discord_edit_webhook_message
53
- system_message: |
54
- Available Discord Tools:
55
- - Discord server interaction via MCP integration
56
- - Message reading, sending, and management
57
- - Channel and server information access
58
51
 
59
52
  - id: "claude_code_discord_agent"
60
53
  backend:
@@ -68,11 +61,6 @@ agents:
68
61
  args: ["-y", "mcp-discord", "--config", "${DISCORD_TOKEN}"]
69
62
  env:
70
63
  DISCORD_TOKEN: "${DISCORD_TOKEN}"
71
- system_message: |
72
- Available Discord Tools:
73
- - Discord server interaction via MCP integration
74
- - Message reading, sending, and management
75
- - Channel and server information access
76
64
 
77
65
  - id: "claude_discord_agent"
78
66
  backend:
@@ -90,11 +78,6 @@ agents:
90
78
  exclude_tools:
91
79
  - mcp__discord__discord_send_webhook_message
92
80
  - mcp__discord__discord_edit_webhook_message
93
- system_message: |
94
- Available Discord Tools:
95
- - Discord server interaction via MCP integration
96
- - Message reading, sending, and management
97
- - Channel and server information access
98
81
 
99
82
  - id: "grok_discord_agent"
100
83
  backend:
@@ -112,11 +95,6 @@ agents:
112
95
  exclude_tools:
113
96
  - mcp__discord__discord_send_webhook_message
114
97
  - mcp__discord__discord_edit_webhook_message
115
- system_message: |
116
- Available Discord Tools:
117
- - Discord server interaction via MCP integration
118
- - Message reading, sending, and management
119
- - Channel and server information access
120
98
 
121
99
  ui:
122
100
  display_type: "rich_terminal"
@@ -124,8 +102,8 @@ ui:
124
102
 
125
103
  # Orchestrator Settings with Coordination Configuration
126
104
  orchestrator:
127
- snapshot_storage: "massgen_logs/snapshots" # Directory for workspace snapshots
128
- agent_temporary_workspace: "massgen_logs/temp_workspaces" # Directory for temporary agent workspaces
105
+ snapshot_storage: "snapshots" # Directory for workspace snapshots
106
+ agent_temporary_workspace: "temp_workspaces" # Directory for temporary agent workspaces
129
107
  coordination:
130
108
  enable_planning_mode: true
131
109
  planning_mode_instruction: |
@@ -134,7 +112,7 @@ orchestrator:
134
112
  1. Describe your intended actions and reasoning
135
113
  2. Analyze other agents' proposals
136
114
  3. Use only the 'vote' or 'new_answer' tools for coordination
137
- 4. DO NOT execute any actual Discord commands or API calls
138
- 5. Save tool execution for the final presentation phase when the winning agent will implement the plan
115
+ 4. Execute read-only actions - DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)
116
+ 5. Save actions that have side effects for the final presentation phase when the winning agent will implement the plan
139
117
 
140
118
  Focus on planning, analysis, and coordination rather than execution.
@@ -134,8 +134,8 @@ ui:
134
134
 
135
135
  # Orchestrator Settings with Coordination Configuration
136
136
  orchestrator:
137
- snapshot_storage: "massgen_logs/snapshots" # Directory for workspace snapshots
138
- agent_temporary_workspace: "massgen_logs/temp_workspaces" # Directory for temporary agent workspaces
137
+ snapshot_storage: "snapshots" # Directory for workspace snapshots
138
+ agent_temporary_workspace: "temp_workspaces" # Directory for temporary agent workspaces
139
139
  coordination:
140
140
  enable_planning_mode: true
141
141
  planning_mode_instruction: |
@@ -144,8 +144,7 @@ orchestrator:
144
144
  1. Describe your intended file operations and reasoning
145
145
  2. Analyze other agents' proposals for the filesystem tasks
146
146
  3. Use only the 'vote' or 'new_answer' tools for coordination
147
- 4. DO NOT execute any actual filesystem operations, file creation, or directory management
148
- 5. Save all file operations for the final presentation phase when the winning agent will implement the plan
147
+ 4. Execute read-only actions - DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)
148
+ 5. Save actions that have side effects for the final presentation phase when the winning agent will implement the plan
149
149
 
150
- Focus on planning, analysis, and coordination rather than execution.
151
- Example: "I would create a 'src' directory and write a main.py file..." rather than actually creating them.
150
+ Focus on planning, analysis, and coordination rather than execution.
@@ -135,8 +135,8 @@ ui:
135
135
 
136
136
  # Orchestrator Settings with Coordination Configuration
137
137
  orchestrator:
138
- snapshot_storage: "massgen_logs/snapshots" # Directory for workspace snapshots
139
- agent_temporary_workspace: "massgen_logs/temp_workspaces" # Directory for temporary agent workspaces
138
+ snapshot_storage: "snapshots" # Directory for workspace snapshots
139
+ agent_temporary_workspace: "temp_workspaces" # Directory for temporary agent workspaces
140
140
  coordination:
141
141
  enable_planning_mode: true
142
142
  planning_mode_instruction: |
@@ -145,7 +145,7 @@ orchestrator:
145
145
  1. Describe your intended actions and reasoning
146
146
  2. Analyze other agents' proposals
147
147
  3. Use only the 'vote' or 'new_answer' tools for coordination
148
- 4. DO NOT execute any actual Notion commands or API calls
149
- 5. Save tool execution for the final presentation phase when the winning agent will implement the plan
148
+ 4. Execute read-only actions - DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)
149
+ 5. Save actions that have side effects for the final presentation phase when the winning agent will implement the plan
150
150
 
151
151
  Focus on planning, analysis, and coordination rather than execution.
@@ -139,8 +139,8 @@ ui:
139
139
 
140
140
  # Orchestrator Settings with Coordination Configuration
141
141
  orchestrator:
142
- snapshot_storage: "massgen_logs/snapshots" # Directory for workspace snapshots
143
- agent_temporary_workspace: "massgen_logs/temp_workspaces" # Directory for temporary agent workspaces
142
+ snapshot_storage: "snapshots" # Directory for workspace snapshots
143
+ agent_temporary_workspace: "temp_workspaces" # Directory for temporary agent workspaces
144
144
  coordination:
145
145
  enable_planning_mode: true
146
146
  planning_mode_instruction: |
@@ -149,7 +149,7 @@ orchestrator:
149
149
  1. Describe your intended actions and reasoning
150
150
  2. Analyze other agents' proposals
151
151
  3. Use only the 'vote' or 'new_answer' tools for coordination
152
- 4. DO NOT execute any actual Twitter commands or API calls
153
- 5. Save tool execution for the final presentation phase when the winning agent will implement the plan
152
+ 4. Execute read-only actions - DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)
153
+ 5. Save actions that have side effects for the final presentation phase when the winning agent will implement the plan
154
154
 
155
155
  Focus on planning, analysis, and coordination rather than execution.
@@ -67,7 +67,7 @@ orchestrator:
67
67
  2. Analyze other agents' proposals
68
68
  3. Use only the 'vote' or 'new_answer' tools for coordination
69
69
  4. You CAN use web search for information gathering
70
- 5. DO NOT execute Discord operations (sending messages, reading channels, etc.) - save these for execution phase
71
- 6. Save tool execution for the final presentation phase when the winning agent will implement the plan
70
+ 5. DO NOT execute any actions that have side effects (e.g., sending messages, modifying data)
71
+ 6. Save actions that have side effects for the final presentation phase when the winning agent will implement the plan
72
72
 
73
73
  Focus on planning, analysis, and coordination rather than execution.
@@ -0,0 +1,67 @@
1
+ # MassGen Voting Sensitivity Configuration Example
2
+ #
3
+ # How to run:
4
+ # massgen --config configs/voting/gemini_gpt_voting_sensitivity.yaml "Your question here"
5
+ #
6
+ # Or in interactive mode:
7
+ # massgen --config configs/voting/gemini_gpt_voting_sensitivity.yaml
8
+ #
9
+ # Try different sensitivity levels by changing the voting_sensitivity value below
10
+
11
+ agents:
12
+ - id: "gemini2.5flash"
13
+ backend:
14
+ type: "gemini"
15
+ model: "gemini-2.5-flash"
16
+ enable_web_search: true
17
+
18
+ - id: "gpt5nano"
19
+ backend:
20
+ type: "openai"
21
+ model: "gpt-5-nano"
22
+ text:
23
+ verbosity: "medium"
24
+ reasoning:
25
+ effort: "medium"
26
+ summary: "auto"
27
+ enable_web_search: true
28
+ enable_code_interpreter: true
29
+
30
+ orchestrator:
31
+ # voting_sensitivity controls how critical agents are when evaluating answers
32
+ #
33
+ # Options:
34
+ # - "lenient" (default): Agents vote for existing answers more readily, fewer new answers
35
+ # Use when you want faster convergence
36
+ #
37
+ # - "balanced": Agents apply detailed criteria (comprehensive, accurate, complete?) before voting,
38
+ # more new answers. Use when you want thorough evaluation
39
+ #
40
+ # - "strict": Agents apply high standards of excellence (all aspects, edge cases, reference-quality) before voting,
41
+ # most new answers. Use when you need maximum quality
42
+ #
43
+ voting_sensitivity: "balanced"
44
+
45
+ # max_new_answers_per_agent controls how many new answers each agent can provide
46
+ # Once an agent reaches this limit, they can only vote (not provide new answers)
47
+ #
48
+ # Options:
49
+ # - null (default): No limit - agents can provide unlimited new answers
50
+ # - 1, 2, 3, etc.: Cap the number of new answers per agent
51
+ #
52
+ # Example: Set to 2 for thorough evaluation but bounded duration
53
+ max_new_answers_per_agent: 2
54
+
55
+ # answer_novelty_requirement controls how different new answers must be from existing ones
56
+ #
57
+ # Options:
58
+ # - "lenient" (default): No additional checks - current behavior (fastest)
59
+ # - "balanced": Reject if >70% token overlap - requires meaningful differences
60
+ # - "strict": Reject if >50% token overlap - requires substantially different solutions
61
+ #
62
+ # Example: Use "balanced" to prevent agents from just rephrasing the same answer
63
+ answer_novelty_requirement: "balanced"
64
+
65
+ ui:
66
+ display_type: "rich_terminal"
67
+ logging_enabled: true
@@ -258,6 +258,110 @@ class ChatCompletionsFormatter(FormatterBase):
258
258
 
259
259
  return converted_tools
260
260
 
261
+ def format_custom_tools(self, custom_tools: Dict[str, Any]) -> List[Dict[str, Any]]:
262
+ """
263
+ Convert custom tools from RegisteredToolEntry format to Chat Completions API format.
264
+
265
+ Custom tools are provided as a dictionary where:
266
+ - Keys are tool names (str)
267
+ - Values are RegisteredToolEntry objects with:
268
+ - tool_name: str
269
+ - schema_def: dict with structure {"type": "function", "function": {...}}
270
+ - get_extended_schema: property that returns the schema with extensions
271
+
272
+ Chat Completions API expects: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
273
+
274
+ Args:
275
+ custom_tools: Dictionary of tool_name -> RegisteredToolEntry objects
276
+
277
+ Returns:
278
+ List of tools in Chat Completions API format
279
+ """
280
+ if not custom_tools:
281
+ return []
282
+
283
+ converted_tools = []
284
+
285
+ # Handle dictionary format: {tool_name: RegisteredToolEntry, ...}
286
+ if isinstance(custom_tools, dict):
287
+ for tool_name, tool_entry in custom_tools.items():
288
+ # Check if it's a RegisteredToolEntry object with schema_def
289
+ if hasattr(tool_entry, "schema_def"):
290
+ tool_schema = tool_entry.schema_def
291
+
292
+ # Schema may already be in Chat Completions format
293
+ if tool_schema.get("type") == "function" and "function" in tool_schema:
294
+ # Already in correct format, just append
295
+ converted_tools.append(tool_schema)
296
+ elif tool_schema.get("type") == "function":
297
+ # Response API format, need to wrap in function object
298
+ converted_tools.append(
299
+ {
300
+ "type": "function",
301
+ "function": {
302
+ "name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
303
+ "description": tool_schema.get("description", ""),
304
+ "parameters": tool_schema.get("parameters", {}),
305
+ },
306
+ },
307
+ )
308
+ # Check if it has get_extended_schema property
309
+ elif hasattr(tool_entry, "get_extended_schema"):
310
+ tool_schema = tool_entry.get_extended_schema
311
+
312
+ if tool_schema.get("type") == "function" and "function" in tool_schema:
313
+ # Already in correct format
314
+ converted_tools.append(tool_schema)
315
+ elif tool_schema.get("type") == "function":
316
+ # Response API format, need to wrap
317
+ converted_tools.append(
318
+ {
319
+ "type": "function",
320
+ "function": {
321
+ "name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
322
+ "description": tool_schema.get("description", ""),
323
+ "parameters": tool_schema.get("parameters", {}),
324
+ },
325
+ },
326
+ )
327
+ # Handle list format for backward compatibility
328
+ elif isinstance(custom_tools, list):
329
+ for tool in custom_tools:
330
+ if hasattr(tool, "schema_def"):
331
+ tool_schema = tool.schema_def
332
+
333
+ if tool_schema.get("type") == "function" and "function" in tool_schema:
334
+ converted_tools.append(tool_schema)
335
+ elif tool_schema.get("type") == "function":
336
+ converted_tools.append(
337
+ {
338
+ "type": "function",
339
+ "function": {
340
+ "name": tool_schema.get("name", tool.tool_name),
341
+ "description": tool_schema.get("description", ""),
342
+ "parameters": tool_schema.get("parameters", {}),
343
+ },
344
+ },
345
+ )
346
+ elif hasattr(tool, "get_extended_schema"):
347
+ tool_schema = tool.get_extended_schema
348
+
349
+ if tool_schema.get("type") == "function" and "function" in tool_schema:
350
+ converted_tools.append(tool_schema)
351
+ elif tool_schema.get("type") == "function":
352
+ converted_tools.append(
353
+ {
354
+ "type": "function",
355
+ "function": {
356
+ "name": tool_schema.get("name", tool.tool_name),
357
+ "description": tool_schema.get("description", ""),
358
+ "parameters": tool_schema.get("parameters", {}),
359
+ },
360
+ },
361
+ )
362
+
363
+ return converted_tools
364
+
261
365
  def format_mcp_tools(self, mcp_functions: Dict[str, Any]) -> List[Dict[str, Any]]:
262
366
  """Convert MCP tools to Chat Completions format."""
263
367
  if not mcp_functions:
@@ -233,3 +233,123 @@ class ClaudeFormatter(FormatterBase):
233
233
  converted_tools.append(tool)
234
234
 
235
235
  return converted_tools
236
+
237
+ def format_custom_tools(self, custom_tools: Dict[str, Any]) -> List[Dict[str, Any]]:
238
+ """
239
+ Convert custom tools from RegisteredToolEntry format to Claude's custom tool format.
240
+
241
+ Custom tools are provided as a dictionary where:
242
+ - Keys are tool names (str)
243
+ - Values are RegisteredToolEntry objects with:
244
+ - tool_name: str
245
+ - schema_def: dict with structure {"type": "function", "function": {...}}
246
+ - get_extended_schema: property that returns the schema with extensions
247
+
248
+ Claude expects: {"type": "custom", "name": ..., "description": ..., "input_schema": ...}
249
+
250
+ Args:
251
+ custom_tools: Dictionary of tool_name -> RegisteredToolEntry objects
252
+
253
+ Returns:
254
+ List of tools in Claude's custom tool format
255
+ """
256
+ if not custom_tools:
257
+ return []
258
+
259
+ converted_tools = []
260
+
261
+ # Handle dictionary format: {tool_name: RegisteredToolEntry, ...}
262
+ if isinstance(custom_tools, dict):
263
+ for tool_name, tool_entry in custom_tools.items():
264
+ # Check if it's a RegisteredToolEntry object with schema_def
265
+ if hasattr(tool_entry, "schema_def"):
266
+ tool_schema = tool_entry.schema_def
267
+
268
+ # Extract function details from Chat Completions format
269
+ if tool_schema.get("type") == "function" and "function" in tool_schema:
270
+ func = tool_schema["function"]
271
+ converted_tools.append(
272
+ {
273
+ "type": "custom",
274
+ "name": func.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
275
+ "description": func.get("description", ""),
276
+ "input_schema": func.get("parameters", {}),
277
+ },
278
+ )
279
+ elif tool_schema.get("type") == "function":
280
+ # Response API format - already has name, description, parameters at top level
281
+ converted_tools.append(
282
+ {
283
+ "type": "custom",
284
+ "name": tool_schema.get("name", tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name),
285
+ "description": tool_schema.get("description", ""),
286
+ "input_schema": tool_schema.get("parameters", {}),
287
+ },
288
+ )
289
+ else:
290
+ # Unknown format, try to extract what we can
291
+ converted_tools.append(
292
+ {
293
+ "type": "custom",
294
+ "name": tool_entry.tool_name if hasattr(tool_entry, "tool_name") else tool_name,
295
+ "description": tool_schema.get("description", ""),
296
+ "input_schema": tool_schema.get("parameters", {}),
297
+ },
298
+ )
299
+ # Handle direct schema format (for backward compatibility)
300
+ elif isinstance(tool_entry, dict):
301
+ if tool_entry.get("type") == "function" and "function" in tool_entry:
302
+ # Chat Completions format
303
+ func = tool_entry["function"]
304
+ converted_tools.append(
305
+ {
306
+ "type": "custom",
307
+ "name": func.get("name", tool_name),
308
+ "description": func.get("description", ""),
309
+ "input_schema": func.get("parameters", {}),
310
+ },
311
+ )
312
+ elif tool_entry.get("type") == "function":
313
+ # Response API format
314
+ converted_tools.append(
315
+ {
316
+ "type": "custom",
317
+ "name": tool_entry.get("name", tool_name),
318
+ "description": tool_entry.get("description", ""),
319
+ "input_schema": tool_entry.get("parameters", {}),
320
+ },
321
+ )
322
+ else:
323
+ # Already in Claude format or unknown
324
+ converted_tools.append(tool_entry)
325
+
326
+ # Handle list format (if custom_tools is already a list)
327
+ elif isinstance(custom_tools, list):
328
+ for tool in custom_tools:
329
+ if isinstance(tool, dict):
330
+ if tool.get("type") == "function" and "function" in tool:
331
+ # Chat Completions format
332
+ func = tool["function"]
333
+ converted_tools.append(
334
+ {
335
+ "type": "custom",
336
+ "name": func.get("name", ""),
337
+ "description": func.get("description", ""),
338
+ "input_schema": func.get("parameters", {}),
339
+ },
340
+ )
341
+ elif tool.get("type") == "function":
342
+ # Response API format
343
+ converted_tools.append(
344
+ {
345
+ "type": "custom",
346
+ "name": tool.get("name", ""),
347
+ "description": tool.get("description", ""),
348
+ "input_schema": tool.get("parameters", {}),
349
+ },
350
+ )
351
+ else:
352
+ # Already in Claude format or unknown
353
+ converted_tools.append(tool)
354
+
355
+ return converted_tools