mito-ai 0.1.56__py3-none-any.whl → 0.1.58__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 (95) hide show
  1. mito_ai/__init__.py +17 -21
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +24 -14
  4. mito_ai/chart_wizard/__init__.py +3 -0
  5. mito_ai/chart_wizard/handlers.py +113 -0
  6. mito_ai/chart_wizard/urls.py +26 -0
  7. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +6 -8
  8. mito_ai/completions/completion_handlers/agent_execution_handler.py +6 -8
  9. mito_ai/completions/completion_handlers/chat_completion_handler.py +13 -17
  10. mito_ai/completions/completion_handlers/code_explain_handler.py +13 -17
  11. mito_ai/completions/completion_handlers/completion_handler.py +14 -7
  12. mito_ai/completions/completion_handlers/inline_completer_handler.py +5 -6
  13. mito_ai/completions/completion_handlers/scratchpad_result_handler.py +64 -0
  14. mito_ai/completions/completion_handlers/smart_debug_handler.py +13 -17
  15. mito_ai/completions/completion_handlers/utils.py +3 -7
  16. mito_ai/completions/handlers.py +36 -21
  17. mito_ai/completions/message_history.py +8 -10
  18. mito_ai/completions/models.py +23 -2
  19. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +5 -3
  20. mito_ai/completions/prompt_builders/agent_system_message.py +97 -5
  21. mito_ai/completions/prompt_builders/chart_add_field_prompt.py +35 -0
  22. mito_ai/completions/prompt_builders/chart_conversion_prompt.py +27 -0
  23. mito_ai/completions/prompt_builders/chat_system_message.py +2 -0
  24. mito_ai/completions/prompt_builders/prompt_constants.py +28 -0
  25. mito_ai/completions/prompt_builders/scratchpad_result_prompt.py +17 -0
  26. mito_ai/constants.py +8 -1
  27. mito_ai/enterprise/__init__.py +1 -1
  28. mito_ai/enterprise/litellm_client.py +137 -0
  29. mito_ai/log/handlers.py +1 -1
  30. mito_ai/openai_client.py +10 -90
  31. mito_ai/{completions/providers.py → provider_manager.py} +157 -53
  32. mito_ai/settings/enterprise_handler.py +26 -0
  33. mito_ai/settings/urls.py +2 -0
  34. mito_ai/streamlit_conversion/agent_utils.py +2 -30
  35. mito_ai/streamlit_conversion/streamlit_agent_handler.py +48 -46
  36. mito_ai/streamlit_preview/handlers.py +6 -3
  37. mito_ai/streamlit_preview/urls.py +5 -3
  38. mito_ai/tests/message_history/test_generate_short_chat_name.py +72 -28
  39. mito_ai/tests/providers/test_anthropic_client.py +174 -16
  40. mito_ai/tests/providers/test_azure.py +13 -13
  41. mito_ai/tests/providers/test_capabilities.py +14 -17
  42. mito_ai/tests/providers/test_gemini_client.py +14 -13
  43. mito_ai/tests/providers/test_model_resolution.py +145 -89
  44. mito_ai/tests/providers/test_openai_client.py +209 -13
  45. mito_ai/tests/providers/test_provider_limits.py +5 -5
  46. mito_ai/tests/providers/test_providers.py +229 -51
  47. mito_ai/tests/providers/test_retry_logic.py +13 -22
  48. mito_ai/tests/providers/utils.py +4 -4
  49. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +57 -85
  50. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +4 -1
  51. mito_ai/tests/test_enterprise_mode.py +162 -0
  52. mito_ai/tests/test_model_utils.py +271 -0
  53. mito_ai/utils/anthropic_utils.py +8 -6
  54. mito_ai/utils/gemini_utils.py +0 -3
  55. mito_ai/utils/litellm_utils.py +84 -0
  56. mito_ai/utils/model_utils.py +178 -0
  57. mito_ai/utils/open_ai_utils.py +0 -8
  58. mito_ai/utils/provider_utils.py +6 -21
  59. mito_ai/utils/telemetry_utils.py +14 -2
  60. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -102
  61. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  62. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  63. mito_ai-0.1.56.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dfd7975de75d64db80d6.js → mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.03302cc521d72eb56b00.js +2992 -282
  64. mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.03302cc521d72eb56b00.js.map +1 -0
  65. mito_ai-0.1.56.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.1e7b5cf362385f109883.js → mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.570df809a692f53a7ab7.js +17 -17
  66. mito_ai-0.1.56.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.1e7b5cf362385f109883.js.map → mito_ai-0.1.58.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.570df809a692f53a7ab7.js.map +1 -1
  67. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.css +7 -2
  68. {mito_ai-0.1.56.dist-info → mito_ai-0.1.58.dist-info}/METADATA +2 -1
  69. {mito_ai-0.1.56.dist-info → mito_ai-0.1.58.dist-info}/RECORD +94 -81
  70. mito_ai-0.1.56.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.dfd7975de75d64db80d6.js.map +0 -1
  71. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  72. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  73. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  74. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  75. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  76. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js +0 -0
  77. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -0
  78. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  79. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  80. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  81. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  82. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  83. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  84. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  85. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  86. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  87. {mito_ai-0.1.56.data → mito_ai-0.1.58.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
  88. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  89. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  90. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  91. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  92. {mito_ai-0.1.56.data → mito_ai-0.1.58.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.js +0 -0
  93. {mito_ai-0.1.56.dist-info → mito_ai-0.1.58.dist-info}/WHEEL +0 -0
  94. {mito_ai-0.1.56.dist-info → mito_ai-0.1.58.dist-info}/entry_points.txt +0 -0
  95. {mito_ai-0.1.56.dist-info → mito_ai-0.1.58.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,271 @@
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 = ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"]
26
+
27
+ result = get_available_models()
28
+
29
+ assert result == ["openai/gpt-4o", "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 = ["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
+
78
+ class TestGetFastModelForSelectedModel:
79
+ """Tests for get_fast_model_for_selected_model() function."""
80
+
81
+ def test_anthropic_sonnet_returns_haiku(self):
82
+ """Test that Claude Sonnet returns Claude Haiku (fastest Anthropic model)."""
83
+ result = get_fast_model_for_selected_model("claude-sonnet-4-5-20250929")
84
+ assert result == "claude-haiku-4-5-20251001"
85
+
86
+ def test_anthropic_haiku_returns_haiku(self):
87
+ """Test that Claude Haiku returns itself (already fastest)."""
88
+ result = get_fast_model_for_selected_model("claude-haiku-4-5-20251001")
89
+ assert result == "claude-haiku-4-5-20251001"
90
+
91
+ def test_openai_gpt_4_1_returns_gpt_4_1(self):
92
+ """Test that GPT 4.1 returns itself (already fastest)."""
93
+ result = get_fast_model_for_selected_model("gpt-4.1")
94
+ assert result == "gpt-4.1"
95
+
96
+ def test_openai_gpt_5_2_returns_gpt_4_1(self):
97
+ """Test that GPT 5.2 returns GPT 4.1 (fastest OpenAI model)."""
98
+ result = get_fast_model_for_selected_model("gpt-5.2")
99
+ assert result == "gpt-4.1"
100
+
101
+ def test_gemini_pro_returns_flash(self):
102
+ """Test that Gemini Pro returns Gemini Flash (fastest Gemini model)."""
103
+ result = get_fast_model_for_selected_model("gemini-3-pro-preview")
104
+ assert result == "gemini-3-flash-preview"
105
+
106
+ def test_gemini_flash_returns_flash(self):
107
+ """Test that Gemini Flash returns itself (already fastest)."""
108
+ result = get_fast_model_for_selected_model("gemini-3-flash-preview")
109
+ assert result == "gemini-3-flash-preview"
110
+
111
+ @patch('mito_ai.utils.model_utils.get_available_models')
112
+ @pytest.mark.parametrize(
113
+ "selected_model,available_models,expected_result",
114
+ [
115
+ # Test case 1: LiteLLM OpenAI model returns fastest overall
116
+ (
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",
120
+ ),
121
+ # Test case 2: LiteLLM Anthropic model returns fastest overall
122
+ (
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",
126
+ ),
127
+ # Test case 3: LiteLLM Google model returns fastest overall
128
+ (
129
+ "google/gemini-3-pro-preview",
130
+ ["google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
131
+ "google/gemini-3-flash-preview",
132
+ ),
133
+ # Test case 4: Unknown LiteLLM model returns fastest known
134
+ (
135
+ "unknown/provider/model",
136
+ ["openai/gpt-4.1", "unknown/provider/model"],
137
+ "openai/gpt-4.1",
138
+ ),
139
+ # Test case 5: Single LiteLLM model returns itself
140
+ (
141
+ "openai/gpt-4o",
142
+ ["openai/gpt-4o"],
143
+ "openai/gpt-4o",
144
+ ),
145
+ # Test case 6: Cross-provider comparison - OpenAI is faster
146
+ (
147
+ "anthropic/claude-sonnet-4-5-20250929",
148
+ [
149
+ "openai/gpt-4.1", # Index 0 in OPENAI_MODEL_ORDER
150
+ "anthropic/claude-sonnet-4-5-20250929", # Index 1 in ANTHROPIC_MODEL_ORDER
151
+ ],
152
+ "openai/gpt-4.1",
153
+ ),
154
+ # Test case 7: Cross-provider comparison - Anthropic is faster
155
+ (
156
+ "openai/gpt-5.2",
157
+ [
158
+ "openai/gpt-5.2", # Index 1 in OPENAI_MODEL_ORDER
159
+ "anthropic/claude-haiku-4-5-20251001", # Index 0 in ANTHROPIC_MODEL_ORDER
160
+ ],
161
+ "anthropic/claude-haiku-4-5-20251001",
162
+ ),
163
+ ],
164
+ ids=[
165
+ "litellm_openai_model_returns_fastest_overall",
166
+ "litellm_anthropic_model_returns_fastest_overall",
167
+ "litellm_google_model_returns_fastest_overall",
168
+ "litellm_unknown_model_returns_fastest_known",
169
+ "litellm_single_model_returns_itself",
170
+ "litellm_cross_provider_comparison_openai_faster",
171
+ "litellm_returns_fastest_when_anthropic_is_faster",
172
+ ]
173
+ )
174
+ def test_litellm_model_returns_fastest(
175
+ self,
176
+ mock_get_available_models,
177
+ selected_model,
178
+ available_models,
179
+ expected_result,
180
+ ):
181
+ """Test that LiteLLM models return fastest model from all available models."""
182
+ mock_get_available_models.return_value = available_models
183
+
184
+ result = get_fast_model_for_selected_model(selected_model)
185
+
186
+ assert result == expected_result
187
+
188
+ def test_unknown_standard_model_returns_itself(self):
189
+ """Test that unknown standard model returns itself."""
190
+ result = get_fast_model_for_selected_model("unknown-model")
191
+ assert result == "unknown-model"
192
+
193
+ def test_claude_model_not_in_order_returns_fastest_anthropic(self):
194
+ """Test that a Claude model not in ANTHROPIC_MODEL_ORDER still returns fastest Anthropic model."""
195
+ # Test with a Claude model that isn't in the order list
196
+ result = get_fast_model_for_selected_model("claude-3-opus-20240229")
197
+ # Should return fastest Anthropic model (claude-haiku-4-5-20251001)
198
+ assert result == "claude-haiku-4-5-20251001"
199
+ assert result.startswith("claude")
200
+
201
+ def test_gpt_model_not_in_order_returns_fastest_openai(self):
202
+ """Test that a GPT model not in OPENAI_MODEL_ORDER still returns fastest OpenAI model."""
203
+ # Test with a GPT model that isn't in the order list
204
+ result = get_fast_model_for_selected_model("gpt-4o-mini")
205
+ # Should return fastest OpenAI model (gpt-4.1)
206
+ assert result == "gpt-4.1"
207
+ assert result.startswith("gpt")
208
+
209
+ def test_gemini_model_not_in_order_returns_fastest_gemini(self):
210
+ """Test that a Gemini model not in GEMINI_MODEL_ORDER still returns fastest Gemini model."""
211
+ # Test with a Gemini model that isn't in the order list
212
+ result = get_fast_model_for_selected_model("gemini-1.5-pro")
213
+ # Should return fastest Gemini model (gemini-3-flash-preview)
214
+ assert result == "gemini-3-flash-preview"
215
+ assert result.startswith("gemini")
216
+
217
+ def test_claude_model_variations_return_same_provider(self):
218
+ """Test that various Claude model name variations return Anthropic models."""
219
+ test_cases = [
220
+ "claude-3-5-sonnet",
221
+ "claude-3-opus",
222
+ "claude-instant",
223
+ "claude-v2",
224
+ ]
225
+ for model in test_cases:
226
+ result = get_fast_model_for_selected_model(model)
227
+ # Should always return an Anthropic model (starts with "claude")
228
+ assert result.startswith("claude"), f"Model {model} should return Anthropic model, got {result}"
229
+ # Should return the fastest Anthropic model
230
+ assert result == "claude-haiku-4-5-20251001", f"Model {model} should return fastest Anthropic model"
231
+
232
+ def test_gpt_model_variations_return_same_provider(self):
233
+ """Test that various GPT model name variations return OpenAI models."""
234
+ test_cases = [
235
+ "gpt-4o",
236
+ "gpt-4-turbo",
237
+ "gpt-3.5-turbo",
238
+ "gpt-4o-mini",
239
+ ]
240
+ for model in test_cases:
241
+ result = get_fast_model_for_selected_model(model)
242
+ # Should always return an OpenAI model (starts with "gpt")
243
+ assert result.startswith("gpt"), f"Model {model} should return OpenAI model, got {result}"
244
+ # Should return the fastest OpenAI model
245
+ assert result == "gpt-4.1", f"Model {model} should return fastest OpenAI model"
246
+
247
+ def test_gemini_model_variations_return_same_provider(self):
248
+ """Test that various Gemini model name variations return Gemini models."""
249
+ test_cases = [
250
+ "gemini-1.5-pro",
251
+ "gemini-1.5-flash",
252
+ "gemini-pro",
253
+ "gemini-ultra",
254
+ ]
255
+ for model in test_cases:
256
+ result = get_fast_model_for_selected_model(model)
257
+ # Should always return a Gemini model (starts with "gemini")
258
+ assert result.startswith("gemini"), f"Model {model} should return Gemini model, got {result}"
259
+ # Should return the fastest Gemini model
260
+ assert result == "gemini-3-flash-preview", f"Model {model} should return fastest Gemini model"
261
+
262
+ def test_case_insensitive_provider_matching(self):
263
+ """Test that provider matching is case-insensitive."""
264
+ test_cases = [
265
+ ("CLAUDE-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"),
266
+ ("GPT-4.1", "gpt-4.1"),
267
+ ("GEMINI-3-flash-preview", "gemini-3-flash-preview"),
268
+ ]
269
+ for model, expected in test_cases:
270
+ result = get_fast_model_for_selected_model(model)
271
+ assert result == expected, f"Case-insensitive matching failed for {model}"
@@ -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
@@ -0,0 +1,178 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from typing import List, Tuple, Union, Optional, cast
5
+ from mito_ai import constants
6
+ from mito_ai.utils.version_utils import is_enterprise
7
+
8
+ # Model ordering: [fastest, ..., slowest] for each provider
9
+ ANTHROPIC_MODEL_ORDER = [
10
+ "claude-haiku-4-5-20251001", # Fastest
11
+ "claude-sonnet-4-5-20250929", # Slower
12
+ ]
13
+
14
+ OPENAI_MODEL_ORDER = [
15
+ "gpt-4.1", # Fastest
16
+ "gpt-5",
17
+ "gpt-5.2", # Slower
18
+ ]
19
+
20
+ GEMINI_MODEL_ORDER = [
21
+ "gemini-3-flash-preview", # Fastest
22
+ "gemini-3-pro-preview", # Slower
23
+ ]
24
+
25
+ # Standard model names (used when not in enterprise mode or when LiteLLM is not configured)
26
+ STANDARD_MODELS = [
27
+ "gpt-4.1",
28
+ "gpt-5.2",
29
+ "claude-sonnet-4-5-20250929",
30
+ "claude-haiku-4-5-20251001",
31
+ "gemini-3-flash-preview",
32
+ "gemini-3-pro-preview",
33
+ ]
34
+
35
+
36
+ def get_available_models() -> List[str]:
37
+ """
38
+ Determine which models are available based on enterprise mode and LiteLLM configuration.
39
+
40
+ Returns:
41
+ List of available model names. If enterprise mode is enabled AND LiteLLM is configured,
42
+ returns LiteLLM models. Otherwise, returns standard models.
43
+ """
44
+ # Check if enterprise mode is enabled AND LiteLLM is configured
45
+ if is_enterprise() and constants.LITELLM_BASE_URL and constants.LITELLM_MODELS:
46
+ # Return LiteLLM models (with provider prefixes)
47
+ return constants.LITELLM_MODELS
48
+ else:
49
+ # Return standard models
50
+ return STANDARD_MODELS
51
+
52
+
53
+ def get_fast_model_for_selected_model(selected_model: str) -> str:
54
+ """
55
+ Get the fastest model for the client of the selected model.
56
+
57
+ - For standard providers, returns the first (fastest) model from that provider's order.
58
+ - For LiteLLM models, finds the fastest available model from LiteLLM by comparing indices in the model order lists.
59
+ """
60
+ # Check if this is a LiteLLM model (has provider prefix like "openai/gpt-4o")
61
+ if "/" in selected_model:
62
+
63
+ # Find the fastest model from available LiteLLM models
64
+ available_models = get_available_models()
65
+ if not available_models:
66
+ return selected_model
67
+
68
+ # Filter to only LiteLLM models (those with "/") before splitting
69
+ litellm_models = [model for model in available_models if "/" in model]
70
+ if not litellm_models:
71
+ return selected_model
72
+
73
+ available_provider_model_pairs: List[List[str]] = [model.split("/", 1) for model in litellm_models]
74
+
75
+ # Get indices for all pairs and filter out None indices (unknown models)
76
+ pairs_with_indices = [(pair, get_model_order_index(pair)) for pair in available_provider_model_pairs]
77
+ valid_pairs_with_indices = [(pair, index) for pair, index in pairs_with_indices if index is not None]
78
+
79
+ if not valid_pairs_with_indices:
80
+ return selected_model
81
+
82
+ # Find the pair with the minimum index (fastest model)
83
+ fastest_pair, _ = min(valid_pairs_with_indices, key=lambda x: x[1])
84
+ fastest_model = f"{fastest_pair[0]}/{fastest_pair[1]}"
85
+
86
+ # If we found a fastest model, return it. Otherwise, use the selected model
87
+ if fastest_model:
88
+ return fastest_model
89
+ else:
90
+ return selected_model
91
+
92
+ # Standard provider logic - ensure we return a model from the same provider
93
+ model_lower = selected_model.lower()
94
+
95
+ # Determine provider and get fastest model
96
+ if model_lower.startswith('claude'):
97
+ return ANTHROPIC_MODEL_ORDER[0]
98
+ elif model_lower.startswith('gpt'):
99
+ return OPENAI_MODEL_ORDER[0]
100
+ elif model_lower.startswith('gemini'):
101
+ return GEMINI_MODEL_ORDER[0]
102
+
103
+ return selected_model
104
+
105
+ def get_smartest_model_for_selected_model(selected_model: str) -> str:
106
+ """
107
+ Get the smartest model for the client of the selected model.
108
+
109
+ - For standard providers, returns the last (smartest) model from that provider's order.
110
+ - For LiteLLM models, finds the smartest available model from LiteLLM by comparing indices in the model order lists.
111
+ """
112
+ # Check if this is a LiteLLM model (has provider prefix like "openai/gpt-4o")
113
+ if "/" in selected_model:
114
+
115
+ # Extract provider from selected model
116
+ selected_provider, _ = selected_model.split("/", 1)
117
+
118
+ # Find the smartest model from available LiteLLM models
119
+ available_models = get_available_models()
120
+ if not available_models:
121
+ return selected_model
122
+
123
+ # Filter to only LiteLLM models (those with "/")
124
+ litellm_models = [model for model in available_models if "/" in model and model.startswith(f"{selected_provider}/")]
125
+ if not litellm_models:
126
+ return selected_model
127
+
128
+ available_provider_model_pairs: List[List[str]] = [model.split("/", 1) for model in litellm_models]
129
+
130
+ # Get indices for all pairs and filter out None indices (unknown models)
131
+ pairs_with_indices = [(pair, get_model_order_index(pair)) for pair in available_provider_model_pairs]
132
+ valid_pairs_with_indices = [(pair, index) for pair, index in pairs_with_indices if index is not None]
133
+
134
+ if not valid_pairs_with_indices:
135
+ return selected_model
136
+
137
+ # Find the pair with the maximum index (smartest model)
138
+ smartest_pair, _ = max(valid_pairs_with_indices, key=lambda x: x[1])
139
+ smartest_model = f"{smartest_pair[0]}/{smartest_pair[1]}"
140
+
141
+ # If we found a smartest model, return it. Otherwise, use the selected model
142
+ if smartest_model:
143
+ return smartest_model
144
+ else:
145
+ return selected_model
146
+
147
+ # Standard provider logic
148
+ model_lower = selected_model.lower()
149
+
150
+ # Determine provider and get smartest model
151
+ if model_lower.startswith('claude'):
152
+ return ANTHROPIC_MODEL_ORDER[-1]
153
+ elif model_lower.startswith('gpt'):
154
+ return OPENAI_MODEL_ORDER[-1]
155
+ elif model_lower.startswith('gemini'):
156
+ return GEMINI_MODEL_ORDER[-1]
157
+
158
+ return selected_model
159
+
160
+ def get_model_order_index(pair: List[str]) -> Optional[int]:
161
+ provider, model_name = pair
162
+ if provider == "openai":
163
+ try:
164
+ return OPENAI_MODEL_ORDER.index(model_name)
165
+ except ValueError:
166
+ return None
167
+ elif provider == "anthropic":
168
+ try:
169
+ return ANTHROPIC_MODEL_ORDER.index(model_name)
170
+ except ValueError:
171
+ return None
172
+ elif provider == "google":
173
+ try:
174
+ return GEMINI_MODEL_ORDER.index(model_name)
175
+ except ValueError:
176
+ return None
177
+ else:
178
+ return None
@@ -11,7 +11,6 @@ import json
11
11
  import time
12
12
  from typing import Any, Dict, List, Optional, Final, Union, AsyncGenerator, Tuple, Callable
13
13
  from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
14
- from mito_ai.utils.provider_utils import does_message_require_fast_model
15
14
  from tornado.httpclient import AsyncHTTPClient
16
15
  from openai.types.chat import ChatCompletionMessageParam
17
16
 
@@ -153,19 +152,12 @@ async def stream_ai_completion_from_mito_server(
153
152
 
154
153
 
155
154
  def get_open_ai_completion_function_params(
156
- message_type: MessageType,
157
155
  model: str,
158
156
  messages: List[ChatCompletionMessageParam],
159
157
  stream: bool,
160
158
  response_format_info: Optional[ResponseFormatInfo] = None,
161
159
  ) -> Dict[str, Any]:
162
160
 
163
- print("MESSAGE TYPE: ", message_type)
164
- message_requires_fast_model = does_message_require_fast_model(message_type)
165
- model = FAST_OPENAI_MODEL if message_requires_fast_model else model
166
-
167
- print(f"model: {model}")
168
-
169
161
  completion_function_params = {
170
162
  "model": model,
171
163
  "stream": stream,
@@ -13,6 +13,12 @@ def get_model_provider(model: str) -> Union[str, None]:
13
13
  if not model:
14
14
  return None
15
15
 
16
+ # Check if model is a LiteLLM model (has provider prefix)
17
+ if "/" in model and any(
18
+ model.startswith(prefix) for prefix in ["openai/", "anthropic/", "google/", "ollama/"]
19
+ ):
20
+ return 'litellm'
21
+
16
22
  model_lower = model.lower()
17
23
 
18
24
  if model_lower.startswith('claude'):
@@ -25,25 +31,4 @@ def get_model_provider(model: str) -> Union[str, None]:
25
31
  return 'openai'
26
32
 
27
33
  return None
28
-
29
-
30
- def does_message_require_fast_model(message_type: MessageType) -> bool:
31
- """
32
- Determines if a message requires the fast model.
33
-
34
- The fast model is used for messages that are not chat messages.
35
- For example, inline completions and chat name generation need to be fast
36
- so they don't slow down the user's experience.
37
- """
38
-
39
- if message_type in (MessageType.CHAT, MessageType.SMART_DEBUG, MessageType.CODE_EXPLAIN, MessageType.AGENT_EXECUTION, MessageType.AGENT_AUTO_ERROR_FIXUP):
40
- return False
41
- elif message_type in (MessageType.INLINE_COMPLETION, MessageType.CHAT_NAME_GENERATION):
42
- return True
43
- elif message_type in (MessageType.START_NEW_CHAT, MessageType.FETCH_HISTORY, MessageType.GET_THREADS, MessageType.DELETE_THREAD, MessageType.UPDATE_MODEL_CONFIG):
44
- # These messages don't use any model, but we add them here for type safety
45
- return True
46
- else:
47
- raise ValueError(f"Invalid message type: {message_type}")
48
-
49
34