mito-ai 0.1.57__py3-none-any.whl → 0.1.59__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.
Files changed (92) hide show
  1. mito_ai/__init__.py +19 -22
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +24 -14
  4. mito_ai/chart_wizard/handlers.py +78 -17
  5. mito_ai/chart_wizard/urls.py +8 -5
  6. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +6 -8
  7. mito_ai/completions/completion_handlers/agent_execution_handler.py +6 -8
  8. mito_ai/completions/completion_handlers/chat_completion_handler.py +13 -17
  9. mito_ai/completions/completion_handlers/code_explain_handler.py +13 -17
  10. mito_ai/completions/completion_handlers/completion_handler.py +3 -5
  11. mito_ai/completions/completion_handlers/inline_completer_handler.py +5 -6
  12. mito_ai/completions/completion_handlers/scratchpad_result_handler.py +6 -8
  13. mito_ai/completions/completion_handlers/smart_debug_handler.py +13 -17
  14. mito_ai/completions/completion_handlers/utils.py +3 -7
  15. mito_ai/completions/handlers.py +32 -22
  16. mito_ai/completions/message_history.py +8 -10
  17. mito_ai/completions/prompt_builders/chart_add_field_prompt.py +35 -0
  18. mito_ai/completions/prompt_builders/prompt_constants.py +2 -0
  19. mito_ai/constants.py +31 -2
  20. mito_ai/enterprise/__init__.py +1 -1
  21. mito_ai/enterprise/litellm_client.py +144 -0
  22. mito_ai/enterprise/utils.py +16 -2
  23. mito_ai/log/handlers.py +1 -1
  24. mito_ai/openai_client.py +36 -96
  25. mito_ai/provider_manager.py +420 -0
  26. mito_ai/settings/enterprise_handler.py +26 -0
  27. mito_ai/settings/urls.py +2 -0
  28. mito_ai/streamlit_conversion/agent_utils.py +2 -30
  29. mito_ai/streamlit_conversion/streamlit_agent_handler.py +48 -46
  30. mito_ai/streamlit_preview/handlers.py +6 -3
  31. mito_ai/streamlit_preview/urls.py +5 -3
  32. mito_ai/tests/message_history/test_generate_short_chat_name.py +103 -28
  33. mito_ai/tests/open_ai_utils_test.py +34 -36
  34. mito_ai/tests/providers/test_anthropic_client.py +174 -16
  35. mito_ai/tests/providers/test_azure.py +15 -15
  36. mito_ai/tests/providers/test_capabilities.py +14 -17
  37. mito_ai/tests/providers/test_gemini_client.py +14 -13
  38. mito_ai/tests/providers/test_model_resolution.py +145 -89
  39. mito_ai/tests/providers/test_openai_client.py +209 -13
  40. mito_ai/tests/providers/test_provider_limits.py +5 -5
  41. mito_ai/tests/providers/test_providers.py +229 -51
  42. mito_ai/tests/providers/test_retry_logic.py +13 -22
  43. mito_ai/tests/providers/utils.py +4 -4
  44. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +57 -85
  45. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +4 -1
  46. mito_ai/tests/test_constants.py +90 -0
  47. mito_ai/tests/test_enterprise_mode.py +217 -0
  48. mito_ai/tests/test_model_utils.py +362 -0
  49. mito_ai/utils/anthropic_utils.py +8 -6
  50. mito_ai/utils/gemini_utils.py +0 -3
  51. mito_ai/utils/litellm_utils.py +84 -0
  52. mito_ai/utils/model_utils.py +257 -0
  53. mito_ai/utils/open_ai_utils.py +29 -41
  54. mito_ai/utils/provider_utils.py +13 -29
  55. mito_ai/utils/telemetry_utils.py +14 -2
  56. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -102
  57. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  58. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  59. mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.9d26322f3e78beb2b666.js → mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.44c109c7be36fb884d25.js +1059 -144
  60. mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.44c109c7be36fb884d25.js.map +1 -0
  61. mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.79c1ea8a3cda73a4cb6f.js → mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.f7decebaf69618541e0f.js +17 -17
  62. mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.79c1ea8a3cda73a4cb6f.js.map → mito_ai-0.1.59.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.f7decebaf69618541e0f.js.map +1 -1
  63. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.css +78 -78
  64. {mito_ai-0.1.57.dist-info → mito_ai-0.1.59.dist-info}/METADATA +2 -1
  65. {mito_ai-0.1.57.dist-info → mito_ai-0.1.59.dist-info}/RECORD +90 -83
  66. mito_ai/completions/providers.py +0 -284
  67. mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.9d26322f3e78beb2b666.js.map +0 -1
  68. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  69. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  70. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  71. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  72. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  73. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js +0 -0
  74. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -0
  75. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  76. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  77. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  78. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  79. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  80. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  81. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  82. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  83. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  84. {mito_ai-0.1.57.data → mito_ai-0.1.59.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
  85. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  86. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  87. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  88. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  89. {mito_ai-0.1.57.data → mito_ai-0.1.59.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.js +0 -0
  90. {mito_ai-0.1.57.dist-info → mito_ai-0.1.59.dist-info}/WHEEL +0 -0
  91. {mito_ai-0.1.57.dist-info → mito_ai-0.1.59.dist-info}/entry_points.txt +0 -0
  92. {mito_ai-0.1.57.dist-info → mito_ai-0.1.59.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,362 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ from unittest.mock import patch, MagicMock
6
+ from mito_ai.utils.model_utils import (
7
+ get_available_models,
8
+ get_fast_model_for_selected_model,
9
+ STANDARD_MODELS,
10
+ ANTHROPIC_MODEL_ORDER,
11
+ OPENAI_MODEL_ORDER,
12
+ GEMINI_MODEL_ORDER,
13
+ )
14
+
15
+
16
+ class TestGetAvailableModels:
17
+ """Tests for get_available_models() function."""
18
+
19
+ @patch('mito_ai.utils.model_utils.is_enterprise')
20
+ @patch('mito_ai.utils.model_utils.constants')
21
+ def test_returns_litellm_models_when_enterprise_and_configured(self, mock_constants, mock_is_enterprise):
22
+ """Test that LiteLLM models are returned when enterprise mode is enabled and LiteLLM is configured."""
23
+ mock_is_enterprise.return_value = True
24
+ mock_constants.LITELLM_BASE_URL = "https://litellm-server.com"
25
+ mock_constants.LITELLM_MODELS = ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
26
+
27
+ result = get_available_models()
28
+
29
+ assert result == ["litellm/openai/gpt-4o", "litellm/anthropic/claude-3-5-sonnet"]
30
+
31
+ @patch('mito_ai.utils.model_utils.is_enterprise')
32
+ @patch('mito_ai.utils.model_utils.constants')
33
+ def test_returns_standard_models_when_not_enterprise(self, mock_constants, mock_is_enterprise):
34
+ """Test that standard models are returned when enterprise mode is not enabled."""
35
+ mock_is_enterprise.return_value = False
36
+
37
+ result = get_available_models()
38
+
39
+ assert result == STANDARD_MODELS
40
+
41
+ @patch('mito_ai.utils.model_utils.is_enterprise')
42
+ @patch('mito_ai.utils.model_utils.constants')
43
+ def test_returns_standard_models_when_enterprise_but_no_litellm(self, mock_constants, mock_is_enterprise):
44
+ """Test that standard models are returned when enterprise mode is enabled but LiteLLM is not configured."""
45
+ mock_is_enterprise.return_value = True
46
+ mock_constants.LITELLM_BASE_URL = None
47
+ mock_constants.LITELLM_MODELS = []
48
+
49
+ result = get_available_models()
50
+
51
+ assert result == STANDARD_MODELS
52
+
53
+ @patch('mito_ai.utils.model_utils.is_enterprise')
54
+ @patch('mito_ai.utils.model_utils.constants')
55
+ def test_returns_standard_models_when_enterprise_but_no_base_url(self, mock_constants, mock_is_enterprise):
56
+ """Test that standard models are returned when enterprise mode is enabled but LITELLM_BASE_URL is not set."""
57
+ mock_is_enterprise.return_value = True
58
+ mock_constants.LITELLM_BASE_URL = None
59
+ mock_constants.LITELLM_MODELS = ["litellm/openai/gpt-4o"]
60
+
61
+ result = get_available_models()
62
+
63
+ assert result == STANDARD_MODELS
64
+
65
+ @patch('mito_ai.utils.model_utils.is_enterprise')
66
+ @patch('mito_ai.utils.model_utils.constants')
67
+ def test_returns_standard_models_when_enterprise_but_no_models(self, mock_constants, mock_is_enterprise):
68
+ """Test that standard models are returned when enterprise mode is enabled but LITELLM_MODELS is empty."""
69
+ mock_is_enterprise.return_value = True
70
+ mock_constants.LITELLM_BASE_URL = "https://litellm-server.com"
71
+ mock_constants.LITELLM_MODELS = []
72
+
73
+ result = get_available_models()
74
+
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"]
90
+
91
+
92
+ class TestGetFastModelForSelectedModel:
93
+ """Tests for get_fast_model_for_selected_model() function."""
94
+
95
+ def test_anthropic_sonnet_returns_haiku(self):
96
+ """Test that Claude Sonnet returns Claude Haiku (fastest Anthropic model)."""
97
+ result = get_fast_model_for_selected_model("claude-sonnet-4-5-20250929")
98
+ assert result == "claude-haiku-4-5-20251001"
99
+
100
+ def test_anthropic_haiku_returns_haiku(self):
101
+ """Test that Claude Haiku returns itself (already fastest)."""
102
+ result = get_fast_model_for_selected_model("claude-haiku-4-5-20251001")
103
+ assert result == "claude-haiku-4-5-20251001"
104
+
105
+ def test_openai_gpt_4_1_returns_gpt_4_1(self):
106
+ """Test that GPT 4.1 returns itself (already fastest)."""
107
+ result = get_fast_model_for_selected_model("gpt-4.1")
108
+ assert result == "gpt-4.1"
109
+
110
+ def test_openai_gpt_5_2_returns_gpt_4_1(self):
111
+ """Test that GPT 5.2 returns GPT 4.1 (fastest OpenAI model)."""
112
+ result = get_fast_model_for_selected_model("gpt-5.2")
113
+ assert result == "gpt-4.1"
114
+
115
+ def test_gemini_pro_returns_flash(self):
116
+ """Test that Gemini Pro returns Gemini Flash (fastest Gemini model)."""
117
+ result = get_fast_model_for_selected_model("gemini-3-pro-preview")
118
+ assert result == "gemini-3-flash-preview"
119
+
120
+ def test_gemini_flash_returns_flash(self):
121
+ """Test that Gemini Flash returns itself (already fastest)."""
122
+ result = get_fast_model_for_selected_model("gemini-3-flash-preview")
123
+ assert result == "gemini-3-flash-preview"
124
+
125
+ @patch('mito_ai.utils.model_utils.get_available_models')
126
+ @pytest.mark.parametrize(
127
+ "selected_model,available_models,expected_result",
128
+ [
129
+ # Test case 1: LiteLLM OpenAI model returns fastest overall
130
+ (
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",
134
+ ),
135
+ # Test case 2: LiteLLM Anthropic model returns fastest overall
136
+ (
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",
140
+ ),
141
+ # Test case 3: LiteLLM Google model returns fastest overall
142
+ (
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",
146
+ ),
147
+ # Test case 4: Unknown LiteLLM model returns fastest known
148
+ (
149
+ "unknown/provider/model",
150
+ ["litellm/openai/gpt-4.1", "unknown/provider/model"],
151
+ "litellm/openai/gpt-4.1",
152
+ ),
153
+ # Test case 5: Single LiteLLM model returns itself
154
+ (
155
+ "litellm/openai/gpt-4o",
156
+ ["litellm/openai/gpt-4o"],
157
+ "litellm/openai/gpt-4o",
158
+ ),
159
+ # Test case 6: Cross-provider comparison - OpenAI is faster
160
+ (
161
+ "litellm/anthropic/claude-sonnet-4-5-20250929",
162
+ [
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
165
+ ],
166
+ "litellm/openai/gpt-4.1",
167
+ ),
168
+ # Test case 7: Cross-provider comparison - Anthropic is faster
169
+ (
170
+ "litellm/openai/gpt-5.2",
171
+ [
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
174
+ ],
175
+ "litellm/anthropic/claude-haiku-4-5-20251001",
176
+ ),
177
+ ],
178
+ ids=[
179
+ "litellm_openai_model_returns_fastest_overall",
180
+ "litellm_anthropic_model_returns_fastest_overall",
181
+ "litellm_google_model_returns_fastest_overall",
182
+ "litellm_unknown_model_returns_fastest_known",
183
+ "litellm_single_model_returns_itself",
184
+ "litellm_cross_provider_comparison_openai_faster",
185
+ "litellm_returns_fastest_when_anthropic_is_faster",
186
+ ]
187
+ )
188
+ def test_litellm_model_returns_fastest(
189
+ self,
190
+ mock_get_available_models,
191
+ selected_model,
192
+ available_models,
193
+ expected_result,
194
+ ):
195
+ """Test that LiteLLM models return fastest model from all available models."""
196
+ mock_get_available_models.return_value = available_models
197
+
198
+ result = get_fast_model_for_selected_model(selected_model)
199
+
200
+ assert result == expected_result
201
+
202
+ def test_unknown_standard_model_returns_itself(self):
203
+ """Test that unknown standard model returns itself."""
204
+ result = get_fast_model_for_selected_model("unknown-model")
205
+ assert result == "unknown-model"
206
+
207
+ def test_claude_model_not_in_order_returns_fastest_anthropic(self):
208
+ """Test that a Claude model not in ANTHROPIC_MODEL_ORDER still returns fastest Anthropic model."""
209
+ # Test with a Claude model that isn't in the order list
210
+ result = get_fast_model_for_selected_model("claude-3-opus-20240229")
211
+ # Should return fastest Anthropic model (claude-haiku-4-5-20251001)
212
+ assert result == "claude-haiku-4-5-20251001"
213
+ assert result.startswith("claude")
214
+
215
+ def test_gpt_model_not_in_order_returns_fastest_openai(self):
216
+ """Test that a GPT model not in OPENAI_MODEL_ORDER still returns fastest OpenAI model."""
217
+ # Test with a GPT model that isn't in the order list
218
+ result = get_fast_model_for_selected_model("gpt-4o-mini")
219
+ # Should return fastest OpenAI model (gpt-4.1)
220
+ assert result == "gpt-4.1"
221
+ assert result.startswith("gpt")
222
+
223
+ def test_gemini_model_not_in_order_returns_fastest_gemini(self):
224
+ """Test that a Gemini model not in GEMINI_MODEL_ORDER still returns fastest Gemini model."""
225
+ # Test with a Gemini model that isn't in the order list
226
+ result = get_fast_model_for_selected_model("gemini-1.5-pro")
227
+ # Should return fastest Gemini model (gemini-3-flash-preview)
228
+ assert result == "gemini-3-flash-preview"
229
+ assert result.startswith("gemini")
230
+
231
+ def test_claude_model_variations_return_same_provider(self):
232
+ """Test that various Claude model name variations return Anthropic models."""
233
+ test_cases = [
234
+ "claude-3-5-sonnet",
235
+ "claude-3-opus",
236
+ "claude-instant",
237
+ "claude-v2",
238
+ ]
239
+ for model in test_cases:
240
+ result = get_fast_model_for_selected_model(model)
241
+ # Should always return an Anthropic model (starts with "claude")
242
+ assert result.startswith("claude"), f"Model {model} should return Anthropic model, got {result}"
243
+ # Should return the fastest Anthropic model
244
+ assert result == "claude-haiku-4-5-20251001", f"Model {model} should return fastest Anthropic model"
245
+
246
+ def test_gpt_model_variations_return_same_provider(self):
247
+ """Test that various GPT model name variations return OpenAI models."""
248
+ test_cases = [
249
+ "gpt-4o",
250
+ "gpt-4-turbo",
251
+ "gpt-3.5-turbo",
252
+ "gpt-4o-mini",
253
+ ]
254
+ for model in test_cases:
255
+ result = get_fast_model_for_selected_model(model)
256
+ # Should always return an OpenAI model (starts with "gpt")
257
+ assert result.startswith("gpt"), f"Model {model} should return OpenAI model, got {result}"
258
+ # Should return the fastest OpenAI model
259
+ assert result == "gpt-4.1", f"Model {model} should return fastest OpenAI model"
260
+
261
+ def test_gemini_model_variations_return_same_provider(self):
262
+ """Test that various Gemini model name variations return Gemini models."""
263
+ test_cases = [
264
+ "gemini-1.5-pro",
265
+ "gemini-1.5-flash",
266
+ "gemini-pro",
267
+ "gemini-ultra",
268
+ ]
269
+ for model in test_cases:
270
+ result = get_fast_model_for_selected_model(model)
271
+ # Should always return a Gemini model (starts with "gemini")
272
+ assert result.startswith("gemini"), f"Model {model} should return Gemini model, got {result}"
273
+ # Should return the fastest Gemini model
274
+ assert result == "gemini-3-flash-preview", f"Model {model} should return fastest Gemini model"
275
+
276
+ def test_case_insensitive_provider_matching(self):
277
+ """Test that provider matching is case-insensitive."""
278
+ test_cases = [
279
+ ("CLAUDE-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"),
280
+ ("GPT-4.1", "gpt-4.1"),
281
+ ("GEMINI-3-flash-preview", "gemini-3-flash-preview"),
282
+ ]
283
+ for model, expected in test_cases:
284
+ result = get_fast_model_for_selected_model(model)
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
@@ -5,7 +5,6 @@ import anthropic
5
5
  from typing import Any, Dict, List, Optional, Union, AsyncGenerator, Tuple, Callable
6
6
  from anthropic.types import MessageParam, TextBlockParam, ToolUnionParam
7
7
  from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
8
- from mito_ai.utils.provider_utils import does_message_require_fast_model
9
8
  from mito_ai.completions.models import AgentResponse, MessageType, ResponseFormatInfo, CompletionReply, CompletionStreamChunk
10
9
  from mito_ai.utils.schema import UJ_STATIC_USER_ID, UJ_USER_EMAIL
11
10
  from mito_ai.utils.db import get_user_field
@@ -20,6 +19,7 @@ max_retries = 1
20
19
 
21
20
  FAST_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001" # This should be in sync with ModelSelector.tsx
22
21
  LARGE_CONTEXT_MODEL = "claude-sonnet-4-5-20250929" # This should be in sync with ModelSelector.tsx
22
+ EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07" # Beta feature for extended context window support
23
23
 
24
24
  def does_message_exceed_max_tokens(system: Union[str, List[TextBlockParam], anthropic.Omit], messages: List[MessageParam]) -> bool:
25
25
  token_estimation = get_rough_token_estimatation_anthropic(system, messages)
@@ -36,10 +36,6 @@ def select_correct_model(default_model: str, message_type: MessageType, system:
36
36
  # but not haiku models
37
37
  return LARGE_CONTEXT_MODEL
38
38
 
39
- message_requires_fast_model = does_message_require_fast_model(message_type)
40
- if message_requires_fast_model:
41
- return FAST_ANTHROPIC_MODEL
42
-
43
39
  return default_model
44
40
 
45
41
  def _prepare_anthropic_request_data_and_headers(
@@ -66,7 +62,7 @@ def _prepare_anthropic_request_data_and_headers(
66
62
  "max_tokens": max_tokens,
67
63
  "temperature": temperature,
68
64
  "messages": messages,
69
- "betas": ["context-1m-2025-08-07"]
65
+ "betas": [EXTENDED_CONTEXT_BETA]
70
66
  }
71
67
 
72
68
  # Add system to inner_data only if it is not anthropic.Omit
@@ -173,6 +169,12 @@ def get_anthropic_completion_function_params(
173
169
  "messages": messages,
174
170
  "system": system,
175
171
  }
172
+
173
+ # Enable extended context beta when using LARGE_CONTEXT_MODEL
174
+ # This is required for messages exceeding the standard context limit
175
+ if model == LARGE_CONTEXT_MODEL:
176
+ provider_data["betas"] = [EXTENDED_CONTEXT_BETA]
177
+
176
178
  if tools:
177
179
  provider_data["tools"] = tools
178
180
  if response_format_info and response_format_info.name == "agent_response":
@@ -8,7 +8,6 @@ from typing import Any, Dict, List, Optional, Callable, Union, AsyncGenerator, T
8
8
  from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
9
9
  from mito_ai.completions.models import AgentResponse, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
10
10
  from mito_ai.constants import MITO_GEMINI_URL
11
- from mito_ai.utils.provider_utils import does_message_require_fast_model
12
11
  from mito_ai.utils.utils import _create_http_client
13
12
 
14
13
  timeout = 30
@@ -114,8 +113,6 @@ def get_gemini_completion_function_params(
114
113
  Build the provider_data dict for Gemini completions, mirroring the OpenAI/Anthropic approach.
115
114
  Only includes fields needed for the Gemini API.
116
115
  """
117
- message_requires_fast_model = does_message_require_fast_model(message_type)
118
- model = FAST_GEMINI_MODEL if message_requires_fast_model else model
119
116
 
120
117
  provider_data: Dict[str, Any] = {
121
118
  "model": model,
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+ # Copyright (c) Saga Inc.
4
+ # Distributed under the terms of the The Mito Enterprise license.
5
+
6
+ from typing import Dict, Any, List, Optional
7
+ from openai.types.chat import ChatCompletionMessageParam
8
+ import copy
9
+
10
+ from mito_ai import constants
11
+ from mito_ai.completions.models import ResponseFormatInfo
12
+ from mito_ai.utils.version_utils import is_enterprise
13
+
14
+ def is_litellm_configured() -> bool:
15
+ """
16
+ Check if LiteLLM is configured for system use.
17
+
18
+ Per enterprise documentation, LITELLM_API_KEY is user-controlled and optional
19
+ for system configuration. This function only checks system-level configuration
20
+ (BASE_URL and MODELS), not user-specific API keys.
21
+ """
22
+ return all([constants.LITELLM_BASE_URL, constants.LITELLM_MODELS, is_enterprise()])
23
+
24
+ def get_litellm_completion_function_params(
25
+ model: str,
26
+ messages: List[ChatCompletionMessageParam],
27
+ api_key: Optional[str],
28
+ api_base: str,
29
+ timeout: int,
30
+ stream: bool,
31
+ response_format_info: Optional[ResponseFormatInfo] = None,
32
+ ) -> Dict[str, Any]:
33
+ """
34
+ Prepare parameters for LiteLLM completion requests.
35
+
36
+ Args:
37
+ model: Model name with provider prefix (e.g., "openai/gpt-4o")
38
+ messages: List of chat messages
39
+ api_key: Optional API key for authentication
40
+ api_base: Base URL for the LiteLLM server
41
+ timeout: Request timeout in seconds
42
+ stream: Whether to stream the response
43
+ response_format_info: Optional response format specification
44
+
45
+ Returns:
46
+ Dictionary of parameters ready to be passed to litellm.acompletion()
47
+ """
48
+ params: Dict[str, Any] = {
49
+ "model": model,
50
+ "messages": messages,
51
+ "api_key": api_key,
52
+ "api_base": api_base,
53
+ "timeout": timeout,
54
+ "stream": stream,
55
+ }
56
+
57
+ # Handle response format if specified
58
+ if response_format_info:
59
+ # LiteLLM supports response_format for structured outputs
60
+ if hasattr(response_format_info.format, 'model_json_schema'):
61
+ # Pydantic model - get JSON schema
62
+ # Make a deep copy to avoid mutating the original schema
63
+ schema = copy.deepcopy(response_format_info.format.model_json_schema())
64
+
65
+ # Add additionalProperties: False to the top-level schema
66
+ # This is required by OpenAI's JSON schema mode
67
+ schema["additionalProperties"] = False
68
+
69
+ # Nested object definitions in $defs need to have additionalProperties set to False also
70
+ if "$defs" in schema:
71
+ for def_name, def_schema in schema["$defs"].items():
72
+ if def_schema.get("type") == "object":
73
+ def_schema["additionalProperties"] = False
74
+
75
+ params["response_format"] = {
76
+ "type": "json_schema",
77
+ "json_schema": {
78
+ "name": response_format_info.name,
79
+ "schema": schema,
80
+ "strict": True
81
+ }
82
+ }
83
+
84
+ return params