massgen 0.0.3__py3-none-any.whl → 0.1.0__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.
- massgen/__init__.py +142 -8
- massgen/adapters/__init__.py +29 -0
- massgen/adapters/ag2_adapter.py +483 -0
- massgen/adapters/base.py +183 -0
- massgen/adapters/tests/__init__.py +0 -0
- massgen/adapters/tests/test_ag2_adapter.py +439 -0
- massgen/adapters/tests/test_agent_adapter.py +128 -0
- massgen/adapters/utils/__init__.py +2 -0
- massgen/adapters/utils/ag2_utils.py +236 -0
- massgen/adapters/utils/tests/__init__.py +0 -0
- massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
- massgen/agent_config.py +329 -55
- massgen/api_params_handler/__init__.py +10 -0
- massgen/api_params_handler/_api_params_handler_base.py +99 -0
- massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
- massgen/api_params_handler/_claude_api_params_handler.py +113 -0
- massgen/api_params_handler/_response_api_params_handler.py +130 -0
- massgen/backend/__init__.py +39 -4
- massgen/backend/azure_openai.py +385 -0
- massgen/backend/base.py +341 -69
- massgen/backend/base_with_mcp.py +1102 -0
- massgen/backend/capabilities.py +386 -0
- massgen/backend/chat_completions.py +577 -130
- massgen/backend/claude.py +1033 -537
- massgen/backend/claude_code.py +1203 -0
- massgen/backend/cli_base.py +209 -0
- massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
- massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
- massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
- massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
- massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
- massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
- massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
- massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
- massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
- massgen/backend/docs/inference_backend.md +257 -0
- massgen/backend/docs/permissions_and_context_files.md +1085 -0
- massgen/backend/external.py +126 -0
- massgen/backend/gemini.py +1850 -241
- massgen/backend/grok.py +40 -156
- massgen/backend/inference.py +156 -0
- massgen/backend/lmstudio.py +171 -0
- massgen/backend/response.py +1095 -322
- massgen/chat_agent.py +131 -113
- massgen/cli.py +1560 -275
- massgen/config_builder.py +2396 -0
- massgen/configs/BACKEND_CONFIGURATION.md +458 -0
- massgen/configs/README.md +559 -216
- massgen/configs/ag2/ag2_case_study.yaml +27 -0
- massgen/configs/ag2/ag2_coder.yaml +34 -0
- massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
- massgen/configs/ag2/ag2_gemini.yaml +27 -0
- massgen/configs/ag2/ag2_groupchat.yaml +108 -0
- massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
- massgen/configs/ag2/ag2_single_agent.yaml +21 -0
- massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
- massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
- massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
- massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
- massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
- massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
- massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
- massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
- massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
- massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
- massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
- massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
- massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
- massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
- massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
- massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
- massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
- massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
- massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
- massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
- massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
- massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
- massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
- massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
- massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
- massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
- massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
- massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
- massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
- massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
- massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
- massgen/configs/debug/skip_coordination_test.yaml +27 -0
- massgen/configs/debug/test_sdk_migration.yaml +17 -0
- massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
- massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
- massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
- massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
- massgen/configs/providers/claude/claude.yaml +14 -0
- massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
- massgen/configs/providers/local/lmstudio.yaml +11 -0
- massgen/configs/providers/openai/gpt5.yaml +46 -0
- massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
- massgen/configs/providers/others/grok_single_agent.yaml +19 -0
- massgen/configs/providers/others/zai_coding_team.yaml +108 -0
- massgen/configs/providers/others/zai_glm45.yaml +12 -0
- massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
- massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
- massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
- massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
- massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
- massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
- massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
- massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
- massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
- massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
- massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
- massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
- massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
- massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
- massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
- massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
- massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
- massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
- massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
- massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
- massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
- massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
- massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
- massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
- massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
- massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
- massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
- massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
- massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
- massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
- massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
- massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
- massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
- massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
- massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
- massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
- massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
- massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
- massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
- massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
- massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
- massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
- massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
- massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
- massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
- massgen/coordination_tracker.py +708 -0
- massgen/docker/README.md +462 -0
- massgen/filesystem_manager/__init__.py +21 -0
- massgen/filesystem_manager/_base.py +9 -0
- massgen/filesystem_manager/_code_execution_server.py +545 -0
- massgen/filesystem_manager/_docker_manager.py +477 -0
- massgen/filesystem_manager/_file_operation_tracker.py +248 -0
- massgen/filesystem_manager/_filesystem_manager.py +813 -0
- massgen/filesystem_manager/_path_permission_manager.py +1261 -0
- massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
- massgen/formatter/__init__.py +10 -0
- massgen/formatter/_chat_completions_formatter.py +284 -0
- massgen/formatter/_claude_formatter.py +235 -0
- massgen/formatter/_formatter_base.py +156 -0
- massgen/formatter/_response_formatter.py +263 -0
- massgen/frontend/__init__.py +1 -2
- massgen/frontend/coordination_ui.py +471 -286
- massgen/frontend/displays/base_display.py +56 -11
- massgen/frontend/displays/create_coordination_table.py +1956 -0
- massgen/frontend/displays/rich_terminal_display.py +1259 -619
- massgen/frontend/displays/simple_display.py +9 -4
- massgen/frontend/displays/terminal_display.py +27 -68
- massgen/logger_config.py +681 -0
- massgen/mcp_tools/README.md +232 -0
- massgen/mcp_tools/__init__.py +105 -0
- massgen/mcp_tools/backend_utils.py +1035 -0
- massgen/mcp_tools/circuit_breaker.py +195 -0
- massgen/mcp_tools/client.py +894 -0
- massgen/mcp_tools/config_validator.py +138 -0
- massgen/mcp_tools/docs/circuit_breaker.md +646 -0
- massgen/mcp_tools/docs/client.md +950 -0
- massgen/mcp_tools/docs/config_validator.md +478 -0
- massgen/mcp_tools/docs/exceptions.md +1165 -0
- massgen/mcp_tools/docs/security.md +854 -0
- massgen/mcp_tools/exceptions.py +338 -0
- massgen/mcp_tools/hooks.py +212 -0
- massgen/mcp_tools/security.py +780 -0
- massgen/message_templates.py +342 -64
- massgen/orchestrator.py +1515 -241
- massgen/stream_chunk/__init__.py +35 -0
- massgen/stream_chunk/base.py +92 -0
- massgen/stream_chunk/multimodal.py +237 -0
- massgen/stream_chunk/text.py +162 -0
- massgen/tests/mcp_test_server.py +150 -0
- massgen/tests/multi_turn_conversation_design.md +0 -8
- massgen/tests/test_azure_openai_backend.py +156 -0
- massgen/tests/test_backend_capabilities.py +262 -0
- massgen/tests/test_backend_event_loop_all.py +179 -0
- massgen/tests/test_chat_completions_refactor.py +142 -0
- massgen/tests/test_claude_backend.py +15 -28
- massgen/tests/test_claude_code.py +268 -0
- massgen/tests/test_claude_code_context_sharing.py +233 -0
- massgen/tests/test_claude_code_orchestrator.py +175 -0
- massgen/tests/test_cli_backends.py +180 -0
- massgen/tests/test_code_execution.py +679 -0
- massgen/tests/test_external_agent_backend.py +134 -0
- massgen/tests/test_final_presentation_fallback.py +237 -0
- massgen/tests/test_gemini_planning_mode.py +351 -0
- massgen/tests/test_grok_backend.py +7 -10
- massgen/tests/test_http_mcp_server.py +42 -0
- massgen/tests/test_integration_simple.py +198 -0
- massgen/tests/test_mcp_blocking.py +125 -0
- massgen/tests/test_message_context_building.py +29 -47
- massgen/tests/test_orchestrator_final_presentation.py +48 -0
- massgen/tests/test_path_permission_manager.py +2087 -0
- massgen/tests/test_rich_terminal_display.py +14 -13
- massgen/tests/test_timeout.py +133 -0
- massgen/tests/test_v3_3agents.py +11 -12
- massgen/tests/test_v3_simple.py +8 -13
- massgen/tests/test_v3_three_agents.py +11 -18
- massgen/tests/test_v3_two_agents.py +8 -13
- massgen/token_manager/__init__.py +7 -0
- massgen/token_manager/token_manager.py +400 -0
- massgen/utils.py +52 -16
- massgen/v1/agent.py +45 -91
- massgen/v1/agents.py +18 -53
- massgen/v1/backends/gemini.py +50 -153
- massgen/v1/backends/grok.py +21 -54
- massgen/v1/backends/oai.py +39 -111
- massgen/v1/cli.py +36 -93
- massgen/v1/config.py +8 -12
- massgen/v1/logging.py +43 -127
- massgen/v1/main.py +18 -32
- massgen/v1/orchestrator.py +68 -209
- massgen/v1/streaming_display.py +62 -163
- massgen/v1/tools.py +8 -12
- massgen/v1/types.py +9 -23
- massgen/v1/utils.py +5 -23
- massgen-0.1.0.dist-info/METADATA +1245 -0
- massgen-0.1.0.dist-info/RECORD +273 -0
- massgen-0.1.0.dist-info/entry_points.txt +2 -0
- massgen/frontend/logging/__init__.py +0 -9
- massgen/frontend/logging/realtime_logger.py +0 -197
- massgen-0.0.3.dist-info/METADATA +0 -568
- massgen-0.0.3.dist-info/RECORD +0 -76
- massgen-0.0.3.dist-info/entry_points.txt +0 -2
- /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Token and Cost Management Module
|
|
4
|
+
Provides unified token estimation and cost calculation for all backends.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from ..logger_config import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TokenUsage:
|
|
17
|
+
"""Token usage and cost tracking."""
|
|
18
|
+
|
|
19
|
+
input_tokens: int = 0
|
|
20
|
+
output_tokens: int = 0
|
|
21
|
+
estimated_cost: float = 0.0
|
|
22
|
+
|
|
23
|
+
def add(self, other: "TokenUsage"):
|
|
24
|
+
"""Add another TokenUsage to this one."""
|
|
25
|
+
self.input_tokens += other.input_tokens
|
|
26
|
+
self.output_tokens += other.output_tokens
|
|
27
|
+
self.estimated_cost += other.estimated_cost
|
|
28
|
+
|
|
29
|
+
def reset(self):
|
|
30
|
+
"""Reset all counters to zero."""
|
|
31
|
+
self.input_tokens = 0
|
|
32
|
+
self.output_tokens = 0
|
|
33
|
+
self.estimated_cost = 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ModelPricing:
|
|
38
|
+
"""Pricing information for a model."""
|
|
39
|
+
|
|
40
|
+
input_cost_per_1k: float # Cost per 1000 input tokens
|
|
41
|
+
output_cost_per_1k: float # Cost per 1000 output tokens
|
|
42
|
+
context_window: Optional[int] = None
|
|
43
|
+
max_output_tokens: Optional[int] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TokenCostCalculator:
|
|
47
|
+
"""Unified token estimation and cost calculation."""
|
|
48
|
+
|
|
49
|
+
# Default pricing data for various providers and models
|
|
50
|
+
PROVIDER_PRICING: Dict[str, Dict[str, ModelPricing]] = {
|
|
51
|
+
"OpenAI": {
|
|
52
|
+
"gpt-4o": ModelPricing(0.0025, 0.01, 128000, 16384),
|
|
53
|
+
"gpt-4o-mini": ModelPricing(0.00015, 0.0006, 128000, 16384),
|
|
54
|
+
"gpt-4-turbo": ModelPricing(0.01, 0.03, 128000, 4096),
|
|
55
|
+
"gpt-4": ModelPricing(0.03, 0.06, 8192, 8192),
|
|
56
|
+
"gpt-3.5-turbo": ModelPricing(0.0005, 0.0015, 16385, 4096),
|
|
57
|
+
"o1-preview": ModelPricing(0.015, 0.06, 128000, 32768),
|
|
58
|
+
"o1-mini": ModelPricing(0.003, 0.012, 128000, 65536),
|
|
59
|
+
"o3-mini": ModelPricing(0.0011, 0.0044, 200000, 100000),
|
|
60
|
+
},
|
|
61
|
+
"Anthropic": {
|
|
62
|
+
"claude-3-5-sonnet": ModelPricing(0.003, 0.015, 200000, 8192),
|
|
63
|
+
"claude-3-5-haiku": ModelPricing(0.001, 0.005, 200000, 8192),
|
|
64
|
+
"claude-3-opus": ModelPricing(0.015, 0.075, 200000, 4096),
|
|
65
|
+
"claude-3-sonnet": ModelPricing(0.003, 0.015, 200000, 4096),
|
|
66
|
+
"claude-3-haiku": ModelPricing(0.00025, 0.00125, 200000, 4096),
|
|
67
|
+
},
|
|
68
|
+
"Google": {
|
|
69
|
+
"gemini-2.0-flash-exp": ModelPricing(0.0, 0.0, 1048576, 8192), # Free during experimental
|
|
70
|
+
"gemini-2.0-flash-thinking-exp": ModelPricing(0.0, 0.0, 32767, 8192),
|
|
71
|
+
"gemini-1.5-pro": ModelPricing(0.00125, 0.005, 2097152, 8192),
|
|
72
|
+
"gemini-1.5-flash": ModelPricing(0.000075, 0.0003, 1048576, 8192),
|
|
73
|
+
"gemini-1.5-flash-8b": ModelPricing(0.0000375, 0.00015, 1048576, 8192),
|
|
74
|
+
"gemini-1.0-pro": ModelPricing(0.00025, 0.00125, 32760, 8192),
|
|
75
|
+
},
|
|
76
|
+
"Cerebras": {
|
|
77
|
+
"llama3.3-70b": ModelPricing(0.00035, 0.00035, 128000, 8192),
|
|
78
|
+
"llama3.1-70b": ModelPricing(0.00035, 0.00035, 128000, 8192),
|
|
79
|
+
"llama3.1-8b": ModelPricing(0.00001, 0.00001, 128000, 8192),
|
|
80
|
+
},
|
|
81
|
+
"Together": {
|
|
82
|
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo": ModelPricing(0.00059, 0.00079, 128000, 32768),
|
|
83
|
+
"meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo": ModelPricing(0.00059, 0.00079, 128000, 32768),
|
|
84
|
+
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": ModelPricing(0.00088, 0.00088, 130000, 4096),
|
|
85
|
+
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": ModelPricing(0.00018, 0.00018, 131072, 65536),
|
|
86
|
+
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": ModelPricing(0.00006, 0.00006, 131072, 16384),
|
|
87
|
+
"Qwen/QwQ-32B-Preview": ModelPricing(0.00015, 0.00015, 32768, 32768),
|
|
88
|
+
"Qwen/Qwen2.5-72B-Instruct-Turbo": ModelPricing(0.00012, 0.00012, 32768, 8192),
|
|
89
|
+
"mistralai/Mixtral-8x22B-Instruct-v0.1": ModelPricing(0.0009, 0.0009, 65536, 65536),
|
|
90
|
+
"deepseek-ai/deepseek-r1-distill-llama-70b": ModelPricing(0.00015, 0.00015, 65536, 8192),
|
|
91
|
+
},
|
|
92
|
+
"Fireworks": {
|
|
93
|
+
"llama-3.3-70b": ModelPricing(0.0002, 0.0002, 128000, 16384),
|
|
94
|
+
"llama-3.1-405b": ModelPricing(0.0009, 0.0009, 131072, 16384),
|
|
95
|
+
"llama-3.1-70b": ModelPricing(0.0002, 0.0002, 131072, 16384),
|
|
96
|
+
"llama-3.1-8b": ModelPricing(0.00002, 0.00002, 131072, 16384),
|
|
97
|
+
"qwen2.5-72b": ModelPricing(0.0002, 0.0002, 32768, 16384),
|
|
98
|
+
},
|
|
99
|
+
"Groq": {
|
|
100
|
+
"llama-3.3-70b-versatile": ModelPricing(0.00059, 0.00079, 128000, 32768),
|
|
101
|
+
"llama-3.1-70b-versatile": ModelPricing(0.00059, 0.00079, 131072, 8000),
|
|
102
|
+
"llama-3.1-8b-instant": ModelPricing(0.00005, 0.00008, 131072, 8000),
|
|
103
|
+
"mixtral-8x7b-32768": ModelPricing(0.00024, 0.00024, 32768, 32768),
|
|
104
|
+
},
|
|
105
|
+
"xAI": {
|
|
106
|
+
"grok-2-latest": ModelPricing(0.005, 0.015, 131072, 131072),
|
|
107
|
+
"grok-2": ModelPricing(0.005, 0.015, 131072, 131072),
|
|
108
|
+
"grok-2-mini": ModelPricing(0.001, 0.003, 131072, 65536),
|
|
109
|
+
},
|
|
110
|
+
"DeepSeek": {
|
|
111
|
+
"deepseek-reasoner": ModelPricing(0.00014, 0.0028, 163840, 8192),
|
|
112
|
+
"deepseek-chat": ModelPricing(0.00014, 0.00028, 64000, 8192),
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
"""Initialize the calculator with optional tiktoken for accurate estimation."""
|
|
118
|
+
self.tiktoken_encoder = None
|
|
119
|
+
self._try_init_tiktoken()
|
|
120
|
+
|
|
121
|
+
def _try_init_tiktoken(self):
|
|
122
|
+
"""Try to initialize tiktoken encoder for more accurate token counting."""
|
|
123
|
+
try:
|
|
124
|
+
import tiktoken
|
|
125
|
+
|
|
126
|
+
# Use cl100k_base encoder (GPT-4/GPT-3.5-turbo tokenizer)
|
|
127
|
+
self.tiktoken_encoder = tiktoken.get_encoding("cl100k_base")
|
|
128
|
+
logger.debug("Tiktoken encoder initialized for accurate token counting")
|
|
129
|
+
except ImportError:
|
|
130
|
+
logger.debug("Tiktoken not available, using simple estimation")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
logger.warning(f"Failed to initialize tiktoken: {e}")
|
|
133
|
+
|
|
134
|
+
def estimate_tokens(self, text: Union[str, List[Dict[str, Any]]], method: str = "auto") -> int:
|
|
135
|
+
"""
|
|
136
|
+
Estimate token count for text or messages.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
text: Text string or list of message dictionaries
|
|
140
|
+
method: Estimation method ("tiktoken", "simple", "auto")
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Estimated token count
|
|
144
|
+
"""
|
|
145
|
+
# Convert messages to text if needed
|
|
146
|
+
if isinstance(text, list):
|
|
147
|
+
text = self._messages_to_text(text)
|
|
148
|
+
|
|
149
|
+
if method == "auto":
|
|
150
|
+
# Use tiktoken if available, otherwise simple
|
|
151
|
+
if self.tiktoken_encoder:
|
|
152
|
+
return self.estimate_tokens_tiktoken(text)
|
|
153
|
+
else:
|
|
154
|
+
return self.estimate_tokens_simple(text)
|
|
155
|
+
elif method == "tiktoken":
|
|
156
|
+
return self.estimate_tokens_tiktoken(text)
|
|
157
|
+
else:
|
|
158
|
+
return self.estimate_tokens_simple(text)
|
|
159
|
+
|
|
160
|
+
def estimate_tokens_tiktoken(self, text: str) -> int:
|
|
161
|
+
"""
|
|
162
|
+
Estimate tokens using tiktoken (OpenAI's tokenizer).
|
|
163
|
+
Most accurate for OpenAI models.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
text: Text to estimate
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Token count
|
|
170
|
+
"""
|
|
171
|
+
if not self.tiktoken_encoder:
|
|
172
|
+
logger.warning("Tiktoken not available, falling back to simple estimation")
|
|
173
|
+
return self.estimate_tokens_simple(text)
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
tokens = self.tiktoken_encoder.encode(text)
|
|
177
|
+
return len(tokens)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.warning(f"Tiktoken encoding failed: {e}, using simple estimation")
|
|
180
|
+
return self.estimate_tokens_simple(text)
|
|
181
|
+
|
|
182
|
+
def estimate_tokens_simple(self, text: str) -> int:
|
|
183
|
+
"""
|
|
184
|
+
Simple token estimation based on character/word count.
|
|
185
|
+
Roughly 1 token ≈ 4 characters or 0.75 words.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
text: Text to estimate
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Estimated token count
|
|
192
|
+
"""
|
|
193
|
+
# Method 1: Character-based (1 token ≈ 4 characters)
|
|
194
|
+
char_estimate = len(text) / 4
|
|
195
|
+
|
|
196
|
+
# Method 2: Word-based (1 token ≈ 0.75 words)
|
|
197
|
+
words = text.split()
|
|
198
|
+
word_estimate = len(words) / 0.75
|
|
199
|
+
|
|
200
|
+
# Take average of both methods for better accuracy
|
|
201
|
+
estimate = (char_estimate + word_estimate) / 2
|
|
202
|
+
|
|
203
|
+
return int(estimate)
|
|
204
|
+
|
|
205
|
+
def _messages_to_text(self, messages: List[Dict[str, Any]]) -> str:
|
|
206
|
+
"""Convert message list to text for token estimation."""
|
|
207
|
+
text_parts = []
|
|
208
|
+
|
|
209
|
+
for msg in messages:
|
|
210
|
+
role = msg.get("role", "")
|
|
211
|
+
content = msg.get("content", "")
|
|
212
|
+
|
|
213
|
+
# Handle different content types
|
|
214
|
+
if isinstance(content, str):
|
|
215
|
+
text_parts.append(f"{role}: {content}")
|
|
216
|
+
elif isinstance(content, list):
|
|
217
|
+
# Handle structured content (like Claude's format)
|
|
218
|
+
for item in content:
|
|
219
|
+
if isinstance(item, dict):
|
|
220
|
+
if item.get("type") == "text":
|
|
221
|
+
text_parts.append(f"{role}: {item.get('text', '')}")
|
|
222
|
+
elif item.get("type") == "tool_result":
|
|
223
|
+
text_parts.append(f"tool_result: {item.get('content', '')}")
|
|
224
|
+
else:
|
|
225
|
+
text_parts.append(f"{role}: {str(item)}")
|
|
226
|
+
else:
|
|
227
|
+
text_parts.append(f"{role}: {str(content)}")
|
|
228
|
+
|
|
229
|
+
# Add tool calls if present
|
|
230
|
+
if "tool_calls" in msg:
|
|
231
|
+
tool_calls = msg["tool_calls"]
|
|
232
|
+
if isinstance(tool_calls, list):
|
|
233
|
+
for call in tool_calls:
|
|
234
|
+
text_parts.append(f"tool_call: {str(call)}")
|
|
235
|
+
|
|
236
|
+
return "\n".join(text_parts)
|
|
237
|
+
|
|
238
|
+
def get_model_pricing(self, provider: str, model: str) -> Optional[ModelPricing]:
|
|
239
|
+
"""
|
|
240
|
+
Get pricing information for a specific model.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
provider: Provider name (e.g., "OpenAI", "Anthropic")
|
|
244
|
+
model: Model name or identifier
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
ModelPricing object or None if not found
|
|
248
|
+
"""
|
|
249
|
+
# Normalize provider name
|
|
250
|
+
provider = self._normalize_provider(provider)
|
|
251
|
+
|
|
252
|
+
# Get provider pricing data
|
|
253
|
+
provider_models = self.PROVIDER_PRICING.get(provider, {})
|
|
254
|
+
|
|
255
|
+
# Try exact match first
|
|
256
|
+
if model in provider_models:
|
|
257
|
+
return provider_models[model]
|
|
258
|
+
|
|
259
|
+
# Try to find by partial match
|
|
260
|
+
for model_key, pricing in provider_models.items():
|
|
261
|
+
if model_key.lower() in model.lower() or model.lower() in model_key.lower():
|
|
262
|
+
return pricing
|
|
263
|
+
|
|
264
|
+
# Try to infer from model name patterns
|
|
265
|
+
model_lower = model.lower()
|
|
266
|
+
|
|
267
|
+
# GPT-4 variants
|
|
268
|
+
if "gpt-4o" in model_lower and "mini" in model_lower:
|
|
269
|
+
return provider_models.get("gpt-4o-mini")
|
|
270
|
+
elif "gpt-4o" in model_lower:
|
|
271
|
+
return provider_models.get("gpt-4o")
|
|
272
|
+
elif "gpt-4" in model_lower and "turbo" in model_lower:
|
|
273
|
+
return provider_models.get("gpt-4-turbo")
|
|
274
|
+
elif "gpt-4" in model_lower:
|
|
275
|
+
return provider_models.get("gpt-4")
|
|
276
|
+
elif "gpt-3.5" in model_lower:
|
|
277
|
+
return provider_models.get("gpt-3.5-turbo")
|
|
278
|
+
|
|
279
|
+
# Claude variants
|
|
280
|
+
elif "claude-3-5-sonnet" in model_lower or "claude-3.5-sonnet" in model_lower:
|
|
281
|
+
return provider_models.get("claude-3-5-sonnet")
|
|
282
|
+
elif "claude-3-5-haiku" in model_lower or "claude-3.5-haiku" in model_lower:
|
|
283
|
+
return provider_models.get("claude-3-5-haiku")
|
|
284
|
+
elif "claude-3-opus" in model_lower:
|
|
285
|
+
return provider_models.get("claude-3-opus")
|
|
286
|
+
elif "claude-3-sonnet" in model_lower:
|
|
287
|
+
return provider_models.get("claude-3-sonnet")
|
|
288
|
+
elif "claude-3-haiku" in model_lower:
|
|
289
|
+
return provider_models.get("claude-3-haiku")
|
|
290
|
+
|
|
291
|
+
# Gemini variants
|
|
292
|
+
elif "gemini-2" in model_lower and "flash" in model_lower:
|
|
293
|
+
return provider_models.get("gemini-2.0-flash-exp")
|
|
294
|
+
elif "gemini-1.5-pro" in model_lower:
|
|
295
|
+
return provider_models.get("gemini-1.5-pro")
|
|
296
|
+
elif "gemini-1.5-flash" in model_lower:
|
|
297
|
+
return provider_models.get("gemini-1.5-flash")
|
|
298
|
+
|
|
299
|
+
logger.debug(f"No pricing found for {provider}/{model}")
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _normalize_provider(self, provider: str) -> str:
|
|
303
|
+
"""Normalize provider name for lookup."""
|
|
304
|
+
provider_map = {
|
|
305
|
+
"openai": "OpenAI",
|
|
306
|
+
"anthropic": "Anthropic",
|
|
307
|
+
"claude": "Anthropic",
|
|
308
|
+
"google": "Google",
|
|
309
|
+
"gemini": "Google",
|
|
310
|
+
"vertex": "Google",
|
|
311
|
+
"cerebras": "Cerebras",
|
|
312
|
+
"cerebras ai": "Cerebras",
|
|
313
|
+
"together": "Together",
|
|
314
|
+
"together ai": "Together",
|
|
315
|
+
"fireworks": "Fireworks",
|
|
316
|
+
"fireworks ai": "Fireworks",
|
|
317
|
+
"groq": "Groq",
|
|
318
|
+
"xai": "xAI",
|
|
319
|
+
"x.ai": "xAI",
|
|
320
|
+
"grok": "xAI",
|
|
321
|
+
"deepseek": "DeepSeek",
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
provider_lower = provider.lower()
|
|
325
|
+
return provider_map.get(provider_lower, provider)
|
|
326
|
+
|
|
327
|
+
def calculate_cost(self, input_tokens: int, output_tokens: int, provider: str, model: str) -> float:
|
|
328
|
+
"""
|
|
329
|
+
Calculate cost for token usage.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
input_tokens: Number of input tokens
|
|
333
|
+
output_tokens: Number of output tokens
|
|
334
|
+
provider: Provider name
|
|
335
|
+
model: Model name
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Estimated cost in USD
|
|
339
|
+
"""
|
|
340
|
+
pricing = self.get_model_pricing(provider, model)
|
|
341
|
+
|
|
342
|
+
if not pricing:
|
|
343
|
+
logger.debug(f"No pricing for {provider}/{model}, returning 0")
|
|
344
|
+
return 0.0
|
|
345
|
+
|
|
346
|
+
# Calculate costs (prices are per 1000 tokens)
|
|
347
|
+
input_cost = (input_tokens / 1000) * pricing.input_cost_per_1k
|
|
348
|
+
output_cost = (output_tokens / 1000) * pricing.output_cost_per_1k
|
|
349
|
+
|
|
350
|
+
total_cost = input_cost + output_cost
|
|
351
|
+
|
|
352
|
+
logger.debug(
|
|
353
|
+
f"Cost calculation for {provider}/{model}: "
|
|
354
|
+
f"{input_tokens} input @ ${pricing.input_cost_per_1k}/1k = ${input_cost:.4f}, "
|
|
355
|
+
f"{output_tokens} output @ ${pricing.output_cost_per_1k}/1k = ${output_cost:.4f}, "
|
|
356
|
+
f"total = ${total_cost:.4f}",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return total_cost
|
|
360
|
+
|
|
361
|
+
def update_token_usage(self, usage: TokenUsage, messages: List[Dict[str, Any]], response_content: str, provider: str, model: str) -> TokenUsage:
|
|
362
|
+
"""
|
|
363
|
+
Update token usage with new conversation turn.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
usage: Existing TokenUsage to update
|
|
367
|
+
messages: Input messages
|
|
368
|
+
response_content: Response content
|
|
369
|
+
provider: Provider name
|
|
370
|
+
model: Model name
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Updated TokenUsage object
|
|
374
|
+
"""
|
|
375
|
+
# Estimate tokens
|
|
376
|
+
input_tokens = self.estimate_tokens(messages)
|
|
377
|
+
output_tokens = self.estimate_tokens(response_content)
|
|
378
|
+
|
|
379
|
+
# Calculate cost
|
|
380
|
+
cost = self.calculate_cost(input_tokens, output_tokens, provider, model)
|
|
381
|
+
|
|
382
|
+
# Update usage
|
|
383
|
+
usage.input_tokens += input_tokens
|
|
384
|
+
usage.output_tokens += output_tokens
|
|
385
|
+
usage.estimated_cost += cost
|
|
386
|
+
|
|
387
|
+
return usage
|
|
388
|
+
|
|
389
|
+
def format_cost(self, cost: float) -> str:
|
|
390
|
+
"""Format cost for display."""
|
|
391
|
+
if cost < 0.01:
|
|
392
|
+
return f"${cost:.4f}"
|
|
393
|
+
elif cost < 1.0:
|
|
394
|
+
return f"${cost:.3f}"
|
|
395
|
+
else:
|
|
396
|
+
return f"${cost:.2f}"
|
|
397
|
+
|
|
398
|
+
def format_usage_summary(self, usage: TokenUsage) -> str:
|
|
399
|
+
"""Format token usage summary for display."""
|
|
400
|
+
return f"Tokens: {usage.input_tokens:,} input, " f"{usage.output_tokens:,} output, " f"Cost: {self.format_cost(usage.estimated_cost)}"
|
massgen/utils.py
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ActionType(Enum):
|
|
6
|
+
"""All types of actions an agent can take -- TODO: Note this is also in masgen/backend/gemini.py; we should have an enums or utils file with this."""
|
|
7
|
+
|
|
8
|
+
NEW_ANSWER = "answer"
|
|
9
|
+
VOTE = "vote"
|
|
10
|
+
VOTE_IGNORED = "vote_ignored" # Vote was cast but ignored due to restart
|
|
11
|
+
ERROR = "error"
|
|
12
|
+
TIMEOUT = "timeout"
|
|
13
|
+
CANCELLED = "cancelled"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentStatus(Enum):
|
|
17
|
+
"""All types of states an agent can be in during coordination."""
|
|
18
|
+
|
|
19
|
+
STREAMING = "streaming" # actively streaming content/reasoning
|
|
20
|
+
VOTED = "voted" # has cast their vote for this round
|
|
21
|
+
ANSWERED = "answered" # has provided an answer this round
|
|
22
|
+
RESTARTING = "restarting" # restarting due to new answer from another agent
|
|
23
|
+
ERROR = "error" # encountered an error
|
|
24
|
+
TIMEOUT = "timeout" # timed out
|
|
25
|
+
COMPLETED = "completed" # finished all work -- will not be called again
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CoordinationStage(Enum):
|
|
29
|
+
"""Stages of the coordination process."""
|
|
30
|
+
|
|
31
|
+
INITIAL_ANSWER = "initial_answer" # initial answer generation
|
|
32
|
+
ENFORCEMENT = "enforcement"
|
|
33
|
+
PRESENTATION = "presentation"
|
|
34
|
+
|
|
4
35
|
|
|
5
36
|
MODEL_MAPPINGS = {
|
|
6
37
|
"openai": [
|
|
38
|
+
# GPT-5 variants
|
|
39
|
+
"gpt-5",
|
|
40
|
+
"gpt-5-mini",
|
|
41
|
+
"gpt-5-nano",
|
|
42
|
+
"gpt-5-codex",
|
|
7
43
|
# GPT-4.1 variants
|
|
8
44
|
"gpt-4.1",
|
|
9
45
|
"gpt-4.1-mini",
|
|
46
|
+
"gpt-4.1-nano",
|
|
10
47
|
# GPT-4o variants
|
|
11
48
|
"gpt-4o-mini",
|
|
12
49
|
"gpt-4o",
|
|
13
|
-
# o1
|
|
14
|
-
"o1", # -> o1-2024-12-17
|
|
15
50
|
# o3
|
|
16
51
|
"o3",
|
|
17
52
|
"o3-low",
|
|
@@ -29,22 +64,19 @@ MODEL_MAPPINGS = {
|
|
|
29
64
|
"o4-mini-high",
|
|
30
65
|
],
|
|
31
66
|
"claude": [
|
|
67
|
+
# Claude 4.5 variants
|
|
68
|
+
"claude-sonnet-4-5-20250929",
|
|
69
|
+
# Claude 4 variants
|
|
70
|
+
"claude-opus-4-1-20250805",
|
|
71
|
+
"claude-opus-4-20250514",
|
|
72
|
+
"claude-sonnet-4-20250514",
|
|
32
73
|
# Claude 3.5 variants
|
|
33
74
|
"claude-3-5-sonnet-latest",
|
|
34
75
|
"claude-3-5-haiku-latest",
|
|
35
|
-
"claude-3-5-sonnet-
|
|
76
|
+
"claude-3-5-sonnet-20250114",
|
|
36
77
|
"claude-3-5-haiku-20241022",
|
|
37
78
|
# Claude 3 variants
|
|
38
|
-
"claude-3-sonnet-20240229",
|
|
39
79
|
"claude-3-opus-20240229",
|
|
40
|
-
"claude-3-haiku-20240307",
|
|
41
|
-
# Claude 2 variants
|
|
42
|
-
"claude-2.1",
|
|
43
|
-
"claude-2.0",
|
|
44
|
-
# Claude instant
|
|
45
|
-
"claude-instant-1.2",
|
|
46
|
-
# Sonnet 4
|
|
47
|
-
"claude-sonnet-4-20250514",
|
|
48
80
|
],
|
|
49
81
|
"gemini": [
|
|
50
82
|
"gemini-2.5-flash",
|
|
@@ -55,6 +87,10 @@ MODEL_MAPPINGS = {
|
|
|
55
87
|
"grok-3",
|
|
56
88
|
"grok-4",
|
|
57
89
|
],
|
|
90
|
+
"zai": [
|
|
91
|
+
"glm-4.5",
|
|
92
|
+
"glm-4.5-air",
|
|
93
|
+
],
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
|
|
@@ -66,7 +102,7 @@ def get_backend_type_from_model(model: str) -> str:
|
|
|
66
102
|
model: The model name (e.g., "gpt-4", "gemini-pro", "grok-1")
|
|
67
103
|
|
|
68
104
|
Returns:
|
|
69
|
-
Agent type string ("openai", "gemini", "grok")
|
|
105
|
+
Agent type string ("openai", "gemini", "grok", etc.)
|
|
70
106
|
"""
|
|
71
107
|
if not model:
|
|
72
108
|
return "openai" # Default to OpenAI
|