mito-ai 0.1.58__py3-none-any.whl → 0.1.60__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.
- mito_ai/__init__.py +5 -2
- mito_ai/_version.py +1 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +7 -1
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +17 -0
- mito_ai/constants.py +25 -3
- mito_ai/enterprise/litellm_client.py +12 -5
- mito_ai/enterprise/utils.py +16 -2
- mito_ai/openai_client.py +26 -6
- mito_ai/provider_manager.py +34 -2
- mito_ai/rules/handlers.py +46 -12
- mito_ai/rules/utils.py +170 -6
- mito_ai/tests/message_history/test_generate_short_chat_name.py +35 -4
- mito_ai/tests/open_ai_utils_test.py +34 -36
- mito_ai/tests/providers/test_azure.py +2 -2
- mito_ai/tests/providers/test_providers.py +5 -5
- mito_ai/tests/rules/rules_test.py +100 -4
- mito_ai/tests/test_constants.py +90 -0
- mito_ai/tests/test_enterprise_mode.py +55 -0
- mito_ai/tests/test_model_utils.py +116 -25
- mito_ai/utils/anthropic_utils.py +1 -2
- mito_ai/utils/model_utils.py +130 -53
- mito_ai/utils/open_ai_utils.py +29 -33
- mito_ai/utils/provider_utils.py +13 -7
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.03302cc521d72eb56b00.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dccfa541c464ee0e5cd4.js +1064 -175
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dccfa541c464ee0e5cd4.js.map +1 -0
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_cjs_js_style_base_css.3594c54c9d209e1ed56e.js +2 -460
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_cjs_js_style_base_css.3594c54c9d209e1ed56e.js.map +1 -0
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_runtime_api_js-node_modules_css-loader_dist_runtime_sourceMaps_j-49e54d.3972dd8e7542bba478ad.js +463 -0
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_css-loader_dist_runtime_api_js-node_modules_css-loader_dist_runtime_sourceMaps_j-49e54d.3972dd8e7542bba478ad.js.map +1 -0
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.570df809a692f53a7ab7.js → mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9735d9bfc8891147fee0.js +6 -6
- mito_ai-0.1.60.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9735d9bfc8891147fee0.js.map +1 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.css +78 -78
- {mito_ai-0.1.58.dist-info → mito_ai-0.1.60.dist-info}/METADATA +1 -1
- {mito_ai-0.1.58.dist-info → mito_ai-0.1.60.dist-info}/RECORD +61 -59
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.03302cc521d72eb56b00.js.map +0 -1
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.570df809a692f53a7ab7.js.map +0 -1
- mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -1
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.58.data → mito_ai-0.1.60.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.js +0 -0
- {mito_ai-0.1.58.dist-info → mito_ai-0.1.60.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.58.dist-info → mito_ai-0.1.60.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.58.dist-info → mito_ai-0.1.60.dist-info}/licenses/LICENSE +0 -0
mito_ai/tests/test_constants.py
CHANGED
|
@@ -7,6 +7,7 @@ from mito_ai.constants import (
|
|
|
7
7
|
ACTIVE_BASE_URL, MITO_PROD_BASE_URL, MITO_DEV_BASE_URL,
|
|
8
8
|
MITO_STREAMLIT_DEV_BASE_URL, MITO_STREAMLIT_TEST_BASE_URL, ACTIVE_STREAMLIT_BASE_URL,
|
|
9
9
|
COGNITO_CONFIG_DEV, ACTIVE_COGNITO_CONFIG,
|
|
10
|
+
parse_comma_separated_models,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
|
|
@@ -45,3 +46,92 @@ def test_cognito_config() -> Any:
|
|
|
45
46
|
|
|
46
47
|
assert COGNITO_CONFIG_DEV == expected_config
|
|
47
48
|
assert ACTIVE_COGNITO_CONFIG == COGNITO_CONFIG_DEV
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestParseCommaSeparatedModels:
|
|
52
|
+
"""Tests for parse_comma_separated_models helper function."""
|
|
53
|
+
|
|
54
|
+
def test_parse_models_no_quotes(self) -> None:
|
|
55
|
+
"""Test parsing models without quotes."""
|
|
56
|
+
models_str = "litellm/openai/gpt-4o,litellm/anthropic/claude-3-5-sonnet"
|
|
57
|
+
result = parse_comma_separated_models(models_str)
|
|
58
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
59
|
+
|
|
60
|
+
def test_parse_models_double_quotes(self) -> None:
|
|
61
|
+
"""Test parsing models with double quotes."""
|
|
62
|
+
# Entire string quoted
|
|
63
|
+
models_str = '"litellm/openai/gpt-4o,litellm/anthropic/claude-3-5-sonnet"'
|
|
64
|
+
result = parse_comma_separated_models(models_str)
|
|
65
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
66
|
+
|
|
67
|
+
# Individual models quoted
|
|
68
|
+
models_str = '"litellm/openai/gpt-4o","litellm/anthropic/claude-3-5-sonnet"'
|
|
69
|
+
result = parse_comma_separated_models(models_str)
|
|
70
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
71
|
+
|
|
72
|
+
def test_parse_models_single_quotes(self) -> None:
|
|
73
|
+
"""Test parsing models with single quotes."""
|
|
74
|
+
# Entire string quoted
|
|
75
|
+
models_str = "'litellm/openai/gpt-4o,litellm/anthropic/claude-3-5-sonnet'"
|
|
76
|
+
result = parse_comma_separated_models(models_str)
|
|
77
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
78
|
+
|
|
79
|
+
# Individual models quoted
|
|
80
|
+
models_str = "'litellm/openai/gpt-4o','litellm/anthropic/claude-3-5-sonnet'"
|
|
81
|
+
result = parse_comma_separated_models(models_str)
|
|
82
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
83
|
+
|
|
84
|
+
def test_parse_models_mixed_quotes(self) -> None:
|
|
85
|
+
"""Test parsing models where some have single quotes and some have double quotes."""
|
|
86
|
+
# Some models with single quotes, some with double quotes
|
|
87
|
+
models_str = "'litellm/openai/gpt-4o',\"litellm/anthropic/claude-3-5-sonnet\""
|
|
88
|
+
result = parse_comma_separated_models(models_str)
|
|
89
|
+
# Should strip both types of quotes
|
|
90
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
91
|
+
|
|
92
|
+
def test_parse_models_with_whitespace(self) -> None:
|
|
93
|
+
"""Test parsing models with whitespace around commas and model names."""
|
|
94
|
+
models_str = " litellm/openai/gpt-4o , litellm/anthropic/claude-3-5-sonnet "
|
|
95
|
+
result = parse_comma_separated_models(models_str)
|
|
96
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
97
|
+
|
|
98
|
+
def test_parse_models_empty_string(self) -> None:
|
|
99
|
+
"""Test parsing empty string."""
|
|
100
|
+
result = parse_comma_separated_models("")
|
|
101
|
+
assert result == []
|
|
102
|
+
|
|
103
|
+
def test_parse_models_single_model(self) -> None:
|
|
104
|
+
"""Test parsing single model."""
|
|
105
|
+
models_str = "litellm/openai/gpt-4o"
|
|
106
|
+
result = parse_comma_separated_models(models_str)
|
|
107
|
+
assert result == ["litellm/openai/gpt-4o"]
|
|
108
|
+
|
|
109
|
+
# With quotes
|
|
110
|
+
models_str = '"litellm/openai/gpt-4o"'
|
|
111
|
+
result = parse_comma_separated_models(models_str)
|
|
112
|
+
assert result == ["litellm/openai/gpt-4o"]
|
|
113
|
+
|
|
114
|
+
def test_parse_models_abacus_format(self) -> None:
|
|
115
|
+
"""Test parsing Abacus model format."""
|
|
116
|
+
models_str = "Abacus/gpt-4.1,Abacus/claude-haiku-4-5-20251001"
|
|
117
|
+
result = parse_comma_separated_models(models_str)
|
|
118
|
+
assert result == ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
119
|
+
|
|
120
|
+
# With quotes
|
|
121
|
+
models_str = '"Abacus/gpt-4.1","Abacus/claude-haiku-4-5-20251001"'
|
|
122
|
+
result = parse_comma_separated_models(models_str)
|
|
123
|
+
assert result == ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize("models_str,description", [
|
|
126
|
+
('"model1,model2"', 'Double quotes, no space after comma'),
|
|
127
|
+
("'model1,model2'", 'Single quotes, no space after comma'),
|
|
128
|
+
("model1,model2", 'No quotes, no space after comma'),
|
|
129
|
+
('"model1, model2"', 'Double quotes, space after comma'),
|
|
130
|
+
("'model1, model2'", 'Single quotes, space after comma'),
|
|
131
|
+
("model1, model2", 'No quotes, space after comma'),
|
|
132
|
+
])
|
|
133
|
+
def test_parse_models_all_scenarios(self, models_str: str, description: str) -> None:
|
|
134
|
+
"""Test all specific scenarios: quotes with and without spaces after commas."""
|
|
135
|
+
expected = ["model1", "model2"]
|
|
136
|
+
result = parse_comma_separated_models(models_str)
|
|
137
|
+
assert result == expected, f"Failed for {description}: {repr(models_str)}"
|
|
@@ -137,6 +137,61 @@ class TestModelValidation:
|
|
|
137
137
|
|
|
138
138
|
from mito_ai.utils.model_utils import STANDARD_MODELS
|
|
139
139
|
assert result == STANDARD_MODELS
|
|
140
|
+
|
|
141
|
+
@patch('mito_ai.utils.model_utils.is_enterprise')
|
|
142
|
+
@patch('mito_ai.utils.model_utils.constants')
|
|
143
|
+
@patch('mito_ai.utils.model_utils.is_abacus_configured')
|
|
144
|
+
def test_provider_manager_validates_abacus_model(self, mock_is_abacus_configured, mock_constants, mock_is_enterprise, provider_config: Config):
|
|
145
|
+
"""Test that ProviderManager validates Abacus models against available models."""
|
|
146
|
+
mock_is_abacus_configured.return_value = True
|
|
147
|
+
mock_is_enterprise.return_value = True
|
|
148
|
+
mock_constants.ABACUS_BASE_URL = "https://routellm.abacus.ai/v1"
|
|
149
|
+
mock_constants.ABACUS_MODELS = ["Abacus/gpt-4.1", "Abacus/gpt-5.2"]
|
|
150
|
+
|
|
151
|
+
provider_manager = ProviderManager(config=provider_config)
|
|
152
|
+
provider_manager.set_selected_model("Abacus/gpt-4.1")
|
|
153
|
+
|
|
154
|
+
# Should not raise an error for valid model
|
|
155
|
+
available_models = get_available_models()
|
|
156
|
+
assert "Abacus/gpt-4.1" in available_models
|
|
157
|
+
|
|
158
|
+
@patch('mito_ai.utils.model_utils.is_enterprise')
|
|
159
|
+
@patch('mito_ai.utils.model_utils.constants')
|
|
160
|
+
@patch('mito_ai.utils.model_utils.is_abacus_configured')
|
|
161
|
+
@pytest.mark.asyncio
|
|
162
|
+
async def test_provider_manager_rejects_invalid_abacus_model(self, mock_is_abacus_configured, mock_constants, mock_is_enterprise, provider_config: Config):
|
|
163
|
+
"""Test that ProviderManager rejects invalid Abacus models."""
|
|
164
|
+
mock_is_abacus_configured.return_value = True
|
|
165
|
+
mock_is_enterprise.return_value = True
|
|
166
|
+
mock_constants.ABACUS_BASE_URL = "https://routellm.abacus.ai/v1"
|
|
167
|
+
mock_constants.ABACUS_MODELS = ["Abacus/gpt-4.1"]
|
|
168
|
+
mock_constants.ABACUS_API_KEY = "test-key"
|
|
169
|
+
|
|
170
|
+
provider_manager = ProviderManager(config=provider_config)
|
|
171
|
+
provider_manager.set_selected_model("invalid-model")
|
|
172
|
+
|
|
173
|
+
messages: list[ChatCompletionMessageParam] = [{"role": "user", "content": "test"}]
|
|
174
|
+
|
|
175
|
+
# Should raise ValueError for invalid model
|
|
176
|
+
with pytest.raises(ValueError, match="is not in the allowed model list"):
|
|
177
|
+
await provider_manager.request_completions(
|
|
178
|
+
message_type=MessageType.CHAT,
|
|
179
|
+
messages=messages
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@patch('mito_ai.utils.model_utils.is_enterprise')
|
|
183
|
+
@patch('mito_ai.utils.model_utils.constants')
|
|
184
|
+
@patch('mito_ai.utils.model_utils.is_abacus_configured')
|
|
185
|
+
def test_available_models_endpoint_returns_abacus_models(self, mock_is_abacus_configured, mock_constants, mock_is_enterprise):
|
|
186
|
+
"""Test that /available-models endpoint returns Abacus models when configured."""
|
|
187
|
+
mock_is_abacus_configured.return_value = True
|
|
188
|
+
mock_is_enterprise.return_value = True
|
|
189
|
+
mock_constants.ABACUS_BASE_URL = "https://routellm.abacus.ai/v1"
|
|
190
|
+
mock_constants.ABACUS_MODELS = ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
191
|
+
|
|
192
|
+
result = get_available_models()
|
|
193
|
+
|
|
194
|
+
assert result == ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
140
195
|
|
|
141
196
|
|
|
142
197
|
class TestModelStorage:
|
|
@@ -22,11 +22,11 @@ class TestGetAvailableModels:
|
|
|
22
22
|
"""Test that LiteLLM models are returned when enterprise mode is enabled and LiteLLM is configured."""
|
|
23
23
|
mock_is_enterprise.return_value = True
|
|
24
24
|
mock_constants.LITELLM_BASE_URL = "https://litellm-server.com"
|
|
25
|
-
mock_constants.LITELLM_MODELS = ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"]
|
|
25
|
+
mock_constants.LITELLM_MODELS = ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
26
26
|
|
|
27
27
|
result = get_available_models()
|
|
28
28
|
|
|
29
|
-
assert result == ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"]
|
|
29
|
+
assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
|
|
30
30
|
|
|
31
31
|
@patch('mito_ai.utils.model_utils.is_enterprise')
|
|
32
32
|
@patch('mito_ai.utils.model_utils.constants')
|
|
@@ -56,7 +56,7 @@ class TestGetAvailableModels:
|
|
|
56
56
|
"""Test that standard models are returned when enterprise mode is enabled but LITELLM_BASE_URL is not set."""
|
|
57
57
|
mock_is_enterprise.return_value = True
|
|
58
58
|
mock_constants.LITELLM_BASE_URL = None
|
|
59
|
-
mock_constants.LITELLM_MODELS = ["openai/gpt-4o"]
|
|
59
|
+
mock_constants.LITELLM_MODELS = ["litellm/openai/gpt-4o"]
|
|
60
60
|
|
|
61
61
|
result = get_available_models()
|
|
62
62
|
|
|
@@ -73,6 +73,20 @@ class TestGetAvailableModels:
|
|
|
73
73
|
result = get_available_models()
|
|
74
74
|
|
|
75
75
|
assert result == STANDARD_MODELS
|
|
76
|
+
|
|
77
|
+
@patch('mito_ai.utils.model_utils.is_abacus_configured')
|
|
78
|
+
@patch('mito_ai.utils.model_utils.is_enterprise')
|
|
79
|
+
@patch('mito_ai.utils.model_utils.constants')
|
|
80
|
+
def test_returns_abacus_models_when_configured(self, mock_constants, mock_is_enterprise, mock_is_abacus_configured):
|
|
81
|
+
"""Test that Abacus models are returned when Abacus is configured (highest priority)."""
|
|
82
|
+
mock_is_abacus_configured.return_value = True
|
|
83
|
+
mock_is_enterprise.return_value = True
|
|
84
|
+
mock_constants.ABACUS_BASE_URL = "https://routellm.abacus.ai/v1"
|
|
85
|
+
mock_constants.ABACUS_MODELS = ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
86
|
+
|
|
87
|
+
result = get_available_models()
|
|
88
|
+
|
|
89
|
+
assert result == ["Abacus/gpt-4.1", "Abacus/claude-haiku-4-5-20251001"]
|
|
76
90
|
|
|
77
91
|
|
|
78
92
|
class TestGetFastModelForSelectedModel:
|
|
@@ -114,51 +128,51 @@ class TestGetFastModelForSelectedModel:
|
|
|
114
128
|
[
|
|
115
129
|
# Test case 1: LiteLLM OpenAI model returns fastest overall
|
|
116
130
|
(
|
|
117
|
-
"openai/gpt-5.2",
|
|
118
|
-
["openai/gpt-4.1", "openai/gpt-5.2", "anthropic/claude-sonnet-4-5-20250929"],
|
|
119
|
-
"openai/gpt-4.1",
|
|
131
|
+
"litellm/openai/gpt-5.2",
|
|
132
|
+
["litellm/openai/gpt-4.1", "litellm/openai/gpt-5.2", "litellm/anthropic/claude-sonnet-4-5-20250929"],
|
|
133
|
+
"litellm/openai/gpt-4.1",
|
|
120
134
|
),
|
|
121
135
|
# Test case 2: LiteLLM Anthropic model returns fastest overall
|
|
122
136
|
(
|
|
123
|
-
"anthropic/claude-sonnet-4-5-20250929",
|
|
124
|
-
["openai/gpt-4.1", "anthropic/claude-sonnet-4-5-20250929", "anthropic/claude-haiku-4-5-20251001"],
|
|
125
|
-
"openai/gpt-4.1",
|
|
137
|
+
"litellm/anthropic/claude-sonnet-4-5-20250929",
|
|
138
|
+
["litellm/openai/gpt-4.1", "litellm/anthropic/claude-sonnet-4-5-20250929", "litellm/anthropic/claude-haiku-4-5-20251001"],
|
|
139
|
+
"litellm/openai/gpt-4.1",
|
|
126
140
|
),
|
|
127
141
|
# Test case 3: LiteLLM Google model returns fastest overall
|
|
128
142
|
(
|
|
129
|
-
"google/gemini-3-pro-preview",
|
|
130
|
-
["google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
|
131
|
-
"google/gemini-3-flash-preview",
|
|
143
|
+
"litellm/google/gemini-3-pro-preview",
|
|
144
|
+
["litellm/google/gemini-3-pro-preview", "litellm/google/gemini-3-flash-preview"],
|
|
145
|
+
"litellm/google/gemini-3-flash-preview",
|
|
132
146
|
),
|
|
133
147
|
# Test case 4: Unknown LiteLLM model returns fastest known
|
|
134
148
|
(
|
|
135
149
|
"unknown/provider/model",
|
|
136
|
-
["openai/gpt-4.1", "unknown/provider/model"],
|
|
137
|
-
"openai/gpt-4.1",
|
|
150
|
+
["litellm/openai/gpt-4.1", "unknown/provider/model"],
|
|
151
|
+
"litellm/openai/gpt-4.1",
|
|
138
152
|
),
|
|
139
153
|
# Test case 5: Single LiteLLM model returns itself
|
|
140
154
|
(
|
|
141
|
-
"openai/gpt-4o",
|
|
142
|
-
["openai/gpt-4o"],
|
|
143
|
-
"openai/gpt-4o",
|
|
155
|
+
"litellm/openai/gpt-4o",
|
|
156
|
+
["litellm/openai/gpt-4o"],
|
|
157
|
+
"litellm/openai/gpt-4o",
|
|
144
158
|
),
|
|
145
159
|
# Test case 6: Cross-provider comparison - OpenAI is faster
|
|
146
160
|
(
|
|
147
|
-
"anthropic/claude-sonnet-4-5-20250929",
|
|
161
|
+
"litellm/anthropic/claude-sonnet-4-5-20250929",
|
|
148
162
|
[
|
|
149
|
-
"openai/gpt-4.1", # Index 0 in OPENAI_MODEL_ORDER
|
|
150
|
-
"anthropic/claude-sonnet-4-5-20250929", # Index 1 in ANTHROPIC_MODEL_ORDER
|
|
163
|
+
"litellm/openai/gpt-4.1", # Index 0 in OPENAI_MODEL_ORDER
|
|
164
|
+
"litellm/anthropic/claude-sonnet-4-5-20250929", # Index 1 in ANTHROPIC_MODEL_ORDER
|
|
151
165
|
],
|
|
152
|
-
"openai/gpt-4.1",
|
|
166
|
+
"litellm/openai/gpt-4.1",
|
|
153
167
|
),
|
|
154
168
|
# Test case 7: Cross-provider comparison - Anthropic is faster
|
|
155
169
|
(
|
|
156
|
-
"openai/gpt-5.2",
|
|
170
|
+
"litellm/openai/gpt-5.2",
|
|
157
171
|
[
|
|
158
|
-
"openai/gpt-5.2", # Index 1 in OPENAI_MODEL_ORDER
|
|
159
|
-
"anthropic/claude-haiku-4-5-20251001", # Index 0 in ANTHROPIC_MODEL_ORDER
|
|
172
|
+
"litellm/openai/gpt-5.2", # Index 1 in OPENAI_MODEL_ORDER
|
|
173
|
+
"litellm/anthropic/claude-haiku-4-5-20251001", # Index 0 in ANTHROPIC_MODEL_ORDER
|
|
160
174
|
],
|
|
161
|
-
"anthropic/claude-haiku-4-5-20251001",
|
|
175
|
+
"litellm/anthropic/claude-haiku-4-5-20251001",
|
|
162
176
|
),
|
|
163
177
|
],
|
|
164
178
|
ids=[
|
|
@@ -269,3 +283,80 @@ class TestGetFastModelForSelectedModel:
|
|
|
269
283
|
for model, expected in test_cases:
|
|
270
284
|
result = get_fast_model_for_selected_model(model)
|
|
271
285
|
assert result == expected, f"Case-insensitive matching failed for {model}"
|
|
286
|
+
|
|
287
|
+
@patch('mito_ai.utils.model_utils.get_available_models')
|
|
288
|
+
@pytest.mark.parametrize(
|
|
289
|
+
"selected_model,available_models,expected_result",
|
|
290
|
+
[
|
|
291
|
+
# Test case 1: Abacus GPT model returns fastest overall
|
|
292
|
+
(
|
|
293
|
+
"Abacus/gpt-5.2",
|
|
294
|
+
["Abacus/gpt-4.1", "Abacus/gpt-5.2", "Abacus/claude-sonnet-4-5-20250929"],
|
|
295
|
+
"Abacus/gpt-4.1",
|
|
296
|
+
),
|
|
297
|
+
# Test case 2: Abacus Claude model returns fastest overall
|
|
298
|
+
(
|
|
299
|
+
"Abacus/claude-sonnet-4-5-20250929",
|
|
300
|
+
["Abacus/gpt-4.1", "Abacus/claude-sonnet-4-5-20250929", "Abacus/claude-haiku-4-5-20251001"],
|
|
301
|
+
"Abacus/gpt-4.1",
|
|
302
|
+
),
|
|
303
|
+
# Test case 3: Abacus Gemini model returns fastest overall
|
|
304
|
+
(
|
|
305
|
+
"Abacus/gemini-3-pro-preview",
|
|
306
|
+
["Abacus/gemini-3-pro-preview", "Abacus/gemini-3-flash-preview"],
|
|
307
|
+
"Abacus/gemini-3-flash-preview",
|
|
308
|
+
),
|
|
309
|
+
# Test case 4: Unknown Abacus model returns fastest known
|
|
310
|
+
(
|
|
311
|
+
"Abacus/unknown-model",
|
|
312
|
+
["Abacus/gpt-4.1", "Abacus/unknown-model"],
|
|
313
|
+
"Abacus/gpt-4.1",
|
|
314
|
+
),
|
|
315
|
+
# Test case 5: Single Abacus model returns itself
|
|
316
|
+
(
|
|
317
|
+
"Abacus/gpt-4.1",
|
|
318
|
+
["Abacus/gpt-4.1"],
|
|
319
|
+
"Abacus/gpt-4.1",
|
|
320
|
+
),
|
|
321
|
+
# Test case 6: Cross-provider comparison - OpenAI is faster
|
|
322
|
+
(
|
|
323
|
+
"Abacus/claude-sonnet-4-5-20250929",
|
|
324
|
+
[
|
|
325
|
+
"Abacus/gpt-4.1", # Index 0 in OPENAI_MODEL_ORDER
|
|
326
|
+
"Abacus/claude-sonnet-4-5-20250929", # Index 1 in ANTHROPIC_MODEL_ORDER
|
|
327
|
+
],
|
|
328
|
+
"Abacus/gpt-4.1",
|
|
329
|
+
),
|
|
330
|
+
# Test case 7: Cross-provider comparison - Anthropic is faster
|
|
331
|
+
(
|
|
332
|
+
"Abacus/gpt-5.2",
|
|
333
|
+
[
|
|
334
|
+
"Abacus/gpt-5.2", # Index 1 in OPENAI_MODEL_ORDER
|
|
335
|
+
"Abacus/claude-haiku-4-5-20251001", # Index 0 in ANTHROPIC_MODEL_ORDER
|
|
336
|
+
],
|
|
337
|
+
"Abacus/claude-haiku-4-5-20251001",
|
|
338
|
+
),
|
|
339
|
+
],
|
|
340
|
+
ids=[
|
|
341
|
+
"abacus_gpt_model_returns_fastest_overall",
|
|
342
|
+
"abacus_anthropic_model_returns_fastest_overall",
|
|
343
|
+
"abacus_google_model_returns_fastest_overall",
|
|
344
|
+
"abacus_unknown_model_returns_fastest_known",
|
|
345
|
+
"abacus_single_model_returns_itself",
|
|
346
|
+
"abacus_cross_provider_comparison_openai_faster",
|
|
347
|
+
"abacus_returns_fastest_when_anthropic_is_faster",
|
|
348
|
+
]
|
|
349
|
+
)
|
|
350
|
+
def test_abacus_model_returns_fastest(
|
|
351
|
+
self,
|
|
352
|
+
mock_get_available_models,
|
|
353
|
+
selected_model,
|
|
354
|
+
available_models,
|
|
355
|
+
expected_result,
|
|
356
|
+
):
|
|
357
|
+
"""Test that Abacus models return fastest model from all available models."""
|
|
358
|
+
mock_get_available_models.return_value = available_models
|
|
359
|
+
|
|
360
|
+
result = get_fast_model_for_selected_model(selected_model)
|
|
361
|
+
|
|
362
|
+
assert result == expected_result
|
mito_ai/utils/anthropic_utils.py
CHANGED
|
@@ -17,8 +17,7 @@ __user_id: Optional[str] = None
|
|
|
17
17
|
ANTHROPIC_TIMEOUT = 60
|
|
18
18
|
max_retries = 1
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929" # This should be in sync with ModelSelector.tsx
|
|
20
|
+
LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929"
|
|
22
21
|
EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07" # Beta feature for extended context window support
|
|
23
22
|
|
|
24
23
|
def does_message_exceed_max_tokens(system: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> bool:
|
mito_ai/utils/model_utils.py
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
from typing import List, Tuple, Union, Optional, cast
|
|
5
5
|
from mito_ai import constants
|
|
6
6
|
from mito_ai.utils.version_utils import is_enterprise
|
|
7
|
+
from mito_ai.enterprise.utils import is_abacus_configured
|
|
7
8
|
|
|
8
9
|
# Model ordering: [fastest, ..., slowest] for each provider
|
|
9
10
|
ANTHROPIC_MODEL_ORDER = [
|
|
10
11
|
"claude-haiku-4-5-20251001", # Fastest
|
|
11
|
-
"claude-sonnet-4-5-20250929", # Slower
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
OPENAI_MODEL_ORDER = [
|
|
@@ -26,7 +26,6 @@ GEMINI_MODEL_ORDER = [
|
|
|
26
26
|
STANDARD_MODELS = [
|
|
27
27
|
"gpt-4.1",
|
|
28
28
|
"gpt-5.2",
|
|
29
|
-
"claude-sonnet-4-5-20250929",
|
|
30
29
|
"claude-haiku-4-5-20251001",
|
|
31
30
|
"gemini-3-flash-preview",
|
|
32
31
|
"gemini-3-pro-preview",
|
|
@@ -35,15 +34,23 @@ STANDARD_MODELS = [
|
|
|
35
34
|
|
|
36
35
|
def get_available_models() -> List[str]:
|
|
37
36
|
"""
|
|
38
|
-
Determine which models are available based on enterprise mode and
|
|
37
|
+
Determine which models are available based on enterprise mode and router configuration.
|
|
38
|
+
|
|
39
|
+
Priority order:
|
|
40
|
+
1. Abacus (if configured)
|
|
41
|
+
2. LiteLLM (if configured)
|
|
42
|
+
3. Standard models
|
|
39
43
|
|
|
40
44
|
Returns:
|
|
41
|
-
List of available model names
|
|
42
|
-
returns LiteLLM models. Otherwise, returns standard models.
|
|
45
|
+
List of available model names with appropriate prefixes.
|
|
43
46
|
"""
|
|
47
|
+
# Check if enterprise mode is enabled AND Abacus is configured (highest priority)
|
|
48
|
+
if is_abacus_configured():
|
|
49
|
+
# Return Abacus models (with Abacus/ prefix)
|
|
50
|
+
return constants.ABACUS_MODELS
|
|
44
51
|
# Check if enterprise mode is enabled AND LiteLLM is configured
|
|
45
|
-
|
|
46
|
-
# Return LiteLLM models (with provider
|
|
52
|
+
elif is_enterprise() and constants.LITELLM_BASE_URL and constants.LITELLM_MODELS:
|
|
53
|
+
# Return LiteLLM models (with LiteLLM/provider/ prefix or legacy provider/ prefix)
|
|
47
54
|
return constants.LITELLM_MODELS
|
|
48
55
|
else:
|
|
49
56
|
# Return standard models
|
|
@@ -55,39 +62,50 @@ def get_fast_model_for_selected_model(selected_model: str) -> str:
|
|
|
55
62
|
Get the fastest model for the client of the selected model.
|
|
56
63
|
|
|
57
64
|
- For standard providers, returns the first (fastest) model from that provider's order.
|
|
58
|
-
- For
|
|
65
|
+
- For enterprise router models (Abacus/LiteLLM), finds the fastest available model by comparing indices.
|
|
59
66
|
"""
|
|
60
|
-
# Check if this is
|
|
61
|
-
if "/" in selected_model:
|
|
62
|
-
|
|
63
|
-
# Find the fastest model from available LiteLLM models
|
|
67
|
+
# Check if this is an enterprise router model (has "/" or router prefix)
|
|
68
|
+
if "/" in selected_model or selected_model.lower().startswith(('abacus/', 'litellm/')):
|
|
69
|
+
# Find the fastest model from available models
|
|
64
70
|
available_models = get_available_models()
|
|
65
71
|
if not available_models:
|
|
66
72
|
return selected_model
|
|
67
73
|
|
|
68
|
-
# Filter to only
|
|
69
|
-
|
|
70
|
-
if not
|
|
74
|
+
# Filter to only router models (those with "/")
|
|
75
|
+
router_models = [model for model in available_models if "/" in model]
|
|
76
|
+
if not router_models:
|
|
71
77
|
return selected_model
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
# Extract provider/model pairs for ordering
|
|
80
|
+
pairs_with_indices = []
|
|
81
|
+
for model in router_models:
|
|
82
|
+
# Strip router prefix to get underlying model info
|
|
83
|
+
model_without_router = strip_router_prefix(model)
|
|
84
|
+
|
|
85
|
+
# For Abacus: model_without_router is just the model name (e.g., "gpt-4.1")
|
|
86
|
+
# For LiteLLM: model_without_router is "provider/model" (e.g., "openai/gpt-4.1")
|
|
87
|
+
if "/" in model_without_router:
|
|
88
|
+
# LiteLLM format: provider/model
|
|
89
|
+
pair = model_without_router.split("/", 1)
|
|
90
|
+
else:
|
|
91
|
+
# Abacus format: just model name, need to determine provider
|
|
92
|
+
provider = get_underlying_model_provider(model)
|
|
93
|
+
if provider:
|
|
94
|
+
pair = [provider, model_without_router]
|
|
95
|
+
else:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
index = get_model_order_index(pair)
|
|
99
|
+
if index is not None:
|
|
100
|
+
pairs_with_indices.append((model, index))
|
|
78
101
|
|
|
79
|
-
if not
|
|
102
|
+
if not pairs_with_indices:
|
|
80
103
|
return selected_model
|
|
81
104
|
|
|
82
|
-
# Find the
|
|
83
|
-
|
|
84
|
-
fastest_model = f"{fastest_pair[0]}/{fastest_pair[1]}"
|
|
105
|
+
# Find the model with the minimum index (fastest model)
|
|
106
|
+
fastest_model, _ = min(pairs_with_indices, key=lambda x: x[1])
|
|
85
107
|
|
|
86
|
-
|
|
87
|
-
if fastest_model:
|
|
88
|
-
return fastest_model
|
|
89
|
-
else:
|
|
90
|
-
return selected_model
|
|
108
|
+
return fastest_model
|
|
91
109
|
|
|
92
110
|
# Standard provider logic - ensure we return a model from the same provider
|
|
93
111
|
model_lower = selected_model.lower()
|
|
@@ -107,42 +125,57 @@ def get_smartest_model_for_selected_model(selected_model: str) -> str:
|
|
|
107
125
|
Get the smartest model for the client of the selected model.
|
|
108
126
|
|
|
109
127
|
- For standard providers, returns the last (smartest) model from that provider's order.
|
|
110
|
-
- For
|
|
128
|
+
- For enterprise router models (Abacus/LiteLLM), finds the smartest available model by comparing indices.
|
|
111
129
|
"""
|
|
112
|
-
# Check if this is
|
|
113
|
-
if "/" in selected_model:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
# Check if this is an enterprise router model (has "/" or router prefix)
|
|
131
|
+
if "/" in selected_model or selected_model.lower().startswith(('abacus/', 'litellm/')):
|
|
132
|
+
# Extract underlying provider from selected model
|
|
133
|
+
selected_provider = get_underlying_model_provider(selected_model)
|
|
134
|
+
if not selected_provider:
|
|
135
|
+
return selected_model
|
|
117
136
|
|
|
118
|
-
# Find the smartest model from available
|
|
137
|
+
# Find the smartest model from available models
|
|
119
138
|
available_models = get_available_models()
|
|
120
139
|
if not available_models:
|
|
121
140
|
return selected_model
|
|
122
141
|
|
|
123
|
-
# Filter to only
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
# Filter to only router models with the same underlying provider
|
|
143
|
+
router_models = []
|
|
144
|
+
for model in available_models:
|
|
145
|
+
if "/" in model:
|
|
146
|
+
model_provider = get_underlying_model_provider(model)
|
|
147
|
+
if model_provider == selected_provider:
|
|
148
|
+
router_models.append(model)
|
|
127
149
|
|
|
128
|
-
|
|
150
|
+
if not router_models:
|
|
151
|
+
return selected_model
|
|
129
152
|
|
|
130
|
-
#
|
|
131
|
-
pairs_with_indices = [
|
|
132
|
-
|
|
153
|
+
# Extract provider/model pairs for ordering
|
|
154
|
+
pairs_with_indices = []
|
|
155
|
+
for model in router_models:
|
|
156
|
+
# Strip router prefix to get underlying model info
|
|
157
|
+
model_without_router = strip_router_prefix(model)
|
|
158
|
+
|
|
159
|
+
# For Abacus: model_without_router is just the model name (e.g., "gpt-4.1")
|
|
160
|
+
# For LiteLLM: model_without_router is "provider/model" (e.g., "openai/gpt-4.1")
|
|
161
|
+
if "/" in model_without_router:
|
|
162
|
+
# LiteLLM format: provider/model
|
|
163
|
+
pair = model_without_router.split("/", 1)
|
|
164
|
+
else:
|
|
165
|
+
# Abacus format: just model name, provider already determined
|
|
166
|
+
pair = [selected_provider, model_without_router]
|
|
167
|
+
|
|
168
|
+
index = get_model_order_index(pair)
|
|
169
|
+
if index is not None:
|
|
170
|
+
pairs_with_indices.append((model, index))
|
|
133
171
|
|
|
134
|
-
if not
|
|
172
|
+
if not pairs_with_indices:
|
|
135
173
|
return selected_model
|
|
136
174
|
|
|
137
|
-
# Find the
|
|
138
|
-
|
|
139
|
-
smartest_model = f"{smartest_pair[0]}/{smartest_pair[1]}"
|
|
175
|
+
# Find the model with the maximum index (smartest model)
|
|
176
|
+
smartest_model, _ = max(pairs_with_indices, key=lambda x: x[1])
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
if smartest_model:
|
|
143
|
-
return smartest_model
|
|
144
|
-
else:
|
|
145
|
-
return selected_model
|
|
178
|
+
return smartest_model
|
|
146
179
|
|
|
147
180
|
# Standard provider logic
|
|
148
181
|
model_lower = selected_model.lower()
|
|
@@ -157,6 +190,50 @@ def get_smartest_model_for_selected_model(selected_model: str) -> str:
|
|
|
157
190
|
|
|
158
191
|
return selected_model
|
|
159
192
|
|
|
193
|
+
def strip_router_prefix(model: str) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Strip router prefix from model name.
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
- "Abacus/gpt-4.1" -> "gpt-4.1"
|
|
199
|
+
- "LiteLLM/openai/gpt-4.1" -> "openai/gpt-4.1"
|
|
200
|
+
- "gpt-4.1" -> "gpt-4.1" (no prefix, return as-is)
|
|
201
|
+
"""
|
|
202
|
+
if model.lower().startswith('abacus/'):
|
|
203
|
+
return model[7:] # Strip "Abacus/"
|
|
204
|
+
elif model.lower().startswith('litellm/'):
|
|
205
|
+
return model[8:] # Strip "LiteLLM/"
|
|
206
|
+
return model
|
|
207
|
+
|
|
208
|
+
def get_underlying_model_provider(full_model_provider_id: str) -> Optional[str]:
|
|
209
|
+
"""
|
|
210
|
+
Determine the underlying AI provider from a model identifier.
|
|
211
|
+
|
|
212
|
+
For Abacus models (Abacus/model), determine the provider from model name pattern.
|
|
213
|
+
For LiteLLM models (LiteLLM/provider/model), extract the provider from the prefix.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Provider name ("openai", "anthropic", "google") or None if cannot determine.
|
|
217
|
+
"""
|
|
218
|
+
# Strip router prefix first
|
|
219
|
+
model_without_router = strip_router_prefix(full_model_provider_id)
|
|
220
|
+
|
|
221
|
+
# Check if it's a LiteLLM format (provider/model)
|
|
222
|
+
if "/" in model_without_router:
|
|
223
|
+
provider, _ = model_without_router.split("/", 1)
|
|
224
|
+
return provider.lower()
|
|
225
|
+
|
|
226
|
+
# For Abacus models without provider prefix, determine from model name
|
|
227
|
+
model_lower = model_without_router.lower()
|
|
228
|
+
if model_lower.startswith('gpt'):
|
|
229
|
+
return 'openai'
|
|
230
|
+
elif model_lower.startswith('claude'):
|
|
231
|
+
return 'anthropic'
|
|
232
|
+
elif model_lower.startswith('gemini'):
|
|
233
|
+
return 'google'
|
|
234
|
+
|
|
235
|
+
return None
|
|
236
|
+
|
|
160
237
|
def get_model_order_index(pair: List[str]) -> Optional[int]:
|
|
161
238
|
provider, model_name = pair
|
|
162
239
|
if provider == "openai":
|